stg rebase must delete stale files
[stgit.git] / stgit / git.py
blob619395628a0acd7cd5a961b407c2f89b9589d039
1 # -*- coding: utf-8 -*-
2 """Python GIT interface"""
4 from __future__ import (absolute_import, division, print_function,
5 unicode_literals)
6 import io
7 import os
8 import re
9 import sys
11 from stgit import basedir
12 from stgit.compat import environ_get
13 from stgit.config import config
14 from stgit.exception import StgException
15 from stgit.out import out
16 from stgit.run import Run
17 from stgit.utils import rename, strip_prefix
19 __copyright__ = """
20 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
22 This program is free software; you can redistribute it and/or modify
23 it under the terms of the GNU General Public License version 2 as
24 published by the Free Software Foundation.
26 This program is distributed in the hope that it will be useful,
27 but WITHOUT ANY WARRANTY; without even the implied warranty of
28 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 GNU General Public License for more details.
31 You should have received a copy of the GNU General Public License
32 along with this program; if not, see http://www.gnu.org/licenses/.
33 """
36 # git exception class
37 class GitException(StgException):
38 pass
40 # When a subprocess has a problem, we want the exception to be a
41 # subclass of GitException.
42 class GitRunException(GitException):
43 pass
44 class GRun(Run):
45 exc = GitRunException
46 def __init__(self, *cmd):
47 """Initialise the Run object and insert the 'git' command name.
48 """
49 Run.__init__(self, 'git', *cmd)
52 # Classes
55 class Person(object):
56 """An author, committer, etc."""
57 def __init__(self, name = None, email = None, date = '',
58 desc = None):
59 self.name = self.email = self.date = None
60 if name or email or date:
61 assert not desc
62 self.name = name
63 self.email = email
64 self.date = date
65 elif desc:
66 assert not (name or email or date)
67 def parse_desc(s):
68 m = re.match(r'^(.+)<(.+)>(.*)$', s)
69 assert m
70 return [x.strip() or None for x in m.groups()]
71 self.name, self.email, self.date = parse_desc(desc)
72 def set_name(self, val):
73 if val:
74 self.name = val
75 def set_email(self, val):
76 if val:
77 self.email = val
78 def set_date(self, val):
79 if val:
80 self.date = val
81 def __str__(self):
82 if self.name and self.email:
83 return '%s <%s>' % (self.name, self.email)
84 else:
85 raise GitException('not enough identity data')
87 class Commit(object):
88 """Handle the commit objects
89 """
90 def __init__(self, id_hash):
91 self.__id_hash = id_hash
93 lines = GRun('cat-file', 'commit', id_hash).output_lines()
94 for i in range(len(lines)):
95 line = lines[i]
96 if not line:
97 break # we've seen all the header fields
98 key, val = line.split(' ', 1)
99 if key == 'tree':
100 self.__tree = val
101 elif key == 'author':
102 self.__author = val
103 elif key == 'committer':
104 self.__committer = val
105 else:
106 pass # ignore other headers
107 self.__log = '\n'.join(lines[i+1:])
109 def get_id_hash(self):
110 return self.__id_hash
112 def get_tree(self):
113 return self.__tree
115 def get_parent(self):
116 parents = self.get_parents()
117 if parents:
118 return parents[0]
119 else:
120 return None
122 def get_parents(self):
123 return GRun('rev-list', '--parents', '--max-count=1', self.__id_hash
124 ).output_one_line().split()[1:]
126 def get_author(self):
127 return self.__author
129 def get_committer(self):
130 return self.__committer
132 def get_log(self):
133 return self.__log
135 def __str__(self):
136 return self.get_id_hash()
138 # dictionary of Commit objects, used to avoid multiple calls to git
139 __commits = dict()
142 # Functions
145 def get_commit(id_hash):
146 """Commit objects factory. Save/look-up them in the __commits
147 dictionary
149 global __commits
151 if id_hash in __commits:
152 return __commits[id_hash]
153 else:
154 commit = Commit(id_hash)
155 __commits[id_hash] = commit
156 return commit
158 def get_conflicts():
159 """Return the list of file conflicts
161 names = set()
162 for line in GRun('ls-files', '-z', '--unmerged').output_lines('\0'):
163 stat, path = line.split('\t', 1)
164 names.add(path)
165 return list(names)
168 def ls_files(files, tree = 'HEAD', full_name = True):
169 """Return the files known to GIT or raise an error otherwise. It also
170 converts the file to the full path relative the the .git directory.
172 if not files:
173 return []
175 args = []
176 if tree:
177 args.append('--with-tree=%s' % tree)
178 if full_name:
179 args.append('--full-name')
180 args.append('--')
181 args.extend(files)
182 try:
183 # use a set to avoid file names duplication due to different stages
184 fileset = set(GRun('ls-files', '--error-unmatch', *args).output_lines())
185 except GitRunException:
186 # just hide the details of the 'git ls-files' command we use
187 raise GitException(
188 'Some of the given paths are either missing or not known to GIT')
189 return list(fileset)
191 def parse_git_ls(lines):
192 """Parse the output of git diff-index, diff-files, etc. Doesn't handle
193 rename/copy output, so don't feed it output generated with the -M
194 or -C flags."""
195 t = None
196 for line in lines:
197 if t is None:
198 mode_a, mode_b, sha1_a, sha1_b, t = line.split(' ')
199 else:
200 yield (t, line)
201 t = None
203 def tree_status(files=None, tree_id='HEAD', verbose=False):
204 """Get the status of all changed files, or of a selected set of
205 files. Returns a list of pairs - (status, filename).
207 If 'not files', it will check all files. If 'files' is a list, it will only
208 check the files in the list.
210 if verbose:
211 out.start('Checking for changes in the working directory')
213 refresh_index()
215 if files is None:
216 files = []
217 cache_files = []
219 # conflicted files
220 conflicts = get_conflicts()
221 cache_files += [('C', filename) for filename in conflicts
222 if not files or filename in files]
223 reported_files = set(conflicts)
224 files_left = [f for f in files if f not in reported_files]
226 # files in the index. Only execute this code if no files were
227 # specified when calling the function (i.e. report all files) or
228 # files were specified but already found in the previous step
229 if not files or files_left:
230 args = [tree_id]
231 if files_left:
232 args += ['--'] + files_left
233 diff_index_lines = GRun('diff-index', '-z', *args).output_lines('\0')
234 for t, fn in parse_git_ls(diff_index_lines):
235 # the condition is needed in case files is emtpy and
236 # diff-index lists those already reported
237 if fn not in reported_files:
238 cache_files.append((t, fn))
239 reported_files.add(fn)
240 files_left = [f for f in files if f not in reported_files]
242 # files in the index but changed on (or removed from) disk. Only
243 # execute this code if no files were specified when calling the
244 # function (i.e. report all files) or files were specified but
245 # already found in the previous step
246 if not files or files_left:
247 args = []
248 if files_left:
249 args += ['--'] + files_left
250 diff_files_lines = GRun('diff-files', '-z', *args).output_lines('\0')
251 for t, fn in parse_git_ls(diff_files_lines):
252 # the condition is needed in case files is empty and
253 # diff-files lists those already reported
254 if fn not in reported_files:
255 cache_files.append((t, fn))
256 reported_files.add(fn)
258 if verbose:
259 out.done()
261 return cache_files
263 def local_changes(verbose = True):
264 """Return true if there are local changes in the tree
266 return len(tree_status(verbose = verbose)) != 0
268 def get_heads():
269 heads = []
270 hr = re.compile(r'^[0-9a-f]{40} refs/heads/(.+)$')
271 for line in GRun('show-ref', '--heads').output_lines():
272 m = hr.match(line)
273 heads.append(m.group(1))
274 return heads
276 # HEAD value cached
277 __head = None
279 def get_head():
280 """Verifies the HEAD and returns the SHA1 id that represents it
282 global __head
284 if not __head:
285 __head = rev_parse('HEAD')
286 return __head
288 class DetachedHeadException(GitException):
289 def __init__(self):
290 GitException.__init__(self, 'Not on any branch')
292 def get_head_file():
293 """Return the name of the file pointed to by the HEAD symref.
294 Throw an exception if HEAD is detached."""
295 try:
296 return strip_prefix(
297 'refs/heads/', GRun('symbolic-ref', '-q', 'HEAD'
298 ).output_one_line())
299 except GitRunException:
300 raise DetachedHeadException()
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
306 # in the new head
307 __clear_head_cache()
308 try:
309 GRun('symbolic-ref', 'HEAD', 'refs/heads/%s' % ref).run()
310 except GitRunException:
311 raise GitException('Could not set head to "%s"' % ref)
313 def set_ref(ref, val):
314 """Point ref at a new commit object."""
315 try:
316 GRun('update-ref', ref, val).run()
317 except GitRunException:
318 raise GitException('Could not update %s to "%s".' % (ref, val))
320 def set_branch(branch, val):
321 set_ref('refs/heads/%s' % branch, val)
323 def __set_head(val):
324 """Sets the HEAD value
326 global __head
328 if not __head or __head != val:
329 set_ref('HEAD', val)
330 __head = val
332 # only allow SHA1 hashes
333 assert(len(__head) == 40)
335 def __clear_head_cache():
336 """Sets the __head to None so that a re-read is forced
338 global __head
340 __head = None
342 def refresh_index():
343 """Refresh index with stat() information from the working directory.
345 GRun('update-index', '-q', '--unmerged', '--refresh').run()
347 def rev_parse(git_id):
348 """Parse the string and return a verified SHA1 id
350 try:
351 return GRun('rev-parse', '--verify', git_id
352 ).discard_stderr().output_one_line()
353 except GitRunException:
354 raise GitException('Unknown revision: %s' % git_id)
356 def ref_exists(ref):
357 try:
358 rev_parse(ref)
359 return True
360 except GitException:
361 return False
363 def branch_exists(branch):
364 return ref_exists('refs/heads/%s' % branch)
366 def create_branch(new_branch, tree_id = None):
367 """Create a new branch in the git repository
369 if branch_exists(new_branch):
370 raise GitException('Branch "%s" already exists' % new_branch)
372 current_head_file = get_head_file()
373 current_head = get_head()
374 set_head_file(new_branch)
375 __set_head(current_head)
377 # a checkout isn't needed if new branch points to the current head
378 if tree_id:
379 try:
380 switch(tree_id)
381 except GitException:
382 # Tree switching failed. Revert the head file
383 set_head_file(current_head_file)
384 delete_branch(new_branch)
385 raise
387 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
388 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
390 def switch_branch(new_branch):
391 """Switch to a git branch
393 global __head
395 if not branch_exists(new_branch):
396 raise GitException('Branch "%s" does not exist' % new_branch)
398 tree_id = rev_parse('refs/heads/%s^{commit}' % new_branch)
399 if tree_id != get_head():
400 refresh_index()
401 try:
402 GRun('read-tree', '-u', '-m', get_head(), tree_id).run()
403 except GitRunException:
404 raise GitException('read-tree failed (local changes maybe?)')
405 __head = tree_id
406 set_head_file(new_branch)
408 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
409 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
411 def delete_ref(ref):
412 if not ref_exists(ref):
413 raise GitException('%s does not exist' % ref)
414 sha1 = GRun('show-ref', '-s', ref).output_one_line()
415 try:
416 GRun('update-ref', '-d', ref, sha1).run()
417 except GitRunException:
418 raise GitException('Failed to delete ref %s' % ref)
420 def delete_branch(name):
421 delete_ref('refs/heads/%s' % name)
423 def rename_ref(from_ref, to_ref):
424 if not ref_exists(from_ref):
425 raise GitException('"%s" does not exist' % from_ref)
426 if ref_exists(to_ref):
427 raise GitException('"%s" already exists' % to_ref)
429 sha1 = GRun('show-ref', '-s', from_ref).output_one_line()
430 try:
431 GRun('update-ref', to_ref, sha1, '0'*40).run()
432 except GitRunException:
433 raise GitException('Failed to create new ref %s' % to_ref)
434 try:
435 GRun('update-ref', '-d', from_ref, sha1).run()
436 except GitRunException:
437 raise GitException('Failed to delete ref %s' % from_ref)
439 def rename_branch(from_name, to_name):
440 """Rename a git branch."""
441 rename_ref('refs/heads/%s' % from_name, 'refs/heads/%s' % to_name)
442 try:
443 if get_head_file() == from_name:
444 set_head_file(to_name)
445 except DetachedHeadException:
446 pass # detached HEAD, so the renamee can't be the current branch
447 reflog_dir = os.path.join(basedir.get(), 'logs', 'refs', 'heads')
448 if os.path.exists(reflog_dir) \
449 and os.path.exists(os.path.join(reflog_dir, from_name)):
450 rename(reflog_dir, from_name, to_name)
452 # Persons caching
453 __user = None
454 __author = None
455 __committer = None
457 def user():
458 """Return the user information.
460 global __user
461 if not __user:
462 name=config.get('user.name')
463 email=config.get('user.email')
464 __user = Person(name, email)
465 return __user
467 def author():
468 """Return the author information.
470 global __author
471 if not __author:
472 # the environment variables take priority over config
473 name = environ_get('GIT_AUTHOR_NAME')
474 email = environ_get('GIT_AUTHOR_EMAIL')
475 if name is None or email is None:
476 __author = user()
477 else:
478 date = environ_get('GIT_AUTHOR_DATE', '')
479 __author = Person(name, email, date)
480 return __author
482 def committer():
483 """Return the author information.
485 global __committer
486 if not __committer:
487 # the environment variables take priority over config
488 name = environ_get('GIT_COMMITTER_NAME')
489 email = environ_get('GIT_COMMITTER_EMAIL')
490 if name is None or email is None:
491 __committer = user()
492 else:
493 date = environ_get('GIT_COMMITTER_DATE', '')
494 __committer = Person(name, email, date)
495 return __committer
497 def update_cache(files = None, force = False):
498 """Update the cache information for the given files
500 cache_files = tree_status(files, verbose = False)
502 # everything is up-to-date
503 if len(cache_files) == 0:
504 return False
506 # check for unresolved conflicts
507 if not force and [x for x in cache_files
508 if x[0] not in ['M', 'N', 'A', 'D']]:
509 raise GitException('Updating cache failed: unresolved conflicts')
511 # update the cache
512 add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
513 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
514 m_files = [x[1] for x in cache_files if x[0] in ['M']]
516 GRun('update-index', '--add', '--').xargs(add_files)
517 GRun('update-index', '--force-remove', '--').xargs(rm_files)
518 GRun('update-index', '--').xargs(m_files)
520 return True
522 def commit(message, files = None, parents = None, allowempty = False,
523 cache_update = True, tree_id = None, set_head = False,
524 author_name = None, author_email = None, author_date = None,
525 committer_name = None, committer_email = None):
526 """Commit the current tree to repository
528 if not parents:
529 parents = []
531 # Get the tree status
532 if cache_update and parents != []:
533 changes = update_cache(files)
534 if not changes and not allowempty:
535 raise GitException('No changes to commit')
537 # get the commit message
538 if not message:
539 message = '\n'
540 elif message[-1:] != '\n':
541 message += '\n'
543 # write the index to repository
544 if tree_id is None:
545 tree_id = GRun('write-tree').output_one_line()
546 set_head = True
548 # the commit
549 env = {}
550 if author_name:
551 env['GIT_AUTHOR_NAME'] = author_name
552 if author_email:
553 env['GIT_AUTHOR_EMAIL'] = author_email
554 if author_date:
555 env['GIT_AUTHOR_DATE'] = author_date
556 if committer_name:
557 env['GIT_COMMITTER_NAME'] = committer_name
558 if committer_email:
559 env['GIT_COMMITTER_EMAIL'] = committer_email
560 commit_id = GRun('commit-tree', tree_id,
561 *sum([['-p', p] for p in parents], [])
562 ).env(env).raw_input(message).output_one_line()
563 if set_head:
564 __set_head(commit_id)
566 return commit_id
568 def apply_diff(rev1, rev2, check_index = True, files = None):
569 """Apply the diff between rev1 and rev2 onto the current
570 index. This function doesn't need to raise an exception since it
571 is only used for fast-pushing a patch. If this operation fails,
572 the pushing would fall back to the three-way merge.
574 if check_index:
575 index_opt = ['--index']
576 else:
577 index_opt = []
579 if not files:
580 files = []
582 diff_str = diff(files, rev1, rev2)
583 if diff_str:
584 try:
585 GRun('apply', *index_opt).raw_input(
586 diff_str).discard_stderr().no_output()
587 except GitRunException:
588 return False
590 return True
592 stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
594 def merge_recursive(base, head1, head2):
595 """Perform a 3-way merge between base, head1 and head2 into the
596 local tree
598 refresh_index()
599 p = GRun('merge-recursive', base, '--', head1, head2).env(
600 { 'GITHEAD_%s' % base: 'ancestor',
601 'GITHEAD_%s' % head1: 'current',
602 'GITHEAD_%s' % head2: 'patched'}).returns([0, 1])
603 output = p.output_lines()
604 if p.exitcode:
605 # There were conflicts
606 if config.getbool('stgit.autoimerge'):
607 mergetool()
608 else:
609 conflicts = [l for l in output if l.startswith('CONFLICT')]
610 out.info(*conflicts)
611 raise GitException("%d conflict(s)" % len(conflicts))
613 def mergetool(files = ()):
614 """Invoke 'git mergetool' to resolve any outstanding conflicts. If 'not
615 files', all the files in an unmerged state will be processed."""
616 GRun('mergetool', *list(files)).returns([0, 1]).run()
617 # check for unmerged entries (prepend 'CONFLICT ' for consistency with
618 # merge_recursive())
619 conflicts = ['CONFLICT ' + f for f in get_conflicts()]
620 if conflicts:
621 out.info(*conflicts)
622 raise GitException("%d conflict(s)" % len(conflicts))
624 def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = [],
625 binary = True):
626 """Show the diff between rev1 and rev2
628 if not files:
629 files = []
630 if binary and '--binary' not in diff_flags:
631 diff_flags = diff_flags + ['--binary']
633 if rev1 and rev2:
634 return GRun('diff-tree', '-p',
635 *(diff_flags + [rev1, rev2, '--'] + files)).raw_output()
636 elif rev1 or rev2:
637 refresh_index()
638 if rev2:
639 return GRun('diff-index', '-p', '-R',
640 *(diff_flags + [rev2, '--'] + files)).raw_output()
641 else:
642 return GRun('diff-index', '-p',
643 *(diff_flags + [rev1, '--'] + files)).raw_output()
644 else:
645 return ''
647 def files(rev1, rev2, diff_flags = []):
648 """Return the files modified between rev1 and rev2
651 result = []
652 for line in GRun('diff-tree', *(diff_flags + ['-r', rev1, rev2])
653 ).output_lines():
654 result.append('%s %s' % tuple(line.split(' ', 4)[-1].split('\t', 1)))
656 return '\n'.join(result)
658 def barefiles(rev1, rev2):
659 """Return the files modified between rev1 and rev2, without status info
662 result = []
663 for line in GRun('diff-tree', '-r', rev1, rev2).output_lines():
664 result.append(line.split(' ', 4)[-1].split('\t', 1)[-1])
666 return '\n'.join(result)
668 def pretty_commit(commit_id = 'HEAD', flags = []):
669 """Return a given commit (log + diff)
671 return GRun('show', *(flags + [commit_id])).raw_output()
673 def checkout(tree_id):
674 """Check out the given tree_id
676 try:
677 GRun('read-tree', '--reset', '-u', tree_id).run()
678 except GitRunException:
679 raise GitException('Failed "git read-tree" --reset %s' % tree_id)
681 def switch(tree_id, keep = False):
682 """Switch the tree to the given id
684 if keep:
685 # only update the index while keeping the local changes
686 GRun('read-tree', tree_id).run()
687 else:
688 refresh_index()
689 try:
690 GRun('read-tree', '-u', '-m', get_head(), tree_id).run()
691 except GitRunException:
692 raise GitException('read-tree failed (local changes maybe?)')
694 __set_head(tree_id)
696 def reset(tree_id = None):
697 """Revert the tree changes relative to the given tree_id. It removes
698 any local changes
700 if not tree_id:
701 tree_id = get_head()
703 checkout(tree_id)
704 __set_head(tree_id)
707 def fetch(repository = 'origin', refspec = None):
708 """Fetches changes from the remote repository, using 'git fetch'
709 by default.
711 # we update the HEAD
712 __clear_head_cache()
714 args = [repository]
715 if refspec:
716 args.append(refspec)
718 command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
719 config.get('stgit.fetchcmd')
720 Run(*(command.split() + args)).run()
722 def pull(repository = 'origin', refspec = None):
723 """Fetches changes from the remote repository, using 'git pull'
724 by default.
726 # we update the HEAD
727 __clear_head_cache()
729 args = [repository]
730 if refspec:
731 args.append(refspec)
733 command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
734 config.get('stgit.pullcmd')
735 Run(*(command.split() + args)).run()
737 def rebase(tree_id = None):
738 """Rebase the current tree to the give tree_id. The tree_id
739 argument may be something other than a GIT id if an external
740 command is invoked.
742 command = config.get('branch.%s.stgit.rebasecmd' % get_head_file()) \
743 or config.get('stgit.rebasecmd')
744 if tree_id:
745 args = [tree_id]
746 elif command:
747 args = []
748 else:
749 raise GitException('Default rebasing requires a commit id')
750 if command:
751 # clear the HEAD cache as the custom rebase command will update it
752 __clear_head_cache()
753 Run(*(command.split() + args)).run()
754 else:
755 # default rebasing
756 reset(tree_id)
758 def repack():
759 """Repack all objects into a single pack
761 GRun('repack', '-a', '-d', '-f').run()
763 def apply_patch(filename = None, diff = None, base = None,
764 reject = False, strip = None):
765 """Apply a patch onto the current or given index. There must not
766 be any local changes in the tree, otherwise the command fails
768 if diff is None:
769 if filename:
770 with io.open(filename, encoding='utf-8') as f:
771 diff = f.read()
772 else:
773 diff = sys.stdin.read()
775 if base:
776 orig_head = get_head()
777 switch(base)
778 else:
779 refresh_index()
781 cmd = ['apply', '--index']
782 if reject:
783 cmd += ['--reject']
784 if strip is not None:
785 cmd += ['-p%s' % (strip,)]
786 try:
787 GRun(*cmd).raw_input(diff).no_output()
788 except GitRunException:
789 if base:
790 switch(orig_head)
791 raise GitException('Diff does not apply cleanly')
793 if base:
794 top = commit(message = 'temporary commit used for applying a patch',
795 parents = [base])
796 switch(orig_head)
797 merge_recursive(base, orig_head, top)
800 def modifying_revs(files, base_rev, head_rev):
801 """Return the revisions from the list modifying the given files."""
802 return GRun('rev-list', '%s..%s' % (base_rev, head_rev), '--', *files
803 ).output_lines()
805 def refspec_localpart(refspec):
806 m = re.match('^[^:]*:([^:]*)$', refspec)
807 if m:
808 return m.group(1)
809 else:
810 raise GitException('Cannot parse refspec "%s"' % refspec)
812 def __remotes_from_config():
813 return config.sections_matching(r'remote\.(.*)\.url')
815 def __remotes_from_dir(dir):
816 d = os.path.join(basedir.get(), dir)
817 if os.path.exists(d):
818 return os.listdir(d)
819 else:
820 return []
822 def remotes_list():
823 """Return the list of remotes in the repository
825 return (set(__remotes_from_config())
826 | set(__remotes_from_dir('remotes'))
827 | set(__remotes_from_dir('branches')))
829 def remotes_local_branches(remote):
830 """Returns the list of local branches fetched from given remote
833 branches = []
834 if remote in __remotes_from_config():
835 for line in config.getall('remote.%s.fetch' % remote):
836 branches.append(refspec_localpart(line))
837 elif remote in __remotes_from_dir('remotes'):
838 stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
839 for line in stream:
840 # Only consider Pull lines
841 m = re.match('^Pull: (.*)\n$', line)
842 if m:
843 branches.append(refspec_localpart(m.group(1)))
844 stream.close()
845 elif remote in __remotes_from_dir('branches'):
846 # old-style branches only declare one branch
847 branches.append('refs/heads/'+remote)
848 else:
849 raise GitException('Unknown remote "%s"' % remote)
851 return branches
853 def identify_remote(branchname):
854 """Return the name for the remote to pull the given branchname
855 from, or None if we believe it is a local branch.
858 for remote in remotes_list():
859 if branchname in remotes_local_branches(remote):
860 return remote
862 # if we get here we've found nothing, the branch is a local one
863 return None
865 def fetch_head():
866 """Return the git id for the tip of the parent branch as left by
867 'git fetch'.
870 fetch_head=None
871 stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
872 for line in stream:
873 # Only consider lines not tagged not-for-merge
874 m = re.match('^([^\t]*)\t\t', line)
875 if m:
876 if fetch_head:
877 raise GitException('StGit does not support multiple FETCH_HEAD')
878 else:
879 fetch_head=m.group(1)
880 stream.close()
882 if not fetch_head:
883 out.warn('No for-merge remote head found in FETCH_HEAD')
885 # here we are sure to have a single fetch_head
886 return fetch_head
888 def all_refs():
889 """Return a list of all refs in the current repository.
892 return [line.split()[1] for line in GRun('show-ref').output_lines()]