chiark / gitweb /
Only check for upstream merges if not fast-forwarding
[stgit] / stgit / git.py
... / ...
CommitLineData
1"""Python GIT interface
2"""
3
4__copyright__ = """
5Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
6
7This program is free software; you can redistribute it and/or modify
8it under the terms of the GNU General Public License version 2 as
9published by the Free Software Foundation.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
15
16You should have received a copy of the GNU General Public License
17along with this program; if not, write to the Free Software
18Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19"""
20
21import sys, os, popen2, re, gitmergeonefile
22
23from stgit import basedir
24from stgit.utils import *
25from stgit.config import config
26from sets import Set
27
28# git exception class
29class GitException(Exception):
30 pass
31
32
33
34#
35# Classes
36#
37
38class 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
70class 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
126def 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
139def 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
151def _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
163def _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
171def _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
179def _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
191def _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
199def __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
218def __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
267def 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
275def 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
284def 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
290def 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
300def __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
313def __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
320def refresh_index():
321 """Refresh index with stat() information from the working directory.
322 """
323 __run('git-update-index -q --unmerged --refresh')
324
325def 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
333def 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
344def 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
361def 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
381def 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
389def 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
407def 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
431def 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
449def 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
459def 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
477def 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
495def 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
526def 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
580def 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
603def 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 if recursive:
610 # this operation tracks renames but it is slower (used in
611 # general when pushing or picking patches)
612 try:
613 # use _output() to mask the verbose prints of the tool
614 _output('git-merge-recursive %s -- %s %s' % (base, head1, head2))
615 except GitException:
616 pass
617 else:
618 # the fast case where we don't track renames (used when the
619 # distance between base and heads is small, i.e. folding or
620 # synchronising patches)
621 if __run('git-read-tree -u -m --aggressive',
622 [base, head1, head2]) != 0:
623 raise GitException, 'git-read-tree failed (local changes maybe?)'
624
625 # check the index for unmerged entries
626 files = {}
627 stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
628
629 for line in _output('git-ls-files --unmerged --stage -z').split('\0'):
630 if not line:
631 continue
632
633 mode, hash, stage, path = stages_re.findall(line)[0]
634
635 if not path in files:
636 files[path] = {}
637 files[path]['1'] = ('', '')
638 files[path]['2'] = ('', '')
639 files[path]['3'] = ('', '')
640
641 files[path][stage] = (mode, hash)
642
643 # merge the unmerged files
644 errors = False
645 for path in files:
646 # remove additional files that might be generated for some
647 # newer versions of GIT
648 for suffix in [base, head1, head2]:
649 if not suffix:
650 continue
651 fname = path + '~' + suffix
652 if os.path.exists(fname):
653 os.remove(fname)
654
655 stages = files[path]
656 if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
657 stages['3'][1], path, stages['1'][0],
658 stages['2'][0], stages['3'][0]) != 0:
659 errors = True
660
661 if errors:
662 raise GitException, 'GIT index merging failed (possible conflicts)'
663
664def status(files = None, modified = False, new = False, deleted = False,
665 conflict = False, unknown = False, noexclude = False):
666 """Show the tree status
667 """
668 if not files:
669 files = []
670
671 cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
672 all = not (modified or new or deleted or conflict or unknown)
673
674 if not all:
675 filestat = []
676 if modified:
677 filestat.append('M')
678 if new:
679 filestat.append('A')
680 filestat.append('N')
681 if deleted:
682 filestat.append('D')
683 if conflict:
684 filestat.append('C')
685 if unknown:
686 filestat.append('?')
687 cache_files = [x for x in cache_files if x[0] in filestat]
688
689 for fs in cache_files:
690 if files and not fs[1] in files:
691 continue
692 if all:
693 print '%s %s' % (fs[0], fs[1])
694 else:
695 print '%s' % fs[1]
696
697def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None):
698 """Show the diff between rev1 and rev2
699 """
700 if not files:
701 files = []
702
703 if rev1 and rev2:
704 diff_str = _output(['git-diff-tree', '-p', rev1, rev2, '--'] + files)
705 elif rev1 or rev2:
706 refresh_index()
707 if rev2:
708 diff_str = _output(['git-diff-index', '-p', '-R', rev2, '--'] + files)
709 else:
710 diff_str = _output(['git-diff-index', '-p', rev1, '--'] + files)
711 else:
712 diff_str = ''
713
714 if out_fd:
715 out_fd.write(diff_str)
716 else:
717 return diff_str
718
719def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
720 """Return the diffstat between rev1 and rev2
721 """
722 if not files:
723 files = []
724
725 p=popen2.Popen3('git-apply --stat')
726 diff(files, rev1, rev2, p.tochild)
727 p.tochild.close()
728 diff_str = p.fromchild.read().rstrip()
729 if p.wait():
730 raise GitException, 'git.diffstat failed'
731 return diff_str
732
733def files(rev1, rev2):
734 """Return the files modified between rev1 and rev2
735 """
736
737 result = ''
738 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
739 result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
740
741 return result.rstrip()
742
743def barefiles(rev1, rev2):
744 """Return the files modified between rev1 and rev2, without status info
745 """
746
747 result = ''
748 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
749 result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
750
751 return result.rstrip()
752
753def pretty_commit(commit_id = 'HEAD'):
754 """Return a given commit (log + diff)
755 """
756 return _output(['git-diff-tree', '--cc', '--always', '--pretty', '-r',
757 commit_id])
758
759def checkout(files = None, tree_id = None, force = False):
760 """Check out the given or all files
761 """
762 if not files:
763 files = []
764
765 if tree_id and __run('git-read-tree --reset', [tree_id]) != 0:
766 raise GitException, 'Failed git-read-tree --reset %s' % tree_id
767
768 checkout_cmd = 'git-checkout-index -q -u'
769 if force:
770 checkout_cmd += ' -f'
771 if len(files) == 0:
772 checkout_cmd += ' -a'
773 else:
774 checkout_cmd += ' --'
775
776 if __run(checkout_cmd, files) != 0:
777 raise GitException, 'Failed git-checkout-index'
778
779def switch(tree_id, keep = False):
780 """Switch the tree to the given id
781 """
782 if not keep:
783 refresh_index()
784 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
785 raise GitException, 'git-read-tree failed (local changes maybe?)'
786
787 __set_head(tree_id)
788
789def reset(files = None, tree_id = None, check_out = True):
790 """Revert the tree changes relative to the given tree_id. It removes
791 any local changes
792 """
793 if not tree_id:
794 tree_id = get_head()
795
796 if check_out:
797 cache_files = __tree_status(files, tree_id)
798 # files which were added but need to be removed
799 rm_files = [x[1] for x in cache_files if x[0] in ['A']]
800
801 checkout(files, tree_id, True)
802 # checkout doesn't remove files
803 map(os.remove, rm_files)
804
805 # if the reset refers to the whole tree, switch the HEAD as well
806 if not files:
807 __set_head(tree_id)
808
809def fetch(repository = 'origin', refspec = None):
810 """Fetches changes from the remote repository, using 'git-fetch'
811 by default.
812 """
813 # we update the HEAD
814 __clear_head_cache()
815
816 args = [repository]
817 if refspec:
818 args.append(refspec)
819
820 command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
821 config.get('stgit.fetchcmd')
822 if __run(command, args) != 0:
823 raise GitException, 'Failed "%s %s"' % (command, repository)
824
825def pull(repository = 'origin', refspec = None):
826 """Fetches changes from the remote repository, using 'git-pull'
827 by default.
828 """
829 # we update the HEAD
830 __clear_head_cache()
831
832 args = [repository]
833 if refspec:
834 args.append(refspec)
835
836 command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
837 config.get('stgit.pullcmd')
838 if __run(command, args) != 0:
839 raise GitException, 'Failed "%s %s"' % (command, repository)
840
841def repack():
842 """Repack all objects into a single pack
843 """
844 __run('git-repack -a -d -f')
845
846def apply_patch(filename = None, diff = None, base = None,
847 fail_dump = True):
848 """Apply a patch onto the current or given index. There must not
849 be any local changes in the tree, otherwise the command fails
850 """
851 if diff is None:
852 if filename:
853 f = file(filename)
854 else:
855 f = sys.stdin
856 diff = f.read()
857 if filename:
858 f.close()
859
860 if base:
861 orig_head = get_head()
862 switch(base)
863 else:
864 refresh_index()
865
866 try:
867 _input_str('git-apply --index', diff)
868 except GitException:
869 if base:
870 switch(orig_head)
871 if fail_dump:
872 # write the failed diff to a file
873 f = file('.stgit-failed.patch', 'w+')
874 f.write(diff)
875 f.close()
876 print >> sys.stderr, 'Diff written to the .stgit-failed.patch file'
877
878 raise
879
880 if base:
881 top = commit(message = 'temporary commit used for applying a patch',
882 parents = [base])
883 switch(orig_head)
884 merge(base, orig_head, top)
885
886def clone(repository, local_dir):
887 """Clone a remote repository. At the moment, just use the
888 'git-clone' script
889 """
890 if __run('git-clone', [repository, local_dir]) != 0:
891 raise GitException, 'Failed "git-clone %s %s"' \
892 % (repository, local_dir)
893
894def modifying_revs(files, base_rev, head_rev):
895 """Return the revisions from the list modifying the given files
896 """
897 cmd = ['git-rev-list', '%s..%s' % (base_rev, head_rev), '--']
898 revs = [line.strip() for line in _output_lines(cmd + files)]
899
900 return revs
901
902
903def refspec_localpart(refspec):
904 m = re.match('^[^:]*:([^:]*)$', refspec)
905 if m:
906 return m.group(1)
907 else:
908 raise GitException, 'Cannot parse refspec "%s"' % line
909
910def refspec_remotepart(refspec):
911 m = re.match('^([^:]*):[^:]*$', refspec)
912 if m:
913 return m.group(1)
914 else:
915 raise GitException, 'Cannot parse refspec "%s"' % line
916
917
918def __remotes_from_config():
919 return config.sections_matching(r'remote\.(.*)\.url')
920
921def __remotes_from_dir(dir):
922 d = os.path.join(basedir.get(), dir)
923 if os.path.exists(d):
924 return os.listdir(d)
925 else:
926 return None
927
928def remotes_list():
929 """Return the list of remotes in the repository
930 """
931
932 return Set(__remotes_from_config()) | \
933 Set(__remotes_from_dir('remotes')) | \
934 Set(__remotes_from_dir('branches'))
935
936def remotes_local_branches(remote):
937 """Returns the list of local branches fetched from given remote
938 """
939
940 branches = []
941 if remote in __remotes_from_config():
942 for line in config.getall('remote.%s.fetch' % remote):
943 branches.append(refspec_localpart(line))
944 elif remote in __remotes_from_dir('remotes'):
945 stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
946 for line in stream:
947 # Only consider Pull lines
948 m = re.match('^Pull: (.*)\n$', line)
949 if m:
950 branches.append(refspec_localpart(m.group(1)))
951 stream.close()
952 elif remote in __remotes_from_dir('branches'):
953 # old-style branches only declare one branch
954 branches.append('refs/heads/'+remote);
955 else:
956 raise GitException, 'Unknown remote "%s"' % remote
957
958 return branches
959
960def identify_remote(branchname):
961 """Return the name for the remote to pull the given branchname
962 from, or None if we believe it is a local branch.
963 """
964
965 for remote in remotes_list():
966 if branchname in remotes_local_branches(remote):
967 return remote
968
969 # if we get here we've found nothing, the branch is a local one
970 return None
971
972def fetch_head():
973 """Return the git id for the tip of the parent branch as left by
974 'git fetch'.
975 """
976
977 fetch_head=None
978 stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
979 for line in stream:
980 # Only consider lines not tagged not-for-merge
981 m = re.match('^([^\t]*)\t\t', line)
982 if m:
983 if fetch_head:
984 raise GitException, "StGit does not support multiple FETCH_HEAD"
985 else:
986 fetch_head=m.group(1)
987 stream.close()
988
989 # here we are sure to have a single fetch_head
990 return fetch_head