chiark / gitweb /
85cceb0f1f3137bdb80800272a446000bdc96332
[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 # TODO: take another parameter representing a diff string as we
644 # usually invoke git.diff() form the calling functions
645 def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
646     """Return the diffstat between rev1 and rev2."""
647     return GRun('apply', '--stat', '--summary'
648                 ).raw_input(diff(files, rev1, rev2)).raw_output()
649
650 def files(rev1, rev2, diff_flags = []):
651     """Return the files modified between rev1 and rev2
652     """
653
654     result = []
655     for line in GRun('diff-tree', *(diff_flags + ['-r', rev1, rev2])
656                      ).output_lines():
657         result.append('%s %s' % tuple(line.split(' ', 4)[-1].split('\t', 1)))
658
659     return '\n'.join(result)
660
661 def barefiles(rev1, rev2):
662     """Return the files modified between rev1 and rev2, without status info
663     """
664
665     result = []
666     for line in GRun('diff-tree', '-r', rev1, rev2).output_lines():
667         result.append(line.split(' ', 4)[-1].split('\t', 1)[-1])
668
669     return '\n'.join(result)
670
671 def pretty_commit(commit_id = 'HEAD', flags = []):
672     """Return a given commit (log + diff)
673     """
674     return GRun('show', *(flags + [commit_id])).raw_output()
675
676 def checkout(files = None, tree_id = None, force = False):
677     """Check out the given or all files
678     """
679     if tree_id:
680         try:
681             GRun('read-tree', '--reset', tree_id).run()
682         except GitRunException:
683             raise GitException, 'Failed "git read-tree" --reset %s' % tree_id
684
685     cmd = ['checkout-index', '-q', '-u']
686     if force:
687         cmd.append('-f')
688     if files:
689         GRun(*(cmd + ['--'])).xargs(files)
690     else:
691         GRun(*(cmd + ['-a'])).run()
692
693 def switch(tree_id, keep = False):
694     """Switch the tree to the given id
695     """
696     if keep:
697         # only update the index while keeping the local changes
698         GRun('read-tree', tree_id).run()
699     else:
700         refresh_index()
701         try:
702             GRun('read-tree', '-u', '-m', get_head(), tree_id).run()
703         except GitRunException:
704             raise GitException, 'read-tree failed (local changes maybe?)'
705
706     __set_head(tree_id)
707
708 def reset(files = None, tree_id = None, check_out = True):
709     """Revert the tree changes relative to the given tree_id. It removes
710     any local changes
711     """
712     if not tree_id:
713         tree_id = get_head()
714
715     if check_out:
716         cache_files = tree_status(files, tree_id)
717         # files which were added but need to be removed
718         rm_files =  [x[1] for x in cache_files if x[0] in ['A']]
719
720         checkout(files, tree_id, True)
721         # checkout doesn't remove files
722         map(os.remove, rm_files)
723
724     # if the reset refers to the whole tree, switch the HEAD as well
725     if not files:
726         __set_head(tree_id)
727
728 def resolved(filenames, reset = None):
729     if reset:
730         stage = {'ancestor': 1, 'current': 2, 'patched': 3}[reset]
731         GRun('checkout-index', '--no-create', '--stage=%d' % stage,
732              '--stdin', '-z').input_nulterm(filenames).no_output()
733     GRun('update-index', '--add', '--').xargs(filenames)
734     for filename in filenames:
735         gitmergeonefile.clean_up(filename)
736         # update the access and modificatied times
737         os.utime(filename, None)
738
739 def fetch(repository = 'origin', refspec = None):
740     """Fetches changes from the remote repository, using 'git fetch'
741     by default.
742     """
743     # we update the HEAD
744     __clear_head_cache()
745
746     args = [repository]
747     if refspec:
748         args.append(refspec)
749
750     command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
751               config.get('stgit.fetchcmd')
752     Run(*(command.split() + args)).run()
753
754 def pull(repository = 'origin', refspec = None):
755     """Fetches changes from the remote repository, using 'git pull'
756     by default.
757     """
758     # we update the HEAD
759     __clear_head_cache()
760
761     args = [repository]
762     if refspec:
763         args.append(refspec)
764
765     command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
766               config.get('stgit.pullcmd')
767     Run(*(command.split() + args)).run()
768
769 def rebase(tree_id = None):
770     """Rebase the current tree to the give tree_id. The tree_id
771     argument may be something other than a GIT id if an external
772     command is invoked.
773     """
774     command = config.get('branch.%s.stgit.rebasecmd' % get_head_file()) \
775                 or config.get('stgit.rebasecmd')
776     if tree_id:
777         args = [tree_id]
778     elif command:
779         args = []
780     else:
781         raise GitException, 'Default rebasing requires a commit id'
782     if command:
783         # clear the HEAD cache as the custom rebase command will update it
784         __clear_head_cache()
785         Run(*(command.split() + args)).run()
786     else:
787         # default rebasing
788         reset(tree_id = tree_id)
789
790 def repack():
791     """Repack all objects into a single pack
792     """
793     GRun('repack', '-a', '-d', '-f').run()
794
795 def apply_patch(filename = None, diff = None, base = None,
796                 fail_dump = True):
797     """Apply a patch onto the current or given index. There must not
798     be any local changes in the tree, otherwise the command fails
799     """
800     if diff is None:
801         if filename:
802             f = file(filename)
803         else:
804             f = sys.stdin
805         diff = f.read()
806         if filename:
807             f.close()
808
809     if base:
810         orig_head = get_head()
811         switch(base)
812     else:
813         refresh_index()
814
815     try:
816         GRun('apply', '--index').raw_input(diff).no_output()
817     except GitRunException:
818         if base:
819             switch(orig_head)
820         if fail_dump:
821             # write the failed diff to a file
822             f = file('.stgit-failed.patch', 'w+')
823             f.write(diff)
824             f.close()
825             out.warn('Diff written to the .stgit-failed.patch file')
826
827         raise
828
829     if base:
830         top = commit(message = 'temporary commit used for applying a patch',
831                      parents = [base])
832         switch(orig_head)
833         merge_recursive(base, orig_head, top)
834
835 def clone(repository, local_dir):
836     """Clone a remote repository. At the moment, just use the
837     'git clone' script
838     """
839     GRun('clone', repository, local_dir).run()
840
841 def modifying_revs(files, base_rev, head_rev):
842     """Return the revisions from the list modifying the given files."""
843     return GRun('rev-list', '%s..%s' % (base_rev, head_rev), '--', *files
844                 ).output_lines()
845
846 def refspec_localpart(refspec):
847     m = re.match('^[^:]*:([^:]*)$', refspec)
848     if m:
849         return m.group(1)
850     else:
851         raise GitException, 'Cannot parse refspec "%s"' % line
852
853 def refspec_remotepart(refspec):
854     m = re.match('^([^:]*):[^:]*$', refspec)
855     if m:
856         return m.group(1)
857     else:
858         raise GitException, 'Cannot parse refspec "%s"' % line
859     
860
861 def __remotes_from_config():
862     return config.sections_matching(r'remote\.(.*)\.url')
863
864 def __remotes_from_dir(dir):
865     d = os.path.join(basedir.get(), dir)
866     if os.path.exists(d):
867         return os.listdir(d)
868     else:
869         return []
870
871 def remotes_list():
872     """Return the list of remotes in the repository
873     """
874     return (set(__remotes_from_config())
875             | set(__remotes_from_dir('remotes'))
876             | set(__remotes_from_dir('branches')))
877
878 def remotes_local_branches(remote):
879     """Returns the list of local branches fetched from given remote
880     """
881
882     branches = []
883     if remote in __remotes_from_config():
884         for line in config.getall('remote.%s.fetch' % remote):
885             branches.append(refspec_localpart(line))
886     elif remote in __remotes_from_dir('remotes'):
887         stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
888         for line in stream:
889             # Only consider Pull lines
890             m = re.match('^Pull: (.*)\n$', line)
891             if m:
892                 branches.append(refspec_localpart(m.group(1)))
893         stream.close()
894     elif remote in __remotes_from_dir('branches'):
895         # old-style branches only declare one branch
896         branches.append('refs/heads/'+remote);
897     else:
898         raise GitException, 'Unknown remote "%s"' % remote
899
900     return branches
901
902 def identify_remote(branchname):
903     """Return the name for the remote to pull the given branchname
904     from, or None if we believe it is a local branch.
905     """
906
907     for remote in remotes_list():
908         if branchname in remotes_local_branches(remote):
909             return remote
910
911     # if we get here we've found nothing, the branch is a local one
912     return None
913
914 def fetch_head():
915     """Return the git id for the tip of the parent branch as left by
916     'git fetch'.
917     """
918
919     fetch_head=None
920     stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
921     for line in stream:
922         # Only consider lines not tagged not-for-merge
923         m = re.match('^([^\t]*)\t\t', line)
924         if m:
925             if fetch_head:
926                 raise GitException, 'StGit does not support multiple FETCH_HEAD'
927             else:
928                 fetch_head=m.group(1)
929     stream.close()
930
931     if not fetch_head:
932         out.warn('No for-merge remote head found in FETCH_HEAD')
933
934     # here we are sure to have a single fetch_head
935     return fetch_head
936
937 def all_refs():
938     """Return a list of all refs in the current repository.
939     """
940
941     return [line.split()[1] for line in GRun('show-ref').output_lines()]