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