1 """Python GIT interface
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
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.
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.
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
21 import sys, os, popen2, re, gitmergeonefile
22 from shutil import copyfile
24 from stgit import basedir
25 from stgit.utils import *
26 from stgit.config import config
30 class GitException(Exception):
40 """An author, committer, etc."""
41 def __init__(self, name = None, email = None, date = '',
43 self.name = self.email = self.date = None
44 if name or email or date:
50 assert not (name or email or date)
52 m = re.match(r'^(.+)<(.+)>(.*)$', s)
54 return [x.strip() or None for x in m.groups()]
55 self.name, self.email, self.date = parse_desc(desc)
56 def set_name(self, val):
59 def set_email(self, val):
62 def set_date(self, val):
66 if self.name and self.email:
67 return '%s <%s>' % (self.name, self.email)
69 raise GitException, 'not enough identity data'
72 """Handle the commit objects
74 def __init__(self, id_hash):
75 self.__id_hash = id_hash
77 lines = _output_lines(['git-cat-file', 'commit', id_hash])
78 for i in range(len(lines)):
82 field = line.strip().split(' ', 1)
83 if field[0] == 'tree':
84 self.__tree = field[1]
85 if field[0] == 'author':
86 self.__author = field[1]
87 if field[0] == 'committer':
88 self.__committer = field[1]
89 self.__log = ''.join(lines[i+1:])
91 def get_id_hash(self):
98 parents = self.get_parents()
104 def get_parents(self):
105 return _output_lines(['git-rev-list', '--parents', '--max-count=1',
106 self.__id_hash])[0].split()[1:]
108 def get_author(self):
111 def get_committer(self):
112 return self.__committer
118 return self.get_id_hash()
120 # dictionary of Commit objects, used to avoid multiple calls to git
127 def get_commit(id_hash):
128 """Commit objects factory. Save/look-up them in the __commits
133 if id_hash in __commits:
134 return __commits[id_hash]
136 commit = Commit(id_hash)
137 __commits[id_hash] = commit
141 """Return the list of file conflicts
143 conflicts_file = os.path.join(basedir.get(), 'conflicts')
144 if os.path.isfile(conflicts_file):
145 f = file(conflicts_file)
146 names = [line.strip() for line in f.readlines()]
152 def _input(cmd, file_desc):
153 p = popen2.Popen3(cmd, True)
155 line = file_desc.readline()
158 p.tochild.write(line)
161 raise GitException, '%s failed (%s)' % (' '.join(cmd),
162 p.childerr.read().strip())
164 def _input_str(cmd, string):
165 p = popen2.Popen3(cmd, True)
166 p.tochild.write(string)
169 raise GitException, '%s failed (%s)' % (' '.join(cmd),
170 p.childerr.read().strip())
173 p=popen2.Popen3(cmd, True)
174 output = p.fromchild.read()
176 raise GitException, '%s failed (%s)' % (' '.join(cmd),
177 p.childerr.read().strip())
180 def _output_one_line(cmd, file_desc = None):
181 p=popen2.Popen3(cmd, True)
182 if file_desc != None:
183 for line in file_desc:
184 p.tochild.write(line)
186 output = p.fromchild.readline().strip()
188 raise GitException, '%s failed (%s)' % (' '.join(cmd),
189 p.childerr.read().strip())
192 def _output_lines(cmd):
193 p=popen2.Popen3(cmd, True)
194 lines = p.fromchild.readlines()
196 raise GitException, '%s failed (%s)' % (' '.join(cmd),
197 p.childerr.read().strip())
200 def __run(cmd, args=None):
201 """__run: runs cmd using spawnvp.
203 Runs cmd using spawnvp. The shell is avoided so it won't mess up
204 our arguments. If args is very large, the command is run multiple
205 times; args is split xargs style: cmd is passed on each
206 invocation. Unlike xargs, returns immediately if any non-zero
207 return code is received.
213 for i in range(0, len(args)+1, 100):
214 r=os.spawnvp(os.P_WAIT, args_l[0], args_l + args[i:min(i+100, len(args))])
220 files = [os.path.join(basedir.get(), 'info', 'exclude')]
221 user_exclude = config.get('core.excludesfile')
223 files.append(user_exclude)
226 def tree_status(files = None, tree_id = 'HEAD', unknown = False,
227 noexclude = True, verbose = False, diff_flags = []):
228 """Returns a list of pairs - [status, filename]
231 out.start('Checking for changes in the working directory')
244 exclude = (['--exclude=%s' % s for s in
245 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
246 + ['--exclude-per-directory=.gitignore']
247 + ['--exclude-from=%s' % fn for fn in exclude_files()
248 if os.path.exists(fn)])
249 lines = _output_lines(['git-ls-files', '--others', '--directory']
251 cache_files += [('?', line.strip()) for line in lines]
254 conflicts = get_conflicts()
257 cache_files += [('C', filename) for filename in conflicts]
260 for line in _output_lines(['git-diff-index'] + diff_flags +
261 [ tree_id, '--'] + files):
262 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
263 if fs[1] not in conflicts:
264 cache_files.append(fs)
271 def local_changes(verbose = True):
272 """Return true if there are local changes in the tree
274 return len(tree_status(verbose = verbose)) != 0
278 hr = re.compile(r'^[0-9a-f]{40} refs/heads/(.+)$')
279 for line in _output_lines(['git-show-ref', '--heads']):
281 heads.append(m.group(1))
288 """Verifies the HEAD and returns the SHA1 id that represents it
293 __head = rev_parse('HEAD')
297 """Returns the name of the file pointed to by the HEAD link
299 return strip_prefix('refs/heads/',
300 _output_one_line(['git-symbolic-ref', 'HEAD']))
302 def set_head_file(ref):
303 """Resets HEAD to point to a new ref
305 # head cache flushing is needed since we might have a different value
308 if __run('git-symbolic-ref HEAD', ['refs/heads/%s' % ref]) != 0:
309 raise GitException, 'Could not set head to "%s"' % ref
311 def set_ref(ref, val):
312 """Point ref at a new commit object."""
313 if __run('git-update-ref', [ref, val]) != 0:
314 raise GitException, 'Could not update %s to "%s".' % (ref, val)
316 def set_branch(branch, val):
317 set_ref('refs/heads/%s' % branch, val)
320 """Sets the HEAD value
324 if not __head or __head != val:
328 # only allow SHA1 hashes
329 assert(len(__head) == 40)
331 def __clear_head_cache():
332 """Sets the __head to None so that a re-read is forced
339 """Refresh index with stat() information from the working directory.
341 __run('git-update-index -q --unmerged --refresh')
343 def rev_parse(git_id):
344 """Parse the string and return a verified SHA1 id
347 return _output_one_line(['git-rev-parse', '--verify', git_id])
349 raise GitException, 'Unknown revision: %s' % git_id
358 def branch_exists(branch):
359 return ref_exists('refs/heads/%s' % branch)
361 def create_branch(new_branch, tree_id = None):
362 """Create a new branch in the git repository
364 if branch_exists(new_branch):
365 raise GitException, 'Branch "%s" already exists' % new_branch
367 current_head = get_head()
368 set_head_file(new_branch)
369 __set_head(current_head)
371 # a checkout isn't needed if new branch points to the current head
375 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
376 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
378 def switch_branch(new_branch):
379 """Switch to a git branch
383 if not branch_exists(new_branch):
384 raise GitException, 'Branch "%s" does not exist' % new_branch
386 tree_id = rev_parse('refs/heads/%s^{commit}' % new_branch)
387 if tree_id != get_head():
389 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
390 raise GitException, 'git-read-tree failed (local changes maybe?)'
392 set_head_file(new_branch)
394 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
395 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
398 if not ref_exists(ref):
399 raise GitException, '%s does not exist' % ref
400 sha1 = _output_one_line(['git-show-ref', '-s', ref])
401 if __run('git-update-ref -d %s %s' % (ref, sha1)):
402 raise GitException, 'Failed to delete ref %s' % ref
404 def delete_branch(name):
405 delete_ref('refs/heads/%s' % name)
407 def rename_ref(from_ref, to_ref):
408 if not ref_exists(from_ref):
409 raise GitException, '"%s" does not exist' % from_ref
410 if ref_exists(to_ref):
411 raise GitException, '"%s" already exists' % to_ref
413 sha1 = _output_one_line(['git-show-ref', '-s', from_ref])
414 if __run('git-update-ref %s %s %s' % (to_ref, sha1, '0'*40)):
415 raise GitException, 'Failed to create new ref %s' % to_ref
416 if __run('git-update-ref -d %s %s' % (from_ref, sha1)):
417 raise GitException, 'Failed to delete ref %s' % from_ref
419 def rename_branch(from_name, to_name):
420 """Rename a git branch."""
421 rename_ref('refs/heads/%s' % from_name, 'refs/heads/%s' % to_name)
422 if get_head_file() == from_name:
423 set_head_file(to_name)
424 reflog_dir = os.path.join(basedir.get(), 'logs', 'refs', 'heads')
425 if os.path.exists(reflog_dir) \
426 and os.path.exists(os.path.join(reflog_dir, from_name)):
427 rename(reflog_dir, from_name, to_name)
430 """Add the files or recursively add the directory contents
432 # generate the file list
435 if not os.path.exists(i):
436 raise GitException, 'Unknown file or directory: %s' % i
439 # recursive search. We only add files
440 for root, dirs, local_files in os.walk(i):
441 for name in [os.path.join(root, f) for f in local_files]:
442 if os.path.isfile(name):
443 files.append(os.path.normpath(name))
444 elif os.path.isfile(i):
445 files.append(os.path.normpath(i))
447 raise GitException, '%s is not a file or directory' % i
450 if __run('git-update-index --add --', files):
451 raise GitException, 'Unable to add file'
453 def __copy_single(source, target, target2=''):
454 """Copy file or dir named 'source' to name target+target2"""
456 # "source" (file or dir) must match one or more git-controlled file
457 realfiles = _output_lines(['git-ls-files', source])
458 if len(realfiles) == 0:
459 raise GitException, '"%s" matches no git-controled files' % source
461 if os.path.isdir(source):
462 # physically copy the files, and record them to add them in one run
464 re_string='^'+source+'/(.*)$'
465 prefix_regexp = re.compile(re_string)
466 for f in [f.strip() for f in realfiles]:
467 m = prefix_regexp.match(f)
469 raise Exception, '"%s" does not match "%s"' % (f, re_string)
470 newname = target+target2+'/'+m.group(1)
471 if not os.path.exists(os.path.dirname(newname)):
472 os.makedirs(os.path.dirname(newname))
474 newfiles.append(newname)
477 else: # files, symlinks, ...
478 newname = target+target2
479 copyfile(source, newname)
483 def copy(filespecs, target):
484 if os.path.isdir(target):
485 # target is a directory: copy each entry on the command line,
486 # with the same name, into the target
487 target = target.rstrip('/')
489 # first, check that none of the children of the target
490 # matching the command line aleady exist
491 for filespec in filespecs:
492 entry = target+ '/' + os.path.basename(filespec.rstrip('/'))
493 if os.path.exists(entry):
494 raise GitException, 'Target "%s" already exists' % entry
496 for filespec in filespecs:
497 filespec = filespec.rstrip('/')
498 basename = '/' + os.path.basename(filespec)
499 __copy_single(filespec, target, basename)
501 elif os.path.exists(target):
502 raise GitException, 'Target "%s" exists but is not a directory' % target
503 elif len(filespecs) != 1:
504 raise GitException, 'Cannot copy more than one file to non-directory'
507 # at this point: len(filespecs)==1 and target does not exist
509 # check target directory
510 targetdir = os.path.dirname(target)
511 if targetdir != '' and not os.path.isdir(targetdir):
512 raise GitException, 'Target directory "%s" does not exist' % targetdir
514 __copy_single(filespecs[0].rstrip('/'), target)
517 def rm(files, force = False):
518 """Remove a file from the repository
522 if os.path.exists(f):
523 raise GitException, '%s exists. Remove it first' %f
525 __run('git-update-index --remove --', files)
528 __run('git-update-index --force-remove --', files)
536 """Return the user information.
540 name=config.get('user.name')
541 email=config.get('user.email')
542 __user = Person(name, email)
546 """Return the author information.
551 # the environment variables take priority over config
553 date = os.environ['GIT_AUTHOR_DATE']
556 __author = Person(os.environ['GIT_AUTHOR_NAME'],
557 os.environ['GIT_AUTHOR_EMAIL'],
564 """Return the author information.
569 # the environment variables take priority over config
571 date = os.environ['GIT_COMMITTER_DATE']
574 __committer = Person(os.environ['GIT_COMMITTER_NAME'],
575 os.environ['GIT_COMMITTER_EMAIL'],
581 def update_cache(files = None, force = False):
582 """Update the cache information for the given files
587 cache_files = tree_status(files, verbose = False)
589 # everything is up-to-date
590 if len(cache_files) == 0:
593 # check for unresolved conflicts
594 if not force and [x for x in cache_files
595 if x[0] not in ['M', 'N', 'A', 'D']]:
596 raise GitException, 'Updating cache failed: unresolved conflicts'
599 add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
600 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
601 m_files = [x[1] for x in cache_files if x[0] in ['M']]
603 if add_files and __run('git-update-index --add --', add_files) != 0:
604 raise GitException, 'Failed git-update-index --add'
605 if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
606 raise GitException, 'Failed git-update-index --rm'
607 if m_files and __run('git-update-index --', m_files) != 0:
608 raise GitException, 'Failed git-update-index'
612 def commit(message, files = None, parents = None, allowempty = False,
613 cache_update = True, tree_id = None, set_head = False,
614 author_name = None, author_email = None, author_date = None,
615 committer_name = None, committer_email = None):
616 """Commit the current tree to repository
623 # Get the tree status
624 if cache_update and parents != []:
625 changes = update_cache(files)
626 if not changes and not allowempty:
627 raise GitException, 'No changes to commit'
629 # get the commit message
632 elif message[-1:] != '\n':
635 # write the index to repository
637 tree_id = _output_one_line(['git-write-tree'])
643 cmd += ['GIT_AUTHOR_NAME=%s' % author_name]
645 cmd += ['GIT_AUTHOR_EMAIL=%s' % author_email]
647 cmd += ['GIT_AUTHOR_DATE=%s' % author_date]
649 cmd += ['GIT_COMMITTER_NAME=%s' % committer_name]
651 cmd += ['GIT_COMMITTER_EMAIL=%s' % committer_email]
652 cmd += ['git-commit-tree', tree_id]
658 commit_id = _output_one_line(cmd, message)
660 __set_head(commit_id)
664 def apply_diff(rev1, rev2, check_index = True, files = None):
665 """Apply the diff between rev1 and rev2 onto the current
666 index. This function doesn't need to raise an exception since it
667 is only used for fast-pushing a patch. If this operation fails,
668 the pushing would fall back to the three-way merge.
671 index_opt = ['--index']
678 diff_str = diff(files, rev1, rev2)
681 _input_str(['git-apply'] + index_opt, diff_str)
687 def merge(base, head1, head2, recursive = False):
688 """Perform a 3-way merge between base, head1 and head2 into the
695 # this operation tracks renames but it is slower (used in
696 # general when pushing or picking patches)
698 # use _output() to mask the verbose prints of the tool
699 _output(['git-merge-recursive', base, '--', head1, head2])
700 except GitException, ex:
704 # the fast case where we don't track renames (used when the
705 # distance between base and heads is small, i.e. folding or
706 # synchronising patches)
707 if __run('git-read-tree -u -m --aggressive',
708 [base, head1, head2]) != 0:
709 raise GitException, 'git-read-tree failed (local changes maybe?)'
711 # check the index for unmerged entries
713 stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
715 for line in _output(['git-ls-files', '--unmerged', '--stage', '-z']).split('\0'):
719 mode, hash, stage, path = stages_re.findall(line)[0]
721 if not path in files:
723 files[path]['1'] = ('', '')
724 files[path]['2'] = ('', '')
725 files[path]['3'] = ('', '')
727 files[path][stage] = (mode, hash)
729 if err_output and not files:
730 # if no unmerged files, there was probably a different type of
731 # error and we have to abort the merge
732 raise GitException, err_output
734 # merge the unmerged files
737 # remove additional files that might be generated for some
738 # newer versions of GIT
739 for suffix in [base, head1, head2]:
742 fname = path + '~' + suffix
743 if os.path.exists(fname):
747 if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
748 stages['3'][1], path, stages['1'][0],
749 stages['2'][0], stages['3'][0]) != 0:
753 raise GitException, 'GIT index merging failed (possible conflicts)'
755 def status(files = None, modified = False, new = False, deleted = False,
756 conflict = False, unknown = False, noexclude = False,
758 """Show the tree status
763 cache_files = tree_status(files, unknown = True, noexclude = noexclude,
764 diff_flags = diff_flags)
765 all = not (modified or new or deleted or conflict or unknown)
780 cache_files = [x for x in cache_files if x[0] in filestat]
782 for fs in cache_files:
783 if files and not fs[1] in files:
786 out.stdout('%s %s' % (fs[0], fs[1]))
788 out.stdout('%s' % fs[1])
790 def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None,
792 """Show the diff between rev1 and rev2
798 diff_str = _output(['git-diff-tree', '-p'] + diff_flags
799 + [rev1, rev2, '--'] + files)
803 diff_str = _output(['git-diff-index', '-p', '-R']
804 + diff_flags + [rev2, '--'] + files)
806 diff_str = _output(['git-diff-index', '-p']
807 + diff_flags + [rev1, '--'] + files)
812 out_fd.write(diff_str)
816 def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
817 """Return the diffstat between rev1 and rev2
822 p=popen2.Popen3('git-apply --stat')
823 diff(files, rev1, rev2, p.tochild)
825 diff_str = p.fromchild.read().rstrip()
827 raise GitException, 'git.diffstat failed'
830 def files(rev1, rev2, diff_flags = []):
831 """Return the files modified between rev1 and rev2
835 for line in _output_lines(['git-diff-tree'] + diff_flags + ['-r', rev1, rev2]):
836 result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
838 return result.rstrip()
840 def barefiles(rev1, rev2):
841 """Return the files modified between rev1 and rev2, without status info
845 for line in _output_lines(['git-diff-tree', '-r', rev1, rev2]):
846 result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
848 return result.rstrip()
850 def pretty_commit(commit_id = 'HEAD', diff_flags = []):
851 """Return a given commit (log + diff)
853 return _output(['git-diff-tree'] + diff_flags +
854 ['--cc', '--always', '--pretty', '-r', commit_id])
856 def checkout(files = None, tree_id = None, force = False):
857 """Check out the given or all files
862 if tree_id and __run('git-read-tree --reset', [tree_id]) != 0:
863 raise GitException, 'Failed git-read-tree --reset %s' % tree_id
865 checkout_cmd = 'git-checkout-index -q -u'
867 checkout_cmd += ' -f'
869 checkout_cmd += ' -a'
871 checkout_cmd += ' --'
873 if __run(checkout_cmd, files) != 0:
874 raise GitException, 'Failed git-checkout-index'
876 def switch(tree_id, keep = False):
877 """Switch the tree to the given id
881 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
882 raise GitException, 'git-read-tree failed (local changes maybe?)'
886 def reset(files = None, tree_id = None, check_out = True):
887 """Revert the tree changes relative to the given tree_id. It removes
894 cache_files = tree_status(files, tree_id)
895 # files which were added but need to be removed
896 rm_files = [x[1] for x in cache_files if x[0] in ['A']]
898 checkout(files, tree_id, True)
899 # checkout doesn't remove files
900 map(os.remove, rm_files)
902 # if the reset refers to the whole tree, switch the HEAD as well
906 def fetch(repository = 'origin', refspec = None):
907 """Fetches changes from the remote repository, using 'git-fetch'
917 command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
918 config.get('stgit.fetchcmd')
919 if __run(command, args) != 0:
920 raise GitException, 'Failed "%s %s"' % (command, repository)
922 def pull(repository = 'origin', refspec = None):
923 """Fetches changes from the remote repository, using 'git-pull'
933 command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
934 config.get('stgit.pullcmd')
935 if __run(command, args) != 0:
936 raise GitException, 'Failed "%s %s"' % (command, repository)
939 """Repack all objects into a single pack
941 __run('git-repack -a -d -f')
943 def apply_patch(filename = None, diff = None, base = None,
945 """Apply a patch onto the current or given index. There must not
946 be any local changes in the tree, otherwise the command fails
958 orig_head = get_head()
964 _input_str(['git-apply', '--index'], diff)
969 # write the failed diff to a file
970 f = file('.stgit-failed.patch', 'w+')
973 out.warn('Diff written to the .stgit-failed.patch file')
978 top = commit(message = 'temporary commit used for applying a patch',
981 merge(base, orig_head, top)
983 def clone(repository, local_dir):
984 """Clone a remote repository. At the moment, just use the
987 if __run('git-clone', [repository, local_dir]) != 0:
988 raise GitException, 'Failed "git-clone %s %s"' \
989 % (repository, local_dir)
991 def modifying_revs(files, base_rev, head_rev):
992 """Return the revisions from the list modifying the given files
994 cmd = ['git-rev-list', '%s..%s' % (base_rev, head_rev), '--']
995 revs = [line.strip() for line in _output_lines(cmd + files)]
1000 def refspec_localpart(refspec):
1001 m = re.match('^[^:]*:([^:]*)$', refspec)
1005 raise GitException, 'Cannot parse refspec "%s"' % line
1007 def refspec_remotepart(refspec):
1008 m = re.match('^([^:]*):[^:]*$', refspec)
1012 raise GitException, 'Cannot parse refspec "%s"' % line
1015 def __remotes_from_config():
1016 return config.sections_matching(r'remote\.(.*)\.url')
1018 def __remotes_from_dir(dir):
1019 d = os.path.join(basedir.get(), dir)
1020 if os.path.exists(d):
1021 return os.listdir(d)
1026 """Return the list of remotes in the repository
1029 return Set(__remotes_from_config()) | \
1030 Set(__remotes_from_dir('remotes')) | \
1031 Set(__remotes_from_dir('branches'))
1033 def remotes_local_branches(remote):
1034 """Returns the list of local branches fetched from given remote
1038 if remote in __remotes_from_config():
1039 for line in config.getall('remote.%s.fetch' % remote):
1040 branches.append(refspec_localpart(line))
1041 elif remote in __remotes_from_dir('remotes'):
1042 stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
1044 # Only consider Pull lines
1045 m = re.match('^Pull: (.*)\n$', line)
1047 branches.append(refspec_localpart(m.group(1)))
1049 elif remote in __remotes_from_dir('branches'):
1050 # old-style branches only declare one branch
1051 branches.append('refs/heads/'+remote);
1053 raise GitException, 'Unknown remote "%s"' % remote
1057 def identify_remote(branchname):
1058 """Return the name for the remote to pull the given branchname
1059 from, or None if we believe it is a local branch.
1062 for remote in remotes_list():
1063 if branchname in remotes_local_branches(remote):
1066 # if we get here we've found nothing, the branch is a local one
1070 """Return the git id for the tip of the parent branch as left by
1075 stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
1077 # Only consider lines not tagged not-for-merge
1078 m = re.match('^([^\t]*)\t\t', line)
1081 raise GitException, "StGit does not support multiple FETCH_HEAD"
1083 fetch_head=m.group(1)
1086 # here we are sure to have a single fetch_head
1090 """Return a list of all refs in the current repository.
1093 return [line.split()[1] for line in _output_lines(['git-show-ref'])]