6744be0e09750578b8e25a9628c27c37a26b1ce7
[stgit] / stgit / git.py
1 """Python GIT interface
2 """
3
4 __copyright__ = """
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
6
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License version 2 as
9 published by the Free Software Foundation.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 """
20
21 import sys, os, re, gitmergeonefile
22 from shutil import copyfile
23
24 from stgit.exception import *
25 from stgit import basedir
26 from stgit.utils import *
27 from stgit.out import *
28 from stgit.run import *
29 from stgit.config import config
30
31 # git exception class
32 class GitException(StgException):
33 pass
34
35 # When a subprocess has a problem, we want the exception to be a
36 # subclass of GitException.
37 class GitRunException(GitException):
38 pass
39 class GRun(Run):
40 exc = GitRunException
41 def __init__(self, *cmd):
42 """Initialise the Run object and insert the 'git' command name.
43 """
44 Run.__init__(self, 'git', *cmd)
45
46
47 #
48 # Classes
49 #
50
51 class Person:
52 """An author, committer, etc."""
53 def __init__(self, name = None, email = None, date = '',
54 desc = None):
55 self.name = self.email = self.date = None
56 if name or email or date:
57 assert not desc
58 self.name = name
59 self.email = email
60 self.date = date
61 elif desc:
62 assert not (name or email or date)
63 def parse_desc(s):
64 m = re.match(r'^(.+)<(.+)>(.*)$', s)
65 assert m
66 return [x.strip() or None for x in m.groups()]
67 self.name, self.email, self.date = parse_desc(desc)
68 def set_name(self, val):
69 if val:
70 self.name = val
71 def set_email(self, val):
72 if val:
73 self.email = val
74 def set_date(self, val):
75 if val:
76 self.date = val
77 def __str__(self):
78 if self.name and self.email:
79 return '%s <%s>' % (self.name, self.email)
80 else:
81 raise GitException, 'not enough identity data'
82
83 class Commit:
84 """Handle the commit objects
85 """
86 def __init__(self, id_hash):
87 self.__id_hash = id_hash
88
89 lines = GRun('cat-file', 'commit', id_hash).output_lines()
90 for i in range(len(lines)):
91 line = lines[i]
92 if not line:
93 break # we've seen all the header fields
94 key, val = line.split(' ', 1)
95 if key == 'tree':
96 self.__tree = val
97 elif key == 'author':
98 self.__author = val
99 elif key == 'committer':
100 self.__committer = val
101 else:
102 pass # ignore other headers
103 self.__log = '\n'.join(lines[i+1:])
104
105 def get_id_hash(self):
106 return self.__id_hash
107
108 def get_tree(self):
109 return self.__tree
110
111 def get_parent(self):
112 parents = self.get_parents()
113 if parents:
114 return parents[0]
115 else:
116 return None
117
118 def get_parents(self):
119 return GRun('rev-list', '--parents', '--max-count=1', self.__id_hash
120 ).output_one_line().split()[1:]
121
122 def get_author(self):
123 return self.__author
124
125 def get_committer(self):
126 return self.__committer
127
128 def get_log(self):
129 return self.__log
130
131 def __str__(self):
132 return self.get_id_hash()
133
134 # dictionary of Commit objects, used to avoid multiple calls to git
135 __commits = dict()
136
137 #
138 # Functions
139 #
140
141 def get_commit(id_hash):
142 """Commit objects factory. Save/look-up them in the __commits
143 dictionary
144 """
145 global __commits
146
147 if id_hash in __commits:
148 return __commits[id_hash]
149 else:
150 commit = Commit(id_hash)
151 __commits[id_hash] = commit
152 return commit
153
154 def get_conflicts():
155 """Return the list of file conflicts
156 """
157 conflicts_file = os.path.join(basedir.get(), 'conflicts')
158 if os.path.isfile(conflicts_file):
159 f = file(conflicts_file)
160 names = [line.strip() for line in f.readlines()]
161 f.close()
162 return names
163 else:
164 return None
165
166 def exclude_files():
167 files = [os.path.join(basedir.get(), 'info', 'exclude')]
168 user_exclude = config.get('core.excludesfile')
169 if user_exclude:
170 files.append(user_exclude)
171 return files
172
173 def ls_files(files, tree = None, full_name = True):
174 """Return the files known to GIT or raise an error otherwise. It also
175 converts the file to the full path relative the the .git directory.
176 """
177 if not files:
178 return []
179
180 args = []
181 if tree:
182 args.append('--with-tree=%s' % tree)
183 if full_name:
184 args.append('--full-name')
185 args.append('--')
186 args.extend(files)
187 try:
188 return GRun('ls-files', '--error-unmatch', *args).output_lines()
189 except GitRunException:
190 # just hide the details of the 'git ls-files' command we use
191 raise GitException, \
192 'Some of the given paths are either missing or not known to GIT'
193
194 def tree_status(files = None, tree_id = 'HEAD', unknown = False,
195 noexclude = True, verbose = False, diff_flags = []):
196 """Get the status of all changed files, or of a selected set of
197 files. Returns a list of pairs - (status, filename).
198
199 If 'not files', it will check all files, and optionally all
200 unknown files. If 'files' is a list, it will only check the files
201 in the list.
202 """
203 assert not files or not unknown
204
205 if verbose:
206 out.start('Checking for changes in the working directory')
207
208 refresh_index()
209
210 cache_files = []
211
212 # unknown files
213 if unknown:
214 cmd = ['ls-files', '-z', '--others', '--directory',
215 '--no-empty-directory']
216 if not noexclude:
217 cmd += ['--exclude=%s' % s for s in
218 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
219 cmd += ['--exclude-per-directory=.gitignore']
220 cmd += ['--exclude-from=%s' % fn
221 for fn in exclude_files()
222 if os.path.exists(fn)]
223
224 lines = GRun(*cmd).raw_output().split('\0')
225 cache_files += [('?', line) for line in lines if line]
226
227 # conflicted files
228 conflicts = get_conflicts()
229 if not conflicts:
230 conflicts = []
231 cache_files += [('C', filename) for filename in conflicts
232 if not files or filename in files]
233 reported_files = set(conflicts)
234
235 # files in the index
236 args = diff_flags + [tree_id]
237 if files:
238 args += ['--'] + files
239 for line in GRun('diff-index', *args).output_lines():
240 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
241 if fs[1] not in reported_files:
242 cache_files.append(fs)
243 reported_files.add(fs[1])
244
245 # files in the index but changed on (or removed from) disk
246 args = list(diff_flags)
247 if files:
248 args += ['--'] + files
249 for line in GRun('diff-files', *args).output_lines():
250 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
251 if fs[1] not in reported_files:
252 cache_files.append(fs)
253 reported_files.add(fs[1])
254
255 if verbose:
256 out.done()
257
258 return cache_files
259
260 def local_changes(verbose = True):
261 """Return true if there are local changes in the tree
262 """
263 return len(tree_status(verbose = verbose)) != 0
264
265 def get_heads():
266 heads = []
267 hr = re.compile(r'^[0-9a-f]{40} refs/heads/(.+)$')
268 for line in GRun('show-ref', '--heads').output_lines():
269 m = hr.match(line)
270 heads.append(m.group(1))
271 return heads
272
273 # HEAD value cached
274 __head = None
275
276 def get_head():
277 """Verifies the HEAD and returns the SHA1 id that represents it
278 """
279 global __head
280
281 if not __head:
282 __head = rev_parse('HEAD')
283 return __head
284
285 class DetachedHeadException(GitException):
286 def __init__(self):
287 GitException.__init__(self, 'Not on any branch')
288
289 def get_head_file():
290 """Return the name of the file pointed to by the HEAD symref.
291 Throw an exception if HEAD is detached."""
292 try:
293 return strip_prefix(
294 'refs/heads/', GRun('symbolic-ref', '-q', 'HEAD'
295 ).output_one_line())
296 except GitRunException:
297 raise DetachedHeadException()
298
299 def set_head_file(ref):
300 """Resets HEAD to point to a new ref
301 """
302 # head cache flushing is needed since we might have a different value
303 # in the new head
304 __clear_head_cache()
305 try:
306 GRun('symbolic-ref', 'HEAD', 'refs/heads/%s' % ref).run()
307 except GitRunException:
308 raise GitException, 'Could not set head to "%s"' % ref
309
310 def set_ref(ref, val):
311 """Point ref at a new commit object."""
312 try:
313 GRun('update-ref', ref, val).run()
314 except GitRunException:
315 raise GitException, 'Could not update %s to "%s".' % (ref, val)
316
317 def set_branch(branch, val):
318 set_ref('refs/heads/%s' % branch, val)
319
320 def __set_head(val):
321 """Sets the HEAD value
322 """
323 global __head
324
325 if not __head or __head != val:
326 set_ref('HEAD', val)
327 __head = val
328
329 # only allow SHA1 hashes
330 assert(len(__head) == 40)
331
332 def __clear_head_cache():
333 """Sets the __head to None so that a re-read is forced
334 """
335 global __head
336
337 __head = None
338
339 def refresh_index():
340 """Refresh index with stat() information from the working directory.
341 """
342 GRun('update-index', '-q', '--unmerged', '--refresh').run()
343
344 def rev_parse(git_id):
345 """Parse the string and return a verified SHA1 id
346 """
347 try:
348 return GRun('rev-parse', '--verify', git_id
349 ).discard_stderr().output_one_line()
350 except GitRunException:
351 raise GitException, 'Unknown revision: %s' % git_id
352
353 def ref_exists(ref):
354 try:
355 rev_parse(ref)
356 return True
357 except GitException:
358 return False
359
360 def branch_exists(branch):
361 return ref_exists('refs/heads/%s' % branch)
362
363 def create_branch(new_branch, tree_id = None):
364 """Create a new branch in the git repository
365 """
366 if branch_exists(new_branch):
367 raise GitException, 'Branch "%s" already exists' % new_branch
368
369 current_head = get_head()
370 set_head_file(new_branch)
371 __set_head(current_head)
372
373 # a checkout isn't needed if new branch points to the current head
374 if tree_id:
375 switch(tree_id)
376
377 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
378 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
379
380 def switch_branch(new_branch):
381 """Switch to a git branch
382 """
383 global __head
384
385 if not branch_exists(new_branch):
386 raise GitException, 'Branch "%s" does not exist' % new_branch
387
388 tree_id = rev_parse('refs/heads/%s^{commit}' % new_branch)
389 if tree_id != get_head():
390 refresh_index()
391 try:
392 GRun('read-tree', '-u', '-m', get_head(), tree_id).run()
393 except GitRunException:
394 raise GitException, 'read-tree failed (local changes maybe?)'
395 __head = tree_id
396 set_head_file(new_branch)
397
398 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
399 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
400
401 def delete_ref(ref):
402 if not ref_exists(ref):
403 raise GitException, '%s does not exist' % ref
404 sha1 = GRun('show-ref', '-s', ref).output_one_line()
405 try:
406 GRun('update-ref', '-d', ref, sha1).run()
407 except GitRunException:
408 raise GitException, 'Failed to delete ref %s' % ref
409
410 def delete_branch(name):
411 delete_ref('refs/heads/%s' % name)
412
413 def rename_ref(from_ref, to_ref):
414 if not ref_exists(from_ref):
415 raise GitException, '"%s" does not exist' % from_ref
416 if ref_exists(to_ref):
417 raise GitException, '"%s" already exists' % to_ref
418
419 sha1 = GRun('show-ref', '-s', from_ref).output_one_line()
420 try:
421 GRun('update-ref', to_ref, sha1, '0'*40).run()
422 except GitRunException:
423 raise GitException, 'Failed to create new ref %s' % to_ref
424 try:
425 GRun('update-ref', '-d', from_ref, sha1).run()
426 except GitRunException:
427 raise GitException, 'Failed to delete ref %s' % from_ref
428
429 def rename_branch(from_name, to_name):
430 """Rename a git branch."""
431 rename_ref('refs/heads/%s' % from_name, 'refs/heads/%s' % to_name)
432 try:
433 if get_head_file() == from_name:
434 set_head_file(to_name)
435 except DetachedHeadException:
436 pass # detached HEAD, so the renamee can't be the current branch
437 reflog_dir = os.path.join(basedir.get(), 'logs', 'refs', 'heads')
438 if os.path.exists(reflog_dir) \
439 and os.path.exists(os.path.join(reflog_dir, from_name)):
440 rename(reflog_dir, from_name, to_name)
441
442 def add(names):
443 """Add the files or recursively add the directory contents
444 """
445 # generate the file list
446 files = []
447 for i in names:
448 if not os.path.exists(i):
449 raise GitException, 'Unknown file or directory: %s' % i
450
451 if os.path.isdir(i):
452 # recursive search. We only add files
453 for root, dirs, local_files in os.walk(i):
454 for name in [os.path.join(root, f) for f in local_files]:
455 if os.path.isfile(name):
456 files.append(os.path.normpath(name))
457 elif os.path.isfile(i):
458 files.append(os.path.normpath(i))
459 else:
460 raise GitException, '%s is not a file or directory' % i
461
462 if files:
463 try:
464 GRun('update-index', '--add', '--').xargs(files)
465 except GitRunException:
466 raise GitException, 'Unable to add file'
467
468 def __copy_single(source, target, target2=''):
469 """Copy file or dir named 'source' to name target+target2"""
470
471 # "source" (file or dir) must match one or more git-controlled file
472 realfiles = GRun('ls-files', source).output_lines()
473 if len(realfiles) == 0:
474 raise GitException, '"%s" matches no git-controled files' % source
475
476 if os.path.isdir(source):
477 # physically copy the files, and record them to add them in one run
478 newfiles = []
479 re_string='^'+source+'/(.*)$'
480 prefix_regexp = re.compile(re_string)
481 for f in [f.strip() for f in realfiles]:
482 m = prefix_regexp.match(f)
483 if not m:
484 raise Exception, '"%s" does not match "%s"' % (f, re_string)
485 newname = target+target2+'/'+m.group(1)
486 if not os.path.exists(os.path.dirname(newname)):
487 os.makedirs(os.path.dirname(newname))
488 copyfile(f, newname)
489 newfiles.append(newname)
490
491 add(newfiles)
492 else: # files, symlinks, ...
493 newname = target+target2
494 copyfile(source, newname)
495 add([newname])
496
497
498 def copy(filespecs, target):
499 if os.path.isdir(target):
500 # target is a directory: copy each entry on the command line,
501 # with the same name, into the target
502 target = target.rstrip('/')
503
504 # first, check that none of the children of the target
505 # matching the command line aleady exist
506 for filespec in filespecs:
507 entry = target+ '/' + os.path.basename(filespec.rstrip('/'))
508 if os.path.exists(entry):
509 raise GitException, 'Target "%s" already exists' % entry
510
511 for filespec in filespecs:
512 filespec = filespec.rstrip('/')
513 basename = '/' + os.path.basename(filespec)
514 __copy_single(filespec, target, basename)
515
516 elif os.path.exists(target):
517 raise GitException, 'Target "%s" exists but is not a directory' % target
518 elif len(filespecs) != 1:
519 raise GitException, 'Cannot copy more than one file to non-directory'
520
521 else:
522 # at this point: len(filespecs)==1 and target does not exist
523
524 # check target directory
525 targetdir = os.path.dirname(target)
526 if targetdir != '' and not os.path.isdir(targetdir):
527 raise GitException, 'Target directory "%s" does not exist' % targetdir
528
529 __copy_single(filespecs[0].rstrip('/'), target)
530
531
532 def rm(files, force = False):
533 """Remove a file from the repository
534 """
535 if not force:
536 for f in files:
537 if os.path.exists(f):
538 raise GitException, '%s exists. Remove it first' %f
539 if files:
540 GRun('update-index', '--remove', '--').xargs(files)
541 else:
542 if files:
543 GRun('update-index', '--force-remove', '--').xargs(files)
544
545 # Persons caching
546 __user = None
547 __author = None
548 __committer = None
549
550 def user():
551 """Return the user information.
552 """
553 global __user
554 if not __user:
555 name=config.get('user.name')
556 email=config.get('user.email')
557 __user = Person(name, email)
558 return __user;
559
560 def author():
561 """Return the author information.
562 """
563 global __author
564 if not __author:
565 try:
566 # the environment variables take priority over config
567 try:
568 date = os.environ['GIT_AUTHOR_DATE']
569 except KeyError:
570 date = ''
571 __author = Person(os.environ['GIT_AUTHOR_NAME'],
572 os.environ['GIT_AUTHOR_EMAIL'],
573 date)
574 except KeyError:
575 __author = user()
576 return __author
577
578 def committer():
579 """Return the author information.
580 """
581 global __committer
582 if not __committer:
583 try:
584 # the environment variables take priority over config
585 try:
586 date = os.environ['GIT_COMMITTER_DATE']
587 except KeyError:
588 date = ''
589 __committer = Person(os.environ['GIT_COMMITTER_NAME'],
590 os.environ['GIT_COMMITTER_EMAIL'],
591 date)
592 except KeyError:
593 __committer = user()
594 return __committer
595
596 def update_cache(files = None, force = False):
597 """Update the cache information for the given files
598 """
599 cache_files = tree_status(files, verbose = False)
600
601 # everything is up-to-date
602 if len(cache_files) == 0:
603 return False
604
605 # check for unresolved conflicts
606 if not force and [x for x in cache_files
607 if x[0] not in ['M', 'N', 'A', 'D']]:
608 raise GitException, 'Updating cache failed: unresolved conflicts'
609
610 # update the cache
611 add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
612 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
613 m_files = [x[1] for x in cache_files if x[0] in ['M']]
614
615 GRun('update-index', '--add', '--').xargs(add_files)
616 GRun('update-index', '--force-remove', '--').xargs(rm_files)
617 GRun('update-index', '--').xargs(m_files)
618
619 return True
620
621 def commit(message, files = None, parents = None, allowempty = False,
622 cache_update = True, tree_id = None, set_head = False,
623 author_name = None, author_email = None, author_date = None,
624 committer_name = None, committer_email = None):
625 """Commit the current tree to repository
626 """
627 if not parents:
628 parents = []
629
630 # Get the tree status
631 if cache_update and parents != []:
632 changes = update_cache(files)
633 if not changes and not allowempty:
634 raise GitException, 'No changes to commit'
635
636 # get the commit message
637 if not message:
638 message = '\n'
639 elif message[-1:] != '\n':
640 message += '\n'
641
642 # write the index to repository
643 if tree_id == None:
644 tree_id = GRun('write-tree').output_one_line()
645 set_head = True
646
647 # the commit
648 env = {}
649 if author_name:
650 env['GIT_AUTHOR_NAME'] = author_name
651 if author_email:
652 env['GIT_AUTHOR_EMAIL'] = author_email
653 if author_date:
654 env['GIT_AUTHOR_DATE'] = author_date
655 if committer_name:
656 env['GIT_COMMITTER_NAME'] = committer_name
657 if committer_email:
658 env['GIT_COMMITTER_EMAIL'] = committer_email
659 commit_id = GRun('commit-tree', tree_id,
660 *sum([['-p', p] for p in parents], [])
661 ).env(env).raw_input(message).output_one_line()
662 if set_head:
663 __set_head(commit_id)
664
665 return commit_id
666
667 def apply_diff(rev1, rev2, check_index = True, files = None):
668 """Apply the diff between rev1 and rev2 onto the current
669 index. This function doesn't need to raise an exception since it
670 is only used for fast-pushing a patch. If this operation fails,
671 the pushing would fall back to the three-way merge.
672 """
673 if check_index:
674 index_opt = ['--index']
675 else:
676 index_opt = []
677
678 if not files:
679 files = []
680
681 diff_str = diff(files, rev1, rev2)
682 if diff_str:
683 try:
684 GRun('apply', *index_opt).raw_input(
685 diff_str).discard_stderr().no_output()
686 except GitRunException:
687 return False
688
689 return True
690
691 def merge(base, head1, head2, recursive = False):
692 """Perform a 3-way merge between base, head1 and head2 into the
693 local tree
694 """
695 refresh_index()
696
697 err_output = None
698 if recursive:
699 # this operation tracks renames but it is slower (used in
700 # general when pushing or picking patches)
701 try:
702 # discard output to mask the verbose prints of the tool
703 GRun('merge-recursive', base, '--', head1, head2
704 ).discard_output()
705 except GitRunException, ex:
706 err_output = str(ex)
707 pass
708 else:
709 # the fast case where we don't track renames (used when the
710 # distance between base and heads is small, i.e. folding or
711 # synchronising patches)
712 try:
713 GRun('read-tree', '-u', '-m', '--aggressive',
714 base, head1, head2).run()
715 except GitRunException:
716 raise GitException, 'read-tree failed (local changes maybe?)'
717
718 # check the index for unmerged entries
719 files = {}
720 stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
721
722 for line in GRun('ls-files', '--unmerged', '--stage', '-z'
723 ).raw_output().split('\0'):
724 if not line:
725 continue
726
727 mode, hash, stage, path = stages_re.findall(line)[0]
728
729 if not path in files:
730 files[path] = {}
731 files[path]['1'] = ('', '')
732 files[path]['2'] = ('', '')
733 files[path]['3'] = ('', '')
734
735 files[path][stage] = (mode, hash)
736
737 if err_output and not files:
738 # if no unmerged files, there was probably a different type of
739 # error and we have to abort the merge
740 raise GitException, err_output
741
742 # merge the unmerged files
743 errors = False
744 for path in files:
745 # remove additional files that might be generated for some
746 # newer versions of GIT
747 for suffix in [base, head1, head2]:
748 if not suffix:
749 continue
750 fname = path + '~' + suffix
751 if os.path.exists(fname):
752 os.remove(fname)
753
754 stages = files[path]
755 if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
756 stages['3'][1], path, stages['1'][0],
757 stages['2'][0], stages['3'][0]) != 0:
758 errors = True
759
760 if errors:
761 raise GitException, 'GIT index merging failed (possible conflicts)'
762
763 def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = []):
764 """Show the diff between rev1 and rev2
765 """
766 if not files:
767 files = []
768
769 if rev1 and rev2:
770 return GRun('diff-tree', '-p',
771 *(diff_flags + [rev1, rev2, '--'] + files)).raw_output()
772 elif rev1 or rev2:
773 refresh_index()
774 if rev2:
775 return GRun('diff-index', '-p', '-R',
776 *(diff_flags + [rev2, '--'] + files)).raw_output()
777 else:
778 return GRun('diff-index', '-p',
779 *(diff_flags + [rev1, '--'] + files)).raw_output()
780 else:
781 return ''
782
783 # TODO: take another parameter representing a diff string as we
784 # usually invoke git.diff() form the calling functions
785 def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
786 """Return the diffstat between rev1 and rev2."""
787 return GRun('apply', '--stat', '--summary'
788 ).raw_input(diff(files, rev1, rev2)).raw_output()
789
790 def files(rev1, rev2, diff_flags = []):
791 """Return the files modified between rev1 and rev2
792 """
793
794 result = []
795 for line in GRun('diff-tree', *(diff_flags + ['-r', rev1, rev2])
796 ).output_lines():
797 result.append('%s %s' % tuple(line.split(' ', 4)[-1].split('\t', 1)))
798
799 return '\n'.join(result)
800
801 def barefiles(rev1, rev2):
802 """Return the files modified between rev1 and rev2, without status info
803 """
804
805 result = []
806 for line in GRun('diff-tree', '-r', rev1, rev2).output_lines():
807 result.append(line.split(' ', 4)[-1].split('\t', 1)[-1])
808
809 return '\n'.join(result)
810
811 def pretty_commit(commit_id = 'HEAD', diff_flags = []):
812 """Return a given commit (log + diff)
813 """
814 return GRun('diff-tree',
815 *(diff_flags
816 + ['--cc', '--always', '--pretty', '-r', commit_id])
817 ).raw_output()
818
819 def checkout(files = None, tree_id = None, force = False):
820 """Check out the given or all files
821 """
822 if tree_id:
823 try:
824 GRun('read-tree', '--reset', tree_id).run()
825 except GitRunException:
826 raise GitException, 'Failed "git read-tree" --reset %s' % tree_id
827
828 cmd = ['checkout-index', '-q', '-u']
829 if force:
830 cmd.append('-f')
831 if files:
832 GRun(*(cmd + ['--'])).xargs(files)
833 else:
834 GRun(*(cmd + ['-a'])).run()
835
836 def switch(tree_id, keep = False):
837 """Switch the tree to the given id
838 """
839 if keep:
840 # only update the index while keeping the local changes
841 GRun('read-tree', tree_id).run()
842 else:
843 refresh_index()
844 try:
845 GRun('read-tree', '-u', '-m', get_head(), tree_id).run()
846 except GitRunException:
847 raise GitException, 'read-tree failed (local changes maybe?)'
848
849 __set_head(tree_id)
850
851 def reset(files = None, tree_id = None, check_out = True):
852 """Revert the tree changes relative to the given tree_id. It removes
853 any local changes
854 """
855 if not tree_id:
856 tree_id = get_head()
857
858 if check_out:
859 cache_files = tree_status(files, tree_id)
860 # files which were added but need to be removed
861 rm_files = [x[1] for x in cache_files if x[0] in ['A']]
862
863 checkout(files, tree_id, True)
864 # checkout doesn't remove files
865 map(os.remove, rm_files)
866
867 # if the reset refers to the whole tree, switch the HEAD as well
868 if not files:
869 __set_head(tree_id)
870
871 def fetch(repository = 'origin', refspec = None):
872 """Fetches changes from the remote repository, using 'git fetch'
873 by default.
874 """
875 # we update the HEAD
876 __clear_head_cache()
877
878 args = [repository]
879 if refspec:
880 args.append(refspec)
881
882 command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
883 config.get('stgit.fetchcmd')
884 Run(*(command.split() + args)).run()
885
886 def pull(repository = 'origin', refspec = None):
887 """Fetches changes from the remote repository, using 'git pull'
888 by default.
889 """
890 # we update the HEAD
891 __clear_head_cache()
892
893 args = [repository]
894 if refspec:
895 args.append(refspec)
896
897 command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
898 config.get('stgit.pullcmd')
899 Run(*(command.split() + args)).run()
900
901 def rebase(tree_id = None):
902 """Rebase the current tree to the give tree_id. The tree_id
903 argument may be something other than a GIT id if an external
904 command is invoked.
905 """
906 command = config.get('branch.%s.stgit.rebasecmd' % get_head_file()) \
907 or config.get('stgit.rebasecmd')
908 if tree_id:
909 args = [tree_id]
910 elif command:
911 args = []
912 else:
913 raise GitException, 'Default rebasing requires a commit id'
914 if command:
915 # clear the HEAD cache as the custom rebase command will update it
916 __clear_head_cache()
917 Run(*(command.split() + args)).run()
918 else:
919 # default rebasing
920 reset(tree_id = tree_id)
921
922 def repack():
923 """Repack all objects into a single pack
924 """
925 GRun('repack', '-a', '-d', '-f').run()
926
927 def apply_patch(filename = None, diff = None, base = None,
928 fail_dump = True):
929 """Apply a patch onto the current or given index. There must not
930 be any local changes in the tree, otherwise the command fails
931 """
932 if diff is None:
933 if filename:
934 f = file(filename)
935 else:
936 f = sys.stdin
937 diff = f.read()
938 if filename:
939 f.close()
940
941 if base:
942 orig_head = get_head()
943 switch(base)
944 else:
945 refresh_index()
946
947 try:
948 GRun('apply', '--index').raw_input(diff).no_output()
949 except GitRunException:
950 if base:
951 switch(orig_head)
952 if fail_dump:
953 # write the failed diff to a file
954 f = file('.stgit-failed.patch', 'w+')
955 f.write(diff)
956 f.close()
957 out.warn('Diff written to the .stgit-failed.patch file')
958
959 raise
960
961 if base:
962 top = commit(message = 'temporary commit used for applying a patch',
963 parents = [base])
964 switch(orig_head)
965 merge(base, orig_head, top)
966
967 def clone(repository, local_dir):
968 """Clone a remote repository. At the moment, just use the
969 'git clone' script
970 """
971 GRun('clone', repository, local_dir).run()
972
973 def modifying_revs(files, base_rev, head_rev):
974 """Return the revisions from the list modifying the given files."""
975 return GRun('rev-list', '%s..%s' % (base_rev, head_rev), '--', *files
976 ).output_lines()
977
978 def refspec_localpart(refspec):
979 m = re.match('^[^:]*:([^:]*)$', refspec)
980 if m:
981 return m.group(1)
982 else:
983 raise GitException, 'Cannot parse refspec "%s"' % line
984
985 def refspec_remotepart(refspec):
986 m = re.match('^([^:]*):[^:]*$', refspec)
987 if m:
988 return m.group(1)
989 else:
990 raise GitException, 'Cannot parse refspec "%s"' % line
991
992
993 def __remotes_from_config():
994 return config.sections_matching(r'remote\.(.*)\.url')
995
996 def __remotes_from_dir(dir):
997 d = os.path.join(basedir.get(), dir)
998 if os.path.exists(d):
999 return os.listdir(d)
1000 else:
1001 return []
1002
1003 def remotes_list():
1004 """Return the list of remotes in the repository
1005 """
1006 return (set(__remotes_from_config())
1007 | set(__remotes_from_dir('remotes'))
1008 | set(__remotes_from_dir('branches')))
1009
1010 def remotes_local_branches(remote):
1011 """Returns the list of local branches fetched from given remote
1012 """
1013
1014 branches = []
1015 if remote in __remotes_from_config():
1016 for line in config.getall('remote.%s.fetch' % remote):
1017 branches.append(refspec_localpart(line))
1018 elif remote in __remotes_from_dir('remotes'):
1019 stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
1020 for line in stream:
1021 # Only consider Pull lines
1022 m = re.match('^Pull: (.*)\n$', line)
1023 if m:
1024 branches.append(refspec_localpart(m.group(1)))
1025 stream.close()
1026 elif remote in __remotes_from_dir('branches'):
1027 # old-style branches only declare one branch
1028 branches.append('refs/heads/'+remote);
1029 else:
1030 raise GitException, 'Unknown remote "%s"' % remote
1031
1032 return branches
1033
1034 def identify_remote(branchname):
1035 """Return the name for the remote to pull the given branchname
1036 from, or None if we believe it is a local branch.
1037 """
1038
1039 for remote in remotes_list():
1040 if branchname in remotes_local_branches(remote):
1041 return remote
1042
1043 # if we get here we've found nothing, the branch is a local one
1044 return None
1045
1046 def fetch_head():
1047 """Return the git id for the tip of the parent branch as left by
1048 'git fetch'.
1049 """
1050
1051 fetch_head=None
1052 stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
1053 for line in stream:
1054 # Only consider lines not tagged not-for-merge
1055 m = re.match('^([^\t]*)\t\t', line)
1056 if m:
1057 if fetch_head:
1058 raise GitException, 'StGit does not support multiple FETCH_HEAD'
1059 else:
1060 fetch_head=m.group(1)
1061 stream.close()
1062
1063 if not fetch_head:
1064 out.warn('No for-merge remote head found in FETCH_HEAD')
1065
1066 # here we are sure to have a single fetch_head
1067 return fetch_head
1068
1069 def all_refs():
1070 """Return a list of all refs in the current repository.
1071 """
1072
1073 return [line.split()[1] for line in GRun('show-ref').output_lines()]