chiark / gitweb /
21d74dd6913af2b49917073786f067b0711c6a86
[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
23 from stgit import basedir
24 from stgit.utils import *
25 from stgit.config import config
26
27 # git exception class
28 class GitException(Exception):
29     pass
30
31
32
33 #
34 # Classes
35 #
36
37 class Person:
38     """An author, committer, etc."""
39     def __init__(self, name = None, email = None, date = '',
40                  desc = None):
41         if name or email or date:
42             assert not desc
43             self.name = name
44             self.email = email
45             self.date = date
46         elif desc:
47             assert not (name or email or date)
48             def parse_desc(s):
49                 m = re.match(r'^(.+)<(.+)>(.*)$', s)
50                 assert m
51                 return [x.strip() or None for x in m.groups()]
52             self.name, self.email, self.date = parse_desc(desc)
53     def set_name(self, val):
54         if val:
55             self.name = val
56     def set_email(self, val):
57         if val:
58             self.email = val
59     def set_date(self, val):
60         if val:
61             self.date = val
62     def __str__(self):
63         if self.name and self.email:
64             return '%s <%s>' % (self.name, self.email)
65         else:
66             raise GitException, 'not enough identity data'
67
68 class Commit:
69     """Handle the commit objects
70     """
71     def __init__(self, id_hash):
72         self.__id_hash = id_hash
73
74         lines = _output_lines('git-cat-file commit %s' % id_hash)
75         for i in range(len(lines)):
76             line = lines[i]
77             if line == '\n':
78                 break
79             field = line.strip().split(' ', 1)
80             if field[0] == 'tree':
81                 self.__tree = field[1]
82             if field[0] == 'author':
83                 self.__author = field[1]
84             if field[0] == 'committer':
85                 self.__committer = field[1]
86         self.__log = ''.join(lines[i+1:])
87
88     def get_id_hash(self):
89         return self.__id_hash
90
91     def get_tree(self):
92         return self.__tree
93
94     def get_parent(self):
95         parents = self.get_parents()
96         if parents:
97             return parents[0]
98         else:
99             return None
100
101     def get_parents(self):
102         return _output_lines('git-rev-list --parents --max-count=1 %s'
103                              % self.__id_hash)[0].split()[1:]
104
105     def get_author(self):
106         return self.__author
107
108     def get_committer(self):
109         return self.__committer
110
111     def get_log(self):
112         return self.__log
113
114     def __str__(self):
115         return self.get_id_hash()
116
117 # dictionary of Commit objects, used to avoid multiple calls to git
118 __commits = dict()
119
120 #
121 # Functions
122 #
123
124 def get_commit(id_hash):
125     """Commit objects factory. Save/look-up them in the __commits
126     dictionary
127     """
128     global __commits
129
130     if id_hash in __commits:
131         return __commits[id_hash]
132     else:
133         commit = Commit(id_hash)
134         __commits[id_hash] = commit
135         return commit
136
137 def get_conflicts():
138     """Return the list of file conflicts
139     """
140     conflicts_file = os.path.join(basedir.get(), 'conflicts')
141     if os.path.isfile(conflicts_file):
142         f = file(conflicts_file)
143         names = [line.strip() for line in f.readlines()]
144         f.close()
145         return names
146     else:
147         return None
148
149 def _input(cmd, file_desc):
150     p = popen2.Popen3(cmd, True)
151     while True:
152         line = file_desc.readline()
153         if not line:
154             break
155         p.tochild.write(line)
156     p.tochild.close()
157     if p.wait():
158         raise GitException, '%s failed (%s)' % (str(cmd),
159                                                 p.childerr.read().strip())
160
161 def _input_str(cmd, string):
162     p = popen2.Popen3(cmd, True)
163     p.tochild.write(string)
164     p.tochild.close()
165     if p.wait():
166         raise GitException, '%s failed (%s)' % (str(cmd),
167                                                 p.childerr.read().strip())
168
169 def _output(cmd):
170     p=popen2.Popen3(cmd, True)
171     output = p.fromchild.read()
172     if p.wait():
173         raise GitException, '%s failed (%s)' % (str(cmd),
174                                                 p.childerr.read().strip())
175     return output
176
177 def _output_one_line(cmd, file_desc = None):
178     p=popen2.Popen3(cmd, True)
179     if file_desc != None:
180         for line in file_desc:
181             p.tochild.write(line)
182         p.tochild.close()
183     output = p.fromchild.readline().strip()
184     if p.wait():
185         raise GitException, '%s failed (%s)' % (str(cmd),
186                                                 p.childerr.read().strip())
187     return output
188
189 def _output_lines(cmd):
190     p=popen2.Popen3(cmd, True)
191     lines = p.fromchild.readlines()
192     if p.wait():
193         raise GitException, '%s failed (%s)' % (str(cmd),
194                                                 p.childerr.read().strip())
195     return lines
196
197 def __run(cmd, args=None):
198     """__run: runs cmd using spawnvp.
199
200     Runs cmd using spawnvp.  The shell is avoided so it won't mess up
201     our arguments.  If args is very large, the command is run multiple
202     times; args is split xargs style: cmd is passed on each
203     invocation.  Unlike xargs, returns immediately if any non-zero
204     return code is received.  
205     """
206     
207     args_l=cmd.split()
208     if args is None:
209         args = []
210     for i in range(0, len(args)+1, 100):
211         r=os.spawnvp(os.P_WAIT, args_l[0], args_l + args[i:min(i+100, len(args))])
212     if r:
213         return r
214     return 0
215
216 def __tree_status(files = None, tree_id = 'HEAD', unknown = False,
217                   noexclude = True, verbose = False):
218     """Returns a list of pairs - [status, filename]
219     """
220     if verbose and sys.stdout.isatty():
221         print 'Checking for changes in the working directory...',
222         sys.stdout.flush()
223
224     refresh_index()
225
226     if not files:
227         files = []
228     cache_files = []
229
230     # unknown files
231     if unknown:
232         exclude_file = os.path.join(basedir.get(), 'info', 'exclude')
233         base_exclude = ['--exclude=%s' % s for s in
234                         ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
235         base_exclude.append('--exclude-per-directory=.gitignore')
236
237         if os.path.exists(exclude_file):
238             extra_exclude = ['--exclude-from=%s' % exclude_file]
239         else:
240             extra_exclude = []
241         if noexclude:
242             extra_exclude = base_exclude = []
243
244         lines = _output_lines(['git-ls-files', '--others', '--directory']
245                         + base_exclude + extra_exclude)
246         cache_files += [('?', line.strip()) for line in lines]
247
248     # conflicted files
249     conflicts = get_conflicts()
250     if not conflicts:
251         conflicts = []
252     cache_files += [('C', filename) for filename in conflicts]
253
254     # the rest
255     for line in _output_lines(['git-diff-index', tree_id, '--'] + files):
256         fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
257         if fs[1] not in conflicts:
258             cache_files.append(fs)
259
260     if verbose and sys.stdout.isatty():
261         print 'done'
262
263     return cache_files
264
265 def local_changes():
266     """Return true if there are local changes in the tree
267     """
268     return len(__tree_status(verbose = True)) != 0
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 def get_head_file():
283     """Returns the name of the file pointed to by the HEAD link
284     """
285     return strip_prefix('refs/heads/',
286                         _output_one_line('git-symbolic-ref HEAD'))
287
288 def set_head_file(ref):
289     """Resets HEAD to point to a new ref
290     """
291     # head cache flushing is needed since we might have a different value
292     # in the new head
293     __clear_head_cache()
294     if __run('git-symbolic-ref HEAD',
295              [os.path.join('refs', 'heads', ref)]) != 0:
296         raise GitException, 'Could not set head to "%s"' % ref
297
298 def __set_head(val):
299     """Sets the HEAD value
300     """
301     global __head
302
303     if not __head or __head != val:
304         if __run('git-update-ref HEAD', [val]) != 0:
305             raise GitException, 'Could not update HEAD to "%s".' % val
306         __head = val
307
308     # only allow SHA1 hashes
309     assert(len(__head) == 40)
310
311 def __clear_head_cache():
312     """Sets the __head to None so that a re-read is forced
313     """
314     global __head
315
316     __head = None
317
318 def refresh_index():
319     """Refresh index with stat() information from the working directory.
320     """
321     __run('git-update-index -q --unmerged --refresh')
322
323 def rev_parse(git_id):
324     """Parse the string and return a verified SHA1 id
325     """
326     try:
327         return _output_one_line(['git-rev-parse', '--verify', git_id])
328     except GitException:
329         raise GitException, 'Unknown revision: %s' % git_id
330
331 def branch_exists(branch):
332     """Existence check for the named branch
333     """
334     branch = os.path.join('refs', 'heads', branch)
335     for line in _output_lines('git-rev-parse --symbolic --all 2>&1'):
336         if line.strip() == branch:
337             return True
338         if re.compile('[ |/]'+branch+' ').search(line):
339             raise GitException, 'Bogus branch: %s' % line
340     return False
341
342 def create_branch(new_branch, tree_id = None):
343     """Create a new branch in the git repository
344     """
345     if branch_exists(new_branch):
346         raise GitException, 'Branch "%s" already exists' % new_branch
347
348     current_head = get_head()
349     set_head_file(new_branch)
350     __set_head(current_head)
351
352     # a checkout isn't needed if new branch points to the current head
353     if tree_id:
354         switch(tree_id)
355
356     if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
357         os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
358
359 def switch_branch(new_branch):
360     """Switch to a git branch
361     """
362     global __head
363
364     if not branch_exists(new_branch):
365         raise GitException, 'Branch "%s" does not exist' % new_branch
366
367     tree_id = rev_parse(os.path.join('refs', 'heads', new_branch)
368                         + '^{commit}')
369     if tree_id != get_head():
370         refresh_index()
371         if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
372             raise GitException, 'git-read-tree failed (local changes maybe?)'
373         __head = tree_id
374     set_head_file(new_branch)
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 delete_branch(name):
380     """Delete a git branch
381     """
382     if not branch_exists(name):
383         raise GitException, 'Branch "%s" does not exist' % name
384     remove_file_and_dirs(os.path.join(basedir.get(), 'refs', 'heads'),
385                          name)
386
387 def rename_branch(from_name, to_name):
388     """Rename a git branch
389     """
390     if not branch_exists(from_name):
391         raise GitException, 'Branch "%s" does not exist' % from_name
392     if branch_exists(to_name):
393         raise GitException, 'Branch "%s" already exists' % to_name
394
395     if get_head_file() == from_name:
396         set_head_file(to_name)
397     rename(os.path.join(basedir.get(), 'refs', 'heads'),
398            from_name, to_name)
399
400 def add(names):
401     """Add the files or recursively add the directory contents
402     """
403     # generate the file list
404     files = []
405     for i in names:
406         if not os.path.exists(i):
407             raise GitException, 'Unknown file or directory: %s' % i
408
409         if os.path.isdir(i):
410             # recursive search. We only add files
411             for root, dirs, local_files in os.walk(i):
412                 for name in [os.path.join(root, f) for f in local_files]:
413                     if os.path.isfile(name):
414                         files.append(os.path.normpath(name))
415         elif os.path.isfile(i):
416             files.append(os.path.normpath(i))
417         else:
418             raise GitException, '%s is not a file or directory' % i
419
420     if files:
421         if __run('git-update-index --add --', files):
422             raise GitException, 'Unable to add file'
423
424 def rm(files, force = False):
425     """Remove a file from the repository
426     """
427     if not force:
428         for f in files:
429             if os.path.exists(f):
430                 raise GitException, '%s exists. Remove it first' %f
431         if files:
432             __run('git-update-index --remove --', files)
433     else:
434         if files:
435             __run('git-update-index --force-remove --', files)
436
437 # Persons caching
438 __user = None
439 __author = None
440 __committer = None
441
442 def user():
443     """Return the user information.
444     """
445     global __user
446     if not __user:
447         if config.has_option('user', 'name') \
448                and config.has_option('user', 'email'):
449             __user = Person(config.get('user', 'name'),
450                             config.get('user', 'email'))
451         else:
452             raise GitException, 'unknown user details'
453     return __user;
454
455 def author():
456     """Return the author information.
457     """
458     global __author
459     if not __author:
460         try:
461             # the environment variables take priority over config
462             try:
463                 date = os.environ['GIT_AUTHOR_DATE']
464             except KeyError:
465                 date = ''
466             __author = Person(os.environ['GIT_AUTHOR_NAME'],
467                               os.environ['GIT_AUTHOR_EMAIL'],
468                               date)
469         except KeyError:
470             __author = user()
471     return __author
472
473 def committer():
474     """Return the author information.
475     """
476     global __committer
477     if not __committer:
478         try:
479             # the environment variables take priority over config
480             try:
481                 date = os.environ['GIT_COMMITTER_DATE']
482             except KeyError:
483                 date = ''
484             __committer = Person(os.environ['GIT_COMMITTER_NAME'],
485                                  os.environ['GIT_COMMITTER_EMAIL'],
486                                  date)
487         except KeyError:
488             __committer = user()
489     return __committer
490
491 def update_cache(files = None, force = False):
492     """Update the cache information for the given files
493     """
494     if not files:
495         files = []
496
497     cache_files = __tree_status(files, verbose = False)
498
499     # everything is up-to-date
500     if len(cache_files) == 0:
501         return False
502
503     # check for unresolved conflicts
504     if not force and [x for x in cache_files
505                       if x[0] not in ['M', 'N', 'A', 'D']]:
506         raise GitException, 'Updating cache failed: unresolved conflicts'
507
508     # update the cache
509     add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
510     rm_files =  [x[1] for x in cache_files if x[0] in ['D']]
511     m_files =   [x[1] for x in cache_files if x[0] in ['M']]
512
513     if add_files and __run('git-update-index --add --', add_files) != 0:
514         raise GitException, 'Failed git-update-index --add'
515     if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
516         raise GitException, 'Failed git-update-index --rm'
517     if m_files and __run('git-update-index --', m_files) != 0:
518         raise GitException, 'Failed git-update-index'
519
520     return True
521
522 def commit(message, files = None, parents = None, allowempty = False,
523            cache_update = True, tree_id = None,
524            author_name = None, author_email = None, author_date = None,
525            committer_name = None, committer_email = None):
526     """Commit the current tree to repository
527     """
528     if not files:
529         files = []
530     if not parents:
531         parents = []
532
533     # Get the tree status
534     if cache_update and parents != []:
535         changes = update_cache(files)
536         if not changes and not allowempty:
537             raise GitException, 'No changes to commit'
538
539     # get the commit message
540     if not message:
541         message = '\n'
542     elif message[-1:] != '\n':
543         message += '\n'
544
545     must_switch = True
546     # write the index to repository
547     if tree_id == None:
548         tree_id = _output_one_line('git-write-tree')
549     else:
550         must_switch = False
551
552     # the commit
553     cmd = ''
554     if author_name:
555         cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
556     if author_email:
557         cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
558     if author_date:
559         cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
560     if committer_name:
561         cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
562     if committer_email:
563         cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
564     cmd += 'git-commit-tree %s' % tree_id
565
566     # get the parents
567     for p in parents:
568         cmd += ' -p %s' % p
569
570     commit_id = _output_one_line(cmd, message)
571     if must_switch:
572         __set_head(commit_id)
573
574     return commit_id
575
576 def apply_diff(rev1, rev2, check_index = True, files = None):
577     """Apply the diff between rev1 and rev2 onto the current
578     index. This function doesn't need to raise an exception since it
579     is only used for fast-pushing a patch. If this operation fails,
580     the pushing would fall back to the three-way merge.
581     """
582     if check_index:
583         index_opt = '--index'
584     else:
585         index_opt = ''
586
587     if not files:
588         files = []
589
590     diff_str = diff(files, rev1, rev2)
591     if diff_str:
592         try:
593             _input_str('git-apply %s' % index_opt, diff_str)
594         except GitException:
595             return False
596
597     return True
598
599 def merge(base, head1, head2):
600     """Perform a 3-way merge between base, head1 and head2 into the
601     local tree
602     """
603     refresh_index()
604
605     try:
606         # use _output() to mask the verbose prints of the tool
607         _output('git-merge-recursive %s -- %s %s' % (base, head1, head2))
608     except GitException:
609         pass
610
611     # check the index for unmerged entries
612     files = {}
613     stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
614
615     for line in _output('git-ls-files --unmerged --stage -z').split('\0'):
616         if not line:
617             continue
618
619         mode, hash, stage, path = stages_re.findall(line)[0]
620
621         if not path in files:
622             files[path] = {}
623             files[path]['1'] = ('', '')
624             files[path]['2'] = ('', '')
625             files[path]['3'] = ('', '')
626
627         files[path][stage] = (mode, hash)
628
629     # merge the unmerged files
630     errors = False
631     for path in files:
632         # remove additional files that might be generated for some
633         # newer versions of GIT
634         for suffix in [base, head1, head2]:
635             if not suffix:
636                 continue
637             fname = path + '~' + suffix
638             if os.path.exists(fname):
639                 os.remove(fname)
640
641         stages = files[path]
642         if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
643                                  stages['3'][1], path, stages['1'][0],
644                                  stages['2'][0], stages['3'][0]) != 0:
645             errors = True
646
647     if errors:
648         raise GitException, 'GIT index merging failed (possible conflicts)'
649
650 def status(files = None, modified = False, new = False, deleted = False,
651            conflict = False, unknown = False, noexclude = False):
652     """Show the tree status
653     """
654     if not files:
655         files = []
656
657     cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
658     all = not (modified or new or deleted or conflict or unknown)
659
660     if not all:
661         filestat = []
662         if modified:
663             filestat.append('M')
664         if new:
665             filestat.append('A')
666             filestat.append('N')
667         if deleted:
668             filestat.append('D')
669         if conflict:
670             filestat.append('C')
671         if unknown:
672             filestat.append('?')
673         cache_files = [x for x in cache_files if x[0] in filestat]
674
675     for fs in cache_files:
676         if all:
677             print '%s %s' % (fs[0], fs[1])
678         else:
679             print '%s' % fs[1]
680
681 def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None):
682     """Show the diff between rev1 and rev2
683     """
684     if not files:
685         files = []
686
687     if rev1 and rev2:
688         diff_str = _output(['git-diff-tree', '-p', rev1, rev2, '--'] + files)
689     elif rev1 or rev2:
690         refresh_index()
691         if rev2:
692             diff_str = _output(['git-diff-index', '-p', '-R', rev2, '--'] + files)
693         else:
694             diff_str = _output(['git-diff-index', '-p', rev1, '--'] + files)
695     else:
696         diff_str = ''
697
698     if out_fd:
699         out_fd.write(diff_str)
700     else:
701         return diff_str
702
703 def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
704     """Return the diffstat between rev1 and rev2
705     """
706     if not files:
707         files = []
708
709     p=popen2.Popen3('git-apply --stat')
710     diff(files, rev1, rev2, p.tochild)
711     p.tochild.close()
712     diff_str = p.fromchild.read().rstrip()
713     if p.wait():
714         raise GitException, 'git.diffstat failed'
715     return diff_str
716
717 def files(rev1, rev2):
718     """Return the files modified between rev1 and rev2
719     """
720
721     result = ''
722     for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
723         result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
724
725     return result.rstrip()
726
727 def barefiles(rev1, rev2):
728     """Return the files modified between rev1 and rev2, without status info
729     """
730
731     result = ''
732     for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
733         result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
734
735     return result.rstrip()
736
737 def pretty_commit(commit_id = 'HEAD'):
738     """Return a given commit (log + diff)
739     """
740     return _output(['git-diff-tree', '--cc', '--always', '--pretty', '-r',
741                     commit_id])
742
743 def checkout(files = None, tree_id = None, force = False):
744     """Check out the given or all files
745     """
746     if not files:
747         files = []
748
749     if tree_id and __run('git-read-tree --reset', [tree_id]) != 0:
750         raise GitException, 'Failed git-read-tree --reset %s' % tree_id
751
752     checkout_cmd = 'git-checkout-index -q -u'
753     if force:
754         checkout_cmd += ' -f'
755     if len(files) == 0:
756         checkout_cmd += ' -a'
757     else:
758         checkout_cmd += ' --'
759
760     if __run(checkout_cmd, files) != 0:
761         raise GitException, 'Failed git-checkout-index'
762
763 def switch(tree_id, keep = False):
764     """Switch the tree to the given id
765     """
766     if not keep:
767         refresh_index()
768         if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
769             raise GitException, 'git-read-tree failed (local changes maybe?)'
770
771     __set_head(tree_id)
772
773 def reset(files = None, tree_id = None, check_out = True):
774     """Revert the tree changes relative to the given tree_id. It removes
775     any local changes
776     """
777     if not tree_id:
778         tree_id = get_head()
779
780     if check_out:
781         cache_files = __tree_status(files, tree_id)
782         # files which were added but need to be removed
783         rm_files =  [x[1] for x in cache_files if x[0] in ['A']]
784
785         checkout(files, tree_id, True)
786         # checkout doesn't remove files
787         map(os.remove, rm_files)
788
789     # if the reset refers to the whole tree, switch the HEAD as well
790     if not files:
791         __set_head(tree_id)
792
793 def pull(repository = 'origin', refspec = None):
794     """Pull changes from the remote repository. At the moment, just
795     use the 'git-pull' command
796     """
797     # 'git-pull' updates the HEAD
798     __clear_head_cache()
799
800     args = [repository]
801     if refspec:
802         args.append(refspec)
803
804     if __run(config.get('stgit', 'pullcmd'), args) != 0:
805         raise GitException, 'Failed "git-pull %s"' % repository
806
807 def repack():
808     """Repack all objects into a single pack
809     """
810     __run('git-repack -a -d -f')
811
812 def apply_patch(filename = None, diff = None, base = None,
813                 fail_dump = True):
814     """Apply a patch onto the current or given index. There must not
815     be any local changes in the tree, otherwise the command fails
816     """
817     if base:
818         orig_head = get_head()
819         switch(base)
820     else:
821         refresh_index()
822
823     if diff is None:
824         if filename:
825             f = file(filename)
826         else:
827             f = sys.stdin
828         diff = f.read()
829         if filename:
830             f.close()
831
832     try:
833         _input_str('git-apply --index', diff)
834     except GitException:
835         if base:
836             switch(orig_head)
837         if fail_dump:
838             # write the failed diff to a file
839             f = file('.stgit-failed.patch', 'w+')
840             f.write(diff)
841             f.close()
842
843         raise
844
845     if base:
846         top = commit(message = 'temporary commit used for applying a patch',
847                      parents = [base])
848         switch(orig_head)
849         merge(base, orig_head, top)
850
851 def clone(repository, local_dir):
852     """Clone a remote repository. At the moment, just use the
853     'git-clone' script
854     """
855     if __run('git-clone', [repository, local_dir]) != 0:
856         raise GitException, 'Failed "git-clone %s %s"' \
857               % (repository, local_dir)
858
859 def modifying_revs(files, base_rev):
860     """Return the revisions from the list modifying the given files
861     """
862     cmd = ['git-rev-list', '%s..' % base_rev, '--']
863     revs = [line.strip() for line in _output_lines(cmd + files)]
864
865     return revs