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