Add the --update option to pick
[stgit.git] / stgit / git.py
blob05d851d69aeee4e7fd16d176556e53e3cbf476c8
1 """Python GIT interface
2 """
4 __copyright__ = """
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
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.
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.
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 """
21 import sys, os, popen2, re, gitmergeonefile
23 from stgit import basedir
24 from stgit.utils import *
26 # git exception class
27 class GitException(Exception):
28 pass
33 # Classes
35 class Commit:
36 """Handle the commit objects
37 """
38 def __init__(self, id_hash):
39 self.__id_hash = id_hash
41 lines = _output_lines('git-cat-file commit %s' % id_hash)
42 self.__parents = []
43 for i in range(len(lines)):
44 line = lines[i]
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':
51 self.__parents.append(field[1])
52 if field[0] == 'author':
53 self.__author = field[1]
54 if field[0] == 'committer':
55 self.__committer = field[1]
56 self.__log = ''.join(lines[i+1:])
58 def get_id_hash(self):
59 return self.__id_hash
61 def get_tree(self):
62 return self.__tree
64 def get_parent(self):
65 return self.__parents[0]
67 def get_parents(self):
68 return self.__parents
70 def get_author(self):
71 return self.__author
73 def get_committer(self):
74 return self.__committer
76 def get_log(self):
77 return self.__log
79 # dictionary of Commit objects, used to avoid multiple calls to git
80 __commits = dict()
83 # Functions
86 def get_commit(id_hash):
87 """Commit objects factory. Save/look-up them in the __commits
88 dictionary
89 """
90 global __commits
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
99 def get_conflicts():
100 """Return the list of file conflicts
102 conflicts_file = os.path.join(basedir.get(), 'conflicts')
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
111 def _input(cmd, file_desc):
112 p = popen2.Popen3(cmd, True)
113 while True:
114 line = file_desc.readline()
115 if not line:
116 break
117 p.tochild.write(line)
118 p.tochild.close()
119 if p.wait():
120 raise GitException, '%s failed' % str(cmd)
122 def _input_str(cmd, string):
123 p = popen2.Popen3(cmd, True)
124 p.tochild.write(string)
125 p.tochild.close()
126 if p.wait():
127 raise GitException, '%s failed' % str(cmd)
129 def _output(cmd):
130 p=popen2.Popen3(cmd, True)
131 output = p.fromchild.read()
132 if p.wait():
133 raise GitException, '%s failed' % str(cmd)
134 return output
136 def _output_one_line(cmd, file_desc = None):
137 p=popen2.Popen3(cmd, True)
138 if file_desc != None:
139 for line in file_desc:
140 p.tochild.write(line)
141 p.tochild.close()
142 output = p.fromchild.readline().strip()
143 if p.wait():
144 raise GitException, '%s failed' % str(cmd)
145 return output
147 def _output_lines(cmd):
148 p=popen2.Popen3(cmd, True)
149 lines = p.fromchild.readlines()
150 if p.wait():
151 raise GitException, '%s failed' % str(cmd)
152 return lines
154 def __run(cmd, args=None):
155 """__run: runs cmd using spawnvp.
157 Runs cmd using spawnvp. The shell is avoided so it won't mess up
158 our arguments. If args is very large, the command is run multiple
159 times; args is split xargs style: cmd is passed on each
160 invocation. Unlike xargs, returns immediately if any non-zero
161 return code is received.
164 args_l=cmd.split()
165 if args is None:
166 args = []
167 for i in range(0, len(args)+1, 100):
168 r=os.spawnvp(os.P_WAIT, args_l[0], args_l + args[i:min(i+100, len(args))])
169 if r:
170 return r
171 return 0
173 def __tree_status(files = None, tree_id = 'HEAD', unknown = False,
174 noexclude = True):
175 """Returns a list of pairs - [status, filename]
177 refresh_index()
179 if not files:
180 files = []
181 cache_files = []
183 # unknown files
184 if unknown:
185 exclude_file = os.path.join(basedir.get(), 'info', 'exclude')
186 base_exclude = ['--exclude=%s' % s for s in
187 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
188 base_exclude.append('--exclude-per-directory=.gitignore')
190 if os.path.exists(exclude_file):
191 extra_exclude = ['--exclude-from=%s' % exclude_file]
192 else:
193 extra_exclude = []
194 if noexclude:
195 extra_exclude = base_exclude = []
197 lines = _output_lines(['git-ls-files', '--others', '--directory']
198 + base_exclude + extra_exclude)
199 cache_files += [('?', line.strip()) for line in lines]
201 # conflicted files
202 conflicts = get_conflicts()
203 if not conflicts:
204 conflicts = []
205 cache_files += [('C', filename) for filename in conflicts]
207 # the rest
208 for line in _output_lines(['git-diff-index', tree_id] + files):
209 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
210 if fs[1] not in conflicts:
211 cache_files.append(fs)
213 return cache_files
215 def local_changes():
216 """Return true if there are local changes in the tree
218 return len(__tree_status()) != 0
220 # HEAD value cached
221 __head = None
223 def get_head():
224 """Verifies the HEAD and returns the SHA1 id that represents it
226 global __head
228 if not __head:
229 __head = rev_parse('HEAD')
230 return __head
232 def get_head_file():
233 """Returns the name of the file pointed to by the HEAD link
235 return strip_prefix('refs/heads/',
236 _output_one_line('git-symbolic-ref HEAD'))
238 def set_head_file(ref):
239 """Resets HEAD to point to a new ref
241 # head cache flushing is needed since we might have a different value
242 # in the new head
243 __clear_head_cache()
244 if __run('git-symbolic-ref HEAD',
245 [os.path.join('refs', 'heads', ref)]) != 0:
246 raise GitException, 'Could not set head to "%s"' % ref
248 def __set_head(val):
249 """Sets the HEAD value
251 global __head
253 if not __head or __head != val:
254 if __run('git-update-ref HEAD', [val]) != 0:
255 raise GitException, 'Could not update HEAD to "%s".' % val
256 __head = val
258 # only allow SHA1 hashes
259 assert(len(__head) == 40)
261 def __clear_head_cache():
262 """Sets the __head to None so that a re-read is forced
264 global __head
266 __head = None
268 def refresh_index():
269 """Refresh index with stat() information from the working directory.
271 __run('git-update-index -q --unmerged --refresh')
273 def rev_parse(git_id):
274 """Parse the string and return a verified SHA1 id
276 try:
277 return _output_one_line(['git-rev-parse', '--verify', git_id])
278 except GitException:
279 raise GitException, 'Unknown revision: %s' % git_id
281 def branch_exists(branch):
282 """Existence check for the named branch
284 branch = os.path.join('refs', 'heads', branch)
285 for line in _output_lines('git-rev-parse --symbolic --all 2>&1'):
286 if line.strip() == branch:
287 return True
288 if re.compile('[ |/]'+branch+' ').search(line):
289 raise GitException, 'Bogus branch: %s' % line
290 return False
292 def create_branch(new_branch, tree_id = None):
293 """Create a new branch in the git repository
295 if branch_exists(new_branch):
296 raise GitException, 'Branch "%s" already exists' % new_branch
298 current_head = get_head()
299 set_head_file(new_branch)
300 __set_head(current_head)
302 # a checkout isn't needed if new branch points to the current head
303 if tree_id:
304 switch(tree_id)
306 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
307 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
309 def switch_branch(new_branch):
310 """Switch to a git branch
312 global __head
314 if not branch_exists(new_branch):
315 raise GitException, 'Branch "%s" does not exist' % new_branch
317 tree_id = rev_parse(os.path.join('refs', 'heads', new_branch)
318 + '^{commit}')
319 if tree_id != get_head():
320 refresh_index()
321 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
322 raise GitException, 'git-read-tree failed (local changes maybe?)'
323 __head = tree_id
324 set_head_file(new_branch)
326 if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
327 os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
329 def delete_branch(name):
330 """Delete a git branch
332 if not branch_exists(name):
333 raise GitException, 'Branch "%s" does not exist' % name
334 remove_file_and_dirs(os.path.join(basedir.get(), 'refs', 'heads'),
335 name)
337 def rename_branch(from_name, to_name):
338 """Rename a git branch
340 if not branch_exists(from_name):
341 raise GitException, 'Branch "%s" does not exist' % from_name
342 if branch_exists(to_name):
343 raise GitException, 'Branch "%s" already exists' % to_name
345 if get_head_file() == from_name:
346 set_head_file(to_name)
347 rename(os.path.join(basedir.get(), 'refs', 'heads'),
348 from_name, to_name)
350 def add(names):
351 """Add the files or recursively add the directory contents
353 # generate the file list
354 files = []
355 for i in names:
356 if not os.path.exists(i):
357 raise GitException, 'Unknown file or directory: %s' % i
359 if os.path.isdir(i):
360 # recursive search. We only add files
361 for root, dirs, local_files in os.walk(i):
362 for name in [os.path.join(root, f) for f in local_files]:
363 if os.path.isfile(name):
364 files.append(os.path.normpath(name))
365 elif os.path.isfile(i):
366 files.append(os.path.normpath(i))
367 else:
368 raise GitException, '%s is not a file or directory' % i
370 if files:
371 if __run('git-update-index --add --', files):
372 raise GitException, 'Unable to add file'
374 def rm(files, force = False):
375 """Remove a file from the repository
377 if not force:
378 for f in files:
379 if os.path.exists(f):
380 raise GitException, '%s exists. Remove it first' %f
381 if files:
382 __run('git-update-index --remove --', files)
383 else:
384 if files:
385 __run('git-update-index --force-remove --', files)
387 def update_cache(files = None, force = False):
388 """Update the cache information for the given files
390 if not files:
391 files = []
393 cache_files = __tree_status(files)
395 # everything is up-to-date
396 if len(cache_files) == 0:
397 return False
399 # check for unresolved conflicts
400 if not force and [x for x in cache_files
401 if x[0] not in ['M', 'N', 'A', 'D']]:
402 raise GitException, 'Updating cache failed: unresolved conflicts'
404 # update the cache
405 add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
406 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
407 m_files = [x[1] for x in cache_files if x[0] in ['M']]
409 if add_files and __run('git-update-index --add --', add_files) != 0:
410 raise GitException, 'Failed git-update-index --add'
411 if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
412 raise GitException, 'Failed git-update-index --rm'
413 if m_files and __run('git-update-index --', m_files) != 0:
414 raise GitException, 'Failed git-update-index'
416 return True
418 def commit(message, files = None, parents = None, allowempty = False,
419 cache_update = True, tree_id = None,
420 author_name = None, author_email = None, author_date = None,
421 committer_name = None, committer_email = None):
422 """Commit the current tree to repository
424 if not files:
425 files = []
426 if not parents:
427 parents = []
429 # Get the tree status
430 if cache_update and parents != []:
431 changes = update_cache(files)
432 if not changes and not allowempty:
433 raise GitException, 'No changes to commit'
435 # get the commit message
436 if message[-1:] != '\n':
437 message += '\n'
439 must_switch = True
440 # write the index to repository
441 if tree_id == None:
442 tree_id = _output_one_line('git-write-tree')
443 else:
444 must_switch = False
446 # the commit
447 cmd = ''
448 if author_name:
449 cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
450 if author_email:
451 cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
452 if author_date:
453 cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
454 if committer_name:
455 cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
456 if committer_email:
457 cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
458 cmd += 'git-commit-tree %s' % tree_id
460 # get the parents
461 for p in parents:
462 cmd += ' -p %s' % p
464 commit_id = _output_one_line(cmd, message)
465 if must_switch:
466 __set_head(commit_id)
468 return commit_id
470 def apply_diff(rev1, rev2, check_index = True, files = None):
471 """Apply the diff between rev1 and rev2 onto the current
472 index. This function doesn't need to raise an exception since it
473 is only used for fast-pushing a patch. If this operation fails,
474 the pushing would fall back to the three-way merge.
476 if check_index:
477 index_opt = '--index'
478 else:
479 index_opt = ''
481 if not files:
482 files = []
484 diff_str = diff(files, rev1, rev2)
485 if diff_str:
486 try:
487 _input_str('git-apply %s' % index_opt, diff_str)
488 except GitException:
489 return False
491 return True
493 def merge(base, head1, head2):
494 """Perform a 3-way merge between base, head1 and head2 into the
495 local tree
497 refresh_index()
498 if __run('git-read-tree -u -m --aggressive', [base, head1, head2]) != 0:
499 raise GitException, 'git-read-tree failed (local changes maybe?)'
501 # check the index for unmerged entries
502 files = {}
503 stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
505 for line in _output('git-ls-files --unmerged --stage -z').split('\0'):
506 if not line:
507 continue
509 mode, hash, stage, path = stages_re.findall(line)[0]
511 if not path in files:
512 files[path] = {}
513 files[path]['1'] = ('', '')
514 files[path]['2'] = ('', '')
515 files[path]['3'] = ('', '')
517 files[path][stage] = (mode, hash)
519 # merge the unmerged files
520 errors = False
521 for path in files:
522 stages = files[path]
523 if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
524 stages['3'][1], path, stages['1'][0],
525 stages['2'][0], stages['3'][0]) != 0:
526 errors = True
528 if errors:
529 raise GitException, 'GIT index merging failed (possible conflicts)'
531 def status(files = None, modified = False, new = False, deleted = False,
532 conflict = False, unknown = False, noexclude = False):
533 """Show the tree status
535 if not files:
536 files = []
538 cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
539 all = not (modified or new or deleted or conflict or unknown)
541 if not all:
542 filestat = []
543 if modified:
544 filestat.append('M')
545 if new:
546 filestat.append('A')
547 filestat.append('N')
548 if deleted:
549 filestat.append('D')
550 if conflict:
551 filestat.append('C')
552 if unknown:
553 filestat.append('?')
554 cache_files = [x for x in cache_files if x[0] in filestat]
556 for fs in cache_files:
557 if all:
558 print '%s %s' % (fs[0], fs[1])
559 else:
560 print '%s' % fs[1]
562 def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None):
563 """Show the diff between rev1 and rev2
565 if not files:
566 files = []
568 if rev1 and rev2:
569 diff_str = _output(['git-diff-tree', '-p', rev1, rev2] + files)
570 elif rev1 or rev2:
571 refresh_index()
572 if rev2:
573 diff_str = _output(['git-diff-index', '-p', '-R', rev2] + files)
574 else:
575 diff_str = _output(['git-diff-index', '-p', rev1] + files)
576 else:
577 diff_str = ''
579 if out_fd:
580 out_fd.write(diff_str)
581 else:
582 return diff_str
584 def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
585 """Return the diffstat between rev1 and rev2
587 if not files:
588 files = []
590 p=popen2.Popen3('git-apply --stat')
591 diff(files, rev1, rev2, p.tochild)
592 p.tochild.close()
593 diff_str = p.fromchild.read().rstrip()
594 if p.wait():
595 raise GitException, 'git.diffstat failed'
596 return diff_str
598 def files(rev1, rev2):
599 """Return the files modified between rev1 and rev2
602 result = ''
603 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
604 result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
606 return result.rstrip()
608 def barefiles(rev1, rev2):
609 """Return the files modified between rev1 and rev2, without status info
612 result = ''
613 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
614 result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
616 return result.rstrip()
618 def pretty_commit(commit_id = 'HEAD'):
619 """Return a given commit (log + diff)
621 return _output(['git-diff-tree', '--cc', '--always', '--pretty', '-r',
622 commit_id])
624 def checkout(files = None, tree_id = None, force = False):
625 """Check out the given or all files
627 if not files:
628 files = []
630 if tree_id and __run('git-read-tree --reset', [tree_id]) != 0:
631 raise GitException, 'Failed git-read-tree --reset %s' % tree_id
633 checkout_cmd = 'git-checkout-index -q -u'
634 if force:
635 checkout_cmd += ' -f'
636 if len(files) == 0:
637 checkout_cmd += ' -a'
638 else:
639 checkout_cmd += ' --'
641 if __run(checkout_cmd, files) != 0:
642 raise GitException, 'Failed git-checkout-index'
644 def switch(tree_id):
645 """Switch the tree to the given id
647 refresh_index()
648 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
649 raise GitException, 'git-read-tree failed (local changes maybe?)'
651 __set_head(tree_id)
653 def reset(files = None, tree_id = None, check_out = True):
654 """Revert the tree changes relative to the given tree_id. It removes
655 any local changes
657 if not tree_id:
658 tree_id = get_head()
660 if check_out:
661 cache_files = __tree_status(files, tree_id)
662 # files which were added but need to be removed
663 rm_files = [x[1] for x in cache_files if x[0] in ['A']]
665 checkout(files, tree_id, True)
666 # checkout doesn't remove files
667 map(os.remove, rm_files)
669 # if the reset refers to the whole tree, switch the HEAD as well
670 if not files:
671 __set_head(tree_id)
673 def pull(repository = 'origin', refspec = None):
674 """Pull changes from the remote repository. At the moment, just
675 use the 'git-pull' command
677 # 'git-pull' updates the HEAD
678 __clear_head_cache()
680 args = [repository]
681 if refspec:
682 args.append(refspec)
684 if __run('git-pull', args) != 0:
685 raise GitException, 'Failed "git-pull %s"' % repository
687 def apply_patch(filename = None, base = None):
688 """Apply a patch onto the current or given index. There must not
689 be any local changes in the tree, otherwise the command fails
691 def __apply_patch():
692 if filename:
693 return __run('git-apply --index', [filename]) == 0
694 else:
695 try:
696 _input('git-apply --index', sys.stdin)
697 except GitException:
698 return False
699 return True
701 if base:
702 orig_head = get_head()
703 switch(base)
704 else:
705 refresh_index() # needed since __apply_patch() doesn't do it
707 if not __apply_patch():
708 if base:
709 switch(orig_head)
710 raise GitException, 'Patch does not apply cleanly'
711 elif base:
712 top = commit(message = 'temporary commit used for applying a patch',
713 parents = [base])
714 switch(orig_head)
715 merge(base, orig_head, top)
717 def clone(repository, local_dir):
718 """Clone a remote repository. At the moment, just use the
719 'git-clone' script
721 if __run('git-clone', [repository, local_dir]) != 0:
722 raise GitException, 'Failed "git-clone %s %s"' \
723 % (repository, local_dir)
725 def modifying_revs(files, base_rev):
726 """Return the revisions from the list modifying the given files
728 cmd = ['git-rev-list', '%s..' % base_rev, '--']
729 revs = [line.strip() for line in _output_lines(cmd + files)]
731 return revs