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