chiark / gitweb /
Add option to automatically invoke the interactive merger
[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     reflog_dir = os.path.join(basedir.get(), 'logs', 'refs', 'heads')
401     if os.path.exists(reflog_dir) \
402            and os.path.exists(os.path.join(reflog_dir, from_name)):
403         rename(reflog_dir, from_name, to_name)
404
405 def add(names):
406     """Add the files or recursively add the directory contents
407     """
408     # generate the file list
409     files = []
410     for i in names:
411         if not os.path.exists(i):
412             raise GitException, 'Unknown file or directory: %s' % i
413
414         if os.path.isdir(i):
415             # recursive search. We only add files
416             for root, dirs, local_files in os.walk(i):
417                 for name in [os.path.join(root, f) for f in local_files]:
418                     if os.path.isfile(name):
419                         files.append(os.path.normpath(name))
420         elif os.path.isfile(i):
421             files.append(os.path.normpath(i))
422         else:
423             raise GitException, '%s is not a file or directory' % i
424
425     if files:
426         if __run('git-update-index --add --', files):
427             raise GitException, 'Unable to add file'
428
429 def rm(files, force = False):
430     """Remove a file from the repository
431     """
432     if not force:
433         for f in files:
434             if os.path.exists(f):
435                 raise GitException, '%s exists. Remove it first' %f
436         if files:
437             __run('git-update-index --remove --', files)
438     else:
439         if files:
440             __run('git-update-index --force-remove --', files)
441
442 # Persons caching
443 __user = None
444 __author = None
445 __committer = None
446
447 def user():
448     """Return the user information.
449     """
450     global __user
451     if not __user:
452         if config.has_option('user', 'name') \
453                and config.has_option('user', 'email'):
454             __user = Person(config.get('user', 'name'),
455                             config.get('user', 'email'))
456         else:
457             raise GitException, 'unknown user details'
458     return __user;
459
460 def author():
461     """Return the author information.
462     """
463     global __author
464     if not __author:
465         try:
466             # the environment variables take priority over config
467             try:
468                 date = os.environ['GIT_AUTHOR_DATE']
469             except KeyError:
470                 date = ''
471             __author = Person(os.environ['GIT_AUTHOR_NAME'],
472                               os.environ['GIT_AUTHOR_EMAIL'],
473                               date)
474         except KeyError:
475             __author = user()
476     return __author
477
478 def committer():
479     """Return the author information.
480     """
481     global __committer
482     if not __committer:
483         try:
484             # the environment variables take priority over config
485             try:
486                 date = os.environ['GIT_COMMITTER_DATE']
487             except KeyError:
488                 date = ''
489             __committer = Person(os.environ['GIT_COMMITTER_NAME'],
490                                  os.environ['GIT_COMMITTER_EMAIL'],
491                                  date)
492         except KeyError:
493             __committer = user()
494     return __committer
495
496 def update_cache(files = None, force = False):
497     """Update the cache information for the given files
498     """
499     if not files:
500         files = []
501
502     cache_files = __tree_status(files, verbose = False)
503
504     # everything is up-to-date
505     if len(cache_files) == 0:
506         return False
507
508     # check for unresolved conflicts
509     if not force and [x for x in cache_files
510                       if x[0] not in ['M', 'N', 'A', 'D']]:
511         raise GitException, 'Updating cache failed: unresolved conflicts'
512
513     # update the cache
514     add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
515     rm_files =  [x[1] for x in cache_files if x[0] in ['D']]
516     m_files =   [x[1] for x in cache_files if x[0] in ['M']]
517
518     if add_files and __run('git-update-index --add --', add_files) != 0:
519         raise GitException, 'Failed git-update-index --add'
520     if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
521         raise GitException, 'Failed git-update-index --rm'
522     if m_files and __run('git-update-index --', m_files) != 0:
523         raise GitException, 'Failed git-update-index'
524
525     return True
526
527 def commit(message, files = None, parents = None, allowempty = False,
528            cache_update = True, tree_id = None,
529            author_name = None, author_email = None, author_date = None,
530            committer_name = None, committer_email = None):
531     """Commit the current tree to repository
532     """
533     if not files:
534         files = []
535     if not parents:
536         parents = []
537
538     # Get the tree status
539     if cache_update and parents != []:
540         changes = update_cache(files)
541         if not changes and not allowempty:
542             raise GitException, 'No changes to commit'
543
544     # get the commit message
545     if not message:
546         message = '\n'
547     elif message[-1:] != '\n':
548         message += '\n'
549
550     must_switch = True
551     # write the index to repository
552     if tree_id == None:
553         tree_id = _output_one_line('git-write-tree')
554     else:
555         must_switch = False
556
557     # the commit
558     cmd = ''
559     if author_name:
560         cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
561     if author_email:
562         cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
563     if author_date:
564         cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
565     if committer_name:
566         cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
567     if committer_email:
568         cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
569     cmd += 'git-commit-tree %s' % tree_id
570
571     # get the parents
572     for p in parents:
573         cmd += ' -p %s' % p
574
575     commit_id = _output_one_line(cmd, message)
576     if must_switch:
577         __set_head(commit_id)
578
579     return commit_id
580
581 def apply_diff(rev1, rev2, check_index = True, files = None):
582     """Apply the diff between rev1 and rev2 onto the current
583     index. This function doesn't need to raise an exception since it
584     is only used for fast-pushing a patch. If this operation fails,
585     the pushing would fall back to the three-way merge.
586     """
587     if check_index:
588         index_opt = '--index'
589     else:
590         index_opt = ''
591
592     if not files:
593         files = []
594
595     diff_str = diff(files, rev1, rev2)
596     if diff_str:
597         try:
598             _input_str('git-apply %s' % index_opt, diff_str)
599         except GitException:
600             return False
601
602     return True
603
604 def merge(base, head1, head2, recursive = False):
605     """Perform a 3-way merge between base, head1 and head2 into the
606     local tree
607     """
608     refresh_index()
609
610     if recursive:
611         # this operation tracks renames but it is slower (used in
612         # general when pushing or picking patches)
613         try:
614             # use _output() to mask the verbose prints of the tool
615             _output('git-merge-recursive %s -- %s %s' % (base, head1, head2))
616         except GitException:
617             pass
618     else:
619         # the fast case where we don't track renames (used when the
620         # distance between base and heads is small, i.e. folding or
621         # synchronising patches)
622         if __run('git-read-tree -u -m --aggressive',
623                  [base, head1, head2]) != 0:
624             raise GitException, 'git-read-tree failed (local changes maybe?)'
625
626     # check the index for unmerged entries
627     files = {}
628     stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
629
630     for line in _output('git-ls-files --unmerged --stage -z').split('\0'):
631         if not line:
632             continue
633
634         mode, hash, stage, path = stages_re.findall(line)[0]
635
636         if not path in files:
637             files[path] = {}
638             files[path]['1'] = ('', '')
639             files[path]['2'] = ('', '')
640             files[path]['3'] = ('', '')
641
642         files[path][stage] = (mode, hash)
643
644     # merge the unmerged files
645     errors = False
646     for path in files:
647         # remove additional files that might be generated for some
648         # newer versions of GIT
649         for suffix in [base, head1, head2]:
650             if not suffix:
651                 continue
652             fname = path + '~' + suffix
653             if os.path.exists(fname):
654                 os.remove(fname)
655
656         stages = files[path]
657         if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
658                                  stages['3'][1], path, stages['1'][0],
659                                  stages['2'][0], stages['3'][0]) != 0:
660             errors = True
661
662     if errors:
663         raise GitException, 'GIT index merging failed (possible conflicts)'
664
665 def status(files = None, modified = False, new = False, deleted = False,
666            conflict = False, unknown = False, noexclude = False):
667     """Show the tree status
668     """
669     if not files:
670         files = []
671
672     cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
673     all = not (modified or new or deleted or conflict or unknown)
674
675     if not all:
676         filestat = []
677         if modified:
678             filestat.append('M')
679         if new:
680             filestat.append('A')
681             filestat.append('N')
682         if deleted:
683             filestat.append('D')
684         if conflict:
685             filestat.append('C')
686         if unknown:
687             filestat.append('?')
688         cache_files = [x for x in cache_files if x[0] in filestat]
689
690     for fs in cache_files:
691         if all:
692             print '%s %s' % (fs[0], fs[1])
693         else:
694             print '%s' % fs[1]
695
696 def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None):
697     """Show the diff between rev1 and rev2
698     """
699     if not files:
700         files = []
701
702     if rev1 and rev2:
703         diff_str = _output(['git-diff-tree', '-p', rev1, rev2, '--'] + files)
704     elif rev1 or rev2:
705         refresh_index()
706         if rev2:
707             diff_str = _output(['git-diff-index', '-p', '-R', rev2, '--'] + files)
708         else:
709             diff_str = _output(['git-diff-index', '-p', rev1, '--'] + files)
710     else:
711         diff_str = ''
712
713     if out_fd:
714         out_fd.write(diff_str)
715     else:
716         return diff_str
717
718 def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
719     """Return the diffstat between rev1 and rev2
720     """
721     if not files:
722         files = []
723
724     p=popen2.Popen3('git-apply --stat')
725     diff(files, rev1, rev2, p.tochild)
726     p.tochild.close()
727     diff_str = p.fromchild.read().rstrip()
728     if p.wait():
729         raise GitException, 'git.diffstat failed'
730     return diff_str
731
732 def files(rev1, rev2):
733     """Return the files modified between rev1 and rev2
734     """
735
736     result = ''
737     for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
738         result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
739
740     return result.rstrip()
741
742 def barefiles(rev1, rev2):
743     """Return the files modified between rev1 and rev2, without status info
744     """
745
746     result = ''
747     for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
748         result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
749
750     return result.rstrip()
751
752 def pretty_commit(commit_id = 'HEAD'):
753     """Return a given commit (log + diff)
754     """
755     return _output(['git-diff-tree', '--cc', '--always', '--pretty', '-r',
756                     commit_id])
757
758 def checkout(files = None, tree_id = None, force = False):
759     """Check out the given or all files
760     """
761     if not files:
762         files = []
763
764     if tree_id and __run('git-read-tree --reset', [tree_id]) != 0:
765         raise GitException, 'Failed git-read-tree --reset %s' % tree_id
766
767     checkout_cmd = 'git-checkout-index -q -u'
768     if force:
769         checkout_cmd += ' -f'
770     if len(files) == 0:
771         checkout_cmd += ' -a'
772     else:
773         checkout_cmd += ' --'
774
775     if __run(checkout_cmd, files) != 0:
776         raise GitException, 'Failed git-checkout-index'
777
778 def switch(tree_id, keep = False):
779     """Switch the tree to the given id
780     """
781     if not keep:
782         refresh_index()
783         if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
784             raise GitException, 'git-read-tree failed (local changes maybe?)'
785
786     __set_head(tree_id)
787
788 def reset(files = None, tree_id = None, check_out = True):
789     """Revert the tree changes relative to the given tree_id. It removes
790     any local changes
791     """
792     if not tree_id:
793         tree_id = get_head()
794
795     if check_out:
796         cache_files = __tree_status(files, tree_id)
797         # files which were added but need to be removed
798         rm_files =  [x[1] for x in cache_files if x[0] in ['A']]
799
800         checkout(files, tree_id, True)
801         # checkout doesn't remove files
802         map(os.remove, rm_files)
803
804     # if the reset refers to the whole tree, switch the HEAD as well
805     if not files:
806         __set_head(tree_id)
807
808 def pull(repository = 'origin', refspec = None):
809     """Pull changes from the remote repository. At the moment, just
810     use the 'git-pull' command
811     """
812     # 'git-pull' updates the HEAD
813     __clear_head_cache()
814
815     args = [repository]
816     if refspec:
817         args.append(refspec)
818
819     if __run(config.get('stgit', 'pullcmd'), args) != 0:
820         raise GitException, 'Failed "git-pull %s"' % repository
821
822 def repack():
823     """Repack all objects into a single pack
824     """
825     __run('git-repack -a -d -f')
826
827 def apply_patch(filename = None, diff = None, base = None,
828                 fail_dump = True):
829     """Apply a patch onto the current or given index. There must not
830     be any local changes in the tree, otherwise the command fails
831     """
832     if diff is None:
833         if filename:
834             f = file(filename)
835         else:
836             f = sys.stdin
837         diff = f.read()
838         if filename:
839             f.close()
840
841     if base:
842         orig_head = get_head()
843         switch(base)
844     else:
845         refresh_index()
846
847     try:
848         _input_str('git-apply --index', diff)
849     except GitException:
850         if base:
851             switch(orig_head)
852         if fail_dump:
853             # write the failed diff to a file
854             f = file('.stgit-failed.patch', 'w+')
855             f.write(diff)
856             f.close()
857             print >> sys.stderr, 'Diff written to the .stgit-failed.patch file'
858
859         raise
860
861     if base:
862         top = commit(message = 'temporary commit used for applying a patch',
863                      parents = [base])
864         switch(orig_head)
865         merge(base, orig_head, top)
866
867 def clone(repository, local_dir):
868     """Clone a remote repository. At the moment, just use the
869     'git-clone' script
870     """
871     if __run('git-clone', [repository, local_dir]) != 0:
872         raise GitException, 'Failed "git-clone %s %s"' \
873               % (repository, local_dir)
874
875 def modifying_revs(files, base_rev):
876     """Return the revisions from the list modifying the given files
877     """
878     cmd = ['git-rev-list', '%s..' % base_rev, '--']
879     revs = [line.strip() for line in _output_lines(cmd + files)]
880
881     return revs