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.out import *
27 from stgit.config import config
31 class GitException(Exception):
41 """An author, committer, etc."""
42 def __init__(self, name = None, email = None, date = '',
44 self.name = self.email = self.date = None
45 if name or email or date:
51 assert not (name or email or date)
53 m = re.match(r'^(.+)<(.+)>(.*)$', s)
55 return [x.strip() or None for x in m.groups()]
56 self.name, self.email, self.date = parse_desc(desc)
57 def set_name(self, val):
60 def set_email(self, val):
63 def set_date(self, val):
67 if self.name and self.email:
68 return '%s <%s>' % (self.name, self.email)
70 raise GitException, 'not enough identity data'
73 """Handle the commit objects
75 def __init__(self, id_hash):
76 self.__id_hash = id_hash
78 lines = _output_lines(['git-cat-file', 'commit', id_hash])
79 for i in range(len(lines)):
83 field = line.strip().split(' ', 1)
84 if field[0] == 'tree':
85 self.__tree = field[1]
86 if field[0] == 'author':
87 self.__author = field[1]
88 if field[0] == 'committer':
89 self.__committer = field[1]
90 self.__log = ''.join(lines[i+1:])
92 def get_id_hash(self):
99 parents = self.get_parents()
105 def get_parents(self):
106 return _output_lines(['git-rev-list', '--parents', '--max-count=1',
107 self.__id_hash])[0].split()[1:]
109 def get_author(self):
112 def get_committer(self):
113 return self.__committer
119 return self.get_id_hash()
121 # dictionary of Commit objects, used to avoid multiple calls to git
128 def get_commit(id_hash):
129 """Commit objects factory. Save/look-up them in the __commits
134 if id_hash in __commits:
135 return __commits[id_hash]
137 commit = Commit(id_hash)
138 __commits[id_hash] = commit
142 """Return the list of file conflicts
144 conflicts_file = os.path.join(basedir.get(), 'conflicts')
145 if os.path.isfile(conflicts_file):
146 f = file(conflicts_file)
147 names = [line.strip() for line in f.readlines()]
153 def _input(cmd, file_desc):
154 p = popen2.Popen3(cmd, True)
156 line = file_desc.readline()
159 p.tochild.write(line)
162 raise GitException, '%s failed (%s)' % (' '.join(cmd),
163 p.childerr.read().strip())
165 def _input_str(cmd, string):
166 p = popen2.Popen3(cmd, True)
167 p.tochild.write(string)
170 raise GitException, '%s failed (%s)' % (' '.join(cmd),
171 p.childerr.read().strip())
174 p=popen2.Popen3(cmd, True)
175 output = p.fromchild.read()
177 raise GitException, '%s failed (%s)' % (' '.join(cmd),
178 p.childerr.read().strip())
181 def _output_one_line(cmd, file_desc = None):
182 p=popen2.Popen3(cmd, True)
183 if file_desc != None:
184 for line in file_desc:
185 p.tochild.write(line)
187 output = p.fromchild.readline().strip()
189 raise GitException, '%s failed (%s)' % (' '.join(cmd),
190 p.childerr.read().strip())
193 def _output_lines(cmd):
194 p=popen2.Popen3(cmd, True)
195 lines = p.fromchild.readlines()
197 raise GitException, '%s failed (%s)' % (' '.join(cmd),
198 p.childerr.read().strip())
201 def __run(cmd, args=None):
202 """__run: runs cmd using spawnvp.
204 Runs cmd using spawnvp. The shell is avoided so it won't mess up
205 our arguments. If args is very large, the command is run multiple
206 times; args is split xargs style: cmd is passed on each
207 invocation. Unlike xargs, returns immediately if any non-zero
208 return code is received.
214 for i in range(0, len(args)+1, 100):
215 r=os.spawnvp(os.P_WAIT, args_l[0], args_l + args[i:min(i+100, len(args))])
221 files = [os.path.join(basedir.get(), 'info', 'exclude')]
222 user_exclude = config.get('core.excludesfile')
224 files.append(user_exclude)
227 def tree_status(files = None, tree_id = 'HEAD', unknown = False,
228 noexclude = True, verbose = False, diff_flags = []):
229 """Returns a list of pairs - [status, filename]
232 out.start('Checking for changes in the working directory')
245 exclude = (['--exclude=%s' % s for s in
246 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
247 + ['--exclude-per-directory=.gitignore']
248 + ['--exclude-from=%s' % fn for fn in exclude_files()
249 if os.path.exists(fn)])
250 lines = _output_lines(['git-ls-files', '--others', '--directory']
252 cache_files += [('?', line.strip()) for line in lines]
255 conflicts = get_conflicts()
258 cache_files += [('C', filename) for filename in conflicts]
261 for line in _output_lines(['git-diff-index'] + diff_flags +
262 [ tree_id, '--'] + files):
263 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
264 if fs[1] not in conflicts:
265 cache_files.append(fs)
272 def local_changes(verbose = True):
273 """Return true if there are local changes in the tree
275 return len(tree_status(verbose = verbose)) != 0
279 hr = re.compile(r'^[0-9a-f]{40} refs/heads/(.+)$')
280 for line in _output_lines(['git-show-ref', '--heads']):
282 heads.append(m.group(1))
289 """Verifies the HEAD and returns the SHA1 id that represents it
294 __head = rev_parse('HEAD')
298 """Returns the name of the file pointed to by the HEAD link
300 return strip_prefix('refs/heads/',
301 _output_one_line(['git-symbolic-ref', 'HEAD']))
303 def set_head_file(ref):
304 """Resets HEAD to point to a new ref
306 # head cache flushing is needed since we might have a different value
309 if __run('git-symbolic-ref HEAD', ['refs/heads/%s' % ref]) != 0:
310 raise GitException, 'Could not set head to "%s"' % ref
312 def set_ref(ref, val):
313 """Point ref at a new commit object."""
314 if __run('git-update-ref', [ref, val]) != 0:
315 raise GitException, 'Could not update %s to "%s".' % (ref, val)
317 def set_branch(branch, val):
318 set_ref('refs/heads/%s' % branch, val)
321 """Sets the HEAD value
325 if not __head or __head != val:
329 # only allow SHA1 hashes
330 assert(len(__head) == 40)
332 def __clear_head_cache():
333 """Sets the __head to None so that a re-read is forced
340 """Refresh index with stat() information from the working directory.
342 __run('git-update-index -q --unmerged --refresh')
344 def rev_parse(git_id):
345 """Parse the string and return a verified SHA1 id
348 return _output_one_line(['git-rev-parse', '--verify', git_id])
350 raise GitException, 'Unknown revision: %s' % git_id
359 def branch_exists(branch):
360 return ref_exists('refs/heads/%s' % branch)
362 def create_branch(new_branch, tree_id = None):
363 """Create a new branch in the git repository
365 if branch_exists(new_branch):
366 raise GitException, 'Branch "%s" already exists' % new_branch
368 current_head = get_head()
369 set_head_file(new_branch)
370 __set_head(current_head)
372 # a checkout isn't needed if new branch points to the current head
376 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
377 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
379 def switch_branch(new_branch):
380 """Switch to a git branch
384 if not branch_exists(new_branch):
385 raise GitException, 'Branch "%s" does not exist' % new_branch
387 tree_id = rev_parse('refs/heads/%s^{commit}' % new_branch)
388 if tree_id != get_head():
390 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
391 raise GitException, 'git-read-tree failed (local changes maybe?)'
393 set_head_file(new_branch)
395 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
396 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
399 if not ref_exists(ref):
400 raise GitException, '%s does not exist' % ref
401 sha1 = _output_one_line(['git-show-ref', '-s', ref])
402 if __run('git-update-ref -d %s %s' % (ref, sha1)):
403 raise GitException, 'Failed to delete ref %s' % ref
405 def delete_branch(name):
406 delete_ref('refs/heads/%s' % name)
408 def rename_ref(from_ref, to_ref):
409 if not ref_exists(from_ref):
410 raise GitException, '"%s" does not exist' % from_ref
411 if ref_exists(to_ref):
412 raise GitException, '"%s" already exists' % to_ref
414 sha1 = _output_one_line(['git-show-ref', '-s', from_ref])
415 if __run('git-update-ref %s %s %s' % (to_ref, sha1, '0'*40)):
416 raise GitException, 'Failed to create new ref %s' % to_ref
417 if __run('git-update-ref -d %s %s' % (from_ref, sha1)):
418 raise GitException, 'Failed to delete ref %s' % from_ref
420 def rename_branch(from_name, to_name):
421 """Rename a git branch."""
422 rename_ref('refs/heads/%s' % from_name, 'refs/heads/%s' % to_name)
423 if get_head_file() == from_name:
424 set_head_file(to_name)
425 reflog_dir = os.path.join(basedir.get(), 'logs', 'refs', 'heads')
426 if os.path.exists(reflog_dir) \
427 and os.path.exists(os.path.join(reflog_dir, from_name)):
428 rename(reflog_dir, from_name, to_name)
431 """Add the files or recursively add the directory contents
433 # generate the file list
436 if not os.path.exists(i):
437 raise GitException, 'Unknown file or directory: %s' % i
440 # recursive search. We only add files
441 for root, dirs, local_files in os.walk(i):
442 for name in [os.path.join(root, f) for f in local_files]:
443 if os.path.isfile(name):
444 files.append(os.path.normpath(name))
445 elif os.path.isfile(i):
446 files.append(os.path.normpath(i))
448 raise GitException, '%s is not a file or directory' % i
451 if __run('git-update-index --add --', files):
452 raise GitException, 'Unable to add file'
454 def __copy_single(source, target, target2=''):
455 """Copy file or dir named 'source' to name target+target2"""
457 # "source" (file or dir) must match one or more git-controlled file
458 realfiles = _output_lines(['git-ls-files', source])
459 if len(realfiles) == 0:
460 raise GitException, '"%s" matches no git-controled files' % source
462 if os.path.isdir(source):
463 # physically copy the files, and record them to add them in one run
465 re_string='^'+source+'/(.*)$'
466 prefix_regexp = re.compile(re_string)
467 for f in [f.strip() for f in realfiles]:
468 m = prefix_regexp.match(f)
470 raise Exception, '"%s" does not match "%s"' % (f, re_string)
471 newname = target+target2+'/'+m.group(1)
472 if not os.path.exists(os.path.dirname(newname)):
473 os.makedirs(os.path.dirname(newname))
475 newfiles.append(newname)
478 else: # files, symlinks, ...
479 newname = target+target2
480 copyfile(source, newname)
484 def copy(filespecs, target):
485 if os.path.isdir(target):
486 # target is a directory: copy each entry on the command line,
487 # with the same name, into the target
488 target = target.rstrip('/')
490 # first, check that none of the children of the target
491 # matching the command line aleady exist
492 for filespec in filespecs:
493 entry = target+ '/' + os.path.basename(filespec.rstrip('/'))
494 if os.path.exists(entry):
495 raise GitException, 'Target "%s" already exists' % entry
497 for filespec in filespecs:
498 filespec = filespec.rstrip('/')
499 basename = '/' + os.path.basename(filespec)
500 __copy_single(filespec, target, basename)
502 elif os.path.exists(target):
503 raise GitException, 'Target "%s" exists but is not a directory' % target
504 elif len(filespecs) != 1:
505 raise GitException, 'Cannot copy more than one file to non-directory'
508 # at this point: len(filespecs)==1 and target does not exist
510 # check target directory
511 targetdir = os.path.dirname(target)
512 if targetdir != '' and not os.path.isdir(targetdir):
513 raise GitException, 'Target directory "%s" does not exist' % targetdir
515 __copy_single(filespecs[0].rstrip('/'), target)
518 def rm(files, force = False):
519 """Remove a file from the repository
523 if os.path.exists(f):
524 raise GitException, '%s exists. Remove it first' %f
526 __run('git-update-index --remove --', files)
529 __run('git-update-index --force-remove --', files)
537 """Return the user information.
541 name=config.get('user.name')
542 email=config.get('user.email')
543 __user = Person(name, email)
547 """Return the author information.
552 # the environment variables take priority over config
554 date = os.environ['GIT_AUTHOR_DATE']
557 __author = Person(os.environ['GIT_AUTHOR_NAME'],
558 os.environ['GIT_AUTHOR_EMAIL'],
565 """Return the author information.
570 # the environment variables take priority over config
572 date = os.environ['GIT_COMMITTER_DATE']
575 __committer = Person(os.environ['GIT_COMMITTER_NAME'],
576 os.environ['GIT_COMMITTER_EMAIL'],
582 def update_cache(files = None, force = False):
583 """Update the cache information for the given files
588 cache_files = tree_status(files, verbose = False)
590 # everything is up-to-date
591 if len(cache_files) == 0:
594 # check for unresolved conflicts
595 if not force and [x for x in cache_files
596 if x[0] not in ['M', 'N', 'A', 'D']]:
597 raise GitException, 'Updating cache failed: unresolved conflicts'
600 add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
601 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
602 m_files = [x[1] for x in cache_files if x[0] in ['M']]
604 if add_files and __run('git-update-index --add --', add_files) != 0:
605 raise GitException, 'Failed git-update-index --add'
606 if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
607 raise GitException, 'Failed git-update-index --rm'
608 if m_files and __run('git-update-index --', m_files) != 0:
609 raise GitException, 'Failed git-update-index'
613 def commit(message, files = None, parents = None, allowempty = False,
614 cache_update = True, tree_id = None, set_head = False,
615 author_name = None, author_email = None, author_date = None,
616 committer_name = None, committer_email = None):
617 """Commit the current tree to repository
624 # Get the tree status
625 if cache_update and parents != []:
626 changes = update_cache(files)
627 if not changes and not allowempty:
628 raise GitException, 'No changes to commit'
630 # get the commit message
633 elif message[-1:] != '\n':
636 # write the index to repository
638 tree_id = _output_one_line(['git-write-tree'])
644 cmd += ['GIT_AUTHOR_NAME=%s' % author_name]
646 cmd += ['GIT_AUTHOR_EMAIL=%s' % author_email]
648 cmd += ['GIT_AUTHOR_DATE=%s' % author_date]
650 cmd += ['GIT_COMMITTER_NAME=%s' % committer_name]
652 cmd += ['GIT_COMMITTER_EMAIL=%s' % committer_email]
653 cmd += ['git-commit-tree', tree_id]
659 commit_id = _output_one_line(cmd, message)
661 __set_head(commit_id)
665 def apply_diff(rev1, rev2, check_index = True, files = None):
666 """Apply the diff between rev1 and rev2 onto the current
667 index. This function doesn't need to raise an exception since it
668 is only used for fast-pushing a patch. If this operation fails,
669 the pushing would fall back to the three-way merge.
672 index_opt = ['--index']
679 diff_str = diff(files, rev1, rev2)
682 _input_str(['git-apply'] + index_opt, diff_str)
688 def merge(base, head1, head2, recursive = False):
689 """Perform a 3-way merge between base, head1 and head2 into the
696 # this operation tracks renames but it is slower (used in
697 # general when pushing or picking patches)
699 # use _output() to mask the verbose prints of the tool
700 _output(['git-merge-recursive', base, '--', head1, head2])
701 except GitException, ex:
705 # the fast case where we don't track renames (used when the
706 # distance between base and heads is small, i.e. folding or
707 # synchronising patches)
708 if __run('git-read-tree -u -m --aggressive',
709 [base, head1, head2]) != 0:
710 raise GitException, 'git-read-tree failed (local changes maybe?)'
712 # check the index for unmerged entries
714 stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
716 for line in _output(['git-ls-files', '--unmerged', '--stage', '-z']).split('\0'):
720 mode, hash, stage, path = stages_re.findall(line)[0]
722 if not path in files:
724 files[path]['1'] = ('', '')
725 files[path]['2'] = ('', '')
726 files[path]['3'] = ('', '')
728 files[path][stage] = (mode, hash)
730 if err_output and not files:
731 # if no unmerged files, there was probably a different type of
732 # error and we have to abort the merge
733 raise GitException, err_output
735 # merge the unmerged files
738 # remove additional files that might be generated for some
739 # newer versions of GIT
740 for suffix in [base, head1, head2]:
743 fname = path + '~' + suffix
744 if os.path.exists(fname):
748 if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
749 stages['3'][1], path, stages['1'][0],
750 stages['2'][0], stages['3'][0]) != 0:
754 raise GitException, 'GIT index merging failed (possible conflicts)'
756 def status(files = None, modified = False, new = False, deleted = False,
757 conflict = False, unknown = False, noexclude = False,
759 """Show the tree status
764 cache_files = tree_status(files, unknown = True, noexclude = noexclude,
765 diff_flags = diff_flags)
766 all = not (modified or new or deleted or conflict or unknown)
781 cache_files = [x for x in cache_files if x[0] in filestat]
783 for fs in cache_files:
784 if files and not fs[1] in files:
787 out.stdout('%s %s' % (fs[0], fs[1]))
789 out.stdout('%s' % fs[1])
791 def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None,
793 """Show the diff between rev1 and rev2
799 diff_str = _output(['git-diff-tree', '-p'] + diff_flags
800 + [rev1, rev2, '--'] + files)
804 diff_str = _output(['git-diff-index', '-p', '-R']
805 + diff_flags + [rev2, '--'] + files)
807 diff_str = _output(['git-diff-index', '-p']
808 + diff_flags + [rev1, '--'] + files)
813 out_fd.write(diff_str)
817 def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
818 """Return the diffstat between rev1 and rev2
823 p=popen2.Popen3('git-apply --stat')
824 diff(files, rev1, rev2, p.tochild)
826 diff_str = p.fromchild.read().rstrip()
828 raise GitException, 'git.diffstat failed'
831 def files(rev1, rev2, diff_flags = []):
832 """Return the files modified between rev1 and rev2
836 for line in _output_lines(['git-diff-tree'] + diff_flags + ['-r', rev1, rev2]):
837 result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
839 return result.rstrip()
841 def barefiles(rev1, rev2):
842 """Return the files modified between rev1 and rev2, without status info
846 for line in _output_lines(['git-diff-tree', '-r', rev1, rev2]):
847 result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
849 return result.rstrip()
851 def pretty_commit(commit_id = 'HEAD', diff_flags = []):
852 """Return a given commit (log + diff)
854 return _output(['git-diff-tree'] + diff_flags +
855 ['--cc', '--always', '--pretty', '-r', commit_id])
857 def checkout(files = None, tree_id = None, force = False):
858 """Check out the given or all files
863 if tree_id and __run('git-read-tree --reset', [tree_id]) != 0:
864 raise GitException, 'Failed git-read-tree --reset %s' % tree_id
866 checkout_cmd = 'git-checkout-index -q -u'
868 checkout_cmd += ' -f'
870 checkout_cmd += ' -a'
872 checkout_cmd += ' --'
874 if __run(checkout_cmd, files) != 0:
875 raise GitException, 'Failed git-checkout-index'
877 def switch(tree_id, keep = False):
878 """Switch the tree to the given id
882 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
883 raise GitException, 'git-read-tree failed (local changes maybe?)'
887 def reset(files = None, tree_id = None, check_out = True):
888 """Revert the tree changes relative to the given tree_id. It removes
895 cache_files = tree_status(files, tree_id)
896 # files which were added but need to be removed
897 rm_files = [x[1] for x in cache_files if x[0] in ['A']]
899 checkout(files, tree_id, True)
900 # checkout doesn't remove files
901 map(os.remove, rm_files)
903 # if the reset refers to the whole tree, switch the HEAD as well
907 def fetch(repository = 'origin', refspec = None):
908 """Fetches changes from the remote repository, using 'git-fetch'
918 command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
919 config.get('stgit.fetchcmd')
920 if __run(command, args) != 0:
921 raise GitException, 'Failed "%s %s"' % (command, repository)
923 def pull(repository = 'origin', refspec = None):
924 """Fetches changes from the remote repository, using 'git-pull'
934 command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
935 config.get('stgit.pullcmd')
936 if __run(command, args) != 0:
937 raise GitException, 'Failed "%s %s"' % (command, repository)
940 """Repack all objects into a single pack
942 __run('git-repack -a -d -f')
944 def apply_patch(filename = None, diff = None, base = None,
946 """Apply a patch onto the current or given index. There must not
947 be any local changes in the tree, otherwise the command fails
959 orig_head = get_head()
965 _input_str(['git-apply', '--index'], diff)
970 # write the failed diff to a file
971 f = file('.stgit-failed.patch', 'w+')
974 out.warn('Diff written to the .stgit-failed.patch file')
979 top = commit(message = 'temporary commit used for applying a patch',
982 merge(base, orig_head, top)
984 def clone(repository, local_dir):
985 """Clone a remote repository. At the moment, just use the
988 if __run('git-clone', [repository, local_dir]) != 0:
989 raise GitException, 'Failed "git-clone %s %s"' \
990 % (repository, local_dir)
992 def modifying_revs(files, base_rev, head_rev):
993 """Return the revisions from the list modifying the given files
995 cmd = ['git-rev-list', '%s..%s' % (base_rev, head_rev), '--']
996 revs = [line.strip() for line in _output_lines(cmd + files)]
1001 def refspec_localpart(refspec):
1002 m = re.match('^[^:]*:([^:]*)$', refspec)
1006 raise GitException, 'Cannot parse refspec "%s"' % line
1008 def refspec_remotepart(refspec):
1009 m = re.match('^([^:]*):[^:]*$', refspec)
1013 raise GitException, 'Cannot parse refspec "%s"' % line
1016 def __remotes_from_config():
1017 return config.sections_matching(r'remote\.(.*)\.url')
1019 def __remotes_from_dir(dir):
1020 d = os.path.join(basedir.get(), dir)
1021 if os.path.exists(d):
1022 return os.listdir(d)
1027 """Return the list of remotes in the repository
1030 return Set(__remotes_from_config()) | \
1031 Set(__remotes_from_dir('remotes')) | \
1032 Set(__remotes_from_dir('branches'))
1034 def remotes_local_branches(remote):
1035 """Returns the list of local branches fetched from given remote
1039 if remote in __remotes_from_config():
1040 for line in config.getall('remote.%s.fetch' % remote):
1041 branches.append(refspec_localpart(line))
1042 elif remote in __remotes_from_dir('remotes'):
1043 stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
1045 # Only consider Pull lines
1046 m = re.match('^Pull: (.*)\n$', line)
1048 branches.append(refspec_localpart(m.group(1)))
1050 elif remote in __remotes_from_dir('branches'):
1051 # old-style branches only declare one branch
1052 branches.append('refs/heads/'+remote);
1054 raise GitException, 'Unknown remote "%s"' % remote
1058 def identify_remote(branchname):
1059 """Return the name for the remote to pull the given branchname
1060 from, or None if we believe it is a local branch.
1063 for remote in remotes_list():
1064 if branchname in remotes_local_branches(remote):
1067 # if we get here we've found nothing, the branch is a local one
1071 """Return the git id for the tip of the parent branch as left by
1076 stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
1078 # Only consider lines not tagged not-for-merge
1079 m = re.match('^([^\t]*)\t\t', line)
1082 raise GitException, "StGit does not support multiple FETCH_HEAD"
1084 fetch_head=m.group(1)
1087 # here we are sure to have a single fetch_head
1091 """Return a list of all refs in the current repository.
1094 return [line.split()[1] for line in _output_lines(['git-show-ref'])]