Allow git.checkout() to work on unmerged indexes
[stgit] / stgit / git.py
CommitLineData
41a6d859
CM
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
3659ef88 21import sys, os, popen2, re, gitmergeonefile
41a6d859 22
170f576b 23from stgit import basedir
41a6d859
CM
24from stgit.utils import *
25
26# git exception class
27class GitException(Exception):
28 pass
29
30
41a6d859 31
41a6d859
CM
32#
33# Classes
34#
35class Commit:
36 """Handle the commit objects
37 """
38 def __init__(self, id_hash):
39 self.__id_hash = id_hash
41a6d859 40
26dba451 41 lines = _output_lines('git-cat-file commit %s' % id_hash)
37a4d1bf 42 self.__parents = []
26dba451
BL
43 for i in range(len(lines)):
44 line = lines[i]
41a6d859
CM
45 if line == '\n':
46 break
47 field = line.strip().split(' ', 1)
48 if field[0] == 'tree':
49 self.__tree = field[1]
50 elif field[0] == 'parent':
37a4d1bf 51 self.__parents.append(field[1])
41a6d859
CM
52 if field[0] == 'author':
53 self.__author = field[1]
dad310d0 54 if field[0] == 'committer':
41a6d859 55 self.__committer = field[1]
0618ea9c 56 self.__log = ''.join(lines[i+1:])
41a6d859
CM
57
58 def get_id_hash(self):
59 return self.__id_hash
60
61 def get_tree(self):
62 return self.__tree
63
64 def get_parent(self):
37a4d1bf
CM
65 return self.__parents[0]
66
67 def get_parents(self):
68 return self.__parents
41a6d859
CM
69
70 def get_author(self):
71 return self.__author
72
73 def get_committer(self):
74 return self.__committer
75
37a4d1bf
CM
76 def get_log(self):
77 return self.__log
78
8e29bcd2
CM
79# dictionary of Commit objects, used to avoid multiple calls to git
80__commits = dict()
41a6d859
CM
81
82#
83# Functions
84#
bae29ddd 85
8e29bcd2
CM
86def get_commit(id_hash):
87 """Commit objects factory. Save/look-up them in the __commits
88 dictionary
89 """
3237b6e4
CM
90 global __commits
91
8e29bcd2
CM
92 if id_hash in __commits:
93 return __commits[id_hash]
94 else:
95 commit = Commit(id_hash)
96 __commits[id_hash] = commit
97 return commit
98
41a6d859
CM
99def get_conflicts():
100 """Return the list of file conflicts
101 """
170f576b 102 conflicts_file = os.path.join(basedir.get(), 'conflicts')
41a6d859
CM
103 if os.path.isfile(conflicts_file):
104 f = file(conflicts_file)
105 names = [line.strip() for line in f.readlines()]
106 f.close()
107 return names
108 else:
109 return None
110
0d2cd1e4 111def _input(cmd, file_desc):
741f2784 112 p = popen2.Popen3(cmd, True)
6fe6b1bd
CM
113 while True:
114 line = file_desc.readline()
115 if not line:
116 break
0d2cd1e4
CM
117 p.tochild.write(line)
118 p.tochild.close()
119 if p.wait():
120 raise GitException, '%s failed' % str(cmd)
121
26dba451 122def _output(cmd):
741f2784 123 p=popen2.Popen3(cmd, True)
7cc615f3 124 output = p.fromchild.read()
26dba451
BL
125 if p.wait():
126 raise GitException, '%s failed' % str(cmd)
7cc615f3 127 return output
26dba451 128
d3cf7d86 129def _output_one_line(cmd, file_desc = None):
741f2784 130 p=popen2.Popen3(cmd, True)
d3cf7d86
PBG
131 if file_desc != None:
132 for line in file_desc:
133 p.tochild.write(line)
134 p.tochild.close()
7cc615f3 135 output = p.fromchild.readline().strip()
26dba451
BL
136 if p.wait():
137 raise GitException, '%s failed' % str(cmd)
7cc615f3 138 return output
41a6d859 139
26dba451 140def _output_lines(cmd):
741f2784 141 p=popen2.Popen3(cmd, True)
26dba451
BL
142 lines = p.fromchild.readlines()
143 if p.wait():
144 raise GitException, '%s failed' % str(cmd)
145 return lines
146
147def __run(cmd, args=None):
148 """__run: runs cmd using spawnvp.
149
150 Runs cmd using spawnvp. The shell is avoided so it won't mess up
151 our arguments. If args is very large, the command is run multiple
152 times; args is split xargs style: cmd is passed on each
153 invocation. Unlike xargs, returns immediately if any non-zero
154 return code is received.
155 """
156
157 args_l=cmd.split()
158 if args is None:
159 args = []
160 for i in range(0, len(args)+1, 100):
161 r=os.spawnvp(os.P_WAIT, args_l[0], args_l + args[i:min(i+100, len(args))])
162 if r:
163 return r
164 return 0
165
9216b602 166def __tree_status(files = None, tree_id = 'HEAD', unknown = False,
be24d874 167 noexclude = True):
41a6d859
CM
168 """Returns a list of pairs - [status, filename]
169 """
f8fb5747 170 refresh_index()
41a6d859 171
9216b602
CL
172 if not files:
173 files = []
41a6d859
CM
174 cache_files = []
175
176 # unknown files
177 if unknown:
170f576b 178 exclude_file = os.path.join(basedir.get(), 'info', 'exclude')
be24d874
CM
179 base_exclude = ['--exclude=%s' % s for s in
180 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
181 base_exclude.append('--exclude-per-directory=.gitignore')
182
41a6d859 183 if os.path.exists(exclude_file):
3c6fbd2c 184 extra_exclude = ['--exclude-from=%s' % exclude_file]
be24d874
CM
185 else:
186 extra_exclude = []
4d4c0e3a
PBG
187 if noexclude:
188 extra_exclude = base_exclude = []
be24d874 189
2c02c3b7
PBG
190 lines = _output_lines(['git-ls-files', '--others', '--directory']
191 + base_exclude + extra_exclude)
26dba451 192 cache_files += [('?', line.strip()) for line in lines]
41a6d859
CM
193
194 # conflicted files
195 conflicts = get_conflicts()
196 if not conflicts:
197 conflicts = []
198 cache_files += [('C', filename) for filename in conflicts]
199
200 # the rest
fec7f658 201 for line in _output_lines(['git-diff-index', tree_id] + files):
26dba451 202 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
41a6d859
CM
203 if fs[1] not in conflicts:
204 cache_files.append(fs)
41a6d859
CM
205
206 return cache_files
207
208def local_changes():
209 """Return true if there are local changes in the tree
210 """
211 return len(__tree_status()) != 0
212
aa01a285
CM
213# HEAD value cached
214__head = None
215
41a6d859 216def get_head():
3097799d 217 """Verifies the HEAD and returns the SHA1 id that represents it
41a6d859 218 """
aa01a285
CM
219 global __head
220
221 if not __head:
222 __head = rev_parse('HEAD')
223 return __head
41a6d859
CM
224
225def get_head_file():
226 """Returns the name of the file pointed to by the HEAD link
227 """
3097799d 228 return os.path.basename(_output_one_line('git-symbolic-ref HEAD'))
41a6d859 229
b99a02b0
CL
230def set_head_file(ref):
231 """Resets HEAD to point to a new ref
232 """
24eede72
CM
233 # head cache flushing is needed since we might have a different value
234 # in the new head
235 __clear_head_cache()
b99a02b0
CL
236 if __run('git-symbolic-ref HEAD', [ref]) != 0:
237 raise GitException, 'Could not set head to "%s"' % ref
238
41a6d859
CM
239def __set_head(val):
240 """Sets the HEAD value
241 """
aa01a285
CM
242 global __head
243
ba1a4550
CM
244 if not __head or __head != val:
245 if __run('git-update-ref HEAD', [val]) != 0:
246 raise GitException, 'Could not update HEAD to "%s".' % val
247 __head = val
248
510d1442
CM
249 # only allow SHA1 hashes
250 assert(len(__head) == 40)
251
ba1a4550
CM
252def __clear_head_cache():
253 """Sets the __head to None so that a re-read is forced
254 """
255 global __head
256
257 __head = None
41a6d859 258
f8fb5747
CL
259def refresh_index():
260 """Refresh index with stat() information from the working directory.
261 """
262 __run('git-update-index -q --unmerged --refresh')
263
d1eb3f85 264def rev_parse(git_id):
3097799d 265 """Parse the string and return a verified SHA1 id
d1eb3f85 266 """
84fcbc3b
CM
267 try:
268 return _output_one_line(['git-rev-parse', '--verify', git_id])
269 except GitException:
270 raise GitException, 'Unknown revision: %s' % git_id
d1eb3f85 271
2b4a8aa5 272def branch_exists(branch):
388f63b6 273 """Existence check for the named branch
2b4a8aa5 274 """
fe847176 275 for line in _output_lines('git-rev-parse --symbolic --all 2>&1'):
2b4a8aa5
CL
276 if line.strip() == branch:
277 return True
fe847176
YD
278 if re.compile('[ |/]'+branch+' ').search(line):
279 raise GitException, 'Bogus branch: %s' % line
2b4a8aa5
CL
280 return False
281
282def create_branch(new_branch, tree_id = None):
283 """Create a new branch in the git repository
284 """
285 new_head = os.path.join('refs', 'heads', new_branch)
286 if branch_exists(new_head):
287 raise GitException, 'Branch "%s" already exists' % new_branch
288
289 current_head = get_head()
290 set_head_file(new_head)
291 __set_head(current_head)
292
293 # a checkout isn't needed if new branch points to the current head
294 if tree_id:
2bc93640 295 switch(tree_id)
2b4a8aa5 296
170f576b
CM
297 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
298 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
2b4a8aa5 299
982b9697
CL
300def switch_branch(name):
301 """Switch to a git branch
302 """
98d6e2c5
CL
303 global __head
304
982b9697
CL
305 new_head = os.path.join('refs', 'heads', name)
306 if not branch_exists(new_head):
307 raise GitException, 'Branch "%s" does not exist' % name
308
2fef9462 309 tree_id = rev_parse(new_head + '^{commit}')
982b9697 310 if tree_id != get_head():
f8fb5747 311 refresh_index()
982b9697
CL
312 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
313 raise GitException, 'git-read-tree failed (local changes maybe?)'
314 __head = tree_id
315 set_head_file(new_head)
316
170f576b
CM
317 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
318 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
982b9697 319
6f48e5f8
CL
320def delete_branch(name):
321 """Delete a git branch
322 """
323 branch_head = os.path.join('refs', 'heads', name)
324 if not branch_exists(branch_head):
325 raise GitException, 'Branch "%s" does not exist' % name
170f576b 326 os.remove(os.path.join(basedir.get(), branch_head))
6f48e5f8 327
72594233
CL
328def rename_branch(from_name, to_name):
329 """Rename a git branch
330 """
c47501f9 331 from_head = os.path.join('refs', 'heads', from_name)
72594233
CL
332 if not branch_exists(from_head):
333 raise GitException, 'Branch "%s" does not exist' % from_name
c47501f9 334 to_head = os.path.join('refs', 'heads', to_name)
72594233
CL
335 if branch_exists(to_head):
336 raise GitException, 'Branch "%s" already exists' % to_name
337
338 if get_head_file() == from_name:
c47501f9 339 set_head_file(to_head)
170f576b
CM
340 os.rename(os.path.join(basedir.get(), from_head), \
341 os.path.join(basedir.get(), to_head))
72594233 342
41a6d859
CM
343def add(names):
344 """Add the files or recursively add the directory contents
345 """
346 # generate the file list
347 files = []
348 for i in names:
349 if not os.path.exists(i):
350 raise GitException, 'Unknown file or directory: %s' % i
351
352 if os.path.isdir(i):
353 # recursive search. We only add files
354 for root, dirs, local_files in os.walk(i):
355 for name in [os.path.join(root, f) for f in local_files]:
356 if os.path.isfile(name):
357 files.append(os.path.normpath(name))
358 elif os.path.isfile(i):
359 files.append(os.path.normpath(i))
360 else:
361 raise GitException, '%s is not a file or directory' % i
362
26dba451 363 if files:
7c09df84 364 if __run('git-update-index --add --', files):
26dba451 365 raise GitException, 'Unable to add file'
41a6d859
CM
366
367def rm(files, force = False):
368 """Remove a file from the repository
369 """
26dba451
BL
370 if not force:
371 for f in files:
372 if os.path.exists(f):
373 raise GitException, '%s exists. Remove it first' %f
374 if files:
7c09df84 375 __run('git-update-index --remove --', files)
26dba451
BL
376 else:
377 if files:
7c09df84 378 __run('git-update-index --force-remove --', files)
41a6d859 379
9216b602 380def update_cache(files = None, force = False):
cfafb945
CM
381 """Update the cache information for the given files
382 """
9216b602
CL
383 if not files:
384 files = []
385
402ad990 386 cache_files = __tree_status(files)
26dba451 387
402ad990
CM
388 # everything is up-to-date
389 if len(cache_files) == 0:
390 return False
391
392 # check for unresolved conflicts
393 if not force and [x for x in cache_files
394 if x[0] not in ['M', 'N', 'A', 'D']]:
395 raise GitException, 'Updating cache failed: unresolved conflicts'
26dba451 396
402ad990
CM
397 # update the cache
398 add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
399 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
400 m_files = [x[1] for x in cache_files if x[0] in ['M']]
401
7c09df84
JH
402 if add_files and __run('git-update-index --add --', add_files) != 0:
403 raise GitException, 'Failed git-update-index --add'
404 if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
405 raise GitException, 'Failed git-update-index --rm'
406 if m_files and __run('git-update-index --', m_files) != 0:
407 raise GitException, 'Failed git-update-index'
402ad990
CM
408
409 return True
cfafb945 410
9216b602 411def commit(message, files = None, parents = None, allowempty = False,
d3cf7d86 412 cache_update = True, tree_id = None,
41a6d859
CM
413 author_name = None, author_email = None, author_date = None,
414 committer_name = None, committer_email = None):
415 """Commit the current tree to repository
416 """
9216b602
CL
417 if not files:
418 files = []
419 if not parents:
420 parents = []
421
41a6d859 422 # Get the tree status
402ad990
CM
423 if cache_update and parents != []:
424 changes = update_cache(files)
425 if not changes and not allowempty:
426 raise GitException, 'No changes to commit'
41a6d859
CM
427
428 # get the commit message
d3cf7d86
PBG
429 if message[-1:] != '\n':
430 message += '\n'
41a6d859 431
d3cf7d86 432 must_switch = True
41a6d859 433 # write the index to repository
d3cf7d86
PBG
434 if tree_id == None:
435 tree_id = _output_one_line('git-write-tree')
436 else:
437 must_switch = False
41a6d859
CM
438
439 # the commit
440 cmd = ''
441 if author_name:
442 cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
443 if author_email:
444 cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
445 if author_date:
446 cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
447 if committer_name:
448 cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
449 if committer_email:
450 cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
451 cmd += 'git-commit-tree %s' % tree_id
452
453 # get the parents
454 for p in parents:
455 cmd += ' -p %s' % p
456
d3cf7d86
PBG
457 commit_id = _output_one_line(cmd, message)
458 if must_switch:
459 __set_head(commit_id)
41a6d859
CM
460
461 return commit_id
462
1777d8cd 463def apply_diff(rev1, rev2, check_index = True):
575a7e7c
CM
464 """Apply the diff between rev1 and rev2 onto the current
465 index. This function doesn't need to raise an exception since it
466 is only used for fast-pushing a patch. If this operation fails,
467 the pushing would fall back to the three-way merge.
468 """
1777d8cd
CM
469 if check_index:
470 index_opt = '--index'
471 else:
472 index_opt = ''
473 cmd = 'git-diff-tree -p %s %s | git-apply %s 2> /dev/null' \
474 % (rev1, rev2, index_opt)
475
476 return os.system(cmd) == 0
575a7e7c 477
41a6d859
CM
478def merge(base, head1, head2):
479 """Perform a 3-way merge between base, head1 and head2 into the
480 local tree
481 """
f8fb5747 482 refresh_index()
edf4f599 483 if __run('git-read-tree -u -m --aggressive', [base, head1, head2]) != 0:
41a6d859
CM
484 raise GitException, 'git-read-tree failed (local changes maybe?)'
485
3659ef88
CM
486 # check the index for unmerged entries
487 files = {}
488 stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
489
490 for line in _output('git-ls-files --unmerged --stage -z').split('\0'):
491 if not line:
492 continue
493
494 mode, hash, stage, path = stages_re.findall(line)[0]
495
496 if not path in files:
497 files[path] = {}
498 files[path]['1'] = ('', '')
499 files[path]['2'] = ('', '')
500 files[path]['3'] = ('', '')
501
502 files[path][stage] = (mode, hash)
503
504 # merge the unmerged files
505 errors = False
506 for path in files:
507 stages = files[path]
508 if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
509 stages['3'][1], path, stages['1'][0],
510 stages['2'][0], stages['3'][0]) != 0:
511 errors = True
512
513 if errors:
514 raise GitException, 'GIT index merging failed (possible conflicts)'
41a6d859 515
9216b602 516def status(files = None, modified = False, new = False, deleted = False,
4d4c0e3a 517 conflict = False, unknown = False, noexclude = False):
41a6d859
CM
518 """Show the tree status
519 """
9216b602
CL
520 if not files:
521 files = []
522
4d4c0e3a 523 cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
41a6d859
CM
524 all = not (modified or new or deleted or conflict or unknown)
525
526 if not all:
527 filestat = []
528 if modified:
529 filestat.append('M')
530 if new:
7371951a 531 filestat.append('A')
41a6d859
CM
532 filestat.append('N')
533 if deleted:
534 filestat.append('D')
535 if conflict:
536 filestat.append('C')
537 if unknown:
538 filestat.append('?')
402ad990 539 cache_files = [x for x in cache_files if x[0] in filestat]
41a6d859
CM
540
541 for fs in cache_files:
542 if all:
543 print '%s %s' % (fs[0], fs[1])
544 else:
545 print '%s' % fs[1]
546
9216b602 547def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None):
41a6d859
CM
548 """Show the diff between rev1 and rev2
549 """
9216b602
CL
550 if not files:
551 files = []
41a6d859 552
fcc1ad70 553 if rev1 and rev2:
b4bddc06 554 diff_str = _output(['git-diff-tree', '-p', rev1, rev2] + files)
fcc1ad70 555 elif rev1 or rev2:
f8fb5747 556 refresh_index()
fcc1ad70
CL
557 if rev2:
558 diff_str = _output(['git-diff-index', '-p', '-R', rev2] + files)
559 else:
560 diff_str = _output(['git-diff-index', '-p', rev1] + files)
561 else:
562 diff_str = ''
b4bddc06
CM
563
564 if out_fd:
565 out_fd.write(diff_str)
566 else:
567 return diff_str
41a6d859 568
9216b602 569def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
41a6d859
CM
570 """Return the diffstat between rev1 and rev2
571 """
9216b602
CL
572 if not files:
573 files = []
41a6d859 574
26dba451
BL
575 p=popen2.Popen3('git-apply --stat')
576 diff(files, rev1, rev2, p.tochild)
577 p.tochild.close()
7cc615f3 578 diff_str = p.fromchild.read().rstrip()
26dba451
BL
579 if p.wait():
580 raise GitException, 'git.diffstat failed'
7cc615f3 581 return diff_str
41a6d859
CM
582
583def files(rev1, rev2):
584 """Return the files modified between rev1 and rev2
585 """
41a6d859 586
7cc615f3 587 result = ''
26dba451 588 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
7cc615f3 589 result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
41a6d859 590
7cc615f3 591 return result.rstrip()
41a6d859 592
faed6770
PBG
593def barefiles(rev1, rev2):
594 """Return the files modified between rev1 and rev2, without status info
595 """
596
7cc615f3 597 result = ''
faed6770 598 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
7cc615f3 599 result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
faed6770 600
7cc615f3 601 return result.rstrip()
faed6770 602
8847a11b
CM
603def pretty_commit(commit_id = 'HEAD'):
604 """Return a given commit (log + diff)
605 """
606 return _output(['git-diff-tree', '--cc', '--always', '--pretty', '-r',
607 commit_id])
608
9216b602 609def checkout(files = None, tree_id = None, force = False):
41a6d859
CM
610 """Check out the given or all files
611 """
9216b602 612 if not files:
97310762 613 files = []
9216b602 614
774fc1e9 615 if tree_id and __run('git-read-tree', [tree_id]) != 0:
1008fbce
CM
616 raise GitException, 'Failed git-read-tree -m %s' % tree_id
617
7c09df84 618 checkout_cmd = 'git-checkout-index -q -u'
41a6d859 619 if force:
1008fbce 620 checkout_cmd += ' -f'
41a6d859 621 if len(files) == 0:
1008fbce 622 checkout_cmd += ' -a'
41a6d859 623 else:
1008fbce 624 checkout_cmd += ' --'
41a6d859 625
1008fbce 626 if __run(checkout_cmd, files) != 0:
7c09df84 627 raise GitException, 'Failed git-checkout-index'
41a6d859
CM
628
629def switch(tree_id):
630 """Switch the tree to the given id
631 """
f8fb5747 632 refresh_index()
a5b29a1c
CM
633 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
634 raise GitException, 'git-read-tree failed (local changes maybe?)'
41a6d859 635
41a6d859
CM
636 __set_head(tree_id)
637
f80bef49 638def reset(files = None, tree_id = None, check_out = True):
05d593c0
CM
639 """Revert the tree changes relative to the given tree_id. It removes
640 any local changes
641 """
510d1442
CM
642 if not tree_id:
643 tree_id = get_head()
644
f80bef49
CM
645 if check_out:
646 checkout(files, tree_id, True)
05d593c0 647
49e316b9 648 # if the reset refers to the whole tree, switch the HEAD as well
510d1442 649 if not files:
49e316b9
CM
650 __set_head(tree_id)
651
1f5e9148
CM
652def pull(repository = 'origin', refspec = None):
653 """Pull changes from the remote repository. At the moment, just
5d12e944 654 use the 'git-pull' command
f338c3c0 655 """
5d12e944 656 # 'git-pull' updates the HEAD
ba1a4550
CM
657 __clear_head_cache()
658
1f5e9148
CM
659 args = [repository]
660 if refspec:
661 args.append(refspec)
f338c3c0 662
5d12e944
CM
663 if __run('git-pull', args) != 0:
664 raise GitException, 'Failed "git-pull %s"' % repository
f338c3c0 665
84fcbc3b
CM
666def apply_patch(filename = None, base = None):
667 """Apply a patch onto the current or given index. There must not
668 be any local changes in the tree, otherwise the command fails
0d2cd1e4 669 """
84fcbc3b
CM
670 def __apply_patch():
671 if filename:
672 return __run('git-apply --index', [filename]) == 0
673 else:
674 try:
675 _input('git-apply --index', sys.stdin)
676 except GitException:
677 return False
678 return True
679
84fcbc3b
CM
680 if base:
681 orig_head = get_head()
682 switch(base)
e1db88d0
CL
683 else:
684 refresh_index() # needed since __apply_patch() doesn't do it
84fcbc3b
CM
685
686 if not __apply_patch():
687 if base:
688 switch(orig_head)
689 raise GitException, 'Patch does not apply cleanly'
690 elif base:
691 top = commit(message = 'temporary commit used for applying a patch',
692 parents = [base])
693 switch(orig_head)
694 merge(base, orig_head, top)
1008fbce
CM
695
696def clone(repository, local_dir):
697 """Clone a remote repository. At the moment, just use the
5d12e944 698 'git-clone' script
1008fbce 699 """
5d12e944
CM
700 if __run('git-clone', [repository, local_dir]) != 0:
701 raise GitException, 'Failed "git-clone %s %s"' \
1008fbce 702 % (repository, local_dir)
cd25e03d
CM
703
704def modifying_revs(files, base_rev):
705 """Return the revisions from the list modifying the given files
706 """
707 cmd = ['git-rev-list', '%s..' % base_rev, '--']
708 revs = [line.strip() for line in _output_lines(cmd + files)]
709
710 return revs