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)' % (str(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)' % (str(cmd
),
170 p
.childerr
.read().strip())
173 p
=popen2
.Popen3(cmd
, True)
174 output
= p
.fromchild
.read()
176 raise GitException
, '%s failed (%s)' % (str(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)' % (str(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)' % (str(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
))])
219 def __tree_status(files
= None, tree_id
= 'HEAD', unknown
= False,
220 noexclude
= True, verbose
= False, diff_flags
= []):
221 """Returns a list of pairs - [status, filename]
224 out
.start('Checking for changes in the working directory')
234 exclude_file
= os
.path
.join(basedir
.get(), 'info', 'exclude')
235 base_exclude
= ['--exclude=%s' % s
for s
in
236 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
237 base_exclude
.append('--exclude-per-directory=.gitignore')
239 if os
.path
.exists(exclude_file
):
240 extra_exclude
= ['--exclude-from=%s' % exclude_file
]
244 extra_exclude
= base_exclude
= []
246 lines
= _output_lines(['git-ls-files', '--others', '--directory']
247 + base_exclude
+ extra_exclude
)
248 cache_files
+= [('?', line
.strip()) for line
in lines
]
251 conflicts
= get_conflicts()
254 cache_files
+= [('C', filename
) for filename
in conflicts
]
257 for line
in _output_lines(['git-diff-index'] + diff_flags
+
258 [ tree_id
, '--'] + files
):
259 fs
= tuple(line
.rstrip().split(' ',4)[-1].split('\t',1))
260 if fs
[1] not in conflicts
:
261 cache_files
.append(fs
)
268 def local_changes(verbose
= True):
269 """Return true if there are local changes in the tree
271 return len(__tree_status(verbose
= verbose
)) != 0
277 """Verifies the HEAD and returns the SHA1 id that represents it
282 __head
= rev_parse('HEAD')
286 """Returns the name of the file pointed to by the HEAD link
288 return strip_prefix('refs/heads/',
289 _output_one_line(['git-symbolic-ref', 'HEAD']))
291 def set_head_file(ref
):
292 """Resets HEAD to point to a new ref
294 # head cache flushing is needed since we might have a different value
297 if __run('git-symbolic-ref HEAD',
298 [os
.path
.join('refs', 'heads', ref
)]) != 0:
299 raise GitException
, 'Could not set head to "%s"' % ref
301 def set_branch(branch
, val
):
302 """Point branch at a new commit object."""
303 if __run('git-update-ref', [branch
, val
]) != 0:
304 raise GitException
, 'Could not update %s to "%s".' % (branch
, val
)
307 """Sets the HEAD value
311 if not __head
or __head
!= val
:
312 set_branch('HEAD', val
)
315 # only allow SHA1 hashes
316 assert(len(__head
) == 40)
318 def __clear_head_cache():
319 """Sets the __head to None so that a re-read is forced
326 """Refresh index with stat() information from the working directory.
328 __run('git-update-index -q --unmerged --refresh')
330 def rev_parse(git_id
):
331 """Parse the string and return a verified SHA1 id
334 return _output_one_line(['git-rev-parse', '--verify', git_id
])
336 raise GitException
, 'Unknown revision: %s' % git_id
338 def branch_exists(branch
):
339 """Existence check for the named branch
341 branch
= os
.path
.join('refs', 'heads', branch
)
342 for line
in _output_lines('git-rev-parse --symbolic --all 2>&1'):
343 if line
.strip() == branch
:
345 if re
.compile('[ |/]'+branch
+' ').search(line
):
346 raise GitException
, 'Bogus branch: %s' % line
349 def create_branch(new_branch
, tree_id
= None):
350 """Create a new branch in the git repository
352 if branch_exists(new_branch
):
353 raise GitException
, 'Branch "%s" already exists' % new_branch
355 current_head
= get_head()
356 set_head_file(new_branch
)
357 __set_head(current_head
)
359 # a checkout isn't needed if new branch points to the current head
363 if os
.path
.isfile(os
.path
.join(basedir
.get(), 'MERGE_HEAD')):
364 os
.remove(os
.path
.join(basedir
.get(), 'MERGE_HEAD'))
366 def switch_branch(new_branch
):
367 """Switch to a git branch
371 if not branch_exists(new_branch
):
372 raise GitException
, 'Branch "%s" does not exist' % new_branch
374 tree_id
= rev_parse(os
.path
.join('refs', 'heads', new_branch
)
376 if tree_id
!= get_head():
378 if __run('git-read-tree -u -m', [get_head(), tree_id
]) != 0:
379 raise GitException
, 'git-read-tree failed (local changes maybe?)'
381 set_head_file(new_branch
)
383 if os
.path
.isfile(os
.path
.join(basedir
.get(), 'MERGE_HEAD')):
384 os
.remove(os
.path
.join(basedir
.get(), 'MERGE_HEAD'))
386 def delete_branch(name
):
387 """Delete a git branch
389 if not branch_exists(name
):
390 raise GitException
, 'Branch "%s" does not exist' % name
391 remove_file_and_dirs(os
.path
.join(basedir
.get(), 'refs', 'heads'),
394 def rename_branch(from_name
, to_name
):
395 """Rename a git branch
397 if not branch_exists(from_name
):
398 raise GitException
, 'Branch "%s" does not exist' % from_name
399 if branch_exists(to_name
):
400 raise GitException
, 'Branch "%s" already exists' % to_name
402 if get_head_file() == from_name
:
403 set_head_file(to_name
)
404 rename(os
.path
.join(basedir
.get(), 'refs', 'heads'),
407 reflog_dir
= os
.path
.join(basedir
.get(), 'logs', 'refs', 'heads')
408 if os
.path
.exists(reflog_dir
) \
409 and os
.path
.exists(os
.path
.join(reflog_dir
, from_name
)):
410 rename(reflog_dir
, from_name
, to_name
)
413 """Add the files or recursively add the directory contents
415 # generate the file list
418 if not os
.path
.exists(i
):
419 raise GitException
, 'Unknown file or directory: %s' % i
422 # recursive search. We only add files
423 for root
, dirs
, local_files
in os
.walk(i
):
424 for name
in [os
.path
.join(root
, f
) for f
in local_files
]:
425 if os
.path
.isfile(name
):
426 files
.append(os
.path
.normpath(name
))
427 elif os
.path
.isfile(i
):
428 files
.append(os
.path
.normpath(i
))
430 raise GitException
, '%s is not a file or directory' % i
433 if __run('git-update-index --add --', files
):
434 raise GitException
, 'Unable to add file'
436 def __copy_single(source
, target
, target2
=''):
437 """Copy file or dir named 'source' to name target+target2"""
439 # "source" (file or dir) must match one or more git-controlled file
440 realfiles
= _output_lines(['git-ls-files', source
])
441 if len(realfiles
) == 0:
442 raise GitException
, '"%s" matches no git-controled files' % source
444 if os
.path
.isdir(source
):
445 # physically copy the files, and record them to add them in one run
447 re_string
='^'+source
+'/(.*)$'
448 prefix_regexp
= re
.compile(re_string
)
449 for f
in [f
.strip() for f
in realfiles
]:
450 m
= prefix_regexp
.match(f
)
452 raise Exception, '"%s" does not match "%s"' % (f
, re_string
)
453 newname
= target
+target2
+'/'+m
.group(1)
454 if not os
.path
.exists(os
.path
.dirname(newname
)):
455 os
.makedirs(os
.path
.dirname(newname
))
457 newfiles
.append(newname
)
460 else: # files, symlinks, ...
461 newname
= target
+target2
462 copyfile(source
, newname
)
466 def copy(filespecs
, target
):
467 if os
.path
.isdir(target
):
468 # target is a directory: copy each entry on the command line,
469 # with the same name, into the target
470 target
= target
.rstrip('/')
472 # first, check that none of the children of the target
473 # matching the command line aleady exist
474 for filespec
in filespecs
:
475 entry
= target
+ '/' + os
.path
.basename(filespec
.rstrip('/'))
476 if os
.path
.exists(entry
):
477 raise GitException
, 'Target "%s" already exists' % entry
479 for filespec
in filespecs
:
480 filespec
= filespec
.rstrip('/')
481 basename
= '/' + os
.path
.basename(filespec
)
482 __copy_single(filespec
, target
, basename
)
484 elif os
.path
.exists(target
):
485 raise GitException
, 'Target "%s" exists but is not a directory' % target
486 elif len(filespecs
) != 1:
487 raise GitException
, 'Cannot copy more than one file to non-directory'
490 # at this point: len(filespecs)==1 and target does not exist
492 # check target directory
493 targetdir
= os
.path
.dirname(target
)
494 if targetdir
!= '' and not os
.path
.isdir(targetdir
):
495 raise GitException
, 'Target directory "%s" does not exist' % targetdir
497 __copy_single(filespecs
[0].rstrip('/'), target
)
500 def rm(files
, force
= False):
501 """Remove a file from the repository
505 if os
.path
.exists(f
):
506 raise GitException
, '%s exists. Remove it first' %f
508 __run('git-update-index --remove --', files
)
511 __run('git-update-index --force-remove --', files
)
519 """Return the user information.
523 name
=config
.get('user.name')
524 email
=config
.get('user.email')
525 __user
= Person(name
, email
)
529 """Return the author information.
534 # the environment variables take priority over config
536 date
= os
.environ
['GIT_AUTHOR_DATE']
539 __author
= Person(os
.environ
['GIT_AUTHOR_NAME'],
540 os
.environ
['GIT_AUTHOR_EMAIL'],
547 """Return the author information.
552 # the environment variables take priority over config
554 date
= os
.environ
['GIT_COMMITTER_DATE']
557 __committer
= Person(os
.environ
['GIT_COMMITTER_NAME'],
558 os
.environ
['GIT_COMMITTER_EMAIL'],
564 def update_cache(files
= None, force
= False):
565 """Update the cache information for the given files
570 cache_files
= __tree_status(files
, verbose
= False)
572 # everything is up-to-date
573 if len(cache_files
) == 0:
576 # check for unresolved conflicts
577 if not force
and [x
for x
in cache_files
578 if x
[0] not in ['M', 'N', 'A', 'D']]:
579 raise GitException
, 'Updating cache failed: unresolved conflicts'
582 add_files
= [x
[1] for x
in cache_files
if x
[0] in ['N', 'A']]
583 rm_files
= [x
[1] for x
in cache_files
if x
[0] in ['D']]
584 m_files
= [x
[1] for x
in cache_files
if x
[0] in ['M']]
586 if add_files
and __run('git-update-index --add --', add_files
) != 0:
587 raise GitException
, 'Failed git-update-index --add'
588 if rm_files
and __run('git-update-index --force-remove --', rm_files
) != 0:
589 raise GitException
, 'Failed git-update-index --rm'
590 if m_files
and __run('git-update-index --', m_files
) != 0:
591 raise GitException
, 'Failed git-update-index'
595 def commit(message
, files
= None, parents
= None, allowempty
= False,
596 cache_update
= True, tree_id
= None,
597 author_name
= None, author_email
= None, author_date
= None,
598 committer_name
= None, committer_email
= None):
599 """Commit the current tree to repository
606 # Get the tree status
607 if cache_update
and parents
!= []:
608 changes
= update_cache(files
)
609 if not changes
and not allowempty
:
610 raise GitException
, 'No changes to commit'
612 # get the commit message
615 elif message
[-1:] != '\n':
619 # write the index to repository
621 tree_id
= _output_one_line(['git-write-tree'])
628 cmd
+= ['GIT_AUTHOR_NAME=%s' % author_name
]
630 cmd
+= ['GIT_AUTHOR_EMAIL=%s' % author_email
]
632 cmd
+= ['GIT_AUTHOR_DATE=%s' % author_date
]
634 cmd
+= ['GIT_COMMITTER_NAME=%s' % committer_name
]
636 cmd
+= ['GIT_COMMITTER_EMAIL=%s' % committer_email
]
637 cmd
+= ['git-commit-tree', tree_id
]
643 commit_id
= _output_one_line(cmd
, message
)
645 __set_head(commit_id
)
649 def apply_diff(rev1
, rev2
, check_index
= True, files
= None):
650 """Apply the diff between rev1 and rev2 onto the current
651 index. This function doesn't need to raise an exception since it
652 is only used for fast-pushing a patch. If this operation fails,
653 the pushing would fall back to the three-way merge.
656 index_opt
= ['--index']
663 diff_str
= diff(files
, rev1
, rev2
)
666 _input_str(['git-apply'] + index_opt
, diff_str
)
672 def merge(base
, head1
, head2
, recursive
= False):
673 """Perform a 3-way merge between base, head1 and head2 into the
680 # this operation tracks renames but it is slower (used in
681 # general when pushing or picking patches)
683 # use _output() to mask the verbose prints of the tool
684 _output(['git-merge-recursive', base
, '--', head1
, head2
])
685 except GitException
, ex
:
689 # the fast case where we don't track renames (used when the
690 # distance between base and heads is small, i.e. folding or
691 # synchronising patches)
692 if __run('git-read-tree -u -m --aggressive',
693 [base
, head1
, head2
]) != 0:
694 raise GitException
, 'git-read-tree failed (local changes maybe?)'
696 # check the index for unmerged entries
698 stages_re
= re
.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re
.S
)
700 for line
in _output(['git-ls-files', '--unmerged', '--stage', '-z']).split('\0'):
704 mode
, hash, stage
, path
= stages_re
.findall(line
)[0]
706 if not path
in files
:
708 files
[path
]['1'] = ('', '')
709 files
[path
]['2'] = ('', '')
710 files
[path
]['3'] = ('', '')
712 files
[path
][stage
] = (mode
, hash)
714 if err_output
and not files
:
715 # if no unmerged files, there was probably a different type of
716 # error and we have to abort the merge
717 raise GitException
, err_output
719 # merge the unmerged files
722 # remove additional files that might be generated for some
723 # newer versions of GIT
724 for suffix
in [base
, head1
, head2
]:
727 fname
= path
+ '~' + suffix
728 if os
.path
.exists(fname
):
732 if gitmergeonefile
.merge(stages
['1'][1], stages
['2'][1],
733 stages
['3'][1], path
, stages
['1'][0],
734 stages
['2'][0], stages
['3'][0]) != 0:
738 raise GitException
, 'GIT index merging failed (possible conflicts)'
740 def status(files
= None, modified
= False, new
= False, deleted
= False,
741 conflict
= False, unknown
= False, noexclude
= False,
743 """Show the tree status
748 cache_files
= __tree_status(files
, unknown
= True, noexclude
= noexclude
,
749 diff_flags
= diff_flags
)
750 all
= not (modified
or new
or deleted
or conflict
or unknown
)
765 cache_files
= [x
for x
in cache_files
if x
[0] in filestat
]
767 for fs
in cache_files
:
768 if files
and not fs
[1] in files
:
771 out
.stdout('%s %s' % (fs
[0], fs
[1]))
773 out
.stdout('%s' % fs
[1])
775 def diff(files
= None, rev1
= 'HEAD', rev2
= None, out_fd
= None,
777 """Show the diff between rev1 and rev2
783 diff_str
= _output(['git-diff-tree', '-p'] + diff_flags
784 + [rev1
, rev2
, '--'] + files
)
788 diff_str
= _output(['git-diff-index', '-p', '-R']
789 + diff_flags
+ [rev2
, '--'] + files
)
791 diff_str
= _output(['git-diff-index', '-p']
792 + diff_flags
+ [rev1
, '--'] + files
)
797 out_fd
.write(diff_str
)
801 def diffstat(files
= None, rev1
= 'HEAD', rev2
= None):
802 """Return the diffstat between rev1 and rev2
807 p
=popen2
.Popen3('git-apply --stat')
808 diff(files
, rev1
, rev2
, p
.tochild
)
810 diff_str
= p
.fromchild
.read().rstrip()
812 raise GitException
, 'git.diffstat failed'
815 def files(rev1
, rev2
, diff_flags
= []):
816 """Return the files modified between rev1 and rev2
820 for line
in _output_lines(['git-diff-tree'] + diff_flags
+ ['-r', rev1
, rev2
]):
821 result
+= '%s %s\n' % tuple(line
.rstrip().split(' ',4)[-1].split('\t',1))
823 return result
.rstrip()
825 def barefiles(rev1
, rev2
):
826 """Return the files modified between rev1 and rev2, without status info
830 for line
in _output_lines(['git-diff-tree', '-r', rev1
, rev2
]):
831 result
+= '%s\n' % line
.rstrip().split(' ',4)[-1].split('\t',1)[-1]
833 return result
.rstrip()
835 def pretty_commit(commit_id
= 'HEAD', diff_flags
= []):
836 """Return a given commit (log + diff)
838 return _output(['git-diff-tree'] + diff_flags
+
839 ['--cc', '--always', '--pretty', '-r', commit_id
])
841 def checkout(files
= None, tree_id
= None, force
= False):
842 """Check out the given or all files
847 if tree_id
and __run('git-read-tree --reset', [tree_id
]) != 0:
848 raise GitException
, 'Failed git-read-tree --reset %s' % tree_id
850 checkout_cmd
= 'git-checkout-index -q -u'
852 checkout_cmd
+= ' -f'
854 checkout_cmd
+= ' -a'
856 checkout_cmd
+= ' --'
858 if __run(checkout_cmd
, files
) != 0:
859 raise GitException
, 'Failed git-checkout-index'
861 def switch(tree_id
, keep
= False):
862 """Switch the tree to the given id
866 if __run('git-read-tree -u -m', [get_head(), tree_id
]) != 0:
867 raise GitException
, 'git-read-tree failed (local changes maybe?)'
871 def reset(files
= None, tree_id
= None, check_out
= True):
872 """Revert the tree changes relative to the given tree_id. It removes
879 cache_files
= __tree_status(files
, tree_id
)
880 # files which were added but need to be removed
881 rm_files
= [x
[1] for x
in cache_files
if x
[0] in ['A']]
883 checkout(files
, tree_id
, True)
884 # checkout doesn't remove files
885 map(os
.remove
, rm_files
)
887 # if the reset refers to the whole tree, switch the HEAD as well
891 def fetch(repository
= 'origin', refspec
= None):
892 """Fetches changes from the remote repository, using 'git-fetch'
902 command
= config
.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
903 config
.get('stgit.fetchcmd')
904 if __run(command
, args
) != 0:
905 raise GitException
, 'Failed "%s %s"' % (command
, repository
)
907 def pull(repository
= 'origin', refspec
= None):
908 """Fetches changes from the remote repository, using 'git-pull'
918 command
= config
.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
919 config
.get('stgit.pullcmd')
920 if __run(command
, args
) != 0:
921 raise GitException
, 'Failed "%s %s"' % (command
, repository
)
924 """Repack all objects into a single pack
926 __run('git-repack -a -d -f')
928 def apply_patch(filename
= None, diff
= None, base
= None,
930 """Apply a patch onto the current or given index. There must not
931 be any local changes in the tree, otherwise the command fails
943 orig_head
= get_head()
949 _input_str(['git-apply', '--index'], diff
)
954 # write the failed diff to a file
955 f
= file('.stgit-failed.patch', 'w+')
958 out
.warn('Diff written to the .stgit-failed.patch file')
963 top
= commit(message
= 'temporary commit used for applying a patch',
966 merge(base
, orig_head
, top
)
968 def clone(repository
, local_dir
):
969 """Clone a remote repository. At the moment, just use the
972 if __run('git-clone', [repository
, local_dir
]) != 0:
973 raise GitException
, 'Failed "git-clone %s %s"' \
974 % (repository
, local_dir
)
976 def modifying_revs(files
, base_rev
, head_rev
):
977 """Return the revisions from the list modifying the given files
979 cmd
= ['git-rev-list', '%s..%s' % (base_rev
, head_rev
), '--']
980 revs
= [line
.strip() for line
in _output_lines(cmd
+ files
)]
985 def refspec_localpart(refspec
):
986 m
= re
.match('^[^:]*:([^:]*)$', refspec
)
990 raise GitException
, 'Cannot parse refspec "%s"' % line
992 def refspec_remotepart(refspec
):
993 m
= re
.match('^([^:]*):[^:]*$', refspec
)
997 raise GitException
, 'Cannot parse refspec "%s"' % line
1000 def __remotes_from_config():
1001 return config
.sections_matching(r
'remote\.(.*)\.url')
1003 def __remotes_from_dir(dir):
1004 d
= os
.path
.join(basedir
.get(), dir)
1005 if os
.path
.exists(d
):
1006 return os
.listdir(d
)
1011 """Return the list of remotes in the repository
1014 return Set(__remotes_from_config()) | \
1015 Set(__remotes_from_dir('remotes')) | \
1016 Set(__remotes_from_dir('branches'))
1018 def remotes_local_branches(remote
):
1019 """Returns the list of local branches fetched from given remote
1023 if remote
in __remotes_from_config():
1024 for line
in config
.getall('remote.%s.fetch' % remote
):
1025 branches
.append(refspec_localpart(line
))
1026 elif remote
in __remotes_from_dir('remotes'):
1027 stream
= open(os
.path
.join(basedir
.get(), 'remotes', remote
), 'r')
1029 # Only consider Pull lines
1030 m
= re
.match('^Pull: (.*)\n$', line
)
1032 branches
.append(refspec_localpart(m
.group(1)))
1034 elif remote
in __remotes_from_dir('branches'):
1035 # old-style branches only declare one branch
1036 branches
.append('refs/heads/'+remote
);
1038 raise GitException
, 'Unknown remote "%s"' % remote
1042 def identify_remote(branchname
):
1043 """Return the name for the remote to pull the given branchname
1044 from, or None if we believe it is a local branch.
1047 for remote
in remotes_list():
1048 if branchname
in remotes_local_branches(remote
):
1051 # if we get here we've found nothing, the branch is a local one
1055 """Return the git id for the tip of the parent branch as left by
1060 stream
= open(os
.path
.join(basedir
.get(), 'FETCH_HEAD'), "r")
1062 # Only consider lines not tagged not-for-merge
1063 m
= re
.match('^([^\t]*)\t\t', line
)
1066 raise GitException
, "StGit does not support multiple FETCH_HEAD"
1068 fetch_head
=m
.group(1)
1071 # here we are sure to have a single fetch_head