chiark / gitweb /
Merge branch 'stable' into stable-master-merge
[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 # Classes
48 #
49
50 class Person:
51     """An author, committer, etc."""
52     def __init__(self, name = None, email = None, date = '',
53                  desc = None):
54         self.name = self.email = self.date = None
55         if name or email or date:
56             assert not desc
57             self.name = name
58             self.email = email
59             self.date = date
60         elif desc:
61             assert not (name or email or date)
62             def parse_desc(s):
63                 m = re.match(r'^(.+)<(.+)>(.*)$', s)
64                 assert m
65                 return [x.strip() or None for x in m.groups()]
66             self.name, self.email, self.date = parse_desc(desc)
67     def set_name(self, val):
68         if val:
69             self.name = val
70     def set_email(self, val):
71         if val:
72             self.email = val
73     def set_date(self, val):
74         if val:
75             self.date = val
76     def __str__(self):
77         if self.name and self.email:
78             return '%s <%s>' % (self.name, self.email)
79         else:
80             raise GitException, 'not enough identity data'
81
82 class Commit:
83     """Handle the commit objects
84     """
85     def __init__(self, id_hash):
86         self.__id_hash = id_hash
87
88         lines = GRun('cat-file', 'commit', id_hash).output_lines()
89         for i in range(len(lines)):
90             line = lines[i]
91             if not line:
92                 break # we've seen all the header fields
93             key, val = line.split(' ', 1)
94             if key == 'tree':
95                 self.__tree = val
96             elif key == 'author':
97                 self.__author = val
98             elif key == 'committer':
99                 self.__committer = val
100             else:
101                 pass # ignore other headers
102         self.__log = '\n'.join(lines[i+1:])
103
104     def get_id_hash(self):
105         return self.__id_hash
106
107     def get_tree(self):
108         return self.__tree
109
110     def get_parent(self):
111         parents = self.get_parents()
112         if parents:
113             return parents[0]
114         else:
115             return None
116
117     def get_parents(self):
118         return GRun('rev-list', '--parents', '--max-count=1', self.__id_hash
119                     ).output_one_line().split()[1:]
120
121     def get_author(self):
122         return self.__author
123
124     def get_committer(self):
125         return self.__committer
126
127     def get_log(self):
128         return self.__log
129
130     def __str__(self):
131         return self.get_id_hash()
132
133 # dictionary of Commit objects, used to avoid multiple calls to git
134 __commits = dict()
135
136 #
137 # Functions
138 #
139
140 def get_commit(id_hash):
141     """Commit objects factory. Save/look-up them in the __commits
142     dictionary
143     """
144     global __commits
145
146     if id_hash in __commits:
147         return __commits[id_hash]
148     else:
149         commit = Commit(id_hash)
150         __commits[id_hash] = commit
151         return commit
152
153 def get_conflicts():
154     """Return the list of file conflicts
155     """
156     names = set()
157     for line in GRun('ls-files', '-z', '--unmerged'
158                      ).raw_output().split('\0')[:-1]:
159         stat, path = line.split('\t', 1)
160         names.add(path)
161     return list(names)
162
163 def exclude_files():
164     files = [os.path.join(basedir.get(), 'info', 'exclude')]
165     user_exclude = config.get('core.excludesfile')
166     if user_exclude:
167         files.append(user_exclude)
168     return files
169
170 def ls_files(files, tree = 'HEAD', full_name = True):
171     """Return the files known to GIT or raise an error otherwise. It also
172     converts the file to the full path relative the the .git directory.
173     """
174     if not files:
175         return []
176
177     args = []
178     if tree:
179         args.append('--with-tree=%s' % tree)
180     if full_name:
181         args.append('--full-name')
182     args.append('--')
183     args.extend(files)
184     try:
185         # use a set to avoid file names duplication due to different stages
186         fileset = set(GRun('ls-files', '--error-unmatch', *args).output_lines())
187     except GitRunException:
188         # just hide the details of the 'git ls-files' command we use
189         raise GitException, \
190             'Some of the given paths are either missing or not known to GIT'
191     return list(fileset)
192
193 def parse_git_ls(output):
194     t = None
195     for line in output.split('\0'):
196         if not line:
197             # There's a zero byte at the end of the output, which
198             # gives us an empty string as the last "line".
199             continue
200         if t == None:
201             mode_a, mode_b, sha1_a, sha1_b, t = line.split(' ')
202         else:
203             yield (t, line)
204             t = None
205
206 def tree_status(files = None, tree_id = 'HEAD', unknown = False,
207                   noexclude = True, verbose = False, diff_flags = []):
208     """Get the status of all changed files, or of a selected set of
209     files. Returns a list of pairs - (status, filename).
210
211     If 'not files', it will check all files, and optionally all
212     unknown files.  If 'files' is a list, it will only check the files
213     in the list.
214     """
215     assert not files or not unknown
216
217     if verbose:
218         out.start('Checking for changes in the working directory')
219
220     refresh_index()
221
222     if files is None:
223         files = []
224     cache_files = []
225
226     # unknown files
227     if unknown:
228         cmd = ['ls-files', '-z', '--others', '--directory',
229                '--no-empty-directory']
230         if not noexclude:
231             cmd += ['--exclude=%s' % s for s in
232                     ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
233             cmd += ['--exclude-per-directory=.gitignore']
234             cmd += ['--exclude-from=%s' % fn
235                     for fn in exclude_files()
236                     if os.path.exists(fn)]
237
238         lines = GRun(*cmd).raw_output().split('\0')
239         cache_files += [('?', line) for line in lines if line]
240
241     # conflicted files
242     conflicts = get_conflicts()
243     cache_files += [('C', filename) for filename in conflicts
244                     if not files or filename in files]
245     reported_files = set(conflicts)
246     files_left = [f for f in files if f not in reported_files]
247
248     # files in the index. Only execute this code if no files were
249     # specified when calling the function (i.e. report all files) or
250     # files were specified but already found in the previous step
251     if not files or files_left:
252         args = diff_flags + [tree_id]
253         if files_left:
254             args += ['--'] + files_left
255         for t, fn in parse_git_ls(GRun('diff-index', '-z', *args).raw_output()):
256             # the condition is needed in case files is emtpy and
257             # diff-index lists those already reported
258             if not fn in reported_files:
259                 cache_files.append((t, fn))
260                 reported_files.add(fn)
261         files_left = [f for f in files if f not in reported_files]
262
263     # files in the index but changed on (or removed from) disk. Only
264     # execute this code if no files were specified when calling the
265     # function (i.e. report all files) or files were specified but
266     # already found in the previous step
267     if not files or files_left:
268         args = list(diff_flags)
269         if files_left:
270             args += ['--'] + files_left
271         for t, fn in parse_git_ls(GRun('diff-files', '-z', *args).raw_output()):
272             # the condition is needed in case files is empty and
273             # diff-files lists those already reported
274             if not fn in reported_files:
275                 cache_files.append((t, fn))
276                 reported_files.add(fn)
277
278     if verbose:
279         out.done()
280
281     return cache_files
282
283 def local_changes(verbose = True):
284     """Return true if there are local changes in the tree
285     """
286     return len(tree_status(verbose = verbose)) != 0
287
288 def get_heads():
289     heads = []
290     hr = re.compile(r'^[0-9a-f]{40} refs/heads/(.+)$')
291     for line in GRun('show-ref', '--heads').output_lines():
292         m = hr.match(line)
293         heads.append(m.group(1))
294     return heads
295
296 # HEAD value cached
297 __head = None
298
299 def get_head():
300     """Verifies the HEAD and returns the SHA1 id that represents it
301     """
302     global __head
303
304     if not __head:
305         __head = rev_parse('HEAD')
306     return __head
307
308 class DetachedHeadException(GitException):
309     def __init__(self):
310         GitException.__init__(self, 'Not on any branch')
311
312 def get_head_file():
313     """Return the name of the file pointed to by the HEAD symref.
314     Throw an exception if HEAD is detached."""
315     try:
316         return strip_prefix(
317             'refs/heads/', GRun('symbolic-ref', '-q', 'HEAD'
318                                 ).output_one_line())
319     except GitRunException:
320         raise DetachedHeadException()
321
322 def set_head_file(ref):
323     """Resets HEAD to point to a new ref
324     """
325     # head cache flushing is needed since we might have a different value
326     # in the new head
327     __clear_head_cache()
328     try:
329         GRun('symbolic-ref', 'HEAD', 'refs/heads/%s' % ref).run()
330     except GitRunException:
331         raise GitException, 'Could not set head to "%s"' % ref
332
333 def set_ref(ref, val):
334     """Point ref at a new commit object."""
335     try:
336         GRun('update-ref', ref, val).run()
337     except GitRunException:
338         raise GitException, 'Could not update %s to "%s".' % (ref, val)
339
340 def set_branch(branch, val):
341     set_ref('refs/heads/%s' % branch, val)
342
343 def __set_head(val):
344     """Sets the HEAD value
345     """
346     global __head
347
348     if not __head or __head != val:
349         set_ref('HEAD', val)
350         __head = val
351
352     # only allow SHA1 hashes
353     assert(len(__head) == 40)
354
355 def __clear_head_cache():
356     """Sets the __head to None so that a re-read is forced
357     """
358     global __head
359
360     __head = None
361
362 def refresh_index():
363     """Refresh index with stat() information from the working directory.
364     """
365     GRun('update-index', '-q', '--unmerged', '--refresh').run()
366
367 def rev_parse(git_id):
368     """Parse the string and return a verified SHA1 id
369     """
370     try:
371         return GRun('rev-parse', '--verify', git_id
372                     ).discard_stderr().output_one_line()
373     except GitRunException:
374         raise GitException, 'Unknown revision: %s' % git_id
375
376 def ref_exists(ref):
377     try:
378         rev_parse(ref)
379         return True
380     except GitException:
381         return False
382
383 def branch_exists(branch):
384     return ref_exists('refs/heads/%s' % branch)
385
386 def create_branch(new_branch, tree_id = None):
387     """Create a new branch in the git repository
388     """
389     if branch_exists(new_branch):
390         raise GitException, 'Branch "%s" already exists' % new_branch
391
392     current_head_file = get_head_file()
393     current_head = get_head()
394     set_head_file(new_branch)
395     __set_head(current_head)
396
397     # a checkout isn't needed if new branch points to the current head
398     if tree_id:
399         try:
400             switch(tree_id)
401         except GitException:
402             # Tree switching failed. Revert the head file
403             set_head_file(current_head_file)
404             delete_branch(new_branch)
405             raise
406
407     if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
408         os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
409
410 def switch_branch(new_branch):
411     """Switch to a git branch
412     """
413     global __head
414
415     if not branch_exists(new_branch):
416         raise GitException, 'Branch "%s" does not exist' % new_branch
417
418     tree_id = rev_parse('refs/heads/%s^{commit}' % new_branch)
419     if tree_id != get_head():
420         refresh_index()
421         try:
422             GRun('read-tree', '-u', '-m', get_head(), tree_id).run()
423         except GitRunException:
424             raise GitException, 'read-tree failed (local changes maybe?)'
425         __head = tree_id
426     set_head_file(new_branch)
427
428     if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
429         os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
430
431 def delete_ref(ref):
432     if not ref_exists(ref):
433         raise GitException, '%s does not exist' % ref
434     sha1 = GRun('show-ref', '-s', ref).output_one_line()
435     try:
436         GRun('update-ref', '-d', ref, sha1).run()
437     except GitRunException:
438         raise GitException, 'Failed to delete ref %s' % ref
439
440 def delete_branch(name):
441     delete_ref('refs/heads/%s' % name)
442
443 def rename_ref(from_ref, to_ref):
444     if not ref_exists(from_ref):
445         raise GitException, '"%s" does not exist' % from_ref
446     if ref_exists(to_ref):
447         raise GitException, '"%s" already exists' % to_ref
448
449     sha1 = GRun('show-ref', '-s', from_ref).output_one_line()
450     try:
451         GRun('update-ref', to_ref, sha1, '0'*40).run()
452     except GitRunException:
453         raise GitException, 'Failed to create new ref %s' % to_ref
454     try:
455         GRun('update-ref', '-d', from_ref, sha1).run()
456     except GitRunException:
457         raise GitException, 'Failed to delete ref %s' % from_ref
458
459 def rename_branch(from_name, to_name):
460     """Rename a git branch."""
461     rename_ref('refs/heads/%s' % from_name, 'refs/heads/%s' % to_name)
462     try:
463         if get_head_file() == from_name:
464             set_head_file(to_name)
465     except DetachedHeadException:
466         pass # detached HEAD, so the renamee can't be the current branch
467     reflog_dir = os.path.join(basedir.get(), 'logs', 'refs', 'heads')
468     if os.path.exists(reflog_dir) \
469            and os.path.exists(os.path.join(reflog_dir, from_name)):
470         rename(reflog_dir, from_name, to_name)
471
472 # Persons caching
473 __user = None
474 __author = None
475 __committer = None
476
477 def user():
478     """Return the user information.
479     """
480     global __user
481     if not __user:
482         name=config.get('user.name')
483         email=config.get('user.email')
484         __user = Person(name, email)
485     return __user;
486
487 def author():
488     """Return the author information.
489     """
490     global __author
491     if not __author:
492         try:
493             # the environment variables take priority over config
494             try:
495                 date = os.environ['GIT_AUTHOR_DATE']
496             except KeyError:
497                 date = ''
498             __author = Person(os.environ['GIT_AUTHOR_NAME'],
499                               os.environ['GIT_AUTHOR_EMAIL'],
500                               date)
501         except KeyError:
502             __author = user()
503     return __author
504
505 def committer():
506     """Return the author information.
507     """
508     global __committer
509     if not __committer:
510         try:
511             # the environment variables take priority over config
512             try:
513                 date = os.environ['GIT_COMMITTER_DATE']
514             except KeyError:
515                 date = ''
516             __committer = Person(os.environ['GIT_COMMITTER_NAME'],
517                                  os.environ['GIT_COMMITTER_EMAIL'],
518                                  date)
519         except KeyError:
520             __committer = user()
521     return __committer
522
523 def update_cache(files = None, force = False):
524     """Update the cache information for the given files
525     """
526     cache_files = tree_status(files, verbose = False)
527
528     # everything is up-to-date
529     if len(cache_files) == 0:
530         return False
531
532     # check for unresolved conflicts
533     if not force and [x for x in cache_files
534                       if x[0] not in ['M', 'N', 'A', 'D']]:
535         raise GitException, 'Updating cache failed: unresolved conflicts'
536
537     # update the cache
538     add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
539     rm_files =  [x[1] for x in cache_files if x[0] in ['D']]
540     m_files =   [x[1] for x in cache_files if x[0] in ['M']]
541
542     GRun('update-index', '--add', '--').xargs(add_files)
543     GRun('update-index', '--force-remove', '--').xargs(rm_files)
544     GRun('update-index', '--').xargs(m_files)
545
546     return True
547
548 def commit(message, files = None, parents = None, allowempty = False,
549            cache_update = True, tree_id = None, set_head = False,
550            author_name = None, author_email = None, author_date = None,
551            committer_name = None, committer_email = None):
552     """Commit the current tree to repository
553     """
554     if not parents:
555         parents = []
556
557     # Get the tree status
558     if cache_update and parents != []:
559         changes = update_cache(files)
560         if not changes and not allowempty:
561             raise GitException, 'No changes to commit'
562
563     # get the commit message
564     if not message:
565         message = '\n'
566     elif message[-1:] != '\n':
567         message += '\n'
568
569     # write the index to repository
570     if tree_id == None:
571         tree_id = GRun('write-tree').output_one_line()
572         set_head = True
573
574     # the commit
575     env = {}
576     if author_name:
577         env['GIT_AUTHOR_NAME'] = author_name
578     if author_email:
579         env['GIT_AUTHOR_EMAIL'] = author_email
580     if author_date:
581         env['GIT_AUTHOR_DATE'] = author_date
582     if committer_name:
583         env['GIT_COMMITTER_NAME'] = committer_name
584     if committer_email:
585         env['GIT_COMMITTER_EMAIL'] = committer_email
586     commit_id = GRun('commit-tree', tree_id,
587                      *sum([['-p', p] for p in parents], [])
588                      ).env(env).raw_input(message).output_one_line()
589     if set_head:
590         __set_head(commit_id)
591
592     return commit_id
593
594 def apply_diff(rev1, rev2, check_index = True, files = None):
595     """Apply the diff between rev1 and rev2 onto the current
596     index. This function doesn't need to raise an exception since it
597     is only used for fast-pushing a patch. If this operation fails,
598     the pushing would fall back to the three-way merge.
599     """
600     if check_index:
601         index_opt = ['--index']
602     else:
603         index_opt = []
604
605     if not files:
606         files = []
607
608     diff_str = diff(files, rev1, rev2)
609     if diff_str:
610         try:
611             GRun('apply', *index_opt).raw_input(
612                 diff_str).discard_stderr().no_output()
613         except GitRunException:
614             return False
615
616     return True
617
618 stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
619
620 def merge_recursive(base, head1, head2):
621     """Perform a 3-way merge between base, head1 and head2 into the
622     local tree
623     """
624     refresh_index()
625     p = GRun('merge-recursive', base, '--', head1, head2).env(
626         { 'GITHEAD_%s' % base: 'ancestor',
627           'GITHEAD_%s' % head1: 'current',
628           'GITHEAD_%s' % head2: 'patched'}).returns([0, 1])
629     output = p.output_lines()
630     if p.exitcode:
631         # There were conflicts
632         conflicts = [l.strip() for l in output if l.startswith('CONFLICT')]
633         out.info(*conflicts)
634
635         # try the interactive merge or stage checkout (if enabled)
636         for filename in get_conflicts():
637             if (gitmergeonefile.merge(filename)):
638                 # interactive merge succeeded
639                 resolved([filename])
640
641         # any conflicts left unsolved?
642         cn = len(get_conflicts())
643         if cn:
644             raise GitException, "%d conflict(s)" % cn
645
646 def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = [],
647          binary = True):
648     """Show the diff between rev1 and rev2
649     """
650     if not files:
651         files = []
652     if binary and '--binary' not in diff_flags:
653         diff_flags = diff_flags + ['--binary']
654
655     if rev1 and rev2:
656         return GRun('diff-tree', '-p',
657                     *(diff_flags + [rev1, rev2, '--'] + files)).raw_output()
658     elif rev1 or rev2:
659         refresh_index()
660         if rev2:
661             return GRun('diff-index', '-p', '-R',
662                         *(diff_flags + [rev2, '--'] + files)).raw_output()
663         else:
664             return GRun('diff-index', '-p',
665                         *(diff_flags + [rev1, '--'] + files)).raw_output()
666     else:
667         return ''
668
669 def diffstat(diff):
670     """Return the diffstat of the supplied diff."""
671     return GRun('apply', '--stat', '--summary').raw_input(diff).raw_output()
672
673 def files(rev1, rev2, diff_flags = []):
674     """Return the files modified between rev1 and rev2
675     """
676
677     result = []
678     for line in GRun('diff-tree', *(diff_flags + ['-r', rev1, rev2])
679                      ).output_lines():
680         result.append('%s %s' % tuple(line.split(' ', 4)[-1].split('\t', 1)))
681
682     return '\n'.join(result)
683
684 def barefiles(rev1, rev2):
685     """Return the files modified between rev1 and rev2, without status info
686     """
687
688     result = []
689     for line in GRun('diff-tree', '-r', rev1, rev2).output_lines():
690         result.append(line.split(' ', 4)[-1].split('\t', 1)[-1])
691
692     return '\n'.join(result)
693
694 def pretty_commit(commit_id = 'HEAD', flags = []):
695     """Return a given commit (log + diff)
696     """
697     return GRun('show', *(flags + [commit_id])).raw_output()
698
699 def checkout(files = None, tree_id = None, force = False):
700     """Check out the given or all files
701     """
702     if tree_id:
703         try:
704             GRun('read-tree', '--reset', tree_id).run()
705         except GitRunException:
706             raise GitException, 'Failed "git read-tree" --reset %s' % tree_id
707
708     cmd = ['checkout-index', '-q', '-u']
709     if force:
710         cmd.append('-f')
711     if files:
712         GRun(*(cmd + ['--'])).xargs(files)
713     else:
714         GRun(*(cmd + ['-a'])).run()
715
716 def switch(tree_id, keep = False):
717     """Switch the tree to the given id
718     """
719     if keep:
720         # only update the index while keeping the local changes
721         GRun('read-tree', tree_id).run()
722     else:
723         refresh_index()
724         try:
725             GRun('read-tree', '-u', '-m', get_head(), tree_id).run()
726         except GitRunException:
727             raise GitException, 'read-tree failed (local changes maybe?)'
728
729     __set_head(tree_id)
730
731 def reset(files = None, tree_id = None, check_out = True):
732     """Revert the tree changes relative to the given tree_id. It removes
733     any local changes
734     """
735     if not tree_id:
736         tree_id = get_head()
737
738     if check_out:
739         cache_files = tree_status(files, tree_id)
740         # files which were added but need to be removed
741         rm_files =  [x[1] for x in cache_files if x[0] in ['A']]
742
743         checkout(files, tree_id, True)
744         # checkout doesn't remove files
745         map(os.remove, rm_files)
746
747     # if the reset refers to the whole tree, switch the HEAD as well
748     if not files:
749         __set_head(tree_id)
750
751 def resolved(filenames, reset = None):
752     if reset:
753         stage = {'ancestor': 1, 'current': 2, 'patched': 3}[reset]
754         GRun('checkout-index', '--no-create', '--stage=%d' % stage,
755              '--stdin', '-z').input_nulterm(filenames).no_output()
756     GRun('update-index', '--add', '--').xargs(filenames)
757     for filename in filenames:
758         gitmergeonefile.clean_up(filename)
759         # update the access and modificatied times
760         os.utime(filename, None)
761
762 def fetch(repository = 'origin', refspec = None):
763     """Fetches changes from the remote repository, using 'git fetch'
764     by default.
765     """
766     # we update the HEAD
767     __clear_head_cache()
768
769     args = [repository]
770     if refspec:
771         args.append(refspec)
772
773     command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
774               config.get('stgit.fetchcmd')
775     Run(*(command.split() + args)).run()
776
777 def pull(repository = 'origin', refspec = None):
778     """Fetches changes from the remote repository, using 'git pull'
779     by default.
780     """
781     # we update the HEAD
782     __clear_head_cache()
783
784     args = [repository]
785     if refspec:
786         args.append(refspec)
787
788     command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
789               config.get('stgit.pullcmd')
790     Run(*(command.split() + args)).run()
791
792 def rebase(tree_id = None):
793     """Rebase the current tree to the give tree_id. The tree_id
794     argument may be something other than a GIT id if an external
795     command is invoked.
796     """
797     command = config.get('branch.%s.stgit.rebasecmd' % get_head_file()) \
798                 or config.get('stgit.rebasecmd')
799     if tree_id:
800         args = [tree_id]
801     elif command:
802         args = []
803     else:
804         raise GitException, 'Default rebasing requires a commit id'
805     if command:
806         # clear the HEAD cache as the custom rebase command will update it
807         __clear_head_cache()
808         Run(*(command.split() + args)).run()
809     else:
810         # default rebasing
811         reset(tree_id = tree_id)
812
813 def repack():
814     """Repack all objects into a single pack
815     """
816     GRun('repack', '-a', '-d', '-f').run()
817
818 def apply_patch(filename = None, diff = None, base = None,
819                 fail_dump = True):
820     """Apply a patch onto the current or given index. There must not
821     be any local changes in the tree, otherwise the command fails
822     """
823     if diff is None:
824         if filename:
825             f = file(filename)
826         else:
827             f = sys.stdin
828         diff = f.read()
829         if filename:
830             f.close()
831
832     if base:
833         orig_head = get_head()
834         switch(base)
835     else:
836         refresh_index()
837
838     try:
839         GRun('apply', '--index').raw_input(diff).no_output()
840     except GitRunException:
841         if base:
842             switch(orig_head)
843         if fail_dump:
844             # write the failed diff to a file
845             f = file('.stgit-failed.patch', 'w+')
846             f.write(diff)
847             f.close()
848             out.warn('Diff written to the .stgit-failed.patch file')
849
850         raise
851
852     if base:
853         top = commit(message = 'temporary commit used for applying a patch',
854                      parents = [base])
855         switch(orig_head)
856         merge_recursive(base, orig_head, top)
857
858 def clone(repository, local_dir):
859     """Clone a remote repository. At the moment, just use the
860     'git clone' script
861     """
862     GRun('clone', repository, local_dir).run()
863
864 def modifying_revs(files, base_rev, head_rev):
865     """Return the revisions from the list modifying the given files."""
866     return GRun('rev-list', '%s..%s' % (base_rev, head_rev), '--', *files
867                 ).output_lines()
868
869 def refspec_localpart(refspec):
870     m = re.match('^[^:]*:([^:]*)$', refspec)
871     if m:
872         return m.group(1)
873     else:
874         raise GitException, 'Cannot parse refspec "%s"' % line
875
876 def refspec_remotepart(refspec):
877     m = re.match('^([^:]*):[^:]*$', refspec)
878     if m:
879         return m.group(1)
880     else:
881         raise GitException, 'Cannot parse refspec "%s"' % line
882
883 def __remotes_from_config():
884     return config.sections_matching(r'remote\.(.*)\.url')
885
886 def __remotes_from_dir(dir):
887     d = os.path.join(basedir.get(), dir)
888     if os.path.exists(d):
889         return os.listdir(d)
890     else:
891         return []
892
893 def remotes_list():
894     """Return the list of remotes in the repository
895     """
896     return (set(__remotes_from_config())
897             | set(__remotes_from_dir('remotes'))
898             | set(__remotes_from_dir('branches')))
899
900 def remotes_local_branches(remote):
901     """Returns the list of local branches fetched from given remote
902     """
903
904     branches = []
905     if remote in __remotes_from_config():
906         for line in config.getall('remote.%s.fetch' % remote):
907             branches.append(refspec_localpart(line))
908     elif remote in __remotes_from_dir('remotes'):
909         stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
910         for line in stream:
911             # Only consider Pull lines
912             m = re.match('^Pull: (.*)\n$', line)
913             if m:
914                 branches.append(refspec_localpart(m.group(1)))
915         stream.close()
916     elif remote in __remotes_from_dir('branches'):
917         # old-style branches only declare one branch
918         branches.append('refs/heads/'+remote);
919     else:
920         raise GitException, 'Unknown remote "%s"' % remote
921
922     return branches
923
924 def identify_remote(branchname):
925     """Return the name for the remote to pull the given branchname
926     from, or None if we believe it is a local branch.
927     """
928
929     for remote in remotes_list():
930         if branchname in remotes_local_branches(remote):
931             return remote
932
933     # if we get here we've found nothing, the branch is a local one
934     return None
935
936 def fetch_head():
937     """Return the git id for the tip of the parent branch as left by
938     'git fetch'.
939     """
940
941     fetch_head=None
942     stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
943     for line in stream:
944         # Only consider lines not tagged not-for-merge
945         m = re.match('^([^\t]*)\t\t', line)
946         if m:
947             if fetch_head:
948                 raise GitException, 'StGit does not support multiple FETCH_HEAD'
949             else:
950                 fetch_head=m.group(1)
951     stream.close()
952
953     if not fetch_head:
954         out.warn('No for-merge remote head found in FETCH_HEAD')
955
956     # here we are sure to have a single fetch_head
957     return fetch_head
958
959 def all_refs():
960     """Return a list of all refs in the current repository.
961     """
962
963     return [line.split()[1] for line in GRun('show-ref').output_lines()]