chiark / gitweb /
Make 'stg cp' 2nd form safe.
[stgit] / stgit / git.py
... / ...
CommitLineData
1"""Python GIT interface
2"""
3
4__copyright__ = """
5Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
6
7This program is free software; you can redistribute it and/or modify
8it under the terms of the GNU General Public License version 2 as
9published by the Free Software Foundation.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
15
16You should have received a copy of the GNU General Public License
17along with this program; if not, write to the Free Software
18Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19"""
20
21import sys, os, popen2, re, gitmergeonefile
22from shutil import copyfile
23
24from stgit import basedir
25from stgit.utils import *
26from stgit.config import config
27from sets import Set
28
29# git exception class
30class GitException(Exception):
31 pass
32
33
34
35#
36# Classes
37#
38
39class 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
71class 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
127def 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
140def 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
152def _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
164def _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
172def _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
180def _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
192def _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
200def __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
219def __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
268def 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
276def 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
285def 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
291def 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
301def __set_head(val):
302 """Sets the HEAD value
303 """
304 global __head
305
306 if not __head or __head != val:
307 if __run('git-update-ref HEAD', [val]) != 0:
308 raise GitException, 'Could not update HEAD to "%s".' % val
309 __head = val
310
311 # only allow SHA1 hashes
312 assert(len(__head) == 40)
313
314def __clear_head_cache():
315 """Sets the __head to None so that a re-read is forced
316 """
317 global __head
318
319 __head = None
320
321def refresh_index():
322 """Refresh index with stat() information from the working directory.
323 """
324 __run('git-update-index -q --unmerged --refresh')
325
326def rev_parse(git_id):
327 """Parse the string and return a verified SHA1 id
328 """
329 try:
330 return _output_one_line(['git-rev-parse', '--verify', git_id])
331 except GitException:
332 raise GitException, 'Unknown revision: %s' % git_id
333
334def branch_exists(branch):
335 """Existence check for the named branch
336 """
337 branch = os.path.join('refs', 'heads', branch)
338 for line in _output_lines('git-rev-parse --symbolic --all 2>&1'):
339 if line.strip() == branch:
340 return True
341 if re.compile('[ |/]'+branch+' ').search(line):
342 raise GitException, 'Bogus branch: %s' % line
343 return False
344
345def create_branch(new_branch, tree_id = None):
346 """Create a new branch in the git repository
347 """
348 if branch_exists(new_branch):
349 raise GitException, 'Branch "%s" already exists' % new_branch
350
351 current_head = get_head()
352 set_head_file(new_branch)
353 __set_head(current_head)
354
355 # a checkout isn't needed if new branch points to the current head
356 if tree_id:
357 switch(tree_id)
358
359 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
360 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
361
362def switch_branch(new_branch):
363 """Switch to a git branch
364 """
365 global __head
366
367 if not branch_exists(new_branch):
368 raise GitException, 'Branch "%s" does not exist' % new_branch
369
370 tree_id = rev_parse(os.path.join('refs', 'heads', new_branch)
371 + '^{commit}')
372 if tree_id != get_head():
373 refresh_index()
374 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
375 raise GitException, 'git-read-tree failed (local changes maybe?)'
376 __head = tree_id
377 set_head_file(new_branch)
378
379 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
380 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
381
382def delete_branch(name):
383 """Delete a git branch
384 """
385 if not branch_exists(name):
386 raise GitException, 'Branch "%s" does not exist' % name
387 remove_file_and_dirs(os.path.join(basedir.get(), 'refs', 'heads'),
388 name)
389
390def rename_branch(from_name, to_name):
391 """Rename a git branch
392 """
393 if not branch_exists(from_name):
394 raise GitException, 'Branch "%s" does not exist' % from_name
395 if branch_exists(to_name):
396 raise GitException, 'Branch "%s" already exists' % to_name
397
398 if get_head_file() == from_name:
399 set_head_file(to_name)
400 rename(os.path.join(basedir.get(), 'refs', 'heads'),
401 from_name, to_name)
402
403 reflog_dir = os.path.join(basedir.get(), 'logs', 'refs', 'heads')
404 if os.path.exists(reflog_dir) \
405 and os.path.exists(os.path.join(reflog_dir, from_name)):
406 rename(reflog_dir, from_name, to_name)
407
408def add(names):
409 """Add the files or recursively add the directory contents
410 """
411 # generate the file list
412 files = []
413 for i in names:
414 if not os.path.exists(i):
415 raise GitException, 'Unknown file or directory: %s' % i
416
417 if os.path.isdir(i):
418 # recursive search. We only add files
419 for root, dirs, local_files in os.walk(i):
420 for name in [os.path.join(root, f) for f in local_files]:
421 if os.path.isfile(name):
422 files.append(os.path.normpath(name))
423 elif os.path.isfile(i):
424 files.append(os.path.normpath(i))
425 else:
426 raise GitException, '%s is not a file or directory' % i
427
428 if files:
429 if __run('git-update-index --add --', files):
430 raise GitException, 'Unable to add file'
431
432def __copy_single(source, target, target2=''):
433 """Copy file or dir named 'source' to name target+target2"""
434
435 # "source" (file or dir) must match one or more git-controlled file
436 realfiles = _output_lines(['git-ls-files', source])
437 if len(realfiles) == 0:
438 raise GitException, '"%s" matches no git-controled files' % source
439
440 if os.path.isdir(source):
441 # physically copy the files, and record them to add them in one run
442 newfiles = []
443 re_string='^'+source+'/(.*)$'
444 prefix_regexp = re.compile(re_string)
445 for f in [f.strip() for f in realfiles]:
446 m = prefix_regexp.match(f)
447 if not m:
448 print '"%s" does not match "%s"' % (f, re_string)
449 assert(m)
450 newname = target+target2+'/'+m.group(1)
451 if not os.path.exists(os.path.dirname(newname)):
452 os.makedirs(os.path.dirname(newname))
453 copyfile(f, newname)
454 newfiles.append(newname)
455
456 add(newfiles)
457 else: # files, symlinks, ...
458 newname = target+target2
459 copyfile(source, newname)
460 add([newname])
461
462
463def copy(filespecs, target):
464 if os.path.isdir(target):
465 # target is a directory: copy each entry on the command line,
466 # with the same name, into the target
467 target = target.rstrip('/')
468
469 # first, check that none of the children of the target
470 # matching the command line aleady exist
471 for filespec in filespecs:
472 entry = target+ '/' + os.path.basename(filespec.rstrip('/'))
473 if os.path.exists(entry):
474 raise GitException, 'Target "%s" already exists' % entry
475
476 for filespec in filespecs:
477 filespec = filespec.rstrip('/')
478 basename = '/' + os.path.basename(filespec)
479 __copy_single(filespec, target, basename)
480
481 elif os.path.exists(target):
482 raise GitException, 'Target "%s" exists but is not a directory' % target
483 elif len(filespecs) != 1:
484 raise GitException, 'Cannot copy more than one file to non-directory'
485
486 else:
487 # at this point: len(filespecs)==1 and target does not exist
488
489 # check target directory
490 targetdir = os.path.dirname(target)
491 if targetdir != '' and not os.path.isdir(targetdir):
492 raise GitException, 'Target directory "%s" does not exist' % targetdir
493
494 __copy_single(filespecs[0].rstrip('/'), target)
495
496
497def rm(files, force = False):
498 """Remove a file from the repository
499 """
500 if not force:
501 for f in files:
502 if os.path.exists(f):
503 raise GitException, '%s exists. Remove it first' %f
504 if files:
505 __run('git-update-index --remove --', files)
506 else:
507 if files:
508 __run('git-update-index --force-remove --', files)
509
510# Persons caching
511__user = None
512__author = None
513__committer = None
514
515def user():
516 """Return the user information.
517 """
518 global __user
519 if not __user:
520 name=config.get('user.name')
521 email=config.get('user.email')
522 __user = Person(name, email)
523 return __user;
524
525def author():
526 """Return the author information.
527 """
528 global __author
529 if not __author:
530 try:
531 # the environment variables take priority over config
532 try:
533 date = os.environ['GIT_AUTHOR_DATE']
534 except KeyError:
535 date = ''
536 __author = Person(os.environ['GIT_AUTHOR_NAME'],
537 os.environ['GIT_AUTHOR_EMAIL'],
538 date)
539 except KeyError:
540 __author = user()
541 return __author
542
543def committer():
544 """Return the author information.
545 """
546 global __committer
547 if not __committer:
548 try:
549 # the environment variables take priority over config
550 try:
551 date = os.environ['GIT_COMMITTER_DATE']
552 except KeyError:
553 date = ''
554 __committer = Person(os.environ['GIT_COMMITTER_NAME'],
555 os.environ['GIT_COMMITTER_EMAIL'],
556 date)
557 except KeyError:
558 __committer = user()
559 return __committer
560
561def update_cache(files = None, force = False):
562 """Update the cache information for the given files
563 """
564 if not files:
565 files = []
566
567 cache_files = __tree_status(files, verbose = False)
568
569 # everything is up-to-date
570 if len(cache_files) == 0:
571 return False
572
573 # check for unresolved conflicts
574 if not force and [x for x in cache_files
575 if x[0] not in ['M', 'N', 'A', 'D']]:
576 raise GitException, 'Updating cache failed: unresolved conflicts'
577
578 # update the cache
579 add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
580 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
581 m_files = [x[1] for x in cache_files if x[0] in ['M']]
582
583 if add_files and __run('git-update-index --add --', add_files) != 0:
584 raise GitException, 'Failed git-update-index --add'
585 if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
586 raise GitException, 'Failed git-update-index --rm'
587 if m_files and __run('git-update-index --', m_files) != 0:
588 raise GitException, 'Failed git-update-index'
589
590 return True
591
592def commit(message, files = None, parents = None, allowempty = False,
593 cache_update = True, tree_id = None,
594 author_name = None, author_email = None, author_date = None,
595 committer_name = None, committer_email = None):
596 """Commit the current tree to repository
597 """
598 if not files:
599 files = []
600 if not parents:
601 parents = []
602
603 # Get the tree status
604 if cache_update and parents != []:
605 changes = update_cache(files)
606 if not changes and not allowempty:
607 raise GitException, 'No changes to commit'
608
609 # get the commit message
610 if not message:
611 message = '\n'
612 elif message[-1:] != '\n':
613 message += '\n'
614
615 must_switch = True
616 # write the index to repository
617 if tree_id == None:
618 tree_id = _output_one_line('git-write-tree')
619 else:
620 must_switch = False
621
622 # the commit
623 cmd = ''
624 if author_name:
625 cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
626 if author_email:
627 cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
628 if author_date:
629 cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
630 if committer_name:
631 cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
632 if committer_email:
633 cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
634 cmd += 'git-commit-tree %s' % tree_id
635
636 # get the parents
637 for p in parents:
638 cmd += ' -p %s' % p
639
640 commit_id = _output_one_line(cmd, message)
641 if must_switch:
642 __set_head(commit_id)
643
644 return commit_id
645
646def apply_diff(rev1, rev2, check_index = True, files = None):
647 """Apply the diff between rev1 and rev2 onto the current
648 index. This function doesn't need to raise an exception since it
649 is only used for fast-pushing a patch. If this operation fails,
650 the pushing would fall back to the three-way merge.
651 """
652 if check_index:
653 index_opt = '--index'
654 else:
655 index_opt = ''
656
657 if not files:
658 files = []
659
660 diff_str = diff(files, rev1, rev2)
661 if diff_str:
662 try:
663 _input_str('git-apply %s' % index_opt, diff_str)
664 except GitException:
665 return False
666
667 return True
668
669def merge(base, head1, head2, recursive = False):
670 """Perform a 3-way merge between base, head1 and head2 into the
671 local tree
672 """
673 refresh_index()
674
675 if recursive:
676 # this operation tracks renames but it is slower (used in
677 # general when pushing or picking patches)
678 try:
679 # use _output() to mask the verbose prints of the tool
680 _output('git-merge-recursive %s -- %s %s' % (base, head1, head2))
681 except GitException:
682 pass
683 else:
684 # the fast case where we don't track renames (used when the
685 # distance between base and heads is small, i.e. folding or
686 # synchronising patches)
687 if __run('git-read-tree -u -m --aggressive',
688 [base, head1, head2]) != 0:
689 raise GitException, 'git-read-tree failed (local changes maybe?)'
690
691 # check the index for unmerged entries
692 files = {}
693 stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
694
695 for line in _output('git-ls-files --unmerged --stage -z').split('\0'):
696 if not line:
697 continue
698
699 mode, hash, stage, path = stages_re.findall(line)[0]
700
701 if not path in files:
702 files[path] = {}
703 files[path]['1'] = ('', '')
704 files[path]['2'] = ('', '')
705 files[path]['3'] = ('', '')
706
707 files[path][stage] = (mode, hash)
708
709 # merge the unmerged files
710 errors = False
711 for path in files:
712 # remove additional files that might be generated for some
713 # newer versions of GIT
714 for suffix in [base, head1, head2]:
715 if not suffix:
716 continue
717 fname = path + '~' + suffix
718 if os.path.exists(fname):
719 os.remove(fname)
720
721 stages = files[path]
722 if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
723 stages['3'][1], path, stages['1'][0],
724 stages['2'][0], stages['3'][0]) != 0:
725 errors = True
726
727 if errors:
728 raise GitException, 'GIT index merging failed (possible conflicts)'
729
730def status(files = None, modified = False, new = False, deleted = False,
731 conflict = False, unknown = False, noexclude = False):
732 """Show the tree status
733 """
734 if not files:
735 files = []
736
737 cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
738 all = not (modified or new or deleted or conflict or unknown)
739
740 if not all:
741 filestat = []
742 if modified:
743 filestat.append('M')
744 if new:
745 filestat.append('A')
746 filestat.append('N')
747 if deleted:
748 filestat.append('D')
749 if conflict:
750 filestat.append('C')
751 if unknown:
752 filestat.append('?')
753 cache_files = [x for x in cache_files if x[0] in filestat]
754
755 for fs in cache_files:
756 if files and not fs[1] in files:
757 continue
758 if all:
759 print '%s %s' % (fs[0], fs[1])
760 else:
761 print '%s' % fs[1]
762
763def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None):
764 """Show the diff between rev1 and rev2
765 """
766 if not files:
767 files = []
768
769 if rev1 and rev2:
770 diff_str = _output(['git-diff-tree', '-p', rev1, rev2, '--'] + files)
771 elif rev1 or rev2:
772 refresh_index()
773 if rev2:
774 diff_str = _output(['git-diff-index', '-p', '-R', rev2, '--'] + files)
775 else:
776 diff_str = _output(['git-diff-index', '-p', rev1, '--'] + files)
777 else:
778 diff_str = ''
779
780 if out_fd:
781 out_fd.write(diff_str)
782 else:
783 return diff_str
784
785def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
786 """Return the diffstat between rev1 and rev2
787 """
788 if not files:
789 files = []
790
791 p=popen2.Popen3('git-apply --stat')
792 diff(files, rev1, rev2, p.tochild)
793 p.tochild.close()
794 diff_str = p.fromchild.read().rstrip()
795 if p.wait():
796 raise GitException, 'git.diffstat failed'
797 return diff_str
798
799def files(rev1, rev2):
800 """Return the files modified between rev1 and rev2
801 """
802
803 result = ''
804 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
805 result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
806
807 return result.rstrip()
808
809def barefiles(rev1, rev2):
810 """Return the files modified between rev1 and rev2, without status info
811 """
812
813 result = ''
814 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
815 result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
816
817 return result.rstrip()
818
819def pretty_commit(commit_id = 'HEAD'):
820 """Return a given commit (log + diff)
821 """
822 return _output(['git-diff-tree', '--cc', '--always', '--pretty', '-r',
823 commit_id])
824
825def checkout(files = None, tree_id = None, force = False):
826 """Check out the given or all files
827 """
828 if not files:
829 files = []
830
831 if tree_id and __run('git-read-tree --reset', [tree_id]) != 0:
832 raise GitException, 'Failed git-read-tree --reset %s' % tree_id
833
834 checkout_cmd = 'git-checkout-index -q -u'
835 if force:
836 checkout_cmd += ' -f'
837 if len(files) == 0:
838 checkout_cmd += ' -a'
839 else:
840 checkout_cmd += ' --'
841
842 if __run(checkout_cmd, files) != 0:
843 raise GitException, 'Failed git-checkout-index'
844
845def switch(tree_id, keep = False):
846 """Switch the tree to the given id
847 """
848 if not keep:
849 refresh_index()
850 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
851 raise GitException, 'git-read-tree failed (local changes maybe?)'
852
853 __set_head(tree_id)
854
855def reset(files = None, tree_id = None, check_out = True):
856 """Revert the tree changes relative to the given tree_id. It removes
857 any local changes
858 """
859 if not tree_id:
860 tree_id = get_head()
861
862 if check_out:
863 cache_files = __tree_status(files, tree_id)
864 # files which were added but need to be removed
865 rm_files = [x[1] for x in cache_files if x[0] in ['A']]
866
867 checkout(files, tree_id, True)
868 # checkout doesn't remove files
869 map(os.remove, rm_files)
870
871 # if the reset refers to the whole tree, switch the HEAD as well
872 if not files:
873 __set_head(tree_id)
874
875def fetch(repository = 'origin', refspec = None):
876 """Fetches changes from the remote repository, using 'git-fetch'
877 by default.
878 """
879 # we update the HEAD
880 __clear_head_cache()
881
882 args = [repository]
883 if refspec:
884 args.append(refspec)
885
886 command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
887 config.get('stgit.fetchcmd')
888 if __run(command, args) != 0:
889 raise GitException, 'Failed "%s %s"' % (command, repository)
890
891def pull(repository = 'origin', refspec = None):
892 """Fetches changes from the remote repository, using 'git-pull'
893 by default.
894 """
895 # we update the HEAD
896 __clear_head_cache()
897
898 args = [repository]
899 if refspec:
900 args.append(refspec)
901
902 command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
903 config.get('stgit.pullcmd')
904 if __run(command, args) != 0:
905 raise GitException, 'Failed "%s %s"' % (command, repository)
906
907def repack():
908 """Repack all objects into a single pack
909 """
910 __run('git-repack -a -d -f')
911
912def apply_patch(filename = None, diff = None, base = None,
913 fail_dump = True):
914 """Apply a patch onto the current or given index. There must not
915 be any local changes in the tree, otherwise the command fails
916 """
917 if diff is None:
918 if filename:
919 f = file(filename)
920 else:
921 f = sys.stdin
922 diff = f.read()
923 if filename:
924 f.close()
925
926 if base:
927 orig_head = get_head()
928 switch(base)
929 else:
930 refresh_index()
931
932 try:
933 _input_str('git-apply --index', diff)
934 except GitException:
935 if base:
936 switch(orig_head)
937 if fail_dump:
938 # write the failed diff to a file
939 f = file('.stgit-failed.patch', 'w+')
940 f.write(diff)
941 f.close()
942 print >> sys.stderr, 'Diff written to the .stgit-failed.patch file'
943
944 raise
945
946 if base:
947 top = commit(message = 'temporary commit used for applying a patch',
948 parents = [base])
949 switch(orig_head)
950 merge(base, orig_head, top)
951
952def clone(repository, local_dir):
953 """Clone a remote repository. At the moment, just use the
954 'git-clone' script
955 """
956 if __run('git-clone', [repository, local_dir]) != 0:
957 raise GitException, 'Failed "git-clone %s %s"' \
958 % (repository, local_dir)
959
960def modifying_revs(files, base_rev, head_rev):
961 """Return the revisions from the list modifying the given files
962 """
963 cmd = ['git-rev-list', '%s..%s' % (base_rev, head_rev), '--']
964 revs = [line.strip() for line in _output_lines(cmd + files)]
965
966 return revs
967
968
969def refspec_localpart(refspec):
970 m = re.match('^[^:]*:([^:]*)$', refspec)
971 if m:
972 return m.group(1)
973 else:
974 raise GitException, 'Cannot parse refspec "%s"' % line
975
976def refspec_remotepart(refspec):
977 m = re.match('^([^:]*):[^:]*$', refspec)
978 if m:
979 return m.group(1)
980 else:
981 raise GitException, 'Cannot parse refspec "%s"' % line
982
983
984def __remotes_from_config():
985 return config.sections_matching(r'remote\.(.*)\.url')
986
987def __remotes_from_dir(dir):
988 d = os.path.join(basedir.get(), dir)
989 if os.path.exists(d):
990 return os.listdir(d)
991 else:
992 return None
993
994def remotes_list():
995 """Return the list of remotes in the repository
996 """
997
998 return Set(__remotes_from_config()) | \
999 Set(__remotes_from_dir('remotes')) | \
1000 Set(__remotes_from_dir('branches'))
1001
1002def remotes_local_branches(remote):
1003 """Returns the list of local branches fetched from given remote
1004 """
1005
1006 branches = []
1007 if remote in __remotes_from_config():
1008 for line in config.getall('remote.%s.fetch' % remote):
1009 branches.append(refspec_localpart(line))
1010 elif remote in __remotes_from_dir('remotes'):
1011 stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
1012 for line in stream:
1013 # Only consider Pull lines
1014 m = re.match('^Pull: (.*)\n$', line)
1015 if m:
1016 branches.append(refspec_localpart(m.group(1)))
1017 stream.close()
1018 elif remote in __remotes_from_dir('branches'):
1019 # old-style branches only declare one branch
1020 branches.append('refs/heads/'+remote);
1021 else:
1022 raise GitException, 'Unknown remote "%s"' % remote
1023
1024 return branches
1025
1026def identify_remote(branchname):
1027 """Return the name for the remote to pull the given branchname
1028 from, or None if we believe it is a local branch.
1029 """
1030
1031 for remote in remotes_list():
1032 if branchname in remotes_local_branches(remote):
1033 return remote
1034
1035 # if we get here we've found nothing, the branch is a local one
1036 return None
1037
1038def fetch_head():
1039 """Return the git id for the tip of the parent branch as left by
1040 'git fetch'.
1041 """
1042
1043 fetch_head=None
1044 stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
1045 for line in stream:
1046 # Only consider lines not tagged not-for-merge
1047 m = re.match('^([^\t]*)\t\t', line)
1048 if m:
1049 if fetch_head:
1050 raise GitException, "StGit does not support multiple FETCH_HEAD"
1051 else:
1052 fetch_head=m.group(1)
1053 stream.close()
1054
1055 # here we are sure to have a single fetch_head
1056 return fetch_head