Fix the behaviour when there is no user information configured
[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 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
664 def 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
697 def 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
719 def 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
733 def 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
743 def 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
753 def 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
759 def 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
779 def 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
789 def 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
809 def 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('stgit.pullcmd')
821 if __run(command, args) != 0:
822 raise GitException, 'Failed "%s %s"' % (command, 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 d = os.path.join(basedir.get(), dir)
906 if os.path.exists(d):
907 return os.listdir(d)
908 else:
909 return None
910
911 def remotes_list():
912 """Return the list of remotes in the repository
913 """
914
915 return Set(__remotes_from_config()) | \
916 Set(__remotes_from_dir('remotes')) | \
917 Set(__remotes_from_dir('branches'))
918
919 def remotes_local_branches(remote):
920 """Returns the list of local branches fetched from given remote
921 """
922
923 branches = []
924 if remote in __remotes_from_config():
925 for line in config.getall('remote.%s.fetch' % remote):
926 branches.append(refspec_localpart(line))
927 elif remote in __remotes_from_dir('remotes'):
928 stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
929 for line in stream:
930 # Only consider Pull lines
931 m = re.match('^Pull: (.*)\n$', line)
932 if m:
933 branches.append(refspec_localpart(m.group(1)))
934 stream.close()
935 elif remote in __remotes_from_dir('branches'):
936 # old-style branches only declare one branch
937 branches.append('refs/heads/'+remote);
938 else:
939 raise GitException, 'Unknown remote "%s"' % remote
940
941 return branches
942
943 def identify_remote(branchname):
944 """Return the name for the remote to pull the given branchname
945 from, or None if we believe it is a local branch.
946 """
947
948 for remote in remotes_list():
949 if branchname in remotes_local_branches(remote):
950 return remote
951
952 # if we get here we've found nothing, the branch is a local one
953 return None
954
955 def fetch_head():
956 """Return the git id for the tip of the parent branch as left by
957 'git fetch'.
958 """
959
960 fetch_head=None
961 stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
962 for line in stream:
963 # Only consider lines not tagged not-for-merge
964 m = re.match('^([^\t]*)\t\t', line)
965 if m:
966 if fetch_head:
967 raise GitException, "StGit does not support multiple FETCH_HEAD"
968 else:
969 fetch_head=m.group(1)
970 stream.close()
971
972 # here we are sure to have a single fetch_head
973 return fetch_head