4d01fc296c389be4155249126cbeb556744d44bf
[stgit] / stgit / git.py
1 """Python GIT interface
2 """
3
4 __copyright__ = """
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
6
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.
10
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.
15
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 """
20
21 import sys, os, re, gitmergeonefile
22 from shutil import copyfile
23
24 from stgit.exception import *
25 from stgit import basedir
26 from stgit.utils import *
27 from stgit.out import *
28 from stgit.run import *
29 from stgit.config import config
30
31 # git exception class
32 class GitException(StgException):
33 pass
34
35 # When a subprocess has a problem, we want the exception to be a
36 # subclass of GitException.
37 class GitRunException(GitException):
38 pass
39 class GRun(Run):
40 exc = GitRunException
41 def __init__(self, *cmd):
42 """Initialise the Run object and insert the 'git' command name.
43 """
44 Run.__init__(self, 'git', *cmd)
45
46 #
47 # Classes
48 #
49
50 class Person:
51 """An author, committer, etc."""
52 def __init__(self, name = None, email = None, date = '',
53 desc = None):
54 self.name = self.email = self.date = None
55 if name or email or date:
56 assert not desc
57 self.name = name
58 self.email = email
59 self.date = date
60 elif desc:
61 assert not (name or email or date)
62 def parse_desc(s):
63 m = re.match(r'^(.+)<(.+)>(.*)$', s)
64 assert m
65 return [x.strip() or None for x in m.groups()]
66 self.name, self.email, self.date = parse_desc(desc)
67 def set_name(self, val):
68 if val:
69 self.name = val
70 def set_email(self, val):
71 if val:
72 self.email = val
73 def set_date(self, val):
74 if val:
75 self.date = val
76 def __str__(self):
77 if self.name and self.email:
78 return '%s <%s>' % (self.name, self.email)
79 else:
80 raise GitException, 'not enough identity data'
81
82 class Commit:
83 """Handle the commit objects
84 """
85 def __init__(self, id_hash):
86 self.__id_hash = id_hash
87
88 lines = GRun('cat-file', 'commit', id_hash).output_lines()
89 for i in range(len(lines)):
90 line = lines[i]
91 if not line:
92 break # we've seen all the header fields
93 key, val = line.split(' ', 1)
94 if key == 'tree':
95 self.__tree = val
96 elif key == 'author':
97 self.__author = val
98 elif key == 'committer':
99 self.__committer = val
100 else:
101 pass # ignore other headers
102 self.__log = '\n'.join(lines[i+1:])
103
104 def get_id_hash(self):
105 return self.__id_hash
106
107 def get_tree(self):
108 return self.__tree
109
110 def get_parent(self):
111 parents = self.get_parents()
112 if parents:
113 return parents[0]
114 else:
115 return None
116
117 def get_parents(self):
118 return GRun('rev-list', '--parents', '--max-count=1', self.__id_hash
119 ).output_one_line().split()[1:]
120
121 def get_author(self):
122 return self.__author
123
124 def get_committer(self):
125 return self.__committer
126
127 def get_log(self):
128 return self.__log
129
130 def __str__(self):
131 return self.get_id_hash()
132
133 # dictionary of Commit objects, used to avoid multiple calls to git
134 __commits = dict()
135
136 #
137 # Functions
138 #
139
140 def get_commit(id_hash):
141 """Commit objects factory. Save/look-up them in the __commits
142 dictionary
143 """
144 global __commits
145
146 if id_hash in __commits:
147 return __commits[id_hash]
148 else:
149 commit = Commit(id_hash)
150 __commits[id_hash] = commit
151 return commit
152
153 def get_conflicts():
154 """Return the list of file conflicts
155 """
156 names = set()
157 for line in GRun('ls-files', '-z', '--unmerged'
158 ).raw_output().split('\0')[:-1]:
159 stat, path = line.split('\t', 1)
160 names.add(path)
161 return list(names)
162
163 def exclude_files():
164 files = [os.path.join(basedir.get(), 'info', 'exclude')]
165 user_exclude = config.get('core.excludesfile')
166 if user_exclude:
167 files.append(user_exclude)
168 return files
169
170 def ls_files(files, tree = 'HEAD', full_name = True):
171 """Return the files known to GIT or raise an error otherwise. It also
172 converts the file to the full path relative the the .git directory.
173 """
174 if not files:
175 return []
176
177 args = []
178 if tree:
179 args.append('--with-tree=%s' % tree)
180 if full_name:
181 args.append('--full-name')
182 args.append('--')
183 args.extend(files)
184 try:
185 # use a set to avoid file names duplication due to different stages
186 fileset = set(GRun('ls-files', '--error-unmatch', *args).output_lines())
187 except GitRunException:
188 # just hide the details of the 'git ls-files' command we use
189 raise GitException, \
190 'Some of the given paths are either missing or not known to GIT'
191 return list(fileset)
192
193 def parse_git_ls(output):
194 """Parse the output of git diff-index, diff-files, etc. Doesn't handle
195 rename/copy output, so don't feed it output generated with the -M
196 or -C flags."""
197 t = None
198 for line in output.split('\0'):
199 if not line:
200 # There's a zero byte at the end of the output, which
201 # gives us an empty string as the last "line".
202 continue
203 if t == None:
204 mode_a, mode_b, sha1_a, sha1_b, t = line.split(' ')
205 else:
206 yield (t, line)
207 t = None
208
209 def tree_status(files = None, tree_id = 'HEAD', unknown = False,
210 noexclude = True, verbose = False):
211 """Get the status of all changed files, or of a selected set of
212 files. Returns a list of pairs - (status, filename).
213
214 If 'not files', it will check all files, and optionally all
215 unknown files. If 'files' is a list, it will only check the files
216 in the list.
217 """
218 assert not files or not unknown
219
220 if verbose:
221 out.start('Checking for changes in the working directory')
222
223 refresh_index()
224
225 if files is None:
226 files = []
227 cache_files = []
228
229 # unknown files
230 if unknown:
231 cmd = ['ls-files', '-z', '--others', '--directory',
232 '--no-empty-directory']
233 if not noexclude:
234 cmd += ['--exclude=%s' % s for s in
235 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
236 cmd += ['--exclude-per-directory=.gitignore']
237 cmd += ['--exclude-from=%s' % fn
238 for fn in exclude_files()
239 if os.path.exists(fn)]
240
241 lines = GRun(*cmd).raw_output().split('\0')
242 cache_files += [('?', line) for line in lines if line]
243
244 # conflicted files
245 conflicts = get_conflicts()
246 cache_files += [('C', filename) for filename in conflicts
247 if not files or filename in files]
248 reported_files = set(conflicts)
249 files_left = [f for f in files if f not in reported_files]
250
251 # files in the index. Only execute this code if no files were
252 # specified when calling the function (i.e. report all files) or
253 # files were specified but already found in the previous step
254 if not files or files_left:
255 args = [tree_id]
256 if files_left:
257 args += ['--'] + files_left
258 for t, fn in parse_git_ls(GRun('diff-index', '-z', *args).raw_output()):
259 # the condition is needed in case files is emtpy and
260 # diff-index lists those already reported
261 if not fn in reported_files:
262 cache_files.append((t, fn))
263 reported_files.add(fn)
264 files_left = [f for f in files if f not in reported_files]
265
266 # files in the index but changed on (or removed from) disk. Only
267 # execute this code if no files were specified when calling the
268 # function (i.e. report all files) or files were specified but
269 # already found in the previous step
270 if not files or files_left:
271 args = []
272 if files_left:
273 args += ['--'] + files_left
274 for t, fn in parse_git_ls(GRun('diff-files', '-z', *args).raw_output()):
275 # the condition is needed in case files is empty and
276 # diff-files lists those already reported
277 if not fn in reported_files:
278 cache_files.append((t, fn))
279 reported_files.add(fn)
280
281 if verbose:
282 out.done()
283
284 return cache_files
285
286 def local_changes(verbose = True):
287 """Return true if there are local changes in the tree
288 """
289 return len(tree_status(verbose = verbose)) != 0
290
291 def get_heads():
292 heads = []
293 hr = re.compile(r'^[0-9a-f]{40} refs/heads/(.+)$')
294 for line in GRun('show-ref', '--heads').output_lines():
295 m = hr.match(line)
296 heads.append(m.group(1))
297 return heads
298
299 # HEAD value cached
300 __head = None
301
302 def get_head():
303 """Verifies the HEAD and returns the SHA1 id that represents it
304 """
305 global __head
306
307 if not __head:
308 __head = rev_parse('HEAD')
309 return __head
310
311 class DetachedHeadException(GitException):
312 def __init__(self):
313 GitException.__init__(self, 'Not on any branch')
314
315 def get_head_file():
316 """Return the name of the file pointed to by the HEAD symref.
317 Throw an exception if HEAD is detached."""
318 try:
319 return strip_prefix(
320 'refs/heads/', GRun('symbolic-ref', '-q', 'HEAD'
321 ).output_one_line())
322 except GitRunException:
323 raise DetachedHeadException()
324
325 def set_head_file(ref):
326 """Resets HEAD to point to a new ref
327 """
328 # head cache flushing is needed since we might have a different value
329 # in the new head
330 __clear_head_cache()
331 try:
332 GRun('symbolic-ref', 'HEAD', 'refs/heads/%s' % ref).run()
333 except GitRunException:
334 raise GitException, 'Could not set head to "%s"' % ref
335
336 def set_ref(ref, val):
337 """Point ref at a new commit object."""
338 try:
339 GRun('update-ref', ref, val).run()
340 except GitRunException:
341 raise GitException, 'Could not update %s to "%s".' % (ref, val)
342
343 def set_branch(branch, val):
344 set_ref('refs/heads/%s' % branch, val)
345
346 def __set_head(val):
347 """Sets the HEAD value
348 """
349 global __head
350
351 if not __head or __head != val:
352 set_ref('HEAD', val)
353 __head = val
354
355 # only allow SHA1 hashes
356 assert(len(__head) == 40)
357
358 def __clear_head_cache():
359 """Sets the __head to None so that a re-read is forced
360 """
361 global __head
362
363 __head = None
364
365 def refresh_index():
366 """Refresh index with stat() information from the working directory.
367 """
368 GRun('update-index', '-q', '--unmerged', '--refresh').run()
369
370 def rev_parse(git_id):
371 """Parse the string and return a verified SHA1 id
372 """
373 try:
374 return GRun('rev-parse', '--verify', git_id
375 ).discard_stderr().output_one_line()
376 except GitRunException:
377 raise GitException, 'Unknown revision: %s' % git_id
378
379 def ref_exists(ref):
380 try:
381 rev_parse(ref)
382 return True
383 except GitException:
384 return False
385
386 def branch_exists(branch):
387 return ref_exists('refs/heads/%s' % branch)
388
389 def create_branch(new_branch, tree_id = None):
390 """Create a new branch in the git repository
391 """
392 if branch_exists(new_branch):
393 raise GitException, 'Branch "%s" already exists' % new_branch
394
395 current_head_file = get_head_file()
396 current_head = get_head()
397 set_head_file(new_branch)
398 __set_head(current_head)
399
400 # a checkout isn't needed if new branch points to the current head
401 if tree_id:
402 try:
403 switch(tree_id)
404 except GitException:
405 # Tree switching failed. Revert the head file
406 set_head_file(current_head_file)
407 delete_branch(new_branch)
408 raise
409
410 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
411 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
412
413 def switch_branch(new_branch):
414 """Switch to a git branch
415 """
416 global __head
417
418 if not branch_exists(new_branch):
419 raise GitException, 'Branch "%s" does not exist' % new_branch
420
421 tree_id = rev_parse('refs/heads/%s^{commit}' % new_branch)
422 if tree_id != get_head():
423 refresh_index()
424 try:
425 GRun('read-tree', '-u', '-m', get_head(), tree_id).run()
426 except GitRunException:
427 raise GitException, 'read-tree failed (local changes maybe?)'
428 __head = tree_id
429 set_head_file(new_branch)
430
431 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
432 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
433
434 def delete_ref(ref):
435 if not ref_exists(ref):
436 raise GitException, '%s does not exist' % ref
437 sha1 = GRun('show-ref', '-s', ref).output_one_line()
438 try:
439 GRun('update-ref', '-d', ref, sha1).run()
440 except GitRunException:
441 raise GitException, 'Failed to delete ref %s' % ref
442
443 def delete_branch(name):
444 delete_ref('refs/heads/%s' % name)
445
446 def rename_ref(from_ref, to_ref):
447 if not ref_exists(from_ref):
448 raise GitException, '"%s" does not exist' % from_ref
449 if ref_exists(to_ref):
450 raise GitException, '"%s" already exists' % to_ref
451
452 sha1 = GRun('show-ref', '-s', from_ref).output_one_line()
453 try:
454 GRun('update-ref', to_ref, sha1, '0'*40).run()
455 except GitRunException:
456 raise GitException, 'Failed to create new ref %s' % to_ref
457 try:
458 GRun('update-ref', '-d', from_ref, sha1).run()
459 except GitRunException:
460 raise GitException, 'Failed to delete ref %s' % from_ref
461
462 def rename_branch(from_name, to_name):
463 """Rename a git branch."""
464 rename_ref('refs/heads/%s' % from_name, 'refs/heads/%s' % to_name)
465 try:
466 if get_head_file() == from_name:
467 set_head_file(to_name)
468 except DetachedHeadException:
469 pass # detached HEAD, so the renamee can't be the current branch
470 reflog_dir = os.path.join(basedir.get(), 'logs', 'refs', 'heads')
471 if os.path.exists(reflog_dir) \
472 and os.path.exists(os.path.join(reflog_dir, from_name)):
473 rename(reflog_dir, from_name, to_name)
474
475 # Persons caching
476 __user = None
477 __author = None
478 __committer = None
479
480 def user():
481 """Return the user information.
482 """
483 global __user
484 if not __user:
485 name=config.get('user.name')
486 email=config.get('user.email')
487 __user = Person(name, email)
488 return __user;
489
490 def author():
491 """Return the author information.
492 """
493 global __author
494 if not __author:
495 try:
496 # the environment variables take priority over config
497 try:
498 date = os.environ['GIT_AUTHOR_DATE']
499 except KeyError:
500 date = ''
501 __author = Person(os.environ['GIT_AUTHOR_NAME'],
502 os.environ['GIT_AUTHOR_EMAIL'],
503 date)
504 except KeyError:
505 __author = user()
506 return __author
507
508 def committer():
509 """Return the author information.
510 """
511 global __committer
512 if not __committer:
513 try:
514 # the environment variables take priority over config
515 try:
516 date = os.environ['GIT_COMMITTER_DATE']
517 except KeyError:
518 date = ''
519 __committer = Person(os.environ['GIT_COMMITTER_NAME'],
520 os.environ['GIT_COMMITTER_EMAIL'],
521 date)
522 except KeyError:
523 __committer = user()
524 return __committer
525
526 def update_cache(files = None, force = False):
527 """Update the cache information for the given files
528 """
529 cache_files = tree_status(files, verbose = False)
530
531 # everything is up-to-date
532 if len(cache_files) == 0:
533 return False
534
535 # check for unresolved conflicts
536 if not force and [x for x in cache_files
537 if x[0] not in ['M', 'N', 'A', 'D']]:
538 raise GitException, 'Updating cache failed: unresolved conflicts'
539
540 # update the cache
541 add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
542 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
543 m_files = [x[1] for x in cache_files if x[0] in ['M']]
544
545 GRun('update-index', '--add', '--').xargs(add_files)
546 GRun('update-index', '--force-remove', '--').xargs(rm_files)
547 GRun('update-index', '--').xargs(m_files)
548
549 return True
550
551 def commit(message, files = None, parents = None, allowempty = False,
552 cache_update = True, tree_id = None, set_head = False,
553 author_name = None, author_email = None, author_date = None,
554 committer_name = None, committer_email = None):
555 """Commit the current tree to repository
556 """
557 if not parents:
558 parents = []
559
560 # Get the tree status
561 if cache_update and parents != []:
562 changes = update_cache(files)
563 if not changes and not allowempty:
564 raise GitException, 'No changes to commit'
565
566 # get the commit message
567 if not message:
568 message = '\n'
569 elif message[-1:] != '\n':
570 message += '\n'
571
572 # write the index to repository
573 if tree_id == None:
574 tree_id = GRun('write-tree').output_one_line()
575 set_head = True
576
577 # the commit
578 env = {}
579 if author_name:
580 env['GIT_AUTHOR_NAME'] = author_name
581 if author_email:
582 env['GIT_AUTHOR_EMAIL'] = author_email
583 if author_date:
584 env['GIT_AUTHOR_DATE'] = author_date
585 if committer_name:
586 env['GIT_COMMITTER_NAME'] = committer_name
587 if committer_email:
588 env['GIT_COMMITTER_EMAIL'] = committer_email
589 commit_id = GRun('commit-tree', tree_id,
590 *sum([['-p', p] for p in parents], [])
591 ).env(env).raw_input(message).output_one_line()
592 if set_head:
593 __set_head(commit_id)
594
595 return commit_id
596
597 def apply_diff(rev1, rev2, check_index = True, files = None):
598 """Apply the diff between rev1 and rev2 onto the current
599 index. This function doesn't need to raise an exception since it
600 is only used for fast-pushing a patch. If this operation fails,
601 the pushing would fall back to the three-way merge.
602 """
603 if check_index:
604 index_opt = ['--index']
605 else:
606 index_opt = []
607
608 if not files:
609 files = []
610
611 diff_str = diff(files, rev1, rev2)
612 if diff_str:
613 try:
614 GRun('apply', *index_opt).raw_input(
615 diff_str).discard_stderr().no_output()
616 except GitRunException:
617 return False
618
619 return True
620
621 stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
622
623 def merge_recursive(base, head1, head2):
624 """Perform a 3-way merge between base, head1 and head2 into the
625 local tree
626 """
627 refresh_index()
628 p = GRun('merge-recursive', base, '--', head1, head2).env(
629 { 'GITHEAD_%s' % base: 'ancestor',
630 'GITHEAD_%s' % head1: 'current',
631 'GITHEAD_%s' % head2: 'patched'}).returns([0, 1])
632 output = p.output_lines()
633 if p.exitcode:
634 # There were conflicts
635 conflicts = [l.strip() for l in output if l.startswith('CONFLICT')]
636 out.info(*conflicts)
637
638 # try the interactive merge or stage checkout (if enabled)
639 for filename in get_conflicts():
640 if (gitmergeonefile.merge(filename)):
641 # interactive merge succeeded
642 resolved([filename])
643
644 # any conflicts left unsolved?
645 cn = len(get_conflicts())
646 if cn:
647 raise GitException, "%d conflict(s)" % cn
648
649 def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = [],
650 binary = True):
651 """Show the diff between rev1 and rev2
652 """
653 if not files:
654 files = []
655 if binary and '--binary' not in diff_flags:
656 diff_flags = diff_flags + ['--binary']
657
658 if rev1 and rev2:
659 return GRun('diff-tree', '-p',
660 *(diff_flags + [rev1, rev2, '--'] + files)).raw_output()
661 elif rev1 or rev2:
662 refresh_index()
663 if rev2:
664 return GRun('diff-index', '-p', '-R',
665 *(diff_flags + [rev2, '--'] + files)).raw_output()
666 else:
667 return GRun('diff-index', '-p',
668 *(diff_flags + [rev1, '--'] + files)).raw_output()
669 else:
670 return ''
671
672 def files(rev1, rev2, diff_flags = []):
673 """Return the files modified between rev1 and rev2
674 """
675
676 result = []
677 for line in GRun('diff-tree', *(diff_flags + ['-r', rev1, rev2])
678 ).output_lines():
679 result.append('%s %s' % tuple(line.split(' ', 4)[-1].split('\t', 1)))
680
681 return '\n'.join(result)
682
683 def barefiles(rev1, rev2):
684 """Return the files modified between rev1 and rev2, without status info
685 """
686
687 result = []
688 for line in GRun('diff-tree', '-r', rev1, rev2).output_lines():
689 result.append(line.split(' ', 4)[-1].split('\t', 1)[-1])
690
691 return '\n'.join(result)
692
693 def pretty_commit(commit_id = 'HEAD', flags = []):
694 """Return a given commit (log + diff)
695 """
696 return GRun('show', *(flags + [commit_id])).raw_output()
697
698 def checkout(files = None, tree_id = None, force = False):
699 """Check out the given or all files
700 """
701 if tree_id:
702 try:
703 GRun('read-tree', '--reset', tree_id).run()
704 except GitRunException:
705 raise GitException, 'Failed "git read-tree" --reset %s' % tree_id
706
707 cmd = ['checkout-index', '-q', '-u']
708 if force:
709 cmd.append('-f')
710 if files:
711 GRun(*(cmd + ['--'])).xargs(files)
712 else:
713 GRun(*(cmd + ['-a'])).run()
714
715 def switch(tree_id, keep = False):
716 """Switch the tree to the given id
717 """
718 if keep:
719 # only update the index while keeping the local changes
720 GRun('read-tree', tree_id).run()
721 else:
722 refresh_index()
723 try:
724 GRun('read-tree', '-u', '-m', get_head(), tree_id).run()
725 except GitRunException:
726 raise GitException, 'read-tree failed (local changes maybe?)'
727
728 __set_head(tree_id)
729
730 def reset(files = None, tree_id = None, check_out = True):
731 """Revert the tree changes relative to the given tree_id. It removes
732 any local changes
733 """
734 if not tree_id:
735 tree_id = get_head()
736
737 if check_out:
738 cache_files = tree_status(files, tree_id)
739 # files which were added but need to be removed
740 rm_files = [x[1] for x in cache_files if x[0] in ['A']]
741
742 checkout(files, tree_id, True)
743 # checkout doesn't remove files
744 map(os.remove, rm_files)
745
746 # if the reset refers to the whole tree, switch the HEAD as well
747 if not files:
748 __set_head(tree_id)
749
750 def resolved(filenames, reset = None):
751 if reset:
752 stage = {'ancestor': 1, 'current': 2, 'patched': 3}[reset]
753 GRun('checkout-index', '--no-create', '--stage=%d' % stage,
754 '--stdin', '-z').input_nulterm(filenames).no_output()
755 GRun('update-index', '--add', '--').xargs(filenames)
756 for filename in filenames:
757 gitmergeonefile.clean_up(filename)
758 # update the access and modificatied times
759 os.utime(filename, None)
760
761 def fetch(repository = 'origin', refspec = None):
762 """Fetches changes from the remote repository, using 'git fetch'
763 by default.
764 """
765 # we update the HEAD
766 __clear_head_cache()
767
768 args = [repository]
769 if refspec:
770 args.append(refspec)
771
772 command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
773 config.get('stgit.fetchcmd')
774 Run(*(command.split() + args)).run()
775
776 def pull(repository = 'origin', refspec = None):
777 """Fetches changes from the remote repository, using 'git pull'
778 by default.
779 """
780 # we update the HEAD
781 __clear_head_cache()
782
783 args = [repository]
784 if refspec:
785 args.append(refspec)
786
787 command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
788 config.get('stgit.pullcmd')
789 Run(*(command.split() + args)).run()
790
791 def rebase(tree_id = None):
792 """Rebase the current tree to the give tree_id. The tree_id
793 argument may be something other than a GIT id if an external
794 command is invoked.
795 """
796 command = config.get('branch.%s.stgit.rebasecmd' % get_head_file()) \
797 or config.get('stgit.rebasecmd')
798 if tree_id:
799 args = [tree_id]
800 elif command:
801 args = []
802 else:
803 raise GitException, 'Default rebasing requires a commit id'
804 if command:
805 # clear the HEAD cache as the custom rebase command will update it
806 __clear_head_cache()
807 Run(*(command.split() + args)).run()
808 else:
809 # default rebasing
810 reset(tree_id = tree_id)
811
812 def repack():
813 """Repack all objects into a single pack
814 """
815 GRun('repack', '-a', '-d', '-f').run()
816
817 def apply_patch(filename = None, diff = None, base = None,
818 fail_dump = True):
819 """Apply a patch onto the current or given index. There must not
820 be any local changes in the tree, otherwise the command fails
821 """
822 if diff is None:
823 if filename:
824 f = file(filename)
825 else:
826 f = sys.stdin
827 diff = f.read()
828 if filename:
829 f.close()
830
831 if base:
832 orig_head = get_head()
833 switch(base)
834 else:
835 refresh_index()
836
837 try:
838 GRun('apply', '--index').raw_input(diff).no_output()
839 except GitRunException:
840 if base:
841 switch(orig_head)
842 if fail_dump:
843 # write the failed diff to a file
844 f = file('.stgit-failed.patch', 'w+')
845 f.write(diff)
846 f.close()
847 out.warn('Diff written to the .stgit-failed.patch file')
848
849 raise
850
851 if base:
852 top = commit(message = 'temporary commit used for applying a patch',
853 parents = [base])
854 switch(orig_head)
855 merge_recursive(base, orig_head, top)
856
857 def clone(repository, local_dir):
858 """Clone a remote repository. At the moment, just use the
859 'git clone' script
860 """
861 GRun('clone', repository, local_dir).run()
862
863 def modifying_revs(files, base_rev, head_rev):
864 """Return the revisions from the list modifying the given files."""
865 return GRun('rev-list', '%s..%s' % (base_rev, head_rev), '--', *files
866 ).output_lines()
867
868 def refspec_localpart(refspec):
869 m = re.match('^[^:]*:([^:]*)$', refspec)
870 if m:
871 return m.group(1)
872 else:
873 raise GitException, 'Cannot parse refspec "%s"' % line
874
875 def refspec_remotepart(refspec):
876 m = re.match('^([^:]*):[^:]*$', refspec)
877 if m:
878 return m.group(1)
879 else:
880 raise GitException, 'Cannot parse refspec "%s"' % line
881
882 def __remotes_from_config():
883 return config.sections_matching(r'remote\.(.*)\.url')
884
885 def __remotes_from_dir(dir):
886 d = os.path.join(basedir.get(), dir)
887 if os.path.exists(d):
888 return os.listdir(d)
889 else:
890 return []
891
892 def remotes_list():
893 """Return the list of remotes in the repository
894 """
895 return (set(__remotes_from_config())
896 | set(__remotes_from_dir('remotes'))
897 | set(__remotes_from_dir('branches')))
898
899 def remotes_local_branches(remote):
900 """Returns the list of local branches fetched from given remote
901 """
902
903 branches = []
904 if remote in __remotes_from_config():
905 for line in config.getall('remote.%s.fetch' % remote):
906 branches.append(refspec_localpart(line))
907 elif remote in __remotes_from_dir('remotes'):
908 stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
909 for line in stream:
910 # Only consider Pull lines
911 m = re.match('^Pull: (.*)\n$', line)
912 if m:
913 branches.append(refspec_localpart(m.group(1)))
914 stream.close()
915 elif remote in __remotes_from_dir('branches'):
916 # old-style branches only declare one branch
917 branches.append('refs/heads/'+remote);
918 else:
919 raise GitException, 'Unknown remote "%s"' % remote
920
921 return branches
922
923 def identify_remote(branchname):
924 """Return the name for the remote to pull the given branchname
925 from, or None if we believe it is a local branch.
926 """
927
928 for remote in remotes_list():
929 if branchname in remotes_local_branches(remote):
930 return remote
931
932 # if we get here we've found nothing, the branch is a local one
933 return None
934
935 def fetch_head():
936 """Return the git id for the tip of the parent branch as left by
937 'git fetch'.
938 """
939
940 fetch_head=None
941 stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
942 for line in stream:
943 # Only consider lines not tagged not-for-merge
944 m = re.match('^([^\t]*)\t\t', line)
945 if m:
946 if fetch_head:
947 raise GitException, 'StGit does not support multiple FETCH_HEAD'
948 else:
949 fetch_head=m.group(1)
950 stream.close()
951
952 if not fetch_head:
953 out.warn('No for-merge remote head found in FETCH_HEAD')
954
955 # here we are sure to have a single fetch_head
956 return fetch_head
957
958 def all_refs():
959 """Return a list of all refs in the current repository.
960 """
961
962 return [line.split()[1] for line in GRun('show-ref').output_lines()]