1 """Python GIT interface
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
21 import sys, os, popen2
23 from stgit.utils import *
26 class GitException(Exception):
30 # Different start-up variables read from the environment
31 if 'GIT_DIR' in os.environ:
32 base_dir = os.environ['GIT_DIR']
36 head_link = os.path.join(base_dir, 'HEAD')
42 """Handle the commit objects
44 def __init__(self, id_hash):
45 self.__id_hash = id_hash
47 lines = _output_lines('git-cat-file commit %s' % id_hash)
49 for i in range(len(lines)):
53 field = line.strip().split(' ', 1)
54 if field[0] == 'tree':
55 self.__tree = field[1]
56 elif field[0] == 'parent':
57 self.__parents.append(field[1])
58 if field[0] == 'author':
59 self.__author = field[1]
60 if field[0] == 'committer':
61 self.__committer = field[1]
62 self.__log = ''.join(lines[i+1:])
64 def get_id_hash(self):
71 return self.__parents[0]
73 def get_parents(self):
79 def get_committer(self):
80 return self.__committer
85 # dictionary of Commit objects, used to avoid multiple calls to git
91 def get_commit(id_hash):
92 """Commit objects factory. Save/look-up them in the __commits
97 if id_hash in __commits:
98 return __commits[id_hash]
100 commit = Commit(id_hash)
101 __commits[id_hash] = commit
105 """Return the list of file conflicts
107 conflicts_file = os.path.join(base_dir, 'conflicts')
108 if os.path.isfile(conflicts_file):
109 f = file(conflicts_file)
110 names = [line.strip() for line in f.readlines()]
116 def _input(cmd, file_desc):
117 p = popen2.Popen3(cmd, True)
119 line = file_desc.readline()
122 p.tochild.write(line)
125 raise GitException, '%s failed' % str(cmd)
128 p=popen2.Popen3(cmd, True)
129 string = p.fromchild.read()
131 raise GitException, '%s failed' % str(cmd)
134 def _output_one_line(cmd, file_desc = None):
135 p=popen2.Popen3(cmd, True)
136 if file_desc != None:
137 for line in file_desc:
138 p.tochild.write(line)
140 string = p.fromchild.readline().strip()
142 raise GitException, '%s failed' % str(cmd)
145 def _output_lines(cmd):
146 p=popen2.Popen3(cmd, True)
147 lines = p.fromchild.readlines()
149 raise GitException, '%s failed' % str(cmd)
152 def __run(cmd, args=None):
153 """__run: runs cmd using spawnvp.
155 Runs cmd using spawnvp. The shell is avoided so it won't mess up
156 our arguments. If args is very large, the command is run multiple
157 times; args is split xargs style: cmd is passed on each
158 invocation. Unlike xargs, returns immediately if any non-zero
159 return code is received.
165 for i in range(0, len(args)+1, 100):
166 r=os.spawnvp(os.P_WAIT, args_l[0], args_l + args[i:min(i+100, len(args))])
171 def __check_base_dir():
172 return os.path.isdir(base_dir)
174 def __tree_status(files = None, tree_id = 'HEAD', unknown = False,
176 """Returns a list of pairs - [status, filename]
186 exclude_file = os.path.join(base_dir, 'info', 'exclude')
187 base_exclude = ['--exclude=%s' % s for s in
188 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
189 base_exclude.append('--exclude-per-directory=.gitignore')
191 if os.path.exists(exclude_file):
192 extra_exclude = ['--exclude-from=%s' % exclude_file]
196 extra_exclude = base_exclude = []
198 lines = _output_lines(['git-ls-files', '--others'] + base_exclude
200 cache_files += [('?', line.strip()) for line in lines]
203 conflicts = get_conflicts()
206 cache_files += [('C', filename) for filename in conflicts]
209 for line in _output_lines(['git-diff-index', '-r', tree_id] + files):
210 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
211 if fs[1] not in conflicts:
212 cache_files.append(fs)
217 """Return true if there are local changes in the tree
219 return len(__tree_status()) != 0
225 """Verifies the HEAD and returns the SHA1 id that represents it
230 __head = rev_parse('HEAD')
234 """Returns the name of the file pointed to by the HEAD link
236 return os.path.basename(_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
244 if __run('git-symbolic-ref HEAD', [ref]) != 0:
245 raise GitException, 'Could not set head to "%s"' % ref
248 """Sets the HEAD value
252 if not __head or __head != val:
253 if __run('git-update-ref HEAD', [val]) != 0:
254 raise GitException, 'Could not update HEAD to "%s".' % val
257 def __clear_head_cache():
258 """Sets the __head to None so that a re-read is forced
265 """Refresh index with stat() information from the working directory.
267 __run('git-update-index -q --unmerged --refresh')
269 def rev_parse(git_id):
270 """Parse the string and return a verified SHA1 id
273 return _output_one_line(['git-rev-parse', '--verify', git_id])
275 raise GitException, 'Unknown revision: %s' % git_id
277 def branch_exists(branch):
278 """Existance check for the named branch
280 for line in _output_lines(['git-rev-parse', '--symbolic', '--all']):
281 if line.strip() == branch:
285 def create_branch(new_branch, tree_id = None):
286 """Create a new branch in the git repository
288 new_head = os.path.join('refs', 'heads', new_branch)
289 if branch_exists(new_head):
290 raise GitException, 'Branch "%s" already exists' % new_branch
292 current_head = get_head()
293 set_head_file(new_head)
294 __set_head(current_head)
296 # a checkout isn't needed if new branch points to the current head
300 if os.path.isfile(os.path.join(base_dir, 'MERGE_HEAD')):
301 os.remove(os.path.join(base_dir, 'MERGE_HEAD'))
303 def switch_branch(name):
304 """Switch to a git branch
308 new_head = os.path.join('refs', 'heads', name)
309 if not branch_exists(new_head):
310 raise GitException, 'Branch "%s" does not exist' % name
312 tree_id = rev_parse(new_head + '^0')
313 if tree_id != get_head():
315 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
316 raise GitException, 'git-read-tree failed (local changes maybe?)'
318 set_head_file(new_head)
320 if os.path.isfile(os.path.join(base_dir, 'MERGE_HEAD')):
321 os.remove(os.path.join(base_dir, 'MERGE_HEAD'))
323 def delete_branch(name):
324 """Delete a git branch
326 branch_head = os.path.join('refs', 'heads', name)
327 if not branch_exists(branch_head):
328 raise GitException, 'Branch "%s" does not exist' % name
329 os.remove(os.path.join(base_dir, branch_head))
331 def rename_branch(from_name, to_name):
332 """Rename a git branch
334 from_head = os.path.join('refs', 'heads', from_name)
335 if not branch_exists(from_head):
336 raise GitException, 'Branch "%s" does not exist' % from_name
337 to_head = os.path.join('refs', 'heads', to_name)
338 if branch_exists(to_head):
339 raise GitException, 'Branch "%s" already exists' % to_name
341 if get_head_file() == from_name:
342 set_head_file(to_head)
343 os.rename(os.path.join(base_dir, from_head), os.path.join(base_dir, to_head))
346 """Add the files or recursively add the directory contents
348 # generate the file list
351 if not os.path.exists(i):
352 raise GitException, 'Unknown file or directory: %s' % i
355 # recursive search. We only add files
356 for root, dirs, local_files in os.walk(i):
357 for name in [os.path.join(root, f) for f in local_files]:
358 if os.path.isfile(name):
359 files.append(os.path.normpath(name))
360 elif os.path.isfile(i):
361 files.append(os.path.normpath(i))
363 raise GitException, '%s is not a file or directory' % i
366 if __run('git-update-index --add --', files):
367 raise GitException, 'Unable to add file'
369 def rm(files, force = False):
370 """Remove a file from the repository
374 if os.path.exists(f):
375 raise GitException, '%s exists. Remove it first' %f
377 __run('git-update-index --remove --', files)
380 __run('git-update-index --force-remove --', files)
382 def update_cache(files = None, force = False):
383 """Update the cache information for the given files
388 cache_files = __tree_status(files)
390 # everything is up-to-date
391 if len(cache_files) == 0:
394 # check for unresolved conflicts
395 if not force and [x for x in cache_files
396 if x[0] not in ['M', 'N', 'A', 'D']]:
397 raise GitException, 'Updating cache failed: unresolved conflicts'
400 add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
401 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
402 m_files = [x[1] for x in cache_files if x[0] in ['M']]
404 if add_files and __run('git-update-index --add --', add_files) != 0:
405 raise GitException, 'Failed git-update-index --add'
406 if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
407 raise GitException, 'Failed git-update-index --rm'
408 if m_files and __run('git-update-index --', m_files) != 0:
409 raise GitException, 'Failed git-update-index'
413 def commit(message, files = None, parents = None, allowempty = False,
414 cache_update = True, tree_id = None,
415 author_name = None, author_email = None, author_date = None,
416 committer_name = None, committer_email = None):
417 """Commit the current tree to repository
424 # Get the tree status
425 if cache_update and parents != []:
426 changes = update_cache(files)
427 if not changes and not allowempty:
428 raise GitException, 'No changes to commit'
430 # get the commit message
431 if message[-1:] != '\n':
435 # write the index to repository
437 tree_id = _output_one_line('git-write-tree')
444 cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
446 cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
448 cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
450 cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
452 cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
453 cmd += 'git-commit-tree %s' % tree_id
459 commit_id = _output_one_line(cmd, message)
461 __set_head(commit_id)
465 def apply_diff(rev1, rev2):
466 """Apply the diff between rev1 and rev2 onto the current
467 index. This function doesn't need to raise an exception since it
468 is only used for fast-pushing a patch. If this operation fails,
469 the pushing would fall back to the three-way merge.
471 return os.system('git-diff-tree -p %s %s | git-apply --index 2> /dev/null'
474 def merge(base, head1, head2):
475 """Perform a 3-way merge between base, head1 and head2 into the
479 if __run('git-read-tree -u -m', [base, head1, head2]) != 0:
480 raise GitException, 'git-read-tree failed (local changes maybe?)'
482 # this can fail if there are conflicts
483 if __run('git-merge-index -o -q gitmergeonefile.py -a') != 0:
484 raise GitException, 'git-merge-index failed (possible conflicts)'
486 def status(files = None, modified = False, new = False, deleted = False,
487 conflict = False, unknown = False, noexclude = False):
488 """Show the tree status
493 cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
494 all = not (modified or new or deleted or conflict or unknown)
509 cache_files = [x for x in cache_files if x[0] in filestat]
511 for fs in cache_files:
513 print '%s %s' % (fs[0], fs[1])
517 def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None):
518 """Show the diff between rev1 and rev2
524 diff_str = _output(['git-diff-tree', '-p', rev1, rev2] + files)
527 diff_str = _output(['git-diff-index', '-p', rev1] + files)
530 out_fd.write(diff_str)
534 def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
535 """Return the diffstat between rev1 and rev2
540 p=popen2.Popen3('git-apply --stat')
541 diff(files, rev1, rev2, p.tochild)
543 str = p.fromchild.read().rstrip()
545 raise GitException, 'git.diffstat failed'
548 def files(rev1, rev2):
549 """Return the files modified between rev1 and rev2
553 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
554 str += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
558 def barefiles(rev1, rev2):
559 """Return the files modified between rev1 and rev2, without status info
563 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
564 str += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
568 def checkout(files = None, tree_id = None, force = False):
569 """Check out the given or all files
574 if tree_id and __run('git-read-tree -m', [tree_id]) != 0:
575 raise GitException, 'Failed git-read-tree -m %s' % tree_id
577 checkout_cmd = 'git-checkout-index -q -u'
579 checkout_cmd += ' -f'
581 checkout_cmd += ' -a'
583 checkout_cmd += ' --'
585 if __run(checkout_cmd, files) != 0:
586 raise GitException, 'Failed git-checkout-index'
589 """Switch the tree to the given id
592 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
593 raise GitException, 'git-read-tree failed (local changes maybe?)'
597 def reset(tree_id = None):
598 """Revert the tree changes relative to the given tree_id. It removes
604 cache_files = __tree_status(tree_id = tree_id)
605 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
607 checkout(tree_id = tree_id, force = True)
610 # checkout doesn't remove files
611 map(os.remove, rm_files)
613 def pull(repository = 'origin', refspec = None):
614 """Pull changes from the remote repository. At the moment, just
615 use the 'git pull' command
617 # 'git pull' updates the HEAD
624 if __run('git pull', args) != 0:
625 raise GitException, 'Failed "git pull %s"' % repository
627 def apply_patch(filename = None, base = None):
628 """Apply a patch onto the current or given index. There must not
629 be any local changes in the tree, otherwise the command fails
633 return __run('git-apply --index', [filename]) == 0
636 _input('git-apply --index', sys.stdin)
644 orig_head = get_head()
647 if not __apply_patch():
650 raise GitException, 'Patch does not apply cleanly'
652 top = commit(message = 'temporary commit used for applying a patch',
655 merge(base, orig_head, top)
657 def clone(repository, local_dir):
658 """Clone a remote repository. At the moment, just use the
661 if __run('git clone', [repository, local_dir]) != 0:
662 raise GitException, 'Failed "git clone %s %s"' \
663 % (repository, local_dir)