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