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