cdf15fd55e246a58a66a5724f7fdf15d66cf58bb
[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 def add(names):
401 """Add the files or recursively add the directory contents
402 """
403 # generate the file list
404 files = []
405 for i in names:
406 if not os.path.exists(i):
407 raise GitException, 'Unknown file or directory: %s' % i
408
409 if os.path.isdir(i):
410 # recursive search. We only add files
411 for root, dirs, local_files in os.walk(i):
412 for name in [os.path.join(root, f) for f in local_files]:
413 if os.path.isfile(name):
414 files.append(os.path.normpath(name))
415 elif os.path.isfile(i):
416 files.append(os.path.normpath(i))
417 else:
418 raise GitException, '%s is not a file or directory' % i
419
420 if files:
421 if __run('git-update-index --add --', files):
422 raise GitException, 'Unable to add file'
423
424 def rm(files, force = False):
425 """Remove a file from the repository
426 """
427 if not force:
428 for f in files:
429 if os.path.exists(f):
430 raise GitException, '%s exists. Remove it first' %f
431 if files:
432 __run('git-update-index --remove --', files)
433 else:
434 if files:
435 __run('git-update-index --force-remove --', files)
436
437 # Persons caching
438 __user = None
439 __author = None
440 __committer = None
441
442 def user():
443 """Return the user information.
444 """
445 global __user
446 if not __user:
447 if config.has_option('user', 'name') \
448 and config.has_option('user', 'email'):
449 __user = Person(config.get('user', 'name'),
450 config.get('user', 'email'))
451 else:
452 raise GitException, 'unknown user details'
453 return __user;
454
455 def author():
456 """Return the author information.
457 """
458 global __author
459 if not __author:
460 try:
461 # the environment variables take priority over config
462 try:
463 date = os.environ['GIT_AUTHOR_DATE']
464 except KeyError:
465 date = ''
466 __author = Person(os.environ['GIT_AUTHOR_NAME'],
467 os.environ['GIT_AUTHOR_EMAIL'],
468 date)
469 except KeyError:
470 __author = user()
471 return __author
472
473 def committer():
474 """Return the author information.
475 """
476 global __committer
477 if not __committer:
478 try:
479 # the environment variables take priority over config
480 try:
481 date = os.environ['GIT_COMMITTER_DATE']
482 except KeyError:
483 date = ''
484 __committer = Person(os.environ['GIT_COMMITTER_NAME'],
485 os.environ['GIT_COMMITTER_EMAIL'],
486 date)
487 except KeyError:
488 __committer = user()
489 return __committer
490
491 def update_cache(files = None, force = False):
492 """Update the cache information for the given files
493 """
494 if not files:
495 files = []
496
497 cache_files = __tree_status(files, verbose = False)
498
499 # everything is up-to-date
500 if len(cache_files) == 0:
501 return False
502
503 # check for unresolved conflicts
504 if not force and [x for x in cache_files
505 if x[0] not in ['M', 'N', 'A', 'D']]:
506 raise GitException, 'Updating cache failed: unresolved conflicts'
507
508 # update the cache
509 add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
510 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
511 m_files = [x[1] for x in cache_files if x[0] in ['M']]
512
513 if add_files and __run('git-update-index --add --', add_files) != 0:
514 raise GitException, 'Failed git-update-index --add'
515 if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
516 raise GitException, 'Failed git-update-index --rm'
517 if m_files and __run('git-update-index --', m_files) != 0:
518 raise GitException, 'Failed git-update-index'
519
520 return True
521
522 def commit(message, files = None, parents = None, allowempty = False,
523 cache_update = True, tree_id = None,
524 author_name = None, author_email = None, author_date = None,
525 committer_name = None, committer_email = None):
526 """Commit the current tree to repository
527 """
528 if not files:
529 files = []
530 if not parents:
531 parents = []
532
533 # Get the tree status
534 if cache_update and parents != []:
535 changes = update_cache(files)
536 if not changes and not allowempty:
537 raise GitException, 'No changes to commit'
538
539 # get the commit message
540 if not message:
541 message = '\n'
542 elif message[-1:] != '\n':
543 message += '\n'
544
545 must_switch = True
546 # write the index to repository
547 if tree_id == None:
548 tree_id = _output_one_line('git-write-tree')
549 else:
550 must_switch = False
551
552 # the commit
553 cmd = ''
554 if author_name:
555 cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
556 if author_email:
557 cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
558 if author_date:
559 cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
560 if committer_name:
561 cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
562 if committer_email:
563 cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
564 cmd += 'git-commit-tree %s' % tree_id
565
566 # get the parents
567 for p in parents:
568 cmd += ' -p %s' % p
569
570 commit_id = _output_one_line(cmd, message)
571 if must_switch:
572 __set_head(commit_id)
573
574 return commit_id
575
576 def apply_diff(rev1, rev2, check_index = True, files = None):
577 """Apply the diff between rev1 and rev2 onto the current
578 index. This function doesn't need to raise an exception since it
579 is only used for fast-pushing a patch. If this operation fails,
580 the pushing would fall back to the three-way merge.
581 """
582 if check_index:
583 index_opt = '--index'
584 else:
585 index_opt = ''
586
587 if not files:
588 files = []
589
590 diff_str = diff(files, rev1, rev2)
591 if diff_str:
592 try:
593 _input_str('git-apply %s' % index_opt, diff_str)
594 except GitException:
595 return False
596
597 return True
598
599 def merge(base, head1, head2, recursive = False):
600 """Perform a 3-way merge between base, head1 and head2 into the
601 local tree
602 """
603 refresh_index()
604
605 if recursive:
606 # this operation tracks renames but it is slower (used in
607 # general when pushing or picking patches)
608 try:
609 # use _output() to mask the verbose prints of the tool
610 _output('git-merge-recursive %s -- %s %s' % (base, head1, head2))
611 except GitException:
612 pass
613 else:
614 # the fast case where we don't track renames (used when the
615 # distance between base and heads is small, i.e. folding or
616 # synchronising patches)
617 if __run('git-read-tree -u -m --aggressive',
618 [base, head1, head2]) != 0:
619 raise GitException, 'git-read-tree failed (local changes maybe?)'
620
621 # check the index for unmerged entries
622 files = {}
623 stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
624
625 for line in _output('git-ls-files --unmerged --stage -z').split('\0'):
626 if not line:
627 continue
628
629 mode, hash, stage, path = stages_re.findall(line)[0]
630
631 if not path in files:
632 files[path] = {}
633 files[path]['1'] = ('', '')
634 files[path]['2'] = ('', '')
635 files[path]['3'] = ('', '')
636
637 files[path][stage] = (mode, hash)
638
639 # merge the unmerged files
640 errors = False
641 for path in files:
642 # remove additional files that might be generated for some
643 # newer versions of GIT
644 for suffix in [base, head1, head2]:
645 if not suffix:
646 continue
647 fname = path + '~' + suffix
648 if os.path.exists(fname):
649 os.remove(fname)
650
651 stages = files[path]
652 if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
653 stages['3'][1], path, stages['1'][0],
654 stages['2'][0], stages['3'][0]) != 0:
655 errors = True
656
657 if errors:
658 raise GitException, 'GIT index merging failed (possible conflicts)'
659
660 def status(files = None, modified = False, new = False, deleted = False,
661 conflict = False, unknown = False, noexclude = False):
662 """Show the tree status
663 """
664 if not files:
665 files = []
666
667 cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
668 all = not (modified or new or deleted or conflict or unknown)
669
670 if not all:
671 filestat = []
672 if modified:
673 filestat.append('M')
674 if new:
675 filestat.append('A')
676 filestat.append('N')
677 if deleted:
678 filestat.append('D')
679 if conflict:
680 filestat.append('C')
681 if unknown:
682 filestat.append('?')
683 cache_files = [x for x in cache_files if x[0] in filestat]
684
685 for fs in cache_files:
686 if all:
687 print '%s %s' % (fs[0], fs[1])
688 else:
689 print '%s' % fs[1]
690
691 def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None):
692 """Show the diff between rev1 and rev2
693 """
694 if not files:
695 files = []
696
697 if rev1 and rev2:
698 diff_str = _output(['git-diff-tree', '-p', rev1, rev2, '--'] + files)
699 elif rev1 or rev2:
700 refresh_index()
701 if rev2:
702 diff_str = _output(['git-diff-index', '-p', '-R', rev2, '--'] + files)
703 else:
704 diff_str = _output(['git-diff-index', '-p', rev1, '--'] + files)
705 else:
706 diff_str = ''
707
708 if out_fd:
709 out_fd.write(diff_str)
710 else:
711 return diff_str
712
713 def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
714 """Return the diffstat between rev1 and rev2
715 """
716 if not files:
717 files = []
718
719 p=popen2.Popen3('git-apply --stat')
720 diff(files, rev1, rev2, p.tochild)
721 p.tochild.close()
722 diff_str = p.fromchild.read().rstrip()
723 if p.wait():
724 raise GitException, 'git.diffstat failed'
725 return diff_str
726
727 def files(rev1, rev2):
728 """Return the files modified between rev1 and rev2
729 """
730
731 result = ''
732 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
733 result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
734
735 return result.rstrip()
736
737 def barefiles(rev1, rev2):
738 """Return the files modified between rev1 and rev2, without status info
739 """
740
741 result = ''
742 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
743 result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
744
745 return result.rstrip()
746
747 def pretty_commit(commit_id = 'HEAD'):
748 """Return a given commit (log + diff)
749 """
750 return _output(['git-diff-tree', '--cc', '--always', '--pretty', '-r',
751 commit_id])
752
753 def checkout(files = None, tree_id = None, force = False):
754 """Check out the given or all files
755 """
756 if not files:
757 files = []
758
759 if tree_id and __run('git-read-tree --reset', [tree_id]) != 0:
760 raise GitException, 'Failed git-read-tree --reset %s' % tree_id
761
762 checkout_cmd = 'git-checkout-index -q -u'
763 if force:
764 checkout_cmd += ' -f'
765 if len(files) == 0:
766 checkout_cmd += ' -a'
767 else:
768 checkout_cmd += ' --'
769
770 if __run(checkout_cmd, files) != 0:
771 raise GitException, 'Failed git-checkout-index'
772
773 def switch(tree_id, keep = False):
774 """Switch the tree to the given id
775 """
776 if not keep:
777 refresh_index()
778 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
779 raise GitException, 'git-read-tree failed (local changes maybe?)'
780
781 __set_head(tree_id)
782
783 def reset(files = None, tree_id = None, check_out = True):
784 """Revert the tree changes relative to the given tree_id. It removes
785 any local changes
786 """
787 if not tree_id:
788 tree_id = get_head()
789
790 if check_out:
791 cache_files = __tree_status(files, tree_id)
792 # files which were added but need to be removed
793 rm_files = [x[1] for x in cache_files if x[0] in ['A']]
794
795 checkout(files, tree_id, True)
796 # checkout doesn't remove files
797 map(os.remove, rm_files)
798
799 # if the reset refers to the whole tree, switch the HEAD as well
800 if not files:
801 __set_head(tree_id)
802
803 def pull(repository = 'origin', refspec = None):
804 """Pull changes from the remote repository. At the moment, just
805 use the 'git-pull' command
806 """
807 # 'git-pull' updates the HEAD
808 __clear_head_cache()
809
810 args = [repository]
811 if refspec:
812 args.append(refspec)
813
814 if __run(config.get('stgit', 'pullcmd'), args) != 0:
815 raise GitException, 'Failed "git-pull %s"' % repository
816
817 def repack():
818 """Repack all objects into a single pack
819 """
820 __run('git-repack -a -d -f')
821
822 def apply_patch(filename = None, diff = None, base = None,
823 fail_dump = True):
824 """Apply a patch onto the current or given index. There must not
825 be any local changes in the tree, otherwise the command fails
826 """
827 if diff is None:
828 if filename:
829 f = file(filename)
830 else:
831 f = sys.stdin
832 diff = f.read()
833 if filename:
834 f.close()
835
836 if base:
837 orig_head = get_head()
838 switch(base)
839 else:
840 refresh_index()
841
842 try:
843 _input_str('git-apply --index', diff)
844 except GitException:
845 if base:
846 switch(orig_head)
847 if fail_dump:
848 # write the failed diff to a file
849 f = file('.stgit-failed.patch', 'w+')
850 f.write(diff)
851 f.close()
852 print >> sys.stderr, 'Diff written to the .stgit-failed.patch file'
853
854 raise
855
856 if base:
857 top = commit(message = 'temporary commit used for applying a patch',
858 parents = [base])
859 switch(orig_head)
860 merge(base, orig_head, top)
861
862 def clone(repository, local_dir):
863 """Clone a remote repository. At the moment, just use the
864 'git-clone' script
865 """
866 if __run('git-clone', [repository, local_dir]) != 0:
867 raise GitException, 'Failed "git-clone %s %s"' \
868 % (repository, local_dir)
869
870 def modifying_revs(files, base_rev):
871 """Return the revisions from the list modifying the given files
872 """
873 cmd = ['git-rev-list', '%s..' % base_rev, '--']
874 revs = [line.strip() for line in _output_lines(cmd + files)]
875
876 return revs