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