Merge branch 'stable'
[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 = None, 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 tree_status(files = None, tree_id = 'HEAD', unknown = False,
194 noexclude = True, verbose = False, diff_flags = []):
195 """Get the status of all changed files, or of a selected set of
196 files. Returns a list of pairs - (status, filename).
197
198 If 'not files', it will check all files, and optionally all
199 unknown files. If 'files' is a list, it will only check the files
200 in the list.
201 """
202 assert not files or not unknown
203
204 if verbose:
205 out.start('Checking for changes in the working directory')
206
207 refresh_index()
208
209 cache_files = []
210
211 # unknown files
212 if unknown:
213 cmd = ['ls-files', '-z', '--others', '--directory',
214 '--no-empty-directory']
215 if not noexclude:
216 cmd += ['--exclude=%s' % s for s in
217 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
218 cmd += ['--exclude-per-directory=.gitignore']
219 cmd += ['--exclude-from=%s' % fn
220 for fn in exclude_files()
221 if os.path.exists(fn)]
222
223 lines = GRun(*cmd).raw_output().split('\0')
224 cache_files += [('?', line) for line in lines if line]
225
226 # conflicted files
227 conflicts = get_conflicts()
228 cache_files += [('C', filename) for filename in conflicts
229 if not files or filename in files]
230 reported_files = set(conflicts)
231
232 # files in the index
233 args = diff_flags + [tree_id]
234 if files:
235 args += ['--'] + files
236 for line in GRun('diff-index', *args).output_lines():
237 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
238 if fs[1] not in reported_files:
239 cache_files.append(fs)
240 reported_files.add(fs[1])
241
242 # files in the index but changed on (or removed from) disk
243 args = list(diff_flags)
244 if files:
245 args += ['--'] + files
246 for line in GRun('diff-files', *args).output_lines():
247 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
248 if fs[1] not in reported_files:
249 cache_files.append(fs)
250 reported_files.add(fs[1])
251
252 if verbose:
253 out.done()
254
255 return cache_files
256
257 def local_changes(verbose = True):
258 """Return true if there are local changes in the tree
259 """
260 return len(tree_status(verbose = verbose)) != 0
261
262 def get_heads():
263 heads = []
264 hr = re.compile(r'^[0-9a-f]{40} refs/heads/(.+)$')
265 for line in GRun('show-ref', '--heads').output_lines():
266 m = hr.match(line)
267 heads.append(m.group(1))
268 return heads
269
270 # HEAD value cached
271 __head = None
272
273 def get_head():
274 """Verifies the HEAD and returns the SHA1 id that represents it
275 """
276 global __head
277
278 if not __head:
279 __head = rev_parse('HEAD')
280 return __head
281
282 class DetachedHeadException(GitException):
283 def __init__(self):
284 GitException.__init__(self, 'Not on any branch')
285
286 def get_head_file():
287 """Return the name of the file pointed to by the HEAD symref.
288 Throw an exception if HEAD is detached."""
289 try:
290 return strip_prefix(
291 'refs/heads/', GRun('symbolic-ref', '-q', 'HEAD'
292 ).output_one_line())
293 except GitRunException:
294 raise DetachedHeadException()
295
296 def set_head_file(ref):
297 """Resets HEAD to point to a new ref
298 """
299 # head cache flushing is needed since we might have a different value
300 # in the new head
301 __clear_head_cache()
302 try:
303 GRun('symbolic-ref', 'HEAD', 'refs/heads/%s' % ref).run()
304 except GitRunException:
305 raise GitException, 'Could not set head to "%s"' % ref
306
307 def set_ref(ref, val):
308 """Point ref at a new commit object."""
309 try:
310 GRun('update-ref', ref, val).run()
311 except GitRunException:
312 raise GitException, 'Could not update %s to "%s".' % (ref, val)
313
314 def set_branch(branch, val):
315 set_ref('refs/heads/%s' % branch, val)
316
317 def __set_head(val):
318 """Sets the HEAD value
319 """
320 global __head
321
322 if not __head or __head != val:
323 set_ref('HEAD', val)
324 __head = val
325
326 # only allow SHA1 hashes
327 assert(len(__head) == 40)
328
329 def __clear_head_cache():
330 """Sets the __head to None so that a re-read is forced
331 """
332 global __head
333
334 __head = None
335
336 def refresh_index():
337 """Refresh index with stat() information from the working directory.
338 """
339 GRun('update-index', '-q', '--unmerged', '--refresh').run()
340
341 def rev_parse(git_id):
342 """Parse the string and return a verified SHA1 id
343 """
344 try:
345 return GRun('rev-parse', '--verify', git_id
346 ).discard_stderr().output_one_line()
347 except GitRunException:
348 raise GitException, 'Unknown revision: %s' % git_id
349
350 def ref_exists(ref):
351 try:
352 rev_parse(ref)
353 return True
354 except GitException:
355 return False
356
357 def branch_exists(branch):
358 return ref_exists('refs/heads/%s' % branch)
359
360 def create_branch(new_branch, tree_id = None):
361 """Create a new branch in the git repository
362 """
363 if branch_exists(new_branch):
364 raise GitException, 'Branch "%s" already exists' % new_branch
365
366 current_head_file = get_head_file()
367 current_head = get_head()
368 set_head_file(new_branch)
369 __set_head(current_head)
370
371 # a checkout isn't needed if new branch points to the current head
372 if tree_id:
373 try:
374 switch(tree_id)
375 except GitException:
376 # Tree switching failed. Revert the head file
377 set_head_file(current_head_file)
378 delete_branch(new_branch)
379 raise
380
381 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
382 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
383
384 def switch_branch(new_branch):
385 """Switch to a git branch
386 """
387 global __head
388
389 if not branch_exists(new_branch):
390 raise GitException, 'Branch "%s" does not exist' % new_branch
391
392 tree_id = rev_parse('refs/heads/%s^{commit}' % new_branch)
393 if tree_id != get_head():
394 refresh_index()
395 try:
396 GRun('read-tree', '-u', '-m', get_head(), tree_id).run()
397 except GitRunException:
398 raise GitException, 'read-tree failed (local changes maybe?)'
399 __head = tree_id
400 set_head_file(new_branch)
401
402 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
403 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
404
405 def delete_ref(ref):
406 if not ref_exists(ref):
407 raise GitException, '%s does not exist' % ref
408 sha1 = GRun('show-ref', '-s', ref).output_one_line()
409 try:
410 GRun('update-ref', '-d', ref, sha1).run()
411 except GitRunException:
412 raise GitException, 'Failed to delete ref %s' % ref
413
414 def delete_branch(name):
415 delete_ref('refs/heads/%s' % name)
416
417 def rename_ref(from_ref, to_ref):
418 if not ref_exists(from_ref):
419 raise GitException, '"%s" does not exist' % from_ref
420 if ref_exists(to_ref):
421 raise GitException, '"%s" already exists' % to_ref
422
423 sha1 = GRun('show-ref', '-s', from_ref).output_one_line()
424 try:
425 GRun('update-ref', to_ref, sha1, '0'*40).run()
426 except GitRunException:
427 raise GitException, 'Failed to create new ref %s' % to_ref
428 try:
429 GRun('update-ref', '-d', from_ref, sha1).run()
430 except GitRunException:
431 raise GitException, 'Failed to delete ref %s' % from_ref
432
433 def rename_branch(from_name, to_name):
434 """Rename a git branch."""
435 rename_ref('refs/heads/%s' % from_name, 'refs/heads/%s' % to_name)
436 try:
437 if get_head_file() == from_name:
438 set_head_file(to_name)
439 except DetachedHeadException:
440 pass # detached HEAD, so the renamee can't be the current branch
441 reflog_dir = os.path.join(basedir.get(), 'logs', 'refs', 'heads')
442 if os.path.exists(reflog_dir) \
443 and os.path.exists(os.path.join(reflog_dir, from_name)):
444 rename(reflog_dir, from_name, to_name)
445
446 # Persons caching
447 __user = None
448 __author = None
449 __committer = None
450
451 def user():
452 """Return the user information.
453 """
454 global __user
455 if not __user:
456 name=config.get('user.name')
457 email=config.get('user.email')
458 __user = Person(name, email)
459 return __user;
460
461 def author():
462 """Return the author information.
463 """
464 global __author
465 if not __author:
466 try:
467 # the environment variables take priority over config
468 try:
469 date = os.environ['GIT_AUTHOR_DATE']
470 except KeyError:
471 date = ''
472 __author = Person(os.environ['GIT_AUTHOR_NAME'],
473 os.environ['GIT_AUTHOR_EMAIL'],
474 date)
475 except KeyError:
476 __author = user()
477 return __author
478
479 def committer():
480 """Return the author information.
481 """
482 global __committer
483 if not __committer:
484 try:
485 # the environment variables take priority over config
486 try:
487 date = os.environ['GIT_COMMITTER_DATE']
488 except KeyError:
489 date = ''
490 __committer = Person(os.environ['GIT_COMMITTER_NAME'],
491 os.environ['GIT_COMMITTER_EMAIL'],
492 date)
493 except KeyError:
494 __committer = user()
495 return __committer
496
497 def update_cache(files = None, force = False):
498 """Update the cache information for the given files
499 """
500 cache_files = tree_status(files, verbose = False)
501
502 # everything is up-to-date
503 if len(cache_files) == 0:
504 return False
505
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'
510
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']]
515
516 GRun('update-index', '--add', '--').xargs(add_files)
517 GRun('update-index', '--force-remove', '--').xargs(rm_files)
518 GRun('update-index', '--').xargs(m_files)
519
520 return True
521
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
527 """
528 if not parents:
529 parents = []
530
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'
536
537 # get the commit message
538 if not message:
539 message = '\n'
540 elif message[-1:] != '\n':
541 message += '\n'
542
543 # write the index to repository
544 if tree_id == None:
545 tree_id = GRun('write-tree').output_one_line()
546 set_head = True
547
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)
565
566 return commit_id
567
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.
573 """
574 if check_index:
575 index_opt = ['--index']
576 else:
577 index_opt = []
578
579 if not files:
580 files = []
581
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
589
590 return True
591
592 stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
593
594 def merge_recursive(base, head1, head2):
595 """Perform a 3-way merge between base, head1 and head2 into the
596 local tree
597 """
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 conflicts = [l.strip() for l in output if l.startswith('CONFLICT')]
607 out.info(*conflicts)
608
609 # try the interactive merge or stage checkout (if enabled)
610 for filename in get_conflicts():
611 if (gitmergeonefile.merge(filename)):
612 # interactive merge succeeded
613 resolved([filename])
614
615 # any conflicts left unsolved?
616 cn = len(get_conflicts())
617 if cn:
618 raise GitException, "%d conflict(s)" % cn
619
620 def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = [],
621 binary = True):
622 """Show the diff between rev1 and rev2
623 """
624 if not files:
625 files = []
626 if binary and '--binary' not in diff_flags:
627 diff_flags = diff_flags + ['--binary']
628
629 if rev1 and rev2:
630 return GRun('diff-tree', '-p',
631 *(diff_flags + [rev1, rev2, '--'] + files)).raw_output()
632 elif rev1 or rev2:
633 refresh_index()
634 if rev2:
635 return GRun('diff-index', '-p', '-R',
636 *(diff_flags + [rev2, '--'] + files)).raw_output()
637 else:
638 return GRun('diff-index', '-p',
639 *(diff_flags + [rev1, '--'] + files)).raw_output()
640 else:
641 return ''
642
643 def diffstat(diff):
644 """Return the diffstat of the supplied diff."""
645 return GRun('apply', '--stat', '--summary').raw_input(diff).raw_output()
646
647 def files(rev1, rev2, diff_flags = []):
648 """Return the files modified between rev1 and rev2
649 """
650
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)))
655
656 return '\n'.join(result)
657
658 def barefiles(rev1, rev2):
659 """Return the files modified between rev1 and rev2, without status info
660 """
661
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])
665
666 return '\n'.join(result)
667
668 def pretty_commit(commit_id = 'HEAD', flags = []):
669 """Return a given commit (log + diff)
670 """
671 return GRun('show', *(flags + [commit_id])).raw_output()
672
673 def checkout(files = None, tree_id = None, force = False):
674 """Check out the given or all files
675 """
676 if tree_id:
677 try:
678 GRun('read-tree', '--reset', tree_id).run()
679 except GitRunException:
680 raise GitException, 'Failed "git read-tree" --reset %s' % tree_id
681
682 cmd = ['checkout-index', '-q', '-u']
683 if force:
684 cmd.append('-f')
685 if files:
686 GRun(*(cmd + ['--'])).xargs(files)
687 else:
688 GRun(*(cmd + ['-a'])).run()
689
690 def switch(tree_id, keep = False):
691 """Switch the tree to the given id
692 """
693 if keep:
694 # only update the index while keeping the local changes
695 GRun('read-tree', tree_id).run()
696 else:
697 refresh_index()
698 try:
699 GRun('read-tree', '-u', '-m', get_head(), tree_id).run()
700 except GitRunException:
701 raise GitException, 'read-tree failed (local changes maybe?)'
702
703 __set_head(tree_id)
704
705 def reset(files = None, tree_id = None, check_out = True):
706 """Revert the tree changes relative to the given tree_id. It removes
707 any local changes
708 """
709 if not tree_id:
710 tree_id = get_head()
711
712 if check_out:
713 cache_files = tree_status(files, tree_id)
714 # files which were added but need to be removed
715 rm_files = [x[1] for x in cache_files if x[0] in ['A']]
716
717 checkout(files, tree_id, True)
718 # checkout doesn't remove files
719 map(os.remove, rm_files)
720
721 # if the reset refers to the whole tree, switch the HEAD as well
722 if not files:
723 __set_head(tree_id)
724
725 def resolved(filenames, reset = None):
726 if reset:
727 stage = {'ancestor': 1, 'current': 2, 'patched': 3}[reset]
728 GRun('checkout-index', '--no-create', '--stage=%d' % stage,
729 '--stdin', '-z').input_nulterm(filenames).no_output()
730 GRun('update-index', '--add', '--').xargs(filenames)
731 for filename in filenames:
732 gitmergeonefile.clean_up(filename)
733 # update the access and modificatied times
734 os.utime(filename, None)
735
736 def fetch(repository = 'origin', refspec = None):
737 """Fetches changes from the remote repository, using 'git fetch'
738 by default.
739 """
740 # we update the HEAD
741 __clear_head_cache()
742
743 args = [repository]
744 if refspec:
745 args.append(refspec)
746
747 command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
748 config.get('stgit.fetchcmd')
749 Run(*(command.split() + args)).run()
750
751 def pull(repository = 'origin', refspec = None):
752 """Fetches changes from the remote repository, using 'git pull'
753 by default.
754 """
755 # we update the HEAD
756 __clear_head_cache()
757
758 args = [repository]
759 if refspec:
760 args.append(refspec)
761
762 command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
763 config.get('stgit.pullcmd')
764 Run(*(command.split() + args)).run()
765
766 def rebase(tree_id = None):
767 """Rebase the current tree to the give tree_id. The tree_id
768 argument may be something other than a GIT id if an external
769 command is invoked.
770 """
771 command = config.get('branch.%s.stgit.rebasecmd' % get_head_file()) \
772 or config.get('stgit.rebasecmd')
773 if tree_id:
774 args = [tree_id]
775 elif command:
776 args = []
777 else:
778 raise GitException, 'Default rebasing requires a commit id'
779 if command:
780 # clear the HEAD cache as the custom rebase command will update it
781 __clear_head_cache()
782 Run(*(command.split() + args)).run()
783 else:
784 # default rebasing
785 reset(tree_id = tree_id)
786
787 def repack():
788 """Repack all objects into a single pack
789 """
790 GRun('repack', '-a', '-d', '-f').run()
791
792 def apply_patch(filename = None, diff = None, base = None,
793 fail_dump = True):
794 """Apply a patch onto the current or given index. There must not
795 be any local changes in the tree, otherwise the command fails
796 """
797 if diff is None:
798 if filename:
799 f = file(filename)
800 else:
801 f = sys.stdin
802 diff = f.read()
803 if filename:
804 f.close()
805
806 if base:
807 orig_head = get_head()
808 switch(base)
809 else:
810 refresh_index()
811
812 try:
813 GRun('apply', '--index').raw_input(diff).no_output()
814 except GitRunException:
815 if base:
816 switch(orig_head)
817 if fail_dump:
818 # write the failed diff to a file
819 f = file('.stgit-failed.patch', 'w+')
820 f.write(diff)
821 f.close()
822 out.warn('Diff written to the .stgit-failed.patch file')
823
824 raise
825
826 if base:
827 top = commit(message = 'temporary commit used for applying a patch',
828 parents = [base])
829 switch(orig_head)
830 merge_recursive(base, orig_head, top)
831
832 def clone(repository, local_dir):
833 """Clone a remote repository. At the moment, just use the
834 'git clone' script
835 """
836 GRun('clone', repository, local_dir).run()
837
838 def modifying_revs(files, base_rev, head_rev):
839 """Return the revisions from the list modifying the given files."""
840 return GRun('rev-list', '%s..%s' % (base_rev, head_rev), '--', *files
841 ).output_lines()
842
843 def refspec_localpart(refspec):
844 m = re.match('^[^:]*:([^:]*)$', refspec)
845 if m:
846 return m.group(1)
847 else:
848 raise GitException, 'Cannot parse refspec "%s"' % line
849
850 def refspec_remotepart(refspec):
851 m = re.match('^([^:]*):[^:]*$', refspec)
852 if m:
853 return m.group(1)
854 else:
855 raise GitException, 'Cannot parse refspec "%s"' % line
856
857
858 def __remotes_from_config():
859 return config.sections_matching(r'remote\.(.*)\.url')
860
861 def __remotes_from_dir(dir):
862 d = os.path.join(basedir.get(), dir)
863 if os.path.exists(d):
864 return os.listdir(d)
865 else:
866 return []
867
868 def remotes_list():
869 """Return the list of remotes in the repository
870 """
871 return (set(__remotes_from_config())
872 | set(__remotes_from_dir('remotes'))
873 | set(__remotes_from_dir('branches')))
874
875 def remotes_local_branches(remote):
876 """Returns the list of local branches fetched from given remote
877 """
878
879 branches = []
880 if remote in __remotes_from_config():
881 for line in config.getall('remote.%s.fetch' % remote):
882 branches.append(refspec_localpart(line))
883 elif remote in __remotes_from_dir('remotes'):
884 stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
885 for line in stream:
886 # Only consider Pull lines
887 m = re.match('^Pull: (.*)\n$', line)
888 if m:
889 branches.append(refspec_localpart(m.group(1)))
890 stream.close()
891 elif remote in __remotes_from_dir('branches'):
892 # old-style branches only declare one branch
893 branches.append('refs/heads/'+remote);
894 else:
895 raise GitException, 'Unknown remote "%s"' % remote
896
897 return branches
898
899 def identify_remote(branchname):
900 """Return the name for the remote to pull the given branchname
901 from, or None if we believe it is a local branch.
902 """
903
904 for remote in remotes_list():
905 if branchname in remotes_local_branches(remote):
906 return remote
907
908 # if we get here we've found nothing, the branch is a local one
909 return None
910
911 def fetch_head():
912 """Return the git id for the tip of the parent branch as left by
913 'git fetch'.
914 """
915
916 fetch_head=None
917 stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
918 for line in stream:
919 # Only consider lines not tagged not-for-merge
920 m = re.match('^([^\t]*)\t\t', line)
921 if m:
922 if fetch_head:
923 raise GitException, 'StGit does not support multiple FETCH_HEAD'
924 else:
925 fetch_head=m.group(1)
926 stream.close()
927
928 if not fetch_head:
929 out.warn('No for-merge remote head found in FETCH_HEAD')
930
931 # here we are sure to have a single fetch_head
932 return fetch_head
933
934 def all_refs():
935 """Return a list of all refs in the current repository.
936 """
937
938 return [line.split()[1] for line in GRun('show-ref').output_lines()]