d7eb48e1023110ee52628514409bd07892b84592
[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_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
314 def __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
321 def refresh_index():
322 """Refresh index with stat() information from the working directory.
323 """
324 __run('git-update-index -q --unmerged --refresh')
325
326 def 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
334 def 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
345 def 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
362 def 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
382 def 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
390 def 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
408 def 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
432 def __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
463 def 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
497 def 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
515 def 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
525 def 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
543 def 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
561 def 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
592 def 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
646 def 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
669 def 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 err_output = None
676 if recursive:
677 # this operation tracks renames but it is slower (used in
678 # general when pushing or picking patches)
679 try:
680 # use _output() to mask the verbose prints of the tool
681 _output('git-merge-recursive %s -- %s %s' % (base, head1, head2))
682 except GitException, ex:
683 err_output = str(ex)
684 pass
685 else:
686 # the fast case where we don't track renames (used when the
687 # distance between base and heads is small, i.e. folding or
688 # synchronising patches)
689 if __run('git-read-tree -u -m --aggressive',
690 [base, head1, head2]) != 0:
691 raise GitException, 'git-read-tree failed (local changes maybe?)'
692
693 # check the index for unmerged entries
694 files = {}
695 stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
696
697 for line in _output('git-ls-files --unmerged --stage -z').split('\0'):
698 if not line:
699 continue
700
701 mode, hash, stage, path = stages_re.findall(line)[0]
702
703 if not path in files:
704 files[path] = {}
705 files[path]['1'] = ('', '')
706 files[path]['2'] = ('', '')
707 files[path]['3'] = ('', '')
708
709 files[path][stage] = (mode, hash)
710
711 if err_output and not files:
712 # if no unmerged files, there was probably a different type of
713 # error and we have to abort the merge
714 raise GitException, err_output
715
716 # merge the unmerged files
717 errors = False
718 for path in files:
719 # remove additional files that might be generated for some
720 # newer versions of GIT
721 for suffix in [base, head1, head2]:
722 if not suffix:
723 continue
724 fname = path + '~' + suffix
725 if os.path.exists(fname):
726 os.remove(fname)
727
728 stages = files[path]
729 if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
730 stages['3'][1], path, stages['1'][0],
731 stages['2'][0], stages['3'][0]) != 0:
732 errors = True
733
734 if errors:
735 raise GitException, 'GIT index merging failed (possible conflicts)'
736
737 def status(files = None, modified = False, new = False, deleted = False,
738 conflict = False, unknown = False, noexclude = False):
739 """Show the tree status
740 """
741 if not files:
742 files = []
743
744 cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
745 all = not (modified or new or deleted or conflict or unknown)
746
747 if not all:
748 filestat = []
749 if modified:
750 filestat.append('M')
751 if new:
752 filestat.append('A')
753 filestat.append('N')
754 if deleted:
755 filestat.append('D')
756 if conflict:
757 filestat.append('C')
758 if unknown:
759 filestat.append('?')
760 cache_files = [x for x in cache_files if x[0] in filestat]
761
762 for fs in cache_files:
763 if files and not fs[1] in files:
764 continue
765 if all:
766 print '%s %s' % (fs[0], fs[1])
767 else:
768 print '%s' % fs[1]
769
770 def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None):
771 """Show the diff between rev1 and rev2
772 """
773 if not files:
774 files = []
775
776 if rev1 and rev2:
777 diff_str = _output(['git-diff-tree', '-p', rev1, rev2, '--'] + files)
778 elif rev1 or rev2:
779 refresh_index()
780 if rev2:
781 diff_str = _output(['git-diff-index', '-p', '-R', rev2, '--'] + files)
782 else:
783 diff_str = _output(['git-diff-index', '-p', rev1, '--'] + files)
784 else:
785 diff_str = ''
786
787 if out_fd:
788 out_fd.write(diff_str)
789 else:
790 return diff_str
791
792 def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
793 """Return the diffstat between rev1 and rev2
794 """
795 if not files:
796 files = []
797
798 p=popen2.Popen3('git-apply --stat')
799 diff(files, rev1, rev2, p.tochild)
800 p.tochild.close()
801 diff_str = p.fromchild.read().rstrip()
802 if p.wait():
803 raise GitException, 'git.diffstat failed'
804 return diff_str
805
806 def files(rev1, rev2):
807 """Return the files modified between rev1 and rev2
808 """
809
810 result = ''
811 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
812 result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
813
814 return result.rstrip()
815
816 def barefiles(rev1, rev2):
817 """Return the files modified between rev1 and rev2, without status info
818 """
819
820 result = ''
821 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
822 result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
823
824 return result.rstrip()
825
826 def pretty_commit(commit_id = 'HEAD'):
827 """Return a given commit (log + diff)
828 """
829 return _output(['git-diff-tree', '--cc', '--always', '--pretty', '-r',
830 commit_id])
831
832 def checkout(files = None, tree_id = None, force = False):
833 """Check out the given or all files
834 """
835 if not files:
836 files = []
837
838 if tree_id and __run('git-read-tree --reset', [tree_id]) != 0:
839 raise GitException, 'Failed git-read-tree --reset %s' % tree_id
840
841 checkout_cmd = 'git-checkout-index -q -u'
842 if force:
843 checkout_cmd += ' -f'
844 if len(files) == 0:
845 checkout_cmd += ' -a'
846 else:
847 checkout_cmd += ' --'
848
849 if __run(checkout_cmd, files) != 0:
850 raise GitException, 'Failed git-checkout-index'
851
852 def switch(tree_id, keep = False):
853 """Switch the tree to the given id
854 """
855 if not keep:
856 refresh_index()
857 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
858 raise GitException, 'git-read-tree failed (local changes maybe?)'
859
860 __set_head(tree_id)
861
862 def reset(files = None, tree_id = None, check_out = True):
863 """Revert the tree changes relative to the given tree_id. It removes
864 any local changes
865 """
866 if not tree_id:
867 tree_id = get_head()
868
869 if check_out:
870 cache_files = __tree_status(files, tree_id)
871 # files which were added but need to be removed
872 rm_files = [x[1] for x in cache_files if x[0] in ['A']]
873
874 checkout(files, tree_id, True)
875 # checkout doesn't remove files
876 map(os.remove, rm_files)
877
878 # if the reset refers to the whole tree, switch the HEAD as well
879 if not files:
880 __set_head(tree_id)
881
882 def fetch(repository = 'origin', refspec = None):
883 """Fetches changes from the remote repository, using 'git-fetch'
884 by default.
885 """
886 # we update the HEAD
887 __clear_head_cache()
888
889 args = [repository]
890 if refspec:
891 args.append(refspec)
892
893 command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
894 config.get('stgit.fetchcmd')
895 if __run(command, args) != 0:
896 raise GitException, 'Failed "%s %s"' % (command, repository)
897
898 def pull(repository = 'origin', refspec = None):
899 """Fetches changes from the remote repository, using 'git-pull'
900 by default.
901 """
902 # we update the HEAD
903 __clear_head_cache()
904
905 args = [repository]
906 if refspec:
907 args.append(refspec)
908
909 command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
910 config.get('stgit.pullcmd')
911 if __run(command, args) != 0:
912 raise GitException, 'Failed "%s %s"' % (command, repository)
913
914 def repack():
915 """Repack all objects into a single pack
916 """
917 __run('git-repack -a -d -f')
918
919 def apply_patch(filename = None, diff = None, base = None,
920 fail_dump = True):
921 """Apply a patch onto the current or given index. There must not
922 be any local changes in the tree, otherwise the command fails
923 """
924 if diff is None:
925 if filename:
926 f = file(filename)
927 else:
928 f = sys.stdin
929 diff = f.read()
930 if filename:
931 f.close()
932
933 if base:
934 orig_head = get_head()
935 switch(base)
936 else:
937 refresh_index()
938
939 try:
940 _input_str('git-apply --index', diff)
941 except GitException:
942 if base:
943 switch(orig_head)
944 if fail_dump:
945 # write the failed diff to a file
946 f = file('.stgit-failed.patch', 'w+')
947 f.write(diff)
948 f.close()
949 print >> sys.stderr, 'Diff written to the .stgit-failed.patch file'
950
951 raise
952
953 if base:
954 top = commit(message = 'temporary commit used for applying a patch',
955 parents = [base])
956 switch(orig_head)
957 merge(base, orig_head, top)
958
959 def clone(repository, local_dir):
960 """Clone a remote repository. At the moment, just use the
961 'git-clone' script
962 """
963 if __run('git-clone', [repository, local_dir]) != 0:
964 raise GitException, 'Failed "git-clone %s %s"' \
965 % (repository, local_dir)
966
967 def modifying_revs(files, base_rev, head_rev):
968 """Return the revisions from the list modifying the given files
969 """
970 cmd = ['git-rev-list', '%s..%s' % (base_rev, head_rev), '--']
971 revs = [line.strip() for line in _output_lines(cmd + files)]
972
973 return revs
974
975
976 def refspec_localpart(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 def refspec_remotepart(refspec):
984 m = re.match('^([^:]*):[^:]*$', refspec)
985 if m:
986 return m.group(1)
987 else:
988 raise GitException, 'Cannot parse refspec "%s"' % line
989
990
991 def __remotes_from_config():
992 return config.sections_matching(r'remote\.(.*)\.url')
993
994 def __remotes_from_dir(dir):
995 d = os.path.join(basedir.get(), dir)
996 if os.path.exists(d):
997 return os.listdir(d)
998 else:
999 return None
1000
1001 def remotes_list():
1002 """Return the list of remotes in the repository
1003 """
1004
1005 return Set(__remotes_from_config()) | \
1006 Set(__remotes_from_dir('remotes')) | \
1007 Set(__remotes_from_dir('branches'))
1008
1009 def remotes_local_branches(remote):
1010 """Returns the list of local branches fetched from given remote
1011 """
1012
1013 branches = []
1014 if remote in __remotes_from_config():
1015 for line in config.getall('remote.%s.fetch' % remote):
1016 branches.append(refspec_localpart(line))
1017 elif remote in __remotes_from_dir('remotes'):
1018 stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
1019 for line in stream:
1020 # Only consider Pull lines
1021 m = re.match('^Pull: (.*)\n$', line)
1022 if m:
1023 branches.append(refspec_localpart(m.group(1)))
1024 stream.close()
1025 elif remote in __remotes_from_dir('branches'):
1026 # old-style branches only declare one branch
1027 branches.append('refs/heads/'+remote);
1028 else:
1029 raise GitException, 'Unknown remote "%s"' % remote
1030
1031 return branches
1032
1033 def identify_remote(branchname):
1034 """Return the name for the remote to pull the given branchname
1035 from, or None if we believe it is a local branch.
1036 """
1037
1038 for remote in remotes_list():
1039 if branchname in remotes_local_branches(remote):
1040 return remote
1041
1042 # if we get here we've found nothing, the branch is a local one
1043 return None
1044
1045 def fetch_head():
1046 """Return the git id for the tip of the parent branch as left by
1047 'git fetch'.
1048 """
1049
1050 fetch_head=None
1051 stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
1052 for line in stream:
1053 # Only consider lines not tagged not-for-merge
1054 m = re.match('^([^\t]*)\t\t', line)
1055 if m:
1056 if fetch_head:
1057 raise GitException, "StGit does not support multiple FETCH_HEAD"
1058 else:
1059 fetch_head=m.group(1)
1060 stream.close()
1061
1062 # here we are sure to have a single fetch_head
1063 return fetch_head