chiark / gitweb /
14b4c81a36f24ec6b56b215be2e3125102ba34af
[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, popen2, re, gitmergeonefile
22 from shutil import copyfile
23
24 from stgit import basedir
25 from stgit.utils import *
26 from stgit.config import config
27 from sets import Set
28
29 # git exception class
30 class GitException(Exception):
31     pass
32
33
34
35 #
36 # Classes
37 #
38
39 class Person:
40     """An author, committer, etc."""
41     def __init__(self, name = None, email = None, date = '',
42                  desc = None):
43         self.name = self.email = self.date = None
44         if name or email or date:
45             assert not desc
46             self.name = name
47             self.email = email
48             self.date = date
49         elif desc:
50             assert not (name or email or date)
51             def parse_desc(s):
52                 m = re.match(r'^(.+)<(.+)>(.*)$', s)
53                 assert m
54                 return [x.strip() or None for x in m.groups()]
55             self.name, self.email, self.date = parse_desc(desc)
56     def set_name(self, val):
57         if val:
58             self.name = val
59     def set_email(self, val):
60         if val:
61             self.email = val
62     def set_date(self, val):
63         if val:
64             self.date = val
65     def __str__(self):
66         if self.name and self.email:
67             return '%s <%s>' % (self.name, self.email)
68         else:
69             raise GitException, 'not enough identity data'
70
71 class Commit:
72     """Handle the commit objects
73     """
74     def __init__(self, id_hash):
75         self.__id_hash = id_hash
76
77         lines = _output_lines(['git-cat-file', 'commit', id_hash])
78         for i in range(len(lines)):
79             line = lines[i]
80             if line == '\n':
81                 break
82             field = line.strip().split(' ', 1)
83             if field[0] == 'tree':
84                 self.__tree = field[1]
85             if field[0] == 'author':
86                 self.__author = field[1]
87             if field[0] == 'committer':
88                 self.__committer = field[1]
89         self.__log = ''.join(lines[i+1:])
90
91     def get_id_hash(self):
92         return self.__id_hash
93
94     def get_tree(self):
95         return self.__tree
96
97     def get_parent(self):
98         parents = self.get_parents()
99         if parents:
100             return parents[0]
101         else:
102             return None
103
104     def get_parents(self):
105         return _output_lines(['git-rev-list', '--parents', '--max-count=1',
106                               self.__id_hash])[0].split()[1:]
107
108     def get_author(self):
109         return self.__author
110
111     def get_committer(self):
112         return self.__committer
113
114     def get_log(self):
115         return self.__log
116
117     def __str__(self):
118         return self.get_id_hash()
119
120 # dictionary of Commit objects, used to avoid multiple calls to git
121 __commits = dict()
122
123 #
124 # Functions
125 #
126
127 def get_commit(id_hash):
128     """Commit objects factory. Save/look-up them in the __commits
129     dictionary
130     """
131     global __commits
132
133     if id_hash in __commits:
134         return __commits[id_hash]
135     else:
136         commit = Commit(id_hash)
137         __commits[id_hash] = commit
138         return commit
139
140 def get_conflicts():
141     """Return the list of file conflicts
142     """
143     conflicts_file = os.path.join(basedir.get(), 'conflicts')
144     if os.path.isfile(conflicts_file):
145         f = file(conflicts_file)
146         names = [line.strip() for line in f.readlines()]
147         f.close()
148         return names
149     else:
150         return None
151
152 def _input(cmd, file_desc):
153     p = popen2.Popen3(cmd, True)
154     while True:
155         line = file_desc.readline()
156         if not line:
157             break
158         p.tochild.write(line)
159     p.tochild.close()
160     if p.wait():
161         raise GitException, '%s failed (%s)' % (' '.join(cmd),
162                                                 p.childerr.read().strip())
163
164 def _input_str(cmd, string):
165     p = popen2.Popen3(cmd, True)
166     p.tochild.write(string)
167     p.tochild.close()
168     if p.wait():
169         raise GitException, '%s failed (%s)' % (' '.join(cmd),
170                                                 p.childerr.read().strip())
171
172 def _output(cmd):
173     p=popen2.Popen3(cmd, True)
174     output = p.fromchild.read()
175     if p.wait():
176         raise GitException, '%s failed (%s)' % (' '.join(cmd),
177                                                 p.childerr.read().strip())
178     return output
179
180 def _output_one_line(cmd, file_desc = None):
181     p=popen2.Popen3(cmd, True)
182     if file_desc != None:
183         for line in file_desc:
184             p.tochild.write(line)
185         p.tochild.close()
186     output = p.fromchild.readline().strip()
187     if p.wait():
188         raise GitException, '%s failed (%s)' % (' '.join(cmd),
189                                                 p.childerr.read().strip())
190     return output
191
192 def _output_lines(cmd):
193     p=popen2.Popen3(cmd, True)
194     lines = p.fromchild.readlines()
195     if p.wait():
196         raise GitException, '%s failed (%s)' % (' '.join(cmd),
197                                                 p.childerr.read().strip())
198     return lines
199
200 def __run(cmd, args=None):
201     """__run: runs cmd using spawnvp.
202
203     Runs cmd using spawnvp.  The shell is avoided so it won't mess up
204     our arguments.  If args is very large, the command is run multiple
205     times; args is split xargs style: cmd is passed on each
206     invocation.  Unlike xargs, returns immediately if any non-zero
207     return code is received.  
208     """
209     
210     args_l=cmd.split()
211     if args is None:
212         args = []
213     for i in range(0, len(args)+1, 100):
214         r=os.spawnvp(os.P_WAIT, args_l[0], args_l + args[i:min(i+100, len(args))])
215     if r:
216         return r
217     return 0
218
219 def exclude_files():
220     files = [os.path.join(basedir.get(), 'info', 'exclude')]
221     user_exclude = config.get('core.excludesfile')
222     if user_exclude:
223         files.append(user_exclude)
224     return files
225
226 def tree_status(files = None, tree_id = 'HEAD', unknown = False,
227                   noexclude = True, verbose = False, diff_flags = []):
228     """Returns a list of pairs - [status, filename]
229     """
230     if verbose:
231         out.start('Checking for changes in the working directory')
232
233     refresh_index()
234
235     if not files:
236         files = []
237     cache_files = []
238
239     # unknown files
240     if unknown:
241         if noexclude:
242             exclude = []
243         else:
244             exclude = (['--exclude=%s' % s for s in
245                         ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
246                        + ['--exclude-per-directory=.gitignore']
247                        + ['--exclude-from=%s' % fn for fn in exclude_files()
248                           if os.path.exists(fn)])
249         lines = _output_lines(['git-ls-files', '--others', '--directory']
250                               + exclude)
251         cache_files += [('?', line.strip()) for line in lines]
252
253     # conflicted files
254     conflicts = get_conflicts()
255     if not conflicts:
256         conflicts = []
257     cache_files += [('C', filename) for filename in conflicts]
258
259     # the rest
260     for line in _output_lines(['git-diff-index'] + diff_flags +
261                               [ tree_id, '--'] + files):
262         fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
263         if fs[1] not in conflicts:
264             cache_files.append(fs)
265
266     if verbose:
267         out.done()
268
269     return cache_files
270
271 def local_changes(verbose = True):
272     """Return true if there are local changes in the tree
273     """
274     return len(tree_status(verbose = verbose)) != 0
275
276 def get_heads():
277     heads = []
278     hr = re.compile(r'^[0-9a-f]{40} refs/heads/(.+)$')
279     for line in _output_lines(['git-show-ref', '--heads']):
280         m = hr.match(line)
281         heads.append(m.group(1))
282     return heads
283
284 # HEAD value cached
285 __head = None
286
287 def get_head():
288     """Verifies the HEAD and returns the SHA1 id that represents it
289     """
290     global __head
291
292     if not __head:
293         __head = rev_parse('HEAD')
294     return __head
295
296 def get_head_file():
297     """Returns the name of the file pointed to by the HEAD link
298     """
299     return strip_prefix('refs/heads/',
300                         _output_one_line(['git-symbolic-ref', 'HEAD']))
301
302 def set_head_file(ref):
303     """Resets HEAD to point to a new ref
304     """
305     # head cache flushing is needed since we might have a different value
306     # in the new head
307     __clear_head_cache()
308     if __run('git-symbolic-ref HEAD', ['refs/heads/%s' % ref]) != 0:
309         raise GitException, 'Could not set head to "%s"' % ref
310
311 def set_ref(ref, val):
312     """Point ref at a new commit object."""
313     if __run('git-update-ref', [ref, val]) != 0:
314         raise GitException, 'Could not update %s to "%s".' % (ref, val)
315
316 def set_branch(branch, val):
317     set_ref('refs/heads/%s' % branch, val)
318
319 def __set_head(val):
320     """Sets the HEAD value
321     """
322     global __head
323
324     if not __head or __head != val:
325         set_ref('HEAD', val)
326         __head = val
327
328     # only allow SHA1 hashes
329     assert(len(__head) == 40)
330
331 def __clear_head_cache():
332     """Sets the __head to None so that a re-read is forced
333     """
334     global __head
335
336     __head = None
337
338 def refresh_index():
339     """Refresh index with stat() information from the working directory.
340     """
341     __run('git-update-index -q --unmerged --refresh')
342
343 def rev_parse(git_id):
344     """Parse the string and return a verified SHA1 id
345     """
346     try:
347         return _output_one_line(['git-rev-parse', '--verify', git_id])
348     except GitException:
349         raise GitException, 'Unknown revision: %s' % git_id
350
351 def ref_exists(ref):
352     try:
353         rev_parse(ref)
354         return True
355     except GitException:
356         return False
357
358 def branch_exists(branch):
359     return ref_exists('refs/heads/%s' % branch)
360
361 def create_branch(new_branch, tree_id = None):
362     """Create a new branch in the git repository
363     """
364     if branch_exists(new_branch):
365         raise GitException, 'Branch "%s" already exists' % new_branch
366
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         switch(tree_id)
374
375     if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
376         os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
377
378 def switch_branch(new_branch):
379     """Switch to a git branch
380     """
381     global __head
382
383     if not branch_exists(new_branch):
384         raise GitException, 'Branch "%s" does not exist' % new_branch
385
386     tree_id = rev_parse('refs/heads/%s^{commit}' % new_branch)
387     if tree_id != get_head():
388         refresh_index()
389         if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
390             raise GitException, 'git-read-tree failed (local changes maybe?)'
391         __head = tree_id
392     set_head_file(new_branch)
393
394     if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
395         os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
396
397 def delete_ref(ref):
398     if not ref_exists(ref):
399         raise GitException, '%s does not exist' % ref
400     sha1 = _output_one_line(['git-show-ref', '-s', ref])
401     if __run('git-update-ref -d %s %s' % (ref, sha1)):
402         raise GitException, 'Failed to delete ref %s' % ref
403
404 def delete_branch(name):
405     delete_ref('refs/heads/%s' % name)
406
407 def rename_ref(from_ref, to_ref):
408     if not ref_exists(from_ref):
409         raise GitException, '"%s" does not exist' % from_ref
410     if ref_exists(to_ref):
411         raise GitException, '"%s" already exists' % to_ref
412
413     sha1 = _output_one_line(['git-show-ref', '-s', from_ref])
414     if __run('git-update-ref %s %s %s' % (to_ref, sha1, '0'*40)):
415         raise GitException, 'Failed to create new ref %s' % to_ref
416     if __run('git-update-ref -d %s %s' % (from_ref, sha1)):
417         raise GitException, 'Failed to delete ref %s' % from_ref
418
419 def rename_branch(from_name, to_name):
420     """Rename a git branch."""
421     rename_ref('refs/heads/%s' % from_name, 'refs/heads/%s' % to_name)
422     if get_head_file() == from_name:
423         set_head_file(to_name)
424     reflog_dir = os.path.join(basedir.get(), 'logs', 'refs', 'heads')
425     if os.path.exists(reflog_dir) \
426            and os.path.exists(os.path.join(reflog_dir, from_name)):
427         rename(reflog_dir, from_name, to_name)
428
429 def add(names):
430     """Add the files or recursively add the directory contents
431     """
432     # generate the file list
433     files = []
434     for i in names:
435         if not os.path.exists(i):
436             raise GitException, 'Unknown file or directory: %s' % i
437
438         if os.path.isdir(i):
439             # recursive search. We only add files
440             for root, dirs, local_files in os.walk(i):
441                 for name in [os.path.join(root, f) for f in local_files]:
442                     if os.path.isfile(name):
443                         files.append(os.path.normpath(name))
444         elif os.path.isfile(i):
445             files.append(os.path.normpath(i))
446         else:
447             raise GitException, '%s is not a file or directory' % i
448
449     if files:
450         if __run('git-update-index --add --', files):
451             raise GitException, 'Unable to add file'
452
453 def __copy_single(source, target, target2=''):
454     """Copy file or dir named 'source' to name target+target2"""
455
456     # "source" (file or dir) must match one or more git-controlled file
457     realfiles = _output_lines(['git-ls-files', source])
458     if len(realfiles) == 0:
459         raise GitException, '"%s" matches no git-controled files' % source
460
461     if os.path.isdir(source):
462         # physically copy the files, and record them to add them in one run
463         newfiles = []
464         re_string='^'+source+'/(.*)$'
465         prefix_regexp = re.compile(re_string)
466         for f in [f.strip() for f in realfiles]:
467             m = prefix_regexp.match(f)
468             if not m:
469                 raise Exception, '"%s" does not match "%s"' % (f, re_string)
470             newname = target+target2+'/'+m.group(1)
471             if not os.path.exists(os.path.dirname(newname)):
472                 os.makedirs(os.path.dirname(newname))
473             copyfile(f, newname)
474             newfiles.append(newname)
475
476         add(newfiles)
477     else: # files, symlinks, ...
478         newname = target+target2
479         copyfile(source, newname)
480         add([newname])
481
482
483 def copy(filespecs, target):
484     if os.path.isdir(target):
485         # target is a directory: copy each entry on the command line,
486         # with the same name, into the target
487         target = target.rstrip('/')
488         
489         # first, check that none of the children of the target
490         # matching the command line aleady exist
491         for filespec in filespecs:
492             entry = target+ '/' + os.path.basename(filespec.rstrip('/'))
493             if os.path.exists(entry):
494                 raise GitException, 'Target "%s" already exists' % entry
495         
496         for filespec in filespecs:
497             filespec = filespec.rstrip('/')
498             basename = '/' + os.path.basename(filespec)
499             __copy_single(filespec, target, basename)
500
501     elif os.path.exists(target):
502         raise GitException, 'Target "%s" exists but is not a directory' % target
503     elif len(filespecs) != 1:
504         raise GitException, 'Cannot copy more than one file to non-directory'
505
506     else:
507         # at this point: len(filespecs)==1 and target does not exist
508
509         # check target directory
510         targetdir = os.path.dirname(target)
511         if targetdir != '' and not os.path.isdir(targetdir):
512             raise GitException, 'Target directory "%s" does not exist' % targetdir
513
514         __copy_single(filespecs[0].rstrip('/'), target)
515         
516
517 def rm(files, force = False):
518     """Remove a file from the repository
519     """
520     if not force:
521         for f in files:
522             if os.path.exists(f):
523                 raise GitException, '%s exists. Remove it first' %f
524         if files:
525             __run('git-update-index --remove --', files)
526     else:
527         if files:
528             __run('git-update-index --force-remove --', files)
529
530 # Persons caching
531 __user = None
532 __author = None
533 __committer = None
534
535 def user():
536     """Return the user information.
537     """
538     global __user
539     if not __user:
540         name=config.get('user.name')
541         email=config.get('user.email')
542         __user = Person(name, email)
543     return __user;
544
545 def author():
546     """Return the author information.
547     """
548     global __author
549     if not __author:
550         try:
551             # the environment variables take priority over config
552             try:
553                 date = os.environ['GIT_AUTHOR_DATE']
554             except KeyError:
555                 date = ''
556             __author = Person(os.environ['GIT_AUTHOR_NAME'],
557                               os.environ['GIT_AUTHOR_EMAIL'],
558                               date)
559         except KeyError:
560             __author = user()
561     return __author
562
563 def committer():
564     """Return the author information.
565     """
566     global __committer
567     if not __committer:
568         try:
569             # the environment variables take priority over config
570             try:
571                 date = os.environ['GIT_COMMITTER_DATE']
572             except KeyError:
573                 date = ''
574             __committer = Person(os.environ['GIT_COMMITTER_NAME'],
575                                  os.environ['GIT_COMMITTER_EMAIL'],
576                                  date)
577         except KeyError:
578             __committer = user()
579     return __committer
580
581 def update_cache(files = None, force = False):
582     """Update the cache information for the given files
583     """
584     if not files:
585         files = []
586
587     cache_files = tree_status(files, verbose = False)
588
589     # everything is up-to-date
590     if len(cache_files) == 0:
591         return False
592
593     # check for unresolved conflicts
594     if not force and [x for x in cache_files
595                       if x[0] not in ['M', 'N', 'A', 'D']]:
596         raise GitException, 'Updating cache failed: unresolved conflicts'
597
598     # update the cache
599     add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
600     rm_files =  [x[1] for x in cache_files if x[0] in ['D']]
601     m_files =   [x[1] for x in cache_files if x[0] in ['M']]
602
603     if add_files and __run('git-update-index --add --', add_files) != 0:
604         raise GitException, 'Failed git-update-index --add'
605     if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
606         raise GitException, 'Failed git-update-index --rm'
607     if m_files and __run('git-update-index --', m_files) != 0:
608         raise GitException, 'Failed git-update-index'
609
610     return True
611
612 def commit(message, files = None, parents = None, allowempty = False,
613            cache_update = True, tree_id = None, set_head = False,
614            author_name = None, author_email = None, author_date = None,
615            committer_name = None, committer_email = None):
616     """Commit the current tree to repository
617     """
618     if not files:
619         files = []
620     if not parents:
621         parents = []
622
623     # Get the tree status
624     if cache_update and parents != []:
625         changes = update_cache(files)
626         if not changes and not allowempty:
627             raise GitException, 'No changes to commit'
628
629     # get the commit message
630     if not message:
631         message = '\n'
632     elif message[-1:] != '\n':
633         message += '\n'
634
635     # write the index to repository
636     if tree_id == None:
637         tree_id = _output_one_line(['git-write-tree'])
638         set_head = True
639
640     # the commit
641     cmd = ['env']
642     if author_name:
643         cmd += ['GIT_AUTHOR_NAME=%s' % author_name]
644     if author_email:
645         cmd += ['GIT_AUTHOR_EMAIL=%s' % author_email]
646     if author_date:
647         cmd += ['GIT_AUTHOR_DATE=%s' % author_date]
648     if committer_name:
649         cmd += ['GIT_COMMITTER_NAME=%s' % committer_name]
650     if committer_email:
651         cmd += ['GIT_COMMITTER_EMAIL=%s' % committer_email]
652     cmd += ['git-commit-tree', tree_id]
653
654     # get the parents
655     for p in parents:
656         cmd += ['-p', p]
657
658     commit_id = _output_one_line(cmd, message)
659     if set_head:
660         __set_head(commit_id)
661
662     return commit_id
663
664 def apply_diff(rev1, rev2, check_index = True, files = None):
665     """Apply the diff between rev1 and rev2 onto the current
666     index. This function doesn't need to raise an exception since it
667     is only used for fast-pushing a patch. If this operation fails,
668     the pushing would fall back to the three-way merge.
669     """
670     if check_index:
671         index_opt = ['--index']
672     else:
673         index_opt = []
674
675     if not files:
676         files = []
677
678     diff_str = diff(files, rev1, rev2)
679     if diff_str:
680         try:
681             _input_str(['git-apply'] + index_opt, diff_str)
682         except GitException:
683             return False
684
685     return True
686
687 def merge(base, head1, head2, recursive = False):
688     """Perform a 3-way merge between base, head1 and head2 into the
689     local tree
690     """
691     refresh_index()
692
693     err_output = None
694     if recursive:
695         # this operation tracks renames but it is slower (used in
696         # general when pushing or picking patches)
697         try:
698             # use _output() to mask the verbose prints of the tool
699             _output(['git-merge-recursive', base, '--', head1, head2])
700         except GitException, ex:
701             err_output = str(ex)
702             pass
703     else:
704         # the fast case where we don't track renames (used when the
705         # distance between base and heads is small, i.e. folding or
706         # synchronising patches)
707         if __run('git-read-tree -u -m --aggressive',
708                  [base, head1, head2]) != 0:
709             raise GitException, 'git-read-tree failed (local changes maybe?)'
710
711     # check the index for unmerged entries
712     files = {}
713     stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
714
715     for line in _output(['git-ls-files', '--unmerged', '--stage', '-z']).split('\0'):
716         if not line:
717             continue
718
719         mode, hash, stage, path = stages_re.findall(line)[0]
720
721         if not path in files:
722             files[path] = {}
723             files[path]['1'] = ('', '')
724             files[path]['2'] = ('', '')
725             files[path]['3'] = ('', '')
726
727         files[path][stage] = (mode, hash)
728
729     if err_output and not files:
730         # if no unmerged files, there was probably a different type of
731         # error and we have to abort the merge
732         raise GitException, err_output
733
734     # merge the unmerged files
735     errors = False
736     for path in files:
737         # remove additional files that might be generated for some
738         # newer versions of GIT
739         for suffix in [base, head1, head2]:
740             if not suffix:
741                 continue
742             fname = path + '~' + suffix
743             if os.path.exists(fname):
744                 os.remove(fname)
745
746         stages = files[path]
747         if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
748                                  stages['3'][1], path, stages['1'][0],
749                                  stages['2'][0], stages['3'][0]) != 0:
750             errors = True
751
752     if errors:
753         raise GitException, 'GIT index merging failed (possible conflicts)'
754
755 def status(files = None, modified = False, new = False, deleted = False,
756            conflict = False, unknown = False, noexclude = False,
757            diff_flags = []):
758     """Show the tree status
759     """
760     if not files:
761         files = []
762
763     cache_files = tree_status(files, unknown = True, noexclude = noexclude,
764                                 diff_flags = diff_flags)
765     all = not (modified or new or deleted or conflict or unknown)
766
767     if not all:
768         filestat = []
769         if modified:
770             filestat.append('M')
771         if new:
772             filestat.append('A')
773             filestat.append('N')
774         if deleted:
775             filestat.append('D')
776         if conflict:
777             filestat.append('C')
778         if unknown:
779             filestat.append('?')
780         cache_files = [x for x in cache_files if x[0] in filestat]
781
782     for fs in cache_files:
783         if files and not fs[1] in files:
784             continue
785         if all:
786             out.stdout('%s %s' % (fs[0], fs[1]))
787         else:
788             out.stdout('%s' % fs[1])
789
790 def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None,
791          diff_flags = []):
792     """Show the diff between rev1 and rev2
793     """
794     if not files:
795         files = []
796
797     if rev1 and rev2:
798         diff_str = _output(['git-diff-tree', '-p'] + diff_flags
799                            + [rev1, rev2, '--'] + files)
800     elif rev1 or rev2:
801         refresh_index()
802         if rev2:
803             diff_str = _output(['git-diff-index', '-p', '-R']
804                                + diff_flags + [rev2, '--'] + files)
805         else:
806             diff_str = _output(['git-diff-index', '-p']
807                                + diff_flags + [rev1, '--'] + files)
808     else:
809         diff_str = ''
810
811     if out_fd:
812         out_fd.write(diff_str)
813     else:
814         return diff_str
815
816 def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
817     """Return the diffstat between rev1 and rev2
818     """
819     if not files:
820         files = []
821
822     p=popen2.Popen3('git-apply --stat')
823     diff(files, rev1, rev2, p.tochild)
824     p.tochild.close()
825     diff_str = p.fromchild.read().rstrip()
826     if p.wait():
827         raise GitException, 'git.diffstat failed'
828     return diff_str
829
830 def files(rev1, rev2, diff_flags = []):
831     """Return the files modified between rev1 and rev2
832     """
833
834     result = ''
835     for line in _output_lines(['git-diff-tree'] + diff_flags + ['-r', rev1, rev2]):
836         result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
837
838     return result.rstrip()
839
840 def barefiles(rev1, rev2):
841     """Return the files modified between rev1 and rev2, without status info
842     """
843
844     result = ''
845     for line in _output_lines(['git-diff-tree', '-r', rev1, rev2]):
846         result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
847
848     return result.rstrip()
849
850 def pretty_commit(commit_id = 'HEAD', diff_flags = []):
851     """Return a given commit (log + diff)
852     """
853     return _output(['git-diff-tree'] + diff_flags +
854                    ['--cc', '--always', '--pretty', '-r', commit_id])
855
856 def checkout(files = None, tree_id = None, force = False):
857     """Check out the given or all files
858     """
859     if not files:
860         files = []
861
862     if tree_id and __run('git-read-tree --reset', [tree_id]) != 0:
863         raise GitException, 'Failed git-read-tree --reset %s' % tree_id
864
865     checkout_cmd = 'git-checkout-index -q -u'
866     if force:
867         checkout_cmd += ' -f'
868     if len(files) == 0:
869         checkout_cmd += ' -a'
870     else:
871         checkout_cmd += ' --'
872
873     if __run(checkout_cmd, files) != 0:
874         raise GitException, 'Failed git-checkout-index'
875
876 def switch(tree_id, keep = False):
877     """Switch the tree to the given id
878     """
879     if not keep:
880         refresh_index()
881         if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
882             raise GitException, 'git-read-tree failed (local changes maybe?)'
883
884     __set_head(tree_id)
885
886 def reset(files = None, tree_id = None, check_out = True):
887     """Revert the tree changes relative to the given tree_id. It removes
888     any local changes
889     """
890     if not tree_id:
891         tree_id = get_head()
892
893     if check_out:
894         cache_files = tree_status(files, tree_id)
895         # files which were added but need to be removed
896         rm_files =  [x[1] for x in cache_files if x[0] in ['A']]
897
898         checkout(files, tree_id, True)
899         # checkout doesn't remove files
900         map(os.remove, rm_files)
901
902     # if the reset refers to the whole tree, switch the HEAD as well
903     if not files:
904         __set_head(tree_id)
905
906 def fetch(repository = 'origin', refspec = None):
907     """Fetches changes from the remote repository, using 'git-fetch'
908     by default.
909     """
910     # we update the HEAD
911     __clear_head_cache()
912
913     args = [repository]
914     if refspec:
915         args.append(refspec)
916
917     command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
918               config.get('stgit.fetchcmd')
919     if __run(command, args) != 0:
920         raise GitException, 'Failed "%s %s"' % (command, repository)
921
922 def pull(repository = 'origin', refspec = None):
923     """Fetches changes from the remote repository, using 'git-pull'
924     by default.
925     """
926     # we update the HEAD
927     __clear_head_cache()
928
929     args = [repository]
930     if refspec:
931         args.append(refspec)
932
933     command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
934               config.get('stgit.pullcmd')
935     if __run(command, args) != 0:
936         raise GitException, 'Failed "%s %s"' % (command, repository)
937
938 def repack():
939     """Repack all objects into a single pack
940     """
941     __run('git-repack -a -d -f')
942
943 def apply_patch(filename = None, diff = None, base = None,
944                 fail_dump = True):
945     """Apply a patch onto the current or given index. There must not
946     be any local changes in the tree, otherwise the command fails
947     """
948     if diff is None:
949         if filename:
950             f = file(filename)
951         else:
952             f = sys.stdin
953         diff = f.read()
954         if filename:
955             f.close()
956
957     if base:
958         orig_head = get_head()
959         switch(base)
960     else:
961         refresh_index()
962
963     try:
964         _input_str(['git-apply', '--index'], diff)
965     except GitException:
966         if base:
967             switch(orig_head)
968         if fail_dump:
969             # write the failed diff to a file
970             f = file('.stgit-failed.patch', 'w+')
971             f.write(diff)
972             f.close()
973             out.warn('Diff written to the .stgit-failed.patch file')
974
975         raise
976
977     if base:
978         top = commit(message = 'temporary commit used for applying a patch',
979                      parents = [base])
980         switch(orig_head)
981         merge(base, orig_head, top)
982
983 def clone(repository, local_dir):
984     """Clone a remote repository. At the moment, just use the
985     'git-clone' script
986     """
987     if __run('git-clone', [repository, local_dir]) != 0:
988         raise GitException, 'Failed "git-clone %s %s"' \
989               % (repository, local_dir)
990
991 def modifying_revs(files, base_rev, head_rev):
992     """Return the revisions from the list modifying the given files
993     """
994     cmd = ['git-rev-list', '%s..%s' % (base_rev, head_rev), '--']
995     revs = [line.strip() for line in _output_lines(cmd + files)]
996
997     return revs
998
999
1000 def refspec_localpart(refspec):
1001     m = re.match('^[^:]*:([^:]*)$', refspec)
1002     if m:
1003         return m.group(1)
1004     else:
1005         raise GitException, 'Cannot parse refspec "%s"' % line
1006
1007 def refspec_remotepart(refspec):
1008     m = re.match('^([^:]*):[^:]*$', refspec)
1009     if m:
1010         return m.group(1)
1011     else:
1012         raise GitException, 'Cannot parse refspec "%s"' % line
1013     
1014
1015 def __remotes_from_config():
1016     return config.sections_matching(r'remote\.(.*)\.url')
1017
1018 def __remotes_from_dir(dir):
1019     d = os.path.join(basedir.get(), dir)
1020     if os.path.exists(d):
1021         return os.listdir(d)
1022     else:
1023         return None
1024
1025 def remotes_list():
1026     """Return the list of remotes in the repository
1027     """
1028
1029     return Set(__remotes_from_config()) | \
1030            Set(__remotes_from_dir('remotes')) | \
1031            Set(__remotes_from_dir('branches'))
1032
1033 def remotes_local_branches(remote):
1034     """Returns the list of local branches fetched from given remote
1035     """
1036
1037     branches = []
1038     if remote in __remotes_from_config():
1039         for line in config.getall('remote.%s.fetch' % remote):
1040             branches.append(refspec_localpart(line))
1041     elif remote in __remotes_from_dir('remotes'):
1042         stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
1043         for line in stream:
1044             # Only consider Pull lines
1045             m = re.match('^Pull: (.*)\n$', line)
1046             if m:
1047                 branches.append(refspec_localpart(m.group(1)))
1048         stream.close()
1049     elif remote in __remotes_from_dir('branches'):
1050         # old-style branches only declare one branch
1051         branches.append('refs/heads/'+remote);
1052     else:
1053         raise GitException, 'Unknown remote "%s"' % remote
1054
1055     return branches
1056
1057 def identify_remote(branchname):
1058     """Return the name for the remote to pull the given branchname
1059     from, or None if we believe it is a local branch.
1060     """
1061
1062     for remote in remotes_list():
1063         if branchname in remotes_local_branches(remote):
1064             return remote
1065
1066     # if we get here we've found nothing, the branch is a local one
1067     return None
1068
1069 def fetch_head():
1070     """Return the git id for the tip of the parent branch as left by
1071     'git fetch'.
1072     """
1073
1074     fetch_head=None
1075     stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
1076     for line in stream:
1077         # Only consider lines not tagged not-for-merge
1078         m = re.match('^([^\t]*)\t\t', line)
1079         if m:
1080             if fetch_head:
1081                 raise GitException, "StGit does not support multiple FETCH_HEAD"
1082             else:
1083                 fetch_head=m.group(1)
1084     stream.close()
1085
1086     # here we are sure to have a single fetch_head
1087     return fetch_head
1088
1089 def all_refs():
1090     """Return a list of all refs in the current repository.
1091     """
1092
1093     return [line.split()[1] for line in _output_lines(['git-show-ref'])]