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 for line in _output_lines(['git-show-ref', '--heads']):
279 m = re.match('^[0-9a-f]{40} refs/heads/(.+)$', line)
280 heads.append(m.group(1))
287 """Verifies the HEAD and returns the SHA1 id that represents it
292 __head = rev_parse('HEAD')
296 """Returns the name of the file pointed to by the HEAD link
298 return strip_prefix('refs/heads/',
299 _output_one_line(['git-symbolic-ref', 'HEAD']))
301 def set_head_file(ref):
302 """Resets HEAD to point to a new ref
304 # head cache flushing is needed since we might have a different value
307 if __run('git-symbolic-ref HEAD', ['refs/heads/%s' % ref]) != 0:
308 raise GitException, 'Could not set head to "%s"' % ref
310 def set_ref(ref, val):
311 """Point ref at a new commit object."""
312 if __run('git-update-ref', [ref, val]) != 0:
313 raise GitException, 'Could not update %s to "%s".' % (ref, val)
315 def set_branch(branch, val):
316 set_ref('refs/heads/%s' % branch, val)
319 """Sets the HEAD value
323 if not __head or __head != val:
327 # only allow SHA1 hashes
328 assert(len(__head) == 40)
330 def __clear_head_cache():
331 """Sets the __head to None so that a re-read is forced
338 """Refresh index with stat() information from the working directory.
340 __run('git-update-index -q --unmerged --refresh')
342 def rev_parse(git_id):
343 """Parse the string and return a verified SHA1 id
346 return _output_one_line(['git-rev-parse', '--verify', git_id])
348 raise GitException, 'Unknown revision: %s' % git_id
357 def branch_exists(branch):
358 return ref_exists('refs/heads/%s' % branch)
360 def create_branch(new_branch, tree_id = None):
361 """Create a new branch in the git repository
363 if branch_exists(new_branch):
364 raise GitException, 'Branch "%s" already exists' % new_branch
366 current_head = get_head()
367 set_head_file(new_branch)
368 __set_head(current_head)
370 # a checkout isn't needed if new branch points to the current head
374 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
375 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
377 def switch_branch(new_branch):
378 """Switch to a git branch
382 if not branch_exists(new_branch):
383 raise GitException, 'Branch "%s" does not exist' % new_branch
385 tree_id = rev_parse('refs/heads/%s^{commit}' % new_branch)
386 if tree_id != get_head():
388 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
389 raise GitException, 'git-read-tree failed (local changes maybe?)'
391 set_head_file(new_branch)
393 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
394 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
397 if not ref_exists(ref):
398 raise GitException, '%s does not exist' % ref
399 sha1 = _output_one_line(['git-show-ref', '-s', ref])
400 if __run('git-update-ref -d %s %s' % (ref, sha1)):
401 raise GitException, 'Failed to delete ref %s' % ref
403 def delete_branch(name):
404 delete_ref('refs/heads/%s' % name)
406 def rename_ref(from_ref, to_ref):
407 if not ref_exists(from_ref):
408 raise GitException, '"%s" does not exist' % from_ref
409 if ref_exists(to_ref):
410 raise GitException, '"%s" already exists' % to_ref
412 sha1 = _output_one_line(['git-show-ref', '-s', from_ref])
413 if __run('git-update-ref %s %s %s' % (to_ref, sha1, '0'*40)):
414 raise GitException, 'Failed to create new ref %s' % to_ref
415 if __run('git-update-ref -d %s %s' % (from_ref, sha1)):
416 raise GitException, 'Failed to delete ref %s' % from_ref
418 def rename_branch(from_name, to_name):
419 """Rename a git branch."""
420 rename_ref('refs/heads/%s' % from_name, 'refs/heads/%s' % to_name)
421 if get_head_file() == from_name:
422 set_head_file(to_name)
423 reflog_dir = os.path.join(basedir.get(), 'logs', 'refs', 'heads')
424 if os.path.exists(reflog_dir) \
425 and os.path.exists(os.path.join(reflog_dir, from_name)):
426 rename(reflog_dir, from_name, to_name)
429 """Add the files or recursively add the directory contents
431 # generate the file list
434 if not os.path.exists(i):
435 raise GitException, 'Unknown file or directory: %s' % i
438 # recursive search. We only add files
439 for root, dirs, local_files in os.walk(i):
440 for name in [os.path.join(root, f) for f in local_files]:
441 if os.path.isfile(name):
442 files.append(os.path.normpath(name))
443 elif os.path.isfile(i):
444 files.append(os.path.normpath(i))
446 raise GitException, '%s is not a file or directory' % i
449 if __run('git-update-index --add --', files):
450 raise GitException, 'Unable to add file'
452 def __copy_single(source, target, target2=''):
453 """Copy file or dir named 'source' to name target+target2"""
455 # "source" (file or dir) must match one or more git-controlled file
456 realfiles = _output_lines(['git-ls-files', source])
457 if len(realfiles) == 0:
458 raise GitException, '"%s" matches no git-controled files' % source
460 if os.path.isdir(source):
461 # physically copy the files, and record them to add them in one run
463 re_string='^'+source+'/(.*)$'
464 prefix_regexp = re.compile(re_string)
465 for f in [f.strip() for f in realfiles]:
466 m = prefix_regexp.match(f)
468 raise Exception, '"%s" does not match "%s"' % (f, re_string)
469 newname = target+target2+'/'+m.group(1)
470 if not os.path.exists(os.path.dirname(newname)):
471 os.makedirs(os.path.dirname(newname))
473 newfiles.append(newname)
476 else: # files, symlinks, ...
477 newname = target+target2
478 copyfile(source, newname)
482 def copy(filespecs, target):
483 if os.path.isdir(target):
484 # target is a directory: copy each entry on the command line,
485 # with the same name, into the target
486 target = target.rstrip('/')
488 # first, check that none of the children of the target
489 # matching the command line aleady exist
490 for filespec in filespecs:
491 entry = target+ '/' + os.path.basename(filespec.rstrip('/'))
492 if os.path.exists(entry):
493 raise GitException, 'Target "%s" already exists' % entry
495 for filespec in filespecs:
496 filespec = filespec.rstrip('/')
497 basename = '/' + os.path.basename(filespec)
498 __copy_single(filespec, target, basename)
500 elif os.path.exists(target):
501 raise GitException, 'Target "%s" exists but is not a directory' % target
502 elif len(filespecs) != 1:
503 raise GitException, 'Cannot copy more than one file to non-directory'
506 # at this point: len(filespecs)==1 and target does not exist
508 # check target directory
509 targetdir = os.path.dirname(target)
510 if targetdir != '' and not os.path.isdir(targetdir):
511 raise GitException, 'Target directory "%s" does not exist' % targetdir
513 __copy_single(filespecs[0].rstrip('/'), target)
516 def rm(files, force = False):
517 """Remove a file from the repository
521 if os.path.exists(f):
522 raise GitException, '%s exists. Remove it first' %f
524 __run('git-update-index --remove --', files)
527 __run('git-update-index --force-remove --', files)
535 """Return the user information.
539 name=config.get('user.name')
540 email=config.get('user.email')
541 __user = Person(name, email)
545 """Return the author information.
550 # the environment variables take priority over config
552 date = os.environ['GIT_AUTHOR_DATE']
555 __author = Person(os.environ['GIT_AUTHOR_NAME'],
556 os.environ['GIT_AUTHOR_EMAIL'],
563 """Return the author information.
568 # the environment variables take priority over config
570 date = os.environ['GIT_COMMITTER_DATE']
573 __committer = Person(os.environ['GIT_COMMITTER_NAME'],
574 os.environ['GIT_COMMITTER_EMAIL'],
580 def update_cache(files = None, force = False):
581 """Update the cache information for the given files
586 cache_files = tree_status(files, verbose = False)
588 # everything is up-to-date
589 if len(cache_files) == 0:
592 # check for unresolved conflicts
593 if not force and [x for x in cache_files
594 if x[0] not in ['M', 'N', 'A', 'D']]:
595 raise GitException, 'Updating cache failed: unresolved conflicts'
598 add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
599 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
600 m_files = [x[1] for x in cache_files if x[0] in ['M']]
602 if add_files and __run('git-update-index --add --', add_files) != 0:
603 raise GitException, 'Failed git-update-index --add'
604 if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
605 raise GitException, 'Failed git-update-index --rm'
606 if m_files and __run('git-update-index --', m_files) != 0:
607 raise GitException, 'Failed git-update-index'
611 def commit(message, files = None, parents = None, allowempty = False,
612 cache_update = True, tree_id = None, set_head = False,
613 author_name = None, author_email = None, author_date = None,
614 committer_name = None, committer_email = None):
615 """Commit the current tree to repository
622 # Get the tree status
623 if cache_update and parents != []:
624 changes = update_cache(files)
625 if not changes and not allowempty:
626 raise GitException, 'No changes to commit'
628 # get the commit message
631 elif message[-1:] != '\n':
634 # write the index to repository
636 tree_id = _output_one_line(['git-write-tree'])
642 cmd += ['GIT_AUTHOR_NAME=%s' % author_name]
644 cmd += ['GIT_AUTHOR_EMAIL=%s' % author_email]
646 cmd += ['GIT_AUTHOR_DATE=%s' % author_date]
648 cmd += ['GIT_COMMITTER_NAME=%s' % committer_name]
650 cmd += ['GIT_COMMITTER_EMAIL=%s' % committer_email]
651 cmd += ['git-commit-tree', tree_id]
657 commit_id = _output_one_line(cmd, message)
659 __set_head(commit_id)
663 def apply_diff(rev1, rev2, check_index = True, files = None):
664 """Apply the diff between rev1 and rev2 onto the current
665 index. This function doesn't need to raise an exception since it
666 is only used for fast-pushing a patch. If this operation fails,
667 the pushing would fall back to the three-way merge.
670 index_opt = ['--index']
677 diff_str = diff(files, rev1, rev2)
680 _input_str(['git-apply'] + index_opt, diff_str)
686 def merge(base, head1, head2, recursive = False):
687 """Perform a 3-way merge between base, head1 and head2 into the
694 # this operation tracks renames but it is slower (used in
695 # general when pushing or picking patches)
697 # use _output() to mask the verbose prints of the tool
698 _output(['git-merge-recursive', base, '--', head1, head2])
699 except GitException, ex:
703 # the fast case where we don't track renames (used when the
704 # distance between base and heads is small, i.e. folding or
705 # synchronising patches)
706 if __run('git-read-tree -u -m --aggressive',
707 [base, head1, head2]) != 0:
708 raise GitException, 'git-read-tree failed (local changes maybe?)'
710 # check the index for unmerged entries
712 stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
714 for line in _output(['git-ls-files', '--unmerged', '--stage', '-z']).split('\0'):
718 mode, hash, stage, path = stages_re.findall(line)[0]
720 if not path in files:
722 files[path]['1'] = ('', '')
723 files[path]['2'] = ('', '')
724 files[path]['3'] = ('', '')
726 files[path][stage] = (mode, hash)
728 if err_output and not files:
729 # if no unmerged files, there was probably a different type of
730 # error and we have to abort the merge
731 raise GitException, err_output
733 # merge the unmerged files
736 # remove additional files that might be generated for some
737 # newer versions of GIT
738 for suffix in [base, head1, head2]:
741 fname = path + '~' + suffix
742 if os.path.exists(fname):
746 if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
747 stages['3'][1], path, stages['1'][0],
748 stages['2'][0], stages['3'][0]) != 0:
752 raise GitException, 'GIT index merging failed (possible conflicts)'
754 def status(files = None, modified = False, new = False, deleted = False,
755 conflict = False, unknown = False, noexclude = False,
757 """Show the tree status
762 cache_files = tree_status(files, unknown = True, noexclude = noexclude,
763 diff_flags = diff_flags)
764 all = not (modified or new or deleted or conflict or unknown)
779 cache_files = [x for x in cache_files if x[0] in filestat]
781 for fs in cache_files:
782 if files and not fs[1] in files:
785 out.stdout('%s %s' % (fs[0], fs[1]))
787 out.stdout('%s' % fs[1])
789 def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None,
791 """Show the diff between rev1 and rev2
797 diff_str = _output(['git-diff-tree', '-p'] + diff_flags
798 + [rev1, rev2, '--'] + files)
802 diff_str = _output(['git-diff-index', '-p', '-R']
803 + diff_flags + [rev2, '--'] + files)
805 diff_str = _output(['git-diff-index', '-p']
806 + diff_flags + [rev1, '--'] + files)
811 out_fd.write(diff_str)
815 def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
816 """Return the diffstat between rev1 and rev2
821 p=popen2.Popen3('git-apply --stat')
822 diff(files, rev1, rev2, p.tochild)
824 diff_str = p.fromchild.read().rstrip()
826 raise GitException, 'git.diffstat failed'
829 def files(rev1, rev2, diff_flags = []):
830 """Return the files modified between rev1 and rev2
834 for line in _output_lines(['git-diff-tree'] + diff_flags + ['-r', rev1, rev2]):
835 result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
837 return result.rstrip()
839 def barefiles(rev1, rev2):
840 """Return the files modified between rev1 and rev2, without status info
844 for line in _output_lines(['git-diff-tree', '-r', rev1, rev2]):
845 result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
847 return result.rstrip()
849 def pretty_commit(commit_id = 'HEAD', diff_flags = []):
850 """Return a given commit (log + diff)
852 return _output(['git-diff-tree'] + diff_flags +
853 ['--cc', '--always', '--pretty', '-r', commit_id])
855 def checkout(files = None, tree_id = None, force = False):
856 """Check out the given or all files
861 if tree_id and __run('git-read-tree --reset', [tree_id]) != 0:
862 raise GitException, 'Failed git-read-tree --reset %s' % tree_id
864 checkout_cmd = 'git-checkout-index -q -u'
866 checkout_cmd += ' -f'
868 checkout_cmd += ' -a'
870 checkout_cmd += ' --'
872 if __run(checkout_cmd, files) != 0:
873 raise GitException, 'Failed git-checkout-index'
875 def switch(tree_id, keep = False):
876 """Switch the tree to the given id
880 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
881 raise GitException, 'git-read-tree failed (local changes maybe?)'
885 def reset(files = None, tree_id = None, check_out = True):
886 """Revert the tree changes relative to the given tree_id. It removes
893 cache_files = tree_status(files, tree_id)
894 # files which were added but need to be removed
895 rm_files = [x[1] for x in cache_files if x[0] in ['A']]
897 checkout(files, tree_id, True)
898 # checkout doesn't remove files
899 map(os.remove, rm_files)
901 # if the reset refers to the whole tree, switch the HEAD as well
905 def fetch(repository = 'origin', refspec = None):
906 """Fetches changes from the remote repository, using 'git-fetch'
916 command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
917 config.get('stgit.fetchcmd')
918 if __run(command, args) != 0:
919 raise GitException, 'Failed "%s %s"' % (command, repository)
921 def pull(repository = 'origin', refspec = None):
922 """Fetches changes from the remote repository, using 'git-pull'
932 command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
933 config.get('stgit.pullcmd')
934 if __run(command, args) != 0:
935 raise GitException, 'Failed "%s %s"' % (command, repository)
938 """Repack all objects into a single pack
940 __run('git-repack -a -d -f')
942 def apply_patch(filename = None, diff = None, base = None,
944 """Apply a patch onto the current or given index. There must not
945 be any local changes in the tree, otherwise the command fails
957 orig_head = get_head()
963 _input_str(['git-apply', '--index'], diff)
968 # write the failed diff to a file
969 f = file('.stgit-failed.patch', 'w+')
972 out.warn('Diff written to the .stgit-failed.patch file')
977 top = commit(message = 'temporary commit used for applying a patch',
980 merge(base, orig_head, top)
982 def clone(repository, local_dir):
983 """Clone a remote repository. At the moment, just use the
986 if __run('git-clone', [repository, local_dir]) != 0:
987 raise GitException, 'Failed "git-clone %s %s"' \
988 % (repository, local_dir)
990 def modifying_revs(files, base_rev, head_rev):
991 """Return the revisions from the list modifying the given files
993 cmd = ['git-rev-list', '%s..%s' % (base_rev, head_rev), '--']
994 revs = [line.strip() for line in _output_lines(cmd + files)]
999 def refspec_localpart(refspec):
1000 m = re.match('^[^:]*:([^:]*)$', refspec)
1004 raise GitException, 'Cannot parse refspec "%s"' % line
1006 def refspec_remotepart(refspec):
1007 m = re.match('^([^:]*):[^:]*$', refspec)
1011 raise GitException, 'Cannot parse refspec "%s"' % line
1014 def __remotes_from_config():
1015 return config.sections_matching(r'remote\.(.*)\.url')
1017 def __remotes_from_dir(dir):
1018 d = os.path.join(basedir.get(), dir)
1019 if os.path.exists(d):
1020 return os.listdir(d)
1025 """Return the list of remotes in the repository
1028 return Set(__remotes_from_config()) | \
1029 Set(__remotes_from_dir('remotes')) | \
1030 Set(__remotes_from_dir('branches'))
1032 def remotes_local_branches(remote):
1033 """Returns the list of local branches fetched from given remote
1037 if remote in __remotes_from_config():
1038 for line in config.getall('remote.%s.fetch' % remote):
1039 branches.append(refspec_localpart(line))
1040 elif remote in __remotes_from_dir('remotes'):
1041 stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
1043 # Only consider Pull lines
1044 m = re.match('^Pull: (.*)\n$', line)
1046 branches.append(refspec_localpart(m.group(1)))
1048 elif remote in __remotes_from_dir('branches'):
1049 # old-style branches only declare one branch
1050 branches.append('refs/heads/'+remote);
1052 raise GitException, 'Unknown remote "%s"' % remote
1056 def identify_remote(branchname):
1057 """Return the name for the remote to pull the given branchname
1058 from, or None if we believe it is a local branch.
1061 for remote in remotes_list():
1062 if branchname in remotes_local_branches(remote):
1065 # if we get here we've found nothing, the branch is a local one
1069 """Return the git id for the tip of the parent branch as left by
1074 stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
1076 # Only consider lines not tagged not-for-merge
1077 m = re.match('^([^\t]*)\t\t', line)
1080 raise GitException, "StGit does not support multiple FETCH_HEAD"
1082 fetch_head=m.group(1)
1085 # here we are sure to have a single fetch_head
1089 """Return a list of all refs in the current repository.
1092 return [line.split()[1] for line in _output_lines(['git-show-ref'])]