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