chiark / gitweb /
cdf15fd55e246a58a66a5724f7fdf15d66cf58bb
[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(verbose = True):
266     """Return true if there are local changes in the tree
267     """
268     return len(__tree_status(verbose = verbose)) != 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, recursive = False):
600     """Perform a 3-way merge between base, head1 and head2 into the
601     local tree
602     """
603     refresh_index()
604
605     if recursive:
606         # this operation tracks renames but it is slower (used in
607         # general when pushing or picking patches)
608         try:
609             # use _output() to mask the verbose prints of the tool
610             _output('git-merge-recursive %s -- %s %s' % (base, head1, head2))
611         except GitException:
612             pass
613     else:
614         # the fast case where we don't track renames (used when the
615         # distance between base and heads is small, i.e. folding or
616         # synchronising patches)
617         if __run('git-read-tree -u -m --aggressive',
618                  [base, head1, head2]) != 0:
619             raise GitException, 'git-read-tree failed (local changes maybe?)'
620
621     # check the index for unmerged entries
622     files = {}
623     stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
624
625     for line in _output('git-ls-files --unmerged --stage -z').split('\0'):
626         if not line:
627             continue
628
629         mode, hash, stage, path = stages_re.findall(line)[0]
630
631         if not path in files:
632             files[path] = {}
633             files[path]['1'] = ('', '')
634             files[path]['2'] = ('', '')
635             files[path]['3'] = ('', '')
636
637         files[path][stage] = (mode, hash)
638
639     # merge the unmerged files
640     errors = False
641     for path in files:
642         # remove additional files that might be generated for some
643         # newer versions of GIT
644         for suffix in [base, head1, head2]:
645             if not suffix:
646                 continue
647             fname = path + '~' + suffix
648             if os.path.exists(fname):
649                 os.remove(fname)
650
651         stages = files[path]
652         if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
653                                  stages['3'][1], path, stages['1'][0],
654                                  stages['2'][0], stages['3'][0]) != 0:
655             errors = True
656
657     if errors:
658         raise GitException, 'GIT index merging failed (possible conflicts)'
659
660 def status(files = None, modified = False, new = False, deleted = False,
661            conflict = False, unknown = False, noexclude = False):
662     """Show the tree status
663     """
664     if not files:
665         files = []
666
667     cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
668     all = not (modified or new or deleted or conflict or unknown)
669
670     if not all:
671         filestat = []
672         if modified:
673             filestat.append('M')
674         if new:
675             filestat.append('A')
676             filestat.append('N')
677         if deleted:
678             filestat.append('D')
679         if conflict:
680             filestat.append('C')
681         if unknown:
682             filestat.append('?')
683         cache_files = [x for x in cache_files if x[0] in filestat]
684
685     for fs in cache_files:
686         if all:
687             print '%s %s' % (fs[0], fs[1])
688         else:
689             print '%s' % fs[1]
690
691 def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None):
692     """Show the diff between rev1 and rev2
693     """
694     if not files:
695         files = []
696
697     if rev1 and rev2:
698         diff_str = _output(['git-diff-tree', '-p', rev1, rev2, '--'] + files)
699     elif rev1 or rev2:
700         refresh_index()
701         if rev2:
702             diff_str = _output(['git-diff-index', '-p', '-R', rev2, '--'] + files)
703         else:
704             diff_str = _output(['git-diff-index', '-p', rev1, '--'] + files)
705     else:
706         diff_str = ''
707
708     if out_fd:
709         out_fd.write(diff_str)
710     else:
711         return diff_str
712
713 def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
714     """Return the diffstat between rev1 and rev2
715     """
716     if not files:
717         files = []
718
719     p=popen2.Popen3('git-apply --stat')
720     diff(files, rev1, rev2, p.tochild)
721     p.tochild.close()
722     diff_str = p.fromchild.read().rstrip()
723     if p.wait():
724         raise GitException, 'git.diffstat failed'
725     return diff_str
726
727 def files(rev1, rev2):
728     """Return the files modified between rev1 and rev2
729     """
730
731     result = ''
732     for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
733         result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
734
735     return result.rstrip()
736
737 def barefiles(rev1, rev2):
738     """Return the files modified between rev1 and rev2, without status info
739     """
740
741     result = ''
742     for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
743         result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
744
745     return result.rstrip()
746
747 def pretty_commit(commit_id = 'HEAD'):
748     """Return a given commit (log + diff)
749     """
750     return _output(['git-diff-tree', '--cc', '--always', '--pretty', '-r',
751                     commit_id])
752
753 def checkout(files = None, tree_id = None, force = False):
754     """Check out the given or all files
755     """
756     if not files:
757         files = []
758
759     if tree_id and __run('git-read-tree --reset', [tree_id]) != 0:
760         raise GitException, 'Failed git-read-tree --reset %s' % tree_id
761
762     checkout_cmd = 'git-checkout-index -q -u'
763     if force:
764         checkout_cmd += ' -f'
765     if len(files) == 0:
766         checkout_cmd += ' -a'
767     else:
768         checkout_cmd += ' --'
769
770     if __run(checkout_cmd, files) != 0:
771         raise GitException, 'Failed git-checkout-index'
772
773 def switch(tree_id, keep = False):
774     """Switch the tree to the given id
775     """
776     if not keep:
777         refresh_index()
778         if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
779             raise GitException, 'git-read-tree failed (local changes maybe?)'
780
781     __set_head(tree_id)
782
783 def reset(files = None, tree_id = None, check_out = True):
784     """Revert the tree changes relative to the given tree_id. It removes
785     any local changes
786     """
787     if not tree_id:
788         tree_id = get_head()
789
790     if check_out:
791         cache_files = __tree_status(files, tree_id)
792         # files which were added but need to be removed
793         rm_files =  [x[1] for x in cache_files if x[0] in ['A']]
794
795         checkout(files, tree_id, True)
796         # checkout doesn't remove files
797         map(os.remove, rm_files)
798
799     # if the reset refers to the whole tree, switch the HEAD as well
800     if not files:
801         __set_head(tree_id)
802
803 def pull(repository = 'origin', refspec = None):
804     """Pull changes from the remote repository. At the moment, just
805     use the 'git-pull' command
806     """
807     # 'git-pull' updates the HEAD
808     __clear_head_cache()
809
810     args = [repository]
811     if refspec:
812         args.append(refspec)
813
814     if __run(config.get('stgit', 'pullcmd'), args) != 0:
815         raise GitException, 'Failed "git-pull %s"' % repository
816
817 def repack():
818     """Repack all objects into a single pack
819     """
820     __run('git-repack -a -d -f')
821
822 def apply_patch(filename = None, diff = None, base = None,
823                 fail_dump = True):
824     """Apply a patch onto the current or given index. There must not
825     be any local changes in the tree, otherwise the command fails
826     """
827     if diff is None:
828         if filename:
829             f = file(filename)
830         else:
831             f = sys.stdin
832         diff = f.read()
833         if filename:
834             f.close()
835
836     if base:
837         orig_head = get_head()
838         switch(base)
839     else:
840         refresh_index()
841
842     try:
843         _input_str('git-apply --index', diff)
844     except GitException:
845         if base:
846             switch(orig_head)
847         if fail_dump:
848             # write the failed diff to a file
849             f = file('.stgit-failed.patch', 'w+')
850             f.write(diff)
851             f.close()
852             print >> sys.stderr, 'Diff written to the .stgit-failed.patch file'
853
854         raise
855
856     if base:
857         top = commit(message = 'temporary commit used for applying a patch',
858                      parents = [base])
859         switch(orig_head)
860         merge(base, orig_head, top)
861
862 def clone(repository, local_dir):
863     """Clone a remote repository. At the moment, just use the
864     'git-clone' script
865     """
866     if __run('git-clone', [repository, local_dir]) != 0:
867         raise GitException, 'Failed "git-clone %s %s"' \
868               % (repository, local_dir)
869
870 def modifying_revs(files, base_rev):
871     """Return the revisions from the list modifying the given files
872     """
873     cmd = ['git-rev-list', '%s..' % base_rev, '--']
874     revs = [line.strip() for line in _output_lines(cmd + files)]
875
876     return revs