Teach StGIT about core.excludesfile
[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', 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',
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)' % (' '.join(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)' % (' '.join(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)' % (' '.join(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)' % (' '.join(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)' % (' '.join(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 exclude_files():
220 files = [os.path.join(basedir.get(), 'info', 'exclude')]
221 user_exclude = config.get('core.excludesfile')
222 if user_exclude:
223 files.append(user_exclude)
224 return files
225
226 def tree_status(files = None, tree_id = 'HEAD', unknown = False,
227 noexclude = True, verbose = False, diff_flags = []):
228 """Returns a list of pairs - [status, filename]
229 """
230 if verbose:
231 out.start('Checking for changes in the working directory')
232
233 refresh_index()
234
235 if not files:
236 files = []
237 cache_files = []
238
239 # unknown files
240 if unknown:
241 if noexclude:
242 exclude = []
243 else:
244 exclude = (['--exclude=%s' % s for s in
245 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
246 + ['--exclude-per-directory=.gitignore']
247 + ['--exclude-from=%s' % fn for fn in exclude_files()
248 if os.path.exists(fn)])
249 lines = _output_lines(['git-ls-files', '--others', '--directory']
250 + exclude)
251 cache_files += [('?', line.strip()) for line in lines]
252
253 # conflicted files
254 conflicts = get_conflicts()
255 if not conflicts:
256 conflicts = []
257 cache_files += [('C', filename) for filename in conflicts]
258
259 # the rest
260 for line in _output_lines(['git-diff-index'] + diff_flags +
261 [ tree_id, '--'] + files):
262 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
263 if fs[1] not in conflicts:
264 cache_files.append(fs)
265
266 if verbose:
267 out.done()
268
269 return cache_files
270
271 def local_changes(verbose = True):
272 """Return true if there are local changes in the tree
273 """
274 return len(tree_status(verbose = verbose)) != 0
275
276 # HEAD value cached
277 __head = None
278
279 def get_head():
280 """Verifies the HEAD and returns the SHA1 id that represents it
281 """
282 global __head
283
284 if not __head:
285 __head = rev_parse('HEAD')
286 return __head
287
288 def get_head_file():
289 """Returns the name of the file pointed to by the HEAD link
290 """
291 return strip_prefix('refs/heads/',
292 _output_one_line(['git-symbolic-ref', 'HEAD']))
293
294 def set_head_file(ref):
295 """Resets HEAD to point to a new ref
296 """
297 # head cache flushing is needed since we might have a different value
298 # in the new head
299 __clear_head_cache()
300 if __run('git-symbolic-ref HEAD',
301 [os.path.join('refs', 'heads', ref)]) != 0:
302 raise GitException, 'Could not set head to "%s"' % ref
303
304 def set_branch(branch, val):
305 """Point branch at a new commit object."""
306 if __run('git-update-ref', [branch, val]) != 0:
307 raise GitException, 'Could not update %s to "%s".' % (branch, val)
308
309 def __set_head(val):
310 """Sets the HEAD value
311 """
312 global __head
313
314 if not __head or __head != val:
315 set_branch('HEAD', val)
316 __head = val
317
318 # only allow SHA1 hashes
319 assert(len(__head) == 40)
320
321 def __clear_head_cache():
322 """Sets the __head to None so that a re-read is forced
323 """
324 global __head
325
326 __head = None
327
328 def refresh_index():
329 """Refresh index with stat() information from the working directory.
330 """
331 __run('git-update-index -q --unmerged --refresh')
332
333 def rev_parse(git_id):
334 """Parse the string and return a verified SHA1 id
335 """
336 try:
337 return _output_one_line(['git-rev-parse', '--verify', git_id])
338 except GitException:
339 raise GitException, 'Unknown revision: %s' % git_id
340
341 def branch_exists(branch):
342 """Existence check for the named branch
343 """
344 branch = os.path.join('refs', 'heads', branch)
345 for line in _output_lines('git-rev-parse --symbolic --all 2>&1'):
346 if line.strip() == branch:
347 return True
348 if re.compile('[ |/]'+branch+' ').search(line):
349 raise GitException, 'Bogus branch: %s' % line
350 return False
351
352 def create_branch(new_branch, tree_id = None):
353 """Create a new branch in the git repository
354 """
355 if branch_exists(new_branch):
356 raise GitException, 'Branch "%s" already exists' % new_branch
357
358 current_head = get_head()
359 set_head_file(new_branch)
360 __set_head(current_head)
361
362 # a checkout isn't needed if new branch points to the current head
363 if tree_id:
364 switch(tree_id)
365
366 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
367 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
368
369 def switch_branch(new_branch):
370 """Switch to a git branch
371 """
372 global __head
373
374 if not branch_exists(new_branch):
375 raise GitException, 'Branch "%s" does not exist' % new_branch
376
377 tree_id = rev_parse(os.path.join('refs', 'heads', new_branch)
378 + '^{commit}')
379 if tree_id != get_head():
380 refresh_index()
381 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
382 raise GitException, 'git-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_branch(name):
390 """Delete a git branch
391 """
392 if not branch_exists(name):
393 raise GitException, 'Branch "%s" does not exist' % name
394 remove_file_and_dirs(os.path.join(basedir.get(), 'refs', 'heads'),
395 name)
396
397 def rename_branch(from_name, to_name):
398 """Rename a git branch
399 """
400 if not branch_exists(from_name):
401 raise GitException, 'Branch "%s" does not exist' % from_name
402 if branch_exists(to_name):
403 raise GitException, 'Branch "%s" already exists' % to_name
404
405 if get_head_file() == from_name:
406 set_head_file(to_name)
407 rename(os.path.join(basedir.get(), 'refs', 'heads'),
408 from_name, to_name)
409
410 reflog_dir = os.path.join(basedir.get(), 'logs', 'refs', 'heads')
411 if os.path.exists(reflog_dir) \
412 and os.path.exists(os.path.join(reflog_dir, from_name)):
413 rename(reflog_dir, from_name, to_name)
414
415 def add(names):
416 """Add the files or recursively add the directory contents
417 """
418 # generate the file list
419 files = []
420 for i in names:
421 if not os.path.exists(i):
422 raise GitException, 'Unknown file or directory: %s' % i
423
424 if os.path.isdir(i):
425 # recursive search. We only add files
426 for root, dirs, local_files in os.walk(i):
427 for name in [os.path.join(root, f) for f in local_files]:
428 if os.path.isfile(name):
429 files.append(os.path.normpath(name))
430 elif os.path.isfile(i):
431 files.append(os.path.normpath(i))
432 else:
433 raise GitException, '%s is not a file or directory' % i
434
435 if files:
436 if __run('git-update-index --add --', files):
437 raise GitException, 'Unable to add file'
438
439 def __copy_single(source, target, target2=''):
440 """Copy file or dir named 'source' to name target+target2"""
441
442 # "source" (file or dir) must match one or more git-controlled file
443 realfiles = _output_lines(['git-ls-files', source])
444 if len(realfiles) == 0:
445 raise GitException, '"%s" matches no git-controled files' % source
446
447 if os.path.isdir(source):
448 # physically copy the files, and record them to add them in one run
449 newfiles = []
450 re_string='^'+source+'/(.*)$'
451 prefix_regexp = re.compile(re_string)
452 for f in [f.strip() for f in realfiles]:
453 m = prefix_regexp.match(f)
454 if not m:
455 raise Exception, '"%s" does not match "%s"' % (f, re_string)
456 newname = target+target2+'/'+m.group(1)
457 if not os.path.exists(os.path.dirname(newname)):
458 os.makedirs(os.path.dirname(newname))
459 copyfile(f, newname)
460 newfiles.append(newname)
461
462 add(newfiles)
463 else: # files, symlinks, ...
464 newname = target+target2
465 copyfile(source, newname)
466 add([newname])
467
468
469 def copy(filespecs, target):
470 if os.path.isdir(target):
471 # target is a directory: copy each entry on the command line,
472 # with the same name, into the target
473 target = target.rstrip('/')
474
475 # first, check that none of the children of the target
476 # matching the command line aleady exist
477 for filespec in filespecs:
478 entry = target+ '/' + os.path.basename(filespec.rstrip('/'))
479 if os.path.exists(entry):
480 raise GitException, 'Target "%s" already exists' % entry
481
482 for filespec in filespecs:
483 filespec = filespec.rstrip('/')
484 basename = '/' + os.path.basename(filespec)
485 __copy_single(filespec, target, basename)
486
487 elif os.path.exists(target):
488 raise GitException, 'Target "%s" exists but is not a directory' % target
489 elif len(filespecs) != 1:
490 raise GitException, 'Cannot copy more than one file to non-directory'
491
492 else:
493 # at this point: len(filespecs)==1 and target does not exist
494
495 # check target directory
496 targetdir = os.path.dirname(target)
497 if targetdir != '' and not os.path.isdir(targetdir):
498 raise GitException, 'Target directory "%s" does not exist' % targetdir
499
500 __copy_single(filespecs[0].rstrip('/'), target)
501
502
503 def rm(files, force = False):
504 """Remove a file from the repository
505 """
506 if not force:
507 for f in files:
508 if os.path.exists(f):
509 raise GitException, '%s exists. Remove it first' %f
510 if files:
511 __run('git-update-index --remove --', files)
512 else:
513 if files:
514 __run('git-update-index --force-remove --', files)
515
516 # Persons caching
517 __user = None
518 __author = None
519 __committer = None
520
521 def user():
522 """Return the user information.
523 """
524 global __user
525 if not __user:
526 name=config.get('user.name')
527 email=config.get('user.email')
528 __user = Person(name, email)
529 return __user;
530
531 def author():
532 """Return the author information.
533 """
534 global __author
535 if not __author:
536 try:
537 # the environment variables take priority over config
538 try:
539 date = os.environ['GIT_AUTHOR_DATE']
540 except KeyError:
541 date = ''
542 __author = Person(os.environ['GIT_AUTHOR_NAME'],
543 os.environ['GIT_AUTHOR_EMAIL'],
544 date)
545 except KeyError:
546 __author = user()
547 return __author
548
549 def committer():
550 """Return the author information.
551 """
552 global __committer
553 if not __committer:
554 try:
555 # the environment variables take priority over config
556 try:
557 date = os.environ['GIT_COMMITTER_DATE']
558 except KeyError:
559 date = ''
560 __committer = Person(os.environ['GIT_COMMITTER_NAME'],
561 os.environ['GIT_COMMITTER_EMAIL'],
562 date)
563 except KeyError:
564 __committer = user()
565 return __committer
566
567 def update_cache(files = None, force = False):
568 """Update the cache information for the given files
569 """
570 if not files:
571 files = []
572
573 cache_files = tree_status(files, verbose = False)
574
575 # everything is up-to-date
576 if len(cache_files) == 0:
577 return False
578
579 # check for unresolved conflicts
580 if not force and [x for x in cache_files
581 if x[0] not in ['M', 'N', 'A', 'D']]:
582 raise GitException, 'Updating cache failed: unresolved conflicts'
583
584 # update the cache
585 add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
586 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
587 m_files = [x[1] for x in cache_files if x[0] in ['M']]
588
589 if add_files and __run('git-update-index --add --', add_files) != 0:
590 raise GitException, 'Failed git-update-index --add'
591 if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
592 raise GitException, 'Failed git-update-index --rm'
593 if m_files and __run('git-update-index --', m_files) != 0:
594 raise GitException, 'Failed git-update-index'
595
596 return True
597
598 def commit(message, files = None, parents = None, allowempty = False,
599 cache_update = True, tree_id = None, set_head = False,
600 author_name = None, author_email = None, author_date = None,
601 committer_name = None, committer_email = None):
602 """Commit the current tree to repository
603 """
604 if not files:
605 files = []
606 if not parents:
607 parents = []
608
609 # Get the tree status
610 if cache_update and parents != []:
611 changes = update_cache(files)
612 if not changes and not allowempty:
613 raise GitException, 'No changes to commit'
614
615 # get the commit message
616 if not message:
617 message = '\n'
618 elif message[-1:] != '\n':
619 message += '\n'
620
621 # write the index to repository
622 if tree_id == None:
623 tree_id = _output_one_line(['git-write-tree'])
624 set_head = True
625
626 # the commit
627 cmd = ['env']
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', tree_id]
639
640 # get the parents
641 for p in parents:
642 cmd += ['-p', p]
643
644 commit_id = _output_one_line(cmd, message)
645 if set_head:
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'] + 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', 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 diff_flags = []):
744 """Show the tree status
745 """
746 if not files:
747 files = []
748
749 cache_files = tree_status(files, unknown = True, noexclude = noexclude,
750 diff_flags = diff_flags)
751 all = not (modified or new or deleted or conflict or unknown)
752
753 if not all:
754 filestat = []
755 if modified:
756 filestat.append('M')
757 if new:
758 filestat.append('A')
759 filestat.append('N')
760 if deleted:
761 filestat.append('D')
762 if conflict:
763 filestat.append('C')
764 if unknown:
765 filestat.append('?')
766 cache_files = [x for x in cache_files if x[0] in filestat]
767
768 for fs in cache_files:
769 if files and not fs[1] in files:
770 continue
771 if all:
772 out.stdout('%s %s' % (fs[0], fs[1]))
773 else:
774 out.stdout('%s' % fs[1])
775
776 def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None,
777 diff_flags = []):
778 """Show the diff between rev1 and rev2
779 """
780 if not files:
781 files = []
782
783 if rev1 and rev2:
784 diff_str = _output(['git-diff-tree', '-p'] + diff_flags
785 + [rev1, rev2, '--'] + files)
786 elif rev1 or rev2:
787 refresh_index()
788 if rev2:
789 diff_str = _output(['git-diff-index', '-p', '-R']
790 + diff_flags + [rev2, '--'] + files)
791 else:
792 diff_str = _output(['git-diff-index', '-p']
793 + diff_flags + [rev1, '--'] + files)
794 else:
795 diff_str = ''
796
797 if out_fd:
798 out_fd.write(diff_str)
799 else:
800 return diff_str
801
802 def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
803 """Return the diffstat between rev1 and rev2
804 """
805 if not files:
806 files = []
807
808 p=popen2.Popen3('git-apply --stat')
809 diff(files, rev1, rev2, p.tochild)
810 p.tochild.close()
811 diff_str = p.fromchild.read().rstrip()
812 if p.wait():
813 raise GitException, 'git.diffstat failed'
814 return diff_str
815
816 def files(rev1, rev2, diff_flags = []):
817 """Return the files modified between rev1 and rev2
818 """
819
820 result = ''
821 for line in _output_lines(['git-diff-tree'] + diff_flags + ['-r', rev1, rev2]):
822 result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
823
824 return result.rstrip()
825
826 def barefiles(rev1, rev2):
827 """Return the files modified between rev1 and rev2, without status info
828 """
829
830 result = ''
831 for line in _output_lines(['git-diff-tree', '-r', rev1, rev2]):
832 result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
833
834 return result.rstrip()
835
836 def pretty_commit(commit_id = 'HEAD', diff_flags = []):
837 """Return a given commit (log + diff)
838 """
839 return _output(['git-diff-tree'] + diff_flags +
840 ['--cc', '--always', '--pretty', '-r', commit_id])
841
842 def checkout(files = None, tree_id = None, force = False):
843 """Check out the given or all files
844 """
845 if not files:
846 files = []
847
848 if tree_id and __run('git-read-tree --reset', [tree_id]) != 0:
849 raise GitException, 'Failed git-read-tree --reset %s' % tree_id
850
851 checkout_cmd = 'git-checkout-index -q -u'
852 if force:
853 checkout_cmd += ' -f'
854 if len(files) == 0:
855 checkout_cmd += ' -a'
856 else:
857 checkout_cmd += ' --'
858
859 if __run(checkout_cmd, files) != 0:
860 raise GitException, 'Failed git-checkout-index'
861
862 def switch(tree_id, keep = False):
863 """Switch the tree to the given id
864 """
865 if not keep:
866 refresh_index()
867 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
868 raise GitException, 'git-read-tree failed (local changes maybe?)'
869
870 __set_head(tree_id)
871
872 def reset(files = None, tree_id = None, check_out = True):
873 """Revert the tree changes relative to the given tree_id. It removes
874 any local changes
875 """
876 if not tree_id:
877 tree_id = get_head()
878
879 if check_out:
880 cache_files = tree_status(files, tree_id)
881 # files which were added but need to be removed
882 rm_files = [x[1] for x in cache_files if x[0] in ['A']]
883
884 checkout(files, tree_id, True)
885 # checkout doesn't remove files
886 map(os.remove, rm_files)
887
888 # if the reset refers to the whole tree, switch the HEAD as well
889 if not files:
890 __set_head(tree_id)
891
892 def fetch(repository = 'origin', refspec = None):
893 """Fetches changes from the remote repository, using 'git-fetch'
894 by default.
895 """
896 # we update the HEAD
897 __clear_head_cache()
898
899 args = [repository]
900 if refspec:
901 args.append(refspec)
902
903 command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
904 config.get('stgit.fetchcmd')
905 if __run(command, args) != 0:
906 raise GitException, 'Failed "%s %s"' % (command, repository)
907
908 def pull(repository = 'origin', refspec = None):
909 """Fetches changes from the remote repository, using 'git-pull'
910 by default.
911 """
912 # we update the HEAD
913 __clear_head_cache()
914
915 args = [repository]
916 if refspec:
917 args.append(refspec)
918
919 command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
920 config.get('stgit.pullcmd')
921 if __run(command, args) != 0:
922 raise GitException, 'Failed "%s %s"' % (command, repository)
923
924 def repack():
925 """Repack all objects into a single pack
926 """
927 __run('git-repack -a -d -f')
928
929 def apply_patch(filename = None, diff = None, base = None,
930 fail_dump = True):
931 """Apply a patch onto the current or given index. There must not
932 be any local changes in the tree, otherwise the command fails
933 """
934 if diff is None:
935 if filename:
936 f = file(filename)
937 else:
938 f = sys.stdin
939 diff = f.read()
940 if filename:
941 f.close()
942
943 if base:
944 orig_head = get_head()
945 switch(base)
946 else:
947 refresh_index()
948
949 try:
950 _input_str(['git-apply', '--index'], diff)
951 except GitException:
952 if base:
953 switch(orig_head)
954 if fail_dump:
955 # write the failed diff to a file
956 f = file('.stgit-failed.patch', 'w+')
957 f.write(diff)
958 f.close()
959 out.warn('Diff written to the .stgit-failed.patch file')
960
961 raise
962
963 if base:
964 top = commit(message = 'temporary commit used for applying a patch',
965 parents = [base])
966 switch(orig_head)
967 merge(base, orig_head, top)
968
969 def clone(repository, local_dir):
970 """Clone a remote repository. At the moment, just use the
971 'git-clone' script
972 """
973 if __run('git-clone', [repository, local_dir]) != 0:
974 raise GitException, 'Failed "git-clone %s %s"' \
975 % (repository, local_dir)
976
977 def modifying_revs(files, base_rev, head_rev):
978 """Return the revisions from the list modifying the given files
979 """
980 cmd = ['git-rev-list', '%s..%s' % (base_rev, head_rev), '--']
981 revs = [line.strip() for line in _output_lines(cmd + files)]
982
983 return revs
984
985
986 def refspec_localpart(refspec):
987 m = re.match('^[^:]*:([^:]*)$', refspec)
988 if m:
989 return m.group(1)
990 else:
991 raise GitException, 'Cannot parse refspec "%s"' % line
992
993 def refspec_remotepart(refspec):
994 m = re.match('^([^:]*):[^:]*$', refspec)
995 if m:
996 return m.group(1)
997 else:
998 raise GitException, 'Cannot parse refspec "%s"' % line
999
1000
1001 def __remotes_from_config():
1002 return config.sections_matching(r'remote\.(.*)\.url')
1003
1004 def __remotes_from_dir(dir):
1005 d = os.path.join(basedir.get(), dir)
1006 if os.path.exists(d):
1007 return os.listdir(d)
1008 else:
1009 return None
1010
1011 def remotes_list():
1012 """Return the list of remotes in the repository
1013 """
1014
1015 return Set(__remotes_from_config()) | \
1016 Set(__remotes_from_dir('remotes')) | \
1017 Set(__remotes_from_dir('branches'))
1018
1019 def remotes_local_branches(remote):
1020 """Returns the list of local branches fetched from given remote
1021 """
1022
1023 branches = []
1024 if remote in __remotes_from_config():
1025 for line in config.getall('remote.%s.fetch' % remote):
1026 branches.append(refspec_localpart(line))
1027 elif remote in __remotes_from_dir('remotes'):
1028 stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
1029 for line in stream:
1030 # Only consider Pull lines
1031 m = re.match('^Pull: (.*)\n$', line)
1032 if m:
1033 branches.append(refspec_localpart(m.group(1)))
1034 stream.close()
1035 elif remote in __remotes_from_dir('branches'):
1036 # old-style branches only declare one branch
1037 branches.append('refs/heads/'+remote);
1038 else:
1039 raise GitException, 'Unknown remote "%s"' % remote
1040
1041 return branches
1042
1043 def identify_remote(branchname):
1044 """Return the name for the remote to pull the given branchname
1045 from, or None if we believe it is a local branch.
1046 """
1047
1048 for remote in remotes_list():
1049 if branchname in remotes_local_branches(remote):
1050 return remote
1051
1052 # if we get here we've found nothing, the branch is a local one
1053 return None
1054
1055 def fetch_head():
1056 """Return the git id for the tip of the parent branch as left by
1057 'git fetch'.
1058 """
1059
1060 fetch_head=None
1061 stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
1062 for line in stream:
1063 # Only consider lines not tagged not-for-merge
1064 m = re.match('^([^\t]*)\t\t', line)
1065 if m:
1066 if fetch_head:
1067 raise GitException, "StGit does not support multiple FETCH_HEAD"
1068 else:
1069 fetch_head=m.group(1)
1070 stream.close()
1071
1072 # here we are sure to have a single fetch_head
1073 return fetch_head
1074
1075 def all_refs():
1076 """Return a list of all refs in the current repository.
1077 """
1078
1079 return [line.split()[1] for line in _output_lines(['git-show-ref'])]