chiark / gitweb /
0cfcd42b6606c39621cc2c1a01197f9fe620aa0a
[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 = []
157     for line in GRun('ls-files', '-z', '--unmerged'
158                      ).raw_output().split('\0')[:-1]:
159         stat, path = line.split('\t', 1)
160         # Look for entries in stage 2 (could equally well use 3)
161         if stat.endswith(' 2'):
162             names.append(path)
163     return names
164
165 def exclude_files():
166     files = [os.path.join(basedir.get(), 'info', 'exclude')]
167     user_exclude = config.get('core.excludesfile')
168     if user_exclude:
169         files.append(user_exclude)
170     return files
171
172 def ls_files(files, tree = None, full_name = True):
173     """Return the files known to GIT or raise an error otherwise. It also
174     converts the file to the full path relative the the .git directory.
175     """
176     if not files:
177         return []
178
179     args = []
180     if tree:
181         args.append('--with-tree=%s' % tree)
182     if full_name:
183         args.append('--full-name')
184     args.append('--')
185     args.extend(files)
186     try:
187         return GRun('ls-files', '--error-unmatch', *args).output_lines()
188     except GitRunException:
189         # just hide the details of the 'git ls-files' command we use
190         raise GitException, \
191             'Some of the given paths are either missing or not known to GIT'
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 def add(names):
447     """Add the files or recursively add the directory contents
448     """
449     # generate the file list
450     files = []
451     for i in names:
452         if not os.path.exists(i):
453             raise GitException, 'Unknown file or directory: %s' % i
454
455         if os.path.isdir(i):
456             # recursive search. We only add files
457             for root, dirs, local_files in os.walk(i):
458                 for name in [os.path.join(root, f) for f in local_files]:
459                     if os.path.isfile(name):
460                         files.append(os.path.normpath(name))
461         elif os.path.isfile(i):
462             files.append(os.path.normpath(i))
463         else:
464             raise GitException, '%s is not a file or directory' % i
465
466     if files:
467         try:
468             GRun('update-index', '--add', '--').xargs(files)
469         except GitRunException:
470             raise GitException, 'Unable to add file'
471
472 def __copy_single(source, target, target2=''):
473     """Copy file or dir named 'source' to name target+target2"""
474
475     # "source" (file or dir) must match one or more git-controlled file
476     realfiles = GRun('ls-files', source).output_lines()
477     if len(realfiles) == 0:
478         raise GitException, '"%s" matches no git-controled files' % source
479
480     if os.path.isdir(source):
481         # physically copy the files, and record them to add them in one run
482         newfiles = []
483         re_string='^'+source+'/(.*)$'
484         prefix_regexp = re.compile(re_string)
485         for f in [f.strip() for f in realfiles]:
486             m = prefix_regexp.match(f)
487             if not m:
488                 raise Exception, '"%s" does not match "%s"' % (f, re_string)
489             newname = target+target2+'/'+m.group(1)
490             if not os.path.exists(os.path.dirname(newname)):
491                 os.makedirs(os.path.dirname(newname))
492             copyfile(f, newname)
493             newfiles.append(newname)
494
495         add(newfiles)
496     else: # files, symlinks, ...
497         newname = target+target2
498         copyfile(source, newname)
499         add([newname])
500
501
502 def copy(filespecs, target):
503     if os.path.isdir(target):
504         # target is a directory: copy each entry on the command line,
505         # with the same name, into the target
506         target = target.rstrip('/')
507         
508         # first, check that none of the children of the target
509         # matching the command line aleady exist
510         for filespec in filespecs:
511             entry = target+ '/' + os.path.basename(filespec.rstrip('/'))
512             if os.path.exists(entry):
513                 raise GitException, 'Target "%s" already exists' % entry
514         
515         for filespec in filespecs:
516             filespec = filespec.rstrip('/')
517             basename = '/' + os.path.basename(filespec)
518             __copy_single(filespec, target, basename)
519
520     elif os.path.exists(target):
521         raise GitException, 'Target "%s" exists but is not a directory' % target
522     elif len(filespecs) != 1:
523         raise GitException, 'Cannot copy more than one file to non-directory'
524
525     else:
526         # at this point: len(filespecs)==1 and target does not exist
527
528         # check target directory
529         targetdir = os.path.dirname(target)
530         if targetdir != '' and not os.path.isdir(targetdir):
531             raise GitException, 'Target directory "%s" does not exist' % targetdir
532
533         __copy_single(filespecs[0].rstrip('/'), target)
534         
535
536 def rm(files, force = False):
537     """Remove a file from the repository
538     """
539     if not force:
540         for f in files:
541             if os.path.exists(f):
542                 raise GitException, '%s exists. Remove it first' %f
543         if files:
544             GRun('update-index', '--remove', '--').xargs(files)
545     else:
546         if files:
547             GRun('update-index', '--force-remove', '--').xargs(files)
548
549 # Persons caching
550 __user = None
551 __author = None
552 __committer = None
553
554 def user():
555     """Return the user information.
556     """
557     global __user
558     if not __user:
559         name=config.get('user.name')
560         email=config.get('user.email')
561         __user = Person(name, email)
562     return __user;
563
564 def author():
565     """Return the author information.
566     """
567     global __author
568     if not __author:
569         try:
570             # the environment variables take priority over config
571             try:
572                 date = os.environ['GIT_AUTHOR_DATE']
573             except KeyError:
574                 date = ''
575             __author = Person(os.environ['GIT_AUTHOR_NAME'],
576                               os.environ['GIT_AUTHOR_EMAIL'],
577                               date)
578         except KeyError:
579             __author = user()
580     return __author
581
582 def committer():
583     """Return the author information.
584     """
585     global __committer
586     if not __committer:
587         try:
588             # the environment variables take priority over config
589             try:
590                 date = os.environ['GIT_COMMITTER_DATE']
591             except KeyError:
592                 date = ''
593             __committer = Person(os.environ['GIT_COMMITTER_NAME'],
594                                  os.environ['GIT_COMMITTER_EMAIL'],
595                                  date)
596         except KeyError:
597             __committer = user()
598     return __committer
599
600 def update_cache(files = None, force = False):
601     """Update the cache information for the given files
602     """
603     cache_files = tree_status(files, verbose = False)
604
605     # everything is up-to-date
606     if len(cache_files) == 0:
607         return False
608
609     # check for unresolved conflicts
610     if not force and [x for x in cache_files
611                       if x[0] not in ['M', 'N', 'A', 'D']]:
612         raise GitException, 'Updating cache failed: unresolved conflicts'
613
614     # update the cache
615     add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
616     rm_files =  [x[1] for x in cache_files if x[0] in ['D']]
617     m_files =   [x[1] for x in cache_files if x[0] in ['M']]
618
619     GRun('update-index', '--add', '--').xargs(add_files)
620     GRun('update-index', '--force-remove', '--').xargs(rm_files)
621     GRun('update-index', '--').xargs(m_files)
622
623     return True
624
625 def commit(message, files = None, parents = None, allowempty = False,
626            cache_update = True, tree_id = None, set_head = False,
627            author_name = None, author_email = None, author_date = None,
628            committer_name = None, committer_email = None):
629     """Commit the current tree to repository
630     """
631     if not parents:
632         parents = []
633
634     # Get the tree status
635     if cache_update and parents != []:
636         changes = update_cache(files)
637         if not changes and not allowempty:
638             raise GitException, 'No changes to commit'
639
640     # get the commit message
641     if not message:
642         message = '\n'
643     elif message[-1:] != '\n':
644         message += '\n'
645
646     # write the index to repository
647     if tree_id == None:
648         tree_id = GRun('write-tree').output_one_line()
649         set_head = True
650
651     # the commit
652     env = {}
653     if author_name:
654         env['GIT_AUTHOR_NAME'] = author_name
655     if author_email:
656         env['GIT_AUTHOR_EMAIL'] = author_email
657     if author_date:
658         env['GIT_AUTHOR_DATE'] = author_date
659     if committer_name:
660         env['GIT_COMMITTER_NAME'] = committer_name
661     if committer_email:
662         env['GIT_COMMITTER_EMAIL'] = committer_email
663     commit_id = GRun('commit-tree', tree_id,
664                      *sum([['-p', p] for p in parents], [])
665                      ).env(env).raw_input(message).output_one_line()
666     if set_head:
667         __set_head(commit_id)
668
669     return commit_id
670
671 def apply_diff(rev1, rev2, check_index = True, files = None):
672     """Apply the diff between rev1 and rev2 onto the current
673     index. This function doesn't need to raise an exception since it
674     is only used for fast-pushing a patch. If this operation fails,
675     the pushing would fall back to the three-way merge.
676     """
677     if check_index:
678         index_opt = ['--index']
679     else:
680         index_opt = []
681
682     if not files:
683         files = []
684
685     diff_str = diff(files, rev1, rev2)
686     if diff_str:
687         try:
688             GRun('apply', *index_opt).raw_input(
689                 diff_str).discard_stderr().no_output()
690         except GitRunException:
691             return False
692
693     return True
694
695 stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
696
697 def merge_recursive(base, head1, head2):
698     """Perform a 3-way merge between base, head1 and head2 into the
699     local tree
700     """
701     refresh_index()
702     p = GRun('merge-recursive', base, '--', head1, head2).env(
703         { 'GITHEAD_%s' % base: 'ancestor',
704           'GITHEAD_%s' % head1: 'current',
705           'GITHEAD_%s' % head2: 'patched'}).returns([0, 1])
706     output = p.output_lines()
707     if p.exitcode:
708         # There were conflicts
709         conflicts = [l.strip() for l in output if l.startswith('CONFLICT')]
710         out.info(*conflicts)
711
712         # try the interactive merge or stage checkout (if enabled)
713         for filename in get_conflicts():
714             if (gitmergeonefile.merge(filename)):
715                 # interactive merge succeeded
716                 resolved([filename])
717
718         # any conflicts left unsolved?
719         cn = len(get_conflicts())
720         if cn:
721             raise GitException, "%d conflict(s)" % cn
722
723 def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = [],
724          binary = True):
725     """Show the diff between rev1 and rev2
726     """
727     if not files:
728         files = []
729     if binary and '--binary' not in diff_flags:
730         diff_flags = diff_flags + ['--binary']
731
732     if rev1 and rev2:
733         return GRun('diff-tree', '-p',
734                     *(diff_flags + [rev1, rev2, '--'] + files)).raw_output()
735     elif rev1 or rev2:
736         refresh_index()
737         if rev2:
738             return GRun('diff-index', '-p', '-R',
739                         *(diff_flags + [rev2, '--'] + files)).raw_output()
740         else:
741             return GRun('diff-index', '-p',
742                         *(diff_flags + [rev1, '--'] + files)).raw_output()
743     else:
744         return ''
745
746 # TODO: take another parameter representing a diff string as we
747 # usually invoke git.diff() form the calling functions
748 def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
749     """Return the diffstat between rev1 and rev2."""
750     return GRun('apply', '--stat', '--summary'
751                 ).raw_input(diff(files, rev1, rev2)).raw_output()
752
753 def files(rev1, rev2, diff_flags = []):
754     """Return the files modified between rev1 and rev2
755     """
756
757     result = []
758     for line in GRun('diff-tree', *(diff_flags + ['-r', rev1, rev2])
759                      ).output_lines():
760         result.append('%s %s' % tuple(line.split(' ', 4)[-1].split('\t', 1)))
761
762     return '\n'.join(result)
763
764 def barefiles(rev1, rev2):
765     """Return the files modified between rev1 and rev2, without status info
766     """
767
768     result = []
769     for line in GRun('diff-tree', '-r', rev1, rev2).output_lines():
770         result.append(line.split(' ', 4)[-1].split('\t', 1)[-1])
771
772     return '\n'.join(result)
773
774 def pretty_commit(commit_id = 'HEAD', flags = []):
775     """Return a given commit (log + diff)
776     """
777     return GRun('show', *(flags + [commit_id])).raw_output()
778
779 def checkout(files = None, tree_id = None, force = False):
780     """Check out the given or all files
781     """
782     if tree_id:
783         try:
784             GRun('read-tree', '--reset', tree_id).run()
785         except GitRunException:
786             raise GitException, 'Failed "git read-tree" --reset %s' % tree_id
787
788     cmd = ['checkout-index', '-q', '-u']
789     if force:
790         cmd.append('-f')
791     if files:
792         GRun(*(cmd + ['--'])).xargs(files)
793     else:
794         GRun(*(cmd + ['-a'])).run()
795
796 def switch(tree_id, keep = False):
797     """Switch the tree to the given id
798     """
799     if keep:
800         # only update the index while keeping the local changes
801         GRun('read-tree', tree_id).run()
802     else:
803         refresh_index()
804         try:
805             GRun('read-tree', '-u', '-m', get_head(), tree_id).run()
806         except GitRunException:
807             raise GitException, 'read-tree failed (local changes maybe?)'
808
809     __set_head(tree_id)
810
811 def reset(files = None, tree_id = None, check_out = True):
812     """Revert the tree changes relative to the given tree_id. It removes
813     any local changes
814     """
815     if not tree_id:
816         tree_id = get_head()
817
818     if check_out:
819         cache_files = tree_status(files, tree_id)
820         # files which were added but need to be removed
821         rm_files =  [x[1] for x in cache_files if x[0] in ['A']]
822
823         checkout(files, tree_id, True)
824         # checkout doesn't remove files
825         map(os.remove, rm_files)
826
827     # if the reset refers to the whole tree, switch the HEAD as well
828     if not files:
829         __set_head(tree_id)
830
831 def resolved(filenames, reset = None):
832     if reset:
833         stage = {'ancestor': 1, 'current': 2, 'patched': 3}[reset]
834         GRun('checkout-index', '--no-create', '--stage=%d' % stage,
835              '--stdin', '-z').input_nulterm(filenames).no_output()
836     GRun('update-index', '--add', '--').xargs(filenames)
837     for filename in filenames:
838         gitmergeonefile.clean_up(filename)
839         # update the access and modificatied times
840         os.utime(filename, None)
841
842 def fetch(repository = 'origin', refspec = None):
843     """Fetches changes from the remote repository, using 'git fetch'
844     by default.
845     """
846     # we update the HEAD
847     __clear_head_cache()
848
849     args = [repository]
850     if refspec:
851         args.append(refspec)
852
853     command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
854               config.get('stgit.fetchcmd')
855     Run(*(command.split() + args)).run()
856
857 def pull(repository = 'origin', refspec = None):
858     """Fetches changes from the remote repository, using 'git pull'
859     by default.
860     """
861     # we update the HEAD
862     __clear_head_cache()
863
864     args = [repository]
865     if refspec:
866         args.append(refspec)
867
868     command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
869               config.get('stgit.pullcmd')
870     Run(*(command.split() + args)).run()
871
872 def rebase(tree_id = None):
873     """Rebase the current tree to the give tree_id. The tree_id
874     argument may be something other than a GIT id if an external
875     command is invoked.
876     """
877     command = config.get('branch.%s.stgit.rebasecmd' % get_head_file()) \
878                 or config.get('stgit.rebasecmd')
879     if tree_id:
880         args = [tree_id]
881     elif command:
882         args = []
883     else:
884         raise GitException, 'Default rebasing requires a commit id'
885     if command:
886         # clear the HEAD cache as the custom rebase command will update it
887         __clear_head_cache()
888         Run(*(command.split() + args)).run()
889     else:
890         # default rebasing
891         reset(tree_id = tree_id)
892
893 def repack():
894     """Repack all objects into a single pack
895     """
896     GRun('repack', '-a', '-d', '-f').run()
897
898 def apply_patch(filename = None, diff = None, base = None,
899                 fail_dump = True):
900     """Apply a patch onto the current or given index. There must not
901     be any local changes in the tree, otherwise the command fails
902     """
903     if diff is None:
904         if filename:
905             f = file(filename)
906         else:
907             f = sys.stdin
908         diff = f.read()
909         if filename:
910             f.close()
911
912     if base:
913         orig_head = get_head()
914         switch(base)
915     else:
916         refresh_index()
917
918     try:
919         GRun('apply', '--index').raw_input(diff).no_output()
920     except GitRunException:
921         if base:
922             switch(orig_head)
923         if fail_dump:
924             # write the failed diff to a file
925             f = file('.stgit-failed.patch', 'w+')
926             f.write(diff)
927             f.close()
928             out.warn('Diff written to the .stgit-failed.patch file')
929
930         raise
931
932     if base:
933         top = commit(message = 'temporary commit used for applying a patch',
934                      parents = [base])
935         switch(orig_head)
936         merge_recursive(base, orig_head, top)
937
938 def clone(repository, local_dir):
939     """Clone a remote repository. At the moment, just use the
940     'git clone' script
941     """
942     GRun('clone', repository, local_dir).run()
943
944 def modifying_revs(files, base_rev, head_rev):
945     """Return the revisions from the list modifying the given files."""
946     return GRun('rev-list', '%s..%s' % (base_rev, head_rev), '--', *files
947                 ).output_lines()
948
949 def refspec_localpart(refspec):
950     m = re.match('^[^:]*:([^:]*)$', refspec)
951     if m:
952         return m.group(1)
953     else:
954         raise GitException, 'Cannot parse refspec "%s"' % line
955
956 def refspec_remotepart(refspec):
957     m = re.match('^([^:]*):[^:]*$', refspec)
958     if m:
959         return m.group(1)
960     else:
961         raise GitException, 'Cannot parse refspec "%s"' % line
962     
963
964 def __remotes_from_config():
965     return config.sections_matching(r'remote\.(.*)\.url')
966
967 def __remotes_from_dir(dir):
968     d = os.path.join(basedir.get(), dir)
969     if os.path.exists(d):
970         return os.listdir(d)
971     else:
972         return []
973
974 def remotes_list():
975     """Return the list of remotes in the repository
976     """
977     return (set(__remotes_from_config())
978             | set(__remotes_from_dir('remotes'))
979             | set(__remotes_from_dir('branches')))
980
981 def remotes_local_branches(remote):
982     """Returns the list of local branches fetched from given remote
983     """
984
985     branches = []
986     if remote in __remotes_from_config():
987         for line in config.getall('remote.%s.fetch' % remote):
988             branches.append(refspec_localpart(line))
989     elif remote in __remotes_from_dir('remotes'):
990         stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
991         for line in stream:
992             # Only consider Pull lines
993             m = re.match('^Pull: (.*)\n$', line)
994             if m:
995                 branches.append(refspec_localpart(m.group(1)))
996         stream.close()
997     elif remote in __remotes_from_dir('branches'):
998         # old-style branches only declare one branch
999         branches.append('refs/heads/'+remote);
1000     else:
1001         raise GitException, 'Unknown remote "%s"' % remote
1002
1003     return branches
1004
1005 def identify_remote(branchname):
1006     """Return the name for the remote to pull the given branchname
1007     from, or None if we believe it is a local branch.
1008     """
1009
1010     for remote in remotes_list():
1011         if branchname in remotes_local_branches(remote):
1012             return remote
1013
1014     # if we get here we've found nothing, the branch is a local one
1015     return None
1016
1017 def fetch_head():
1018     """Return the git id for the tip of the parent branch as left by
1019     'git fetch'.
1020     """
1021
1022     fetch_head=None
1023     stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
1024     for line in stream:
1025         # Only consider lines not tagged not-for-merge
1026         m = re.match('^([^\t]*)\t\t', line)
1027         if m:
1028             if fetch_head:
1029                 raise GitException, 'StGit does not support multiple FETCH_HEAD'
1030             else:
1031                 fetch_head=m.group(1)
1032     stream.close()
1033
1034     if not fetch_head:
1035         out.warn('No for-merge remote head found in FETCH_HEAD')
1036
1037     # here we are sure to have a single fetch_head
1038     return fetch_head
1039
1040 def all_refs():
1041     """Return a list of all refs in the current repository.
1042     """
1043
1044     return [line.split()[1] for line in GRun('show-ref').output_lines()]