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