Fix the behaviour when there is no user information configured
[stgit.git] / stgit / git.py
blob46ba5c8614e892e44ede8816f8d19fb76fe6baef
1 """Python GIT interface
2 """
4 __copyright__ = """
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
19 """
21 import sys, os, popen2, re, gitmergeonefile
23 from stgit import basedir
24 from stgit.utils import *
25 from stgit.config import config
26 from sets import Set
28 # git exception class
29 class GitException(Exception):
30 pass
35 # Classes
38 class Person:
39 """An author, committer, etc."""
40 def __init__(self, name = None, email = None, date = '',
41 desc = None):
42 self.name = self.email = self.date = None
43 if name or email or date:
44 assert not desc
45 self.name = name
46 self.email = email
47 self.date = date
48 elif desc:
49 assert not (name or email or date)
50 def parse_desc(s):
51 m = re.match(r'^(.+)<(.+)>(.*)$', s)
52 assert m
53 return [x.strip() or None for x in m.groups()]
54 self.name, self.email, self.date = parse_desc(desc)
55 def set_name(self, val):
56 if val:
57 self.name = val
58 def set_email(self, val):
59 if val:
60 self.email = val
61 def set_date(self, val):
62 if val:
63 self.date = val
64 def __str__(self):
65 if self.name and self.email:
66 return '%s <%s>' % (self.name, self.email)
67 else:
68 raise GitException, 'not enough identity data'
70 class Commit:
71 """Handle the commit objects
72 """
73 def __init__(self, id_hash):
74 self.__id_hash = id_hash
76 lines = _output_lines('git-cat-file commit %s' % id_hash)
77 for i in range(len(lines)):
78 line = lines[i]
79 if line == '\n':
80 break
81 field = line.strip().split(' ', 1)
82 if field[0] == 'tree':
83 self.__tree = field[1]
84 if field[0] == 'author':
85 self.__author = field[1]
86 if field[0] == 'committer':
87 self.__committer = field[1]
88 self.__log = ''.join(lines[i+1:])
90 def get_id_hash(self):
91 return self.__id_hash
93 def get_tree(self):
94 return self.__tree
96 def get_parent(self):
97 parents = self.get_parents()
98 if parents:
99 return parents[0]
100 else:
101 return None
103 def get_parents(self):
104 return _output_lines('git-rev-list --parents --max-count=1 %s'
105 % self.__id_hash)[0].split()[1:]
107 def get_author(self):
108 return self.__author
110 def get_committer(self):
111 return self.__committer
113 def get_log(self):
114 return self.__log
116 def __str__(self):
117 return self.get_id_hash()
119 # dictionary of Commit objects, used to avoid multiple calls to git
120 __commits = dict()
123 # Functions
126 def get_commit(id_hash):
127 """Commit objects factory. Save/look-up them in the __commits
128 dictionary
130 global __commits
132 if id_hash in __commits:
133 return __commits[id_hash]
134 else:
135 commit = Commit(id_hash)
136 __commits[id_hash] = commit
137 return commit
139 def get_conflicts():
140 """Return the list of file conflicts
142 conflicts_file = os.path.join(basedir.get(), 'conflicts')
143 if os.path.isfile(conflicts_file):
144 f = file(conflicts_file)
145 names = [line.strip() for line in f.readlines()]
146 f.close()
147 return names
148 else:
149 return None
151 def _input(cmd, file_desc):
152 p = popen2.Popen3(cmd, True)
153 while True:
154 line = file_desc.readline()
155 if not line:
156 break
157 p.tochild.write(line)
158 p.tochild.close()
159 if p.wait():
160 raise GitException, '%s failed (%s)' % (str(cmd),
161 p.childerr.read().strip())
163 def _input_str(cmd, string):
164 p = popen2.Popen3(cmd, True)
165 p.tochild.write(string)
166 p.tochild.close()
167 if p.wait():
168 raise GitException, '%s failed (%s)' % (str(cmd),
169 p.childerr.read().strip())
171 def _output(cmd):
172 p=popen2.Popen3(cmd, True)
173 output = p.fromchild.read()
174 if p.wait():
175 raise GitException, '%s failed (%s)' % (str(cmd),
176 p.childerr.read().strip())
177 return output
179 def _output_one_line(cmd, file_desc = None):
180 p=popen2.Popen3(cmd, True)
181 if file_desc != None:
182 for line in file_desc:
183 p.tochild.write(line)
184 p.tochild.close()
185 output = p.fromchild.readline().strip()
186 if p.wait():
187 raise GitException, '%s failed (%s)' % (str(cmd),
188 p.childerr.read().strip())
189 return output
191 def _output_lines(cmd):
192 p=popen2.Popen3(cmd, True)
193 lines = p.fromchild.readlines()
194 if p.wait():
195 raise GitException, '%s failed (%s)' % (str(cmd),
196 p.childerr.read().strip())
197 return lines
199 def __run(cmd, args=None):
200 """__run: runs cmd using spawnvp.
202 Runs cmd using spawnvp. The shell is avoided so it won't mess up
203 our arguments. If args is very large, the command is run multiple
204 times; args is split xargs style: cmd is passed on each
205 invocation. Unlike xargs, returns immediately if any non-zero
206 return code is received.
209 args_l=cmd.split()
210 if args is None:
211 args = []
212 for i in range(0, len(args)+1, 100):
213 r=os.spawnvp(os.P_WAIT, args_l[0], args_l + args[i:min(i+100, len(args))])
214 if r:
215 return r
216 return 0
218 def __tree_status(files = None, tree_id = 'HEAD', unknown = False,
219 noexclude = True, verbose = False):
220 """Returns a list of pairs - [status, filename]
222 if verbose and sys.stdout.isatty():
223 print 'Checking for changes in the working directory...',
224 sys.stdout.flush()
226 refresh_index()
228 if not files:
229 files = []
230 cache_files = []
232 # unknown files
233 if unknown:
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]
241 else:
242 extra_exclude = []
243 if noexclude:
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]
250 # conflicted files
251 conflicts = get_conflicts()
252 if not conflicts:
253 conflicts = []
254 cache_files += [('C', filename) for filename in conflicts]
256 # the rest
257 for line in _output_lines(['git-diff-index', tree_id, '--'] + files):
258 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
259 if fs[1] not in conflicts:
260 cache_files.append(fs)
262 if verbose and sys.stdout.isatty():
263 print 'done'
265 return cache_files
267 def local_changes(verbose = True):
268 """Return true if there are local changes in the tree
270 return len(__tree_status(verbose = verbose)) != 0
272 # HEAD value cached
273 __head = None
275 def get_head():
276 """Verifies the HEAD and returns the SHA1 id that represents it
278 global __head
280 if not __head:
281 __head = rev_parse('HEAD')
282 return __head
284 def get_head_file():
285 """Returns the name of the file pointed to by the HEAD link
287 return strip_prefix('refs/heads/',
288 _output_one_line('git-symbolic-ref HEAD'))
290 def set_head_file(ref):
291 """Resets HEAD to point to a new ref
293 # head cache flushing is needed since we might have a different value
294 # in the new head
295 __clear_head_cache()
296 if __run('git-symbolic-ref HEAD',
297 [os.path.join('refs', 'heads', ref)]) != 0:
298 raise GitException, 'Could not set head to "%s"' % ref
300 def __set_head(val):
301 """Sets the HEAD value
303 global __head
305 if not __head or __head != val:
306 if __run('git-update-ref HEAD', [val]) != 0:
307 raise GitException, 'Could not update HEAD to "%s".' % val
308 __head = val
310 # only allow SHA1 hashes
311 assert(len(__head) == 40)
313 def __clear_head_cache():
314 """Sets the __head to None so that a re-read is forced
316 global __head
318 __head = None
320 def refresh_index():
321 """Refresh index with stat() information from the working directory.
323 __run('git-update-index -q --unmerged --refresh')
325 def rev_parse(git_id):
326 """Parse the string and return a verified SHA1 id
328 try:
329 return _output_one_line(['git-rev-parse', '--verify', git_id])
330 except GitException:
331 raise GitException, 'Unknown revision: %s' % git_id
333 def branch_exists(branch):
334 """Existence check for the named branch
336 branch = os.path.join('refs', 'heads', branch)
337 for line in _output_lines('git-rev-parse --symbolic --all 2>&1'):
338 if line.strip() == branch:
339 return True
340 if re.compile('[ |/]'+branch+' ').search(line):
341 raise GitException, 'Bogus branch: %s' % line
342 return False
344 def create_branch(new_branch, tree_id = None):
345 """Create a new branch in the git repository
347 if branch_exists(new_branch):
348 raise GitException, 'Branch "%s" already exists' % new_branch
350 current_head = get_head()
351 set_head_file(new_branch)
352 __set_head(current_head)
354 # a checkout isn't needed if new branch points to the current head
355 if tree_id:
356 switch(tree_id)
358 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
359 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
361 def switch_branch(new_branch):
362 """Switch to a git branch
364 global __head
366 if not branch_exists(new_branch):
367 raise GitException, 'Branch "%s" does not exist' % new_branch
369 tree_id = rev_parse(os.path.join('refs', 'heads', new_branch)
370 + '^{commit}')
371 if tree_id != get_head():
372 refresh_index()
373 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
374 raise GitException, 'git-read-tree failed (local changes maybe?)'
375 __head = tree_id
376 set_head_file(new_branch)
378 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
379 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
381 def delete_branch(name):
382 """Delete a git branch
384 if not branch_exists(name):
385 raise GitException, 'Branch "%s" does not exist' % name
386 remove_file_and_dirs(os.path.join(basedir.get(), 'refs', 'heads'),
387 name)
389 def rename_branch(from_name, to_name):
390 """Rename a git branch
392 if not branch_exists(from_name):
393 raise GitException, 'Branch "%s" does not exist' % from_name
394 if branch_exists(to_name):
395 raise GitException, 'Branch "%s" already exists' % to_name
397 if get_head_file() == from_name:
398 set_head_file(to_name)
399 rename(os.path.join(basedir.get(), 'refs', 'heads'),
400 from_name, to_name)
402 reflog_dir = os.path.join(basedir.get(), 'logs', 'refs', 'heads')
403 if os.path.exists(reflog_dir) \
404 and os.path.exists(os.path.join(reflog_dir, from_name)):
405 rename(reflog_dir, from_name, to_name)
407 def add(names):
408 """Add the files or recursively add the directory contents
410 # generate the file list
411 files = []
412 for i in names:
413 if not os.path.exists(i):
414 raise GitException, 'Unknown file or directory: %s' % i
416 if os.path.isdir(i):
417 # recursive search. We only add files
418 for root, dirs, local_files in os.walk(i):
419 for name in [os.path.join(root, f) for f in local_files]:
420 if os.path.isfile(name):
421 files.append(os.path.normpath(name))
422 elif os.path.isfile(i):
423 files.append(os.path.normpath(i))
424 else:
425 raise GitException, '%s is not a file or directory' % i
427 if files:
428 if __run('git-update-index --add --', files):
429 raise GitException, 'Unable to add file'
431 def rm(files, force = False):
432 """Remove a file from the repository
434 if not force:
435 for f in files:
436 if os.path.exists(f):
437 raise GitException, '%s exists. Remove it first' %f
438 if files:
439 __run('git-update-index --remove --', files)
440 else:
441 if files:
442 __run('git-update-index --force-remove --', files)
444 # Persons caching
445 __user = None
446 __author = None
447 __committer = None
449 def user():
450 """Return the user information.
452 global __user
453 if not __user:
454 name=config.get('user.name')
455 email=config.get('user.email')
456 __user = Person(name, email)
457 return __user;
459 def author():
460 """Return the author information.
462 global __author
463 if not __author:
464 try:
465 # the environment variables take priority over config
466 try:
467 date = os.environ['GIT_AUTHOR_DATE']
468 except KeyError:
469 date = ''
470 __author = Person(os.environ['GIT_AUTHOR_NAME'],
471 os.environ['GIT_AUTHOR_EMAIL'],
472 date)
473 except KeyError:
474 __author = user()
475 return __author
477 def committer():
478 """Return the author information.
480 global __committer
481 if not __committer:
482 try:
483 # the environment variables take priority over config
484 try:
485 date = os.environ['GIT_COMMITTER_DATE']
486 except KeyError:
487 date = ''
488 __committer = Person(os.environ['GIT_COMMITTER_NAME'],
489 os.environ['GIT_COMMITTER_EMAIL'],
490 date)
491 except KeyError:
492 __committer = user()
493 return __committer
495 def update_cache(files = None, force = False):
496 """Update the cache information for the given files
498 if not files:
499 files = []
501 cache_files = __tree_status(files, verbose = False)
503 # everything is up-to-date
504 if len(cache_files) == 0:
505 return False
507 # check for unresolved conflicts
508 if not force and [x for x in cache_files
509 if x[0] not in ['M', 'N', 'A', 'D']]:
510 raise GitException, 'Updating cache failed: unresolved conflicts'
512 # update the cache
513 add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
514 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
515 m_files = [x[1] for x in cache_files if x[0] in ['M']]
517 if add_files and __run('git-update-index --add --', add_files) != 0:
518 raise GitException, 'Failed git-update-index --add'
519 if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
520 raise GitException, 'Failed git-update-index --rm'
521 if m_files and __run('git-update-index --', m_files) != 0:
522 raise GitException, 'Failed git-update-index'
524 return True
526 def commit(message, files = None, parents = None, allowempty = False,
527 cache_update = True, tree_id = None,
528 author_name = None, author_email = None, author_date = None,
529 committer_name = None, committer_email = None):
530 """Commit the current tree to repository
532 if not files:
533 files = []
534 if not parents:
535 parents = []
537 # Get the tree status
538 if cache_update and parents != []:
539 changes = update_cache(files)
540 if not changes and not allowempty:
541 raise GitException, 'No changes to commit'
543 # get the commit message
544 if not message:
545 message = '\n'
546 elif message[-1:] != '\n':
547 message += '\n'
549 must_switch = True
550 # write the index to repository
551 if tree_id == None:
552 tree_id = _output_one_line('git-write-tree')
553 else:
554 must_switch = False
556 # the commit
557 cmd = ''
558 if author_name:
559 cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
560 if author_email:
561 cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
562 if author_date:
563 cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
564 if committer_name:
565 cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
566 if committer_email:
567 cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
568 cmd += 'git-commit-tree %s' % tree_id
570 # get the parents
571 for p in parents:
572 cmd += ' -p %s' % p
574 commit_id = _output_one_line(cmd, message)
575 if must_switch:
576 __set_head(commit_id)
578 return commit_id
580 def apply_diff(rev1, rev2, check_index = True, files = None):
581 """Apply the diff between rev1 and rev2 onto the current
582 index. This function doesn't need to raise an exception since it
583 is only used for fast-pushing a patch. If this operation fails,
584 the pushing would fall back to the three-way merge.
586 if check_index:
587 index_opt = '--index'
588 else:
589 index_opt = ''
591 if not files:
592 files = []
594 diff_str = diff(files, rev1, rev2)
595 if diff_str:
596 try:
597 _input_str('git-apply %s' % index_opt, diff_str)
598 except GitException:
599 return False
601 return True
603 def merge(base, head1, head2, recursive = False):
604 """Perform a 3-way merge between base, head1 and head2 into the
605 local tree
607 refresh_index()
609 if recursive:
610 # this operation tracks renames but it is slower (used in
611 # general when pushing or picking patches)
612 try:
613 # use _output() to mask the verbose prints of the tool
614 _output('git-merge-recursive %s -- %s %s' % (base, head1, head2))
615 except GitException:
616 pass
617 else:
618 # the fast case where we don't track renames (used when the
619 # distance between base and heads is small, i.e. folding or
620 # synchronising patches)
621 if __run('git-read-tree -u -m --aggressive',
622 [base, head1, head2]) != 0:
623 raise GitException, 'git-read-tree failed (local changes maybe?)'
625 # check the index for unmerged entries
626 files = {}
627 stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
629 for line in _output('git-ls-files --unmerged --stage -z').split('\0'):
630 if not line:
631 continue
633 mode, hash, stage, path = stages_re.findall(line)[0]
635 if not path in files:
636 files[path] = {}
637 files[path]['1'] = ('', '')
638 files[path]['2'] = ('', '')
639 files[path]['3'] = ('', '')
641 files[path][stage] = (mode, hash)
643 # merge the unmerged files
644 errors = False
645 for path in files:
646 # remove additional files that might be generated for some
647 # newer versions of GIT
648 for suffix in [base, head1, head2]:
649 if not suffix:
650 continue
651 fname = path + '~' + suffix
652 if os.path.exists(fname):
653 os.remove(fname)
655 stages = files[path]
656 if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
657 stages['3'][1], path, stages['1'][0],
658 stages['2'][0], stages['3'][0]) != 0:
659 errors = True
661 if errors:
662 raise GitException, 'GIT index merging failed (possible conflicts)'
664 def status(files = None, modified = False, new = False, deleted = False,
665 conflict = False, unknown = False, noexclude = False):
666 """Show the tree status
668 if not files:
669 files = []
671 cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
672 all = not (modified or new or deleted or conflict or unknown)
674 if not all:
675 filestat = []
676 if modified:
677 filestat.append('M')
678 if new:
679 filestat.append('A')
680 filestat.append('N')
681 if deleted:
682 filestat.append('D')
683 if conflict:
684 filestat.append('C')
685 if unknown:
686 filestat.append('?')
687 cache_files = [x for x in cache_files if x[0] in filestat]
689 for fs in cache_files:
690 if files and not fs[1] in files:
691 continue
692 if all:
693 print '%s %s' % (fs[0], fs[1])
694 else:
695 print '%s' % fs[1]
697 def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None):
698 """Show the diff between rev1 and rev2
700 if not files:
701 files = []
703 if rev1 and rev2:
704 diff_str = _output(['git-diff-tree', '-p', rev1, rev2, '--'] + files)
705 elif rev1 or rev2:
706 refresh_index()
707 if rev2:
708 diff_str = _output(['git-diff-index', '-p', '-R', rev2, '--'] + files)
709 else:
710 diff_str = _output(['git-diff-index', '-p', rev1, '--'] + files)
711 else:
712 diff_str = ''
714 if out_fd:
715 out_fd.write(diff_str)
716 else:
717 return diff_str
719 def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
720 """Return the diffstat between rev1 and rev2
722 if not files:
723 files = []
725 p=popen2.Popen3('git-apply --stat')
726 diff(files, rev1, rev2, p.tochild)
727 p.tochild.close()
728 diff_str = p.fromchild.read().rstrip()
729 if p.wait():
730 raise GitException, 'git.diffstat failed'
731 return diff_str
733 def files(rev1, rev2):
734 """Return the files modified between rev1 and rev2
737 result = ''
738 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
739 result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
741 return result.rstrip()
743 def barefiles(rev1, rev2):
744 """Return the files modified between rev1 and rev2, without status info
747 result = ''
748 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
749 result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
751 return result.rstrip()
753 def pretty_commit(commit_id = 'HEAD'):
754 """Return a given commit (log + diff)
756 return _output(['git-diff-tree', '--cc', '--always', '--pretty', '-r',
757 commit_id])
759 def checkout(files = None, tree_id = None, force = False):
760 """Check out the given or all files
762 if not files:
763 files = []
765 if tree_id and __run('git-read-tree --reset', [tree_id]) != 0:
766 raise GitException, 'Failed git-read-tree --reset %s' % tree_id
768 checkout_cmd = 'git-checkout-index -q -u'
769 if force:
770 checkout_cmd += ' -f'
771 if len(files) == 0:
772 checkout_cmd += ' -a'
773 else:
774 checkout_cmd += ' --'
776 if __run(checkout_cmd, files) != 0:
777 raise GitException, 'Failed git-checkout-index'
779 def switch(tree_id, keep = False):
780 """Switch the tree to the given id
782 if not keep:
783 refresh_index()
784 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
785 raise GitException, 'git-read-tree failed (local changes maybe?)'
787 __set_head(tree_id)
789 def reset(files = None, tree_id = None, check_out = True):
790 """Revert the tree changes relative to the given tree_id. It removes
791 any local changes
793 if not tree_id:
794 tree_id = get_head()
796 if check_out:
797 cache_files = __tree_status(files, tree_id)
798 # files which were added but need to be removed
799 rm_files = [x[1] for x in cache_files if x[0] in ['A']]
801 checkout(files, tree_id, True)
802 # checkout doesn't remove files
803 map(os.remove, rm_files)
805 # if the reset refers to the whole tree, switch the HEAD as well
806 if not files:
807 __set_head(tree_id)
809 def fetch(repository = 'origin', refspec = None):
810 """Fetches changes from the remote repository, using 'git-fetch'
811 by default.
813 # we update the HEAD
814 __clear_head_cache()
816 args = [repository]
817 if refspec:
818 args.append(refspec)
820 command = config.get('stgit.pullcmd')
821 if __run(command, args) != 0:
822 raise GitException, 'Failed "%s %s"' % (command, repository)
824 def repack():
825 """Repack all objects into a single pack
827 __run('git-repack -a -d -f')
829 def apply_patch(filename = None, diff = None, base = None,
830 fail_dump = True):
831 """Apply a patch onto the current or given index. There must not
832 be any local changes in the tree, otherwise the command fails
834 if diff is None:
835 if filename:
836 f = file(filename)
837 else:
838 f = sys.stdin
839 diff = f.read()
840 if filename:
841 f.close()
843 if base:
844 orig_head = get_head()
845 switch(base)
846 else:
847 refresh_index()
849 try:
850 _input_str('git-apply --index', diff)
851 except GitException:
852 if base:
853 switch(orig_head)
854 if fail_dump:
855 # write the failed diff to a file
856 f = file('.stgit-failed.patch', 'w+')
857 f.write(diff)
858 f.close()
859 print >> sys.stderr, 'Diff written to the .stgit-failed.patch file'
861 raise
863 if base:
864 top = commit(message = 'temporary commit used for applying a patch',
865 parents = [base])
866 switch(orig_head)
867 merge(base, orig_head, top)
869 def clone(repository, local_dir):
870 """Clone a remote repository. At the moment, just use the
871 'git-clone' script
873 if __run('git-clone', [repository, local_dir]) != 0:
874 raise GitException, 'Failed "git-clone %s %s"' \
875 % (repository, local_dir)
877 def modifying_revs(files, base_rev):
878 """Return the revisions from the list modifying the given files
880 cmd = ['git-rev-list', '%s..' % base_rev, '--']
881 revs = [line.strip() for line in _output_lines(cmd + files)]
883 return revs
886 def refspec_localpart(refspec):
887 m = re.match('^[^:]*:([^:]*)$', refspec)
888 if m:
889 return m.group(1)
890 else:
891 raise GitException, 'Cannot parse refspec "%s"' % line
893 def refspec_remotepart(refspec):
894 m = re.match('^([^:]*):[^:]*$', refspec)
895 if m:
896 return m.group(1)
897 else:
898 raise GitException, 'Cannot parse refspec "%s"' % line
901 def __remotes_from_config():
902 return config.sections_matching(r'remote\.(.*)\.url')
904 def __remotes_from_dir(dir):
905 d = os.path.join(basedir.get(), dir)
906 if os.path.exists(d):
907 return os.listdir(d)
908 else:
909 return None
911 def remotes_list():
912 """Return the list of remotes in the repository
915 return Set(__remotes_from_config()) | \
916 Set(__remotes_from_dir('remotes')) | \
917 Set(__remotes_from_dir('branches'))
919 def remotes_local_branches(remote):
920 """Returns the list of local branches fetched from given remote
923 branches = []
924 if remote in __remotes_from_config():
925 for line in config.getall('remote.%s.fetch' % remote):
926 branches.append(refspec_localpart(line))
927 elif remote in __remotes_from_dir('remotes'):
928 stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
929 for line in stream:
930 # Only consider Pull lines
931 m = re.match('^Pull: (.*)\n$', line)
932 if m:
933 branches.append(refspec_localpart(m.group(1)))
934 stream.close()
935 elif remote in __remotes_from_dir('branches'):
936 # old-style branches only declare one branch
937 branches.append('refs/heads/'+remote);
938 else:
939 raise GitException, 'Unknown remote "%s"' % remote
941 return branches
943 def identify_remote(branchname):
944 """Return the name for the remote to pull the given branchname
945 from, or None if we believe it is a local branch.
948 for remote in remotes_list():
949 if branchname in remotes_local_branches(remote):
950 return remote
952 # if we get here we've found nothing, the branch is a local one
953 return None
955 def fetch_head():
956 """Return the git id for the tip of the parent branch as left by
957 'git fetch'.
960 fetch_head=None
961 stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
962 for line in stream:
963 # Only consider lines not tagged not-for-merge
964 m = re.match('^([^\t]*)\t\t', line)
965 if m:
966 if fetch_head:
967 raise GitException, "StGit does not support multiple FETCH_HEAD"
968 else:
969 fetch_head=m.group(1)
970 stream.close()
972 # here we are sure to have a single fetch_head
973 return fetch_head