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