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