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