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