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, glob, 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] == 'comitter':
61 self.__committer = field[1]
62 self.__log = ''.join(lines[i:])
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
95 if id_hash in __commits:
96 return __commits[id_hash]
98 commit = Commit(id_hash)
99 __commits[id_hash] = commit
103 """Return the list of file conflicts
105 conflicts_file = os.path.join(base_dir, 'conflicts')
106 if os.path.isfile(conflicts_file):
107 f = file(conflicts_file)
108 names = [line.strip() for line in f.readlines()]
114 def _input(cmd, file_desc):
115 p = popen2.Popen3(cmd)
116 for line in file_desc:
117 p.tochild.write(line)
120 raise GitException, '%s failed' % str(cmd)
124 string = p.fromchild.read()
126 raise GitException, '%s failed' % str(cmd)
129 def _output_one_line(cmd):
131 string = p.fromchild.readline().strip()
133 raise GitException, '%s failed' % str(cmd)
136 def _output_lines(cmd):
138 lines = p.fromchild.readlines()
140 raise GitException, '%s failed' % str(cmd)
143 def __run(cmd, args=None):
144 """__run: runs cmd using spawnvp.
146 Runs cmd using spawnvp. The shell is avoided so it won't mess up
147 our arguments. If args is very large, the command is run multiple
148 times; args is split xargs style: cmd is passed on each
149 invocation. Unlike xargs, returns immediately if any non-zero
150 return code is received.
156 for i in range(0, len(args)+1, 100):
157 r=os.spawnvp(os.P_WAIT, args_l[0], args_l + args[i:min(i+100, len(args))])
162 def __check_base_dir():
163 return os.path.isdir(base_dir)
165 def __tree_status(files = [], tree_id = 'HEAD', unknown = False,
167 """Returns a list of pairs - [status, filename]
169 os.system('git-update-cache --refresh > /dev/null')
175 exclude_file = os.path.join(base_dir, 'info', 'exclude')
176 base_exclude = ['--exclude=%s' % s for s in
177 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
178 base_exclude.append('--exclude-per-directory=.gitignore')
180 if os.path.exists(exclude_file):
181 extra_exclude = '--exclude-from=%s' % exclude_file
185 extra_exclude = base_exclude = []
187 lines = _output_lines(['git-ls-files', '--others'] + base_exclude
189 cache_files += [('?', line.strip()) for line in lines]
192 conflicts = get_conflicts()
195 cache_files += [('C', filename) for filename in conflicts]
198 for line in _output_lines(['git-diff-cache', '-r', tree_id] + files):
199 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
200 if fs[1] not in conflicts:
201 cache_files.append(fs)
206 """Return true if there are local changes in the tree
208 return len(__tree_status()) != 0
211 """Returns a string representing the HEAD
213 return read_string(head_link)
216 """Returns the name of the file pointed to by the HEAD link
219 if os.path.islink(head_link) and os.path.isfile(head_link):
220 return os.path.basename(os.readlink(head_link))
222 raise GitException, 'Invalid .git/HEAD link. Git tree not initialised?'
225 """Sets the HEAD value
227 write_string(head_link, val)
230 """Add the files or recursively add the directory contents
232 # generate the file list
235 if not os.path.exists(i):
236 raise GitException, 'Unknown file or directory: %s' % i
239 # recursive search. We only add files
240 for root, dirs, local_files in os.walk(i):
241 for name in [os.path.join(root, f) for f in local_files]:
242 if os.path.isfile(name):
243 files.append(os.path.normpath(name))
244 elif os.path.isfile(i):
245 files.append(os.path.normpath(i))
247 raise GitException, '%s is not a file or directory' % i
250 if __run('git-update-cache --add --', files):
251 raise GitException, 'Unable to add file'
253 def rm(files, force = False):
254 """Remove a file from the repository
257 git_opt = '--force-remove'
263 if os.path.exists(f):
264 raise GitException, '%s exists. Remove it first' %f
266 __run('git-update-cache --remove --', files)
269 __run('git-update-cache --force-remove --', files)
271 def update_cache(files = [], force = False):
272 """Update the cache information for the given files
274 cache_files = __tree_status(files)
276 # everything is up-to-date
277 if len(cache_files) == 0:
280 # check for unresolved conflicts
281 if not force and [x for x in cache_files
282 if x[0] not in ['M', 'N', 'A', 'D']]:
283 raise GitException, 'Updating cache failed: unresolved conflicts'
286 add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
287 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
288 m_files = [x[1] for x in cache_files if x[0] in ['M']]
290 if add_files and __run('git-update-cache --add --', add_files) != 0:
291 raise GitException, 'Failed git-update-cache --add'
292 if rm_files and __run('git-update-cache --force-remove --', rm_files) != 0:
293 raise GitException, 'Failed git-update-cache --rm'
294 if m_files and __run('git-update-cache --', m_files) != 0:
295 raise GitException, 'Failed git-update-cache'
299 def commit(message, files = [], parents = [], allowempty = False,
301 author_name = None, author_email = None, author_date = None,
302 committer_name = None, committer_email = None):
303 """Commit the current tree to repository
305 # Get the tree status
306 if cache_update and parents != []:
307 changes = update_cache(files)
308 if not changes and not allowempty:
309 raise GitException, 'No changes to commit'
311 # get the commit message
312 f = file('.commitmsg', 'w+')
313 if message[-1:] == '\n':
319 # write the index to repository
320 tree_id = _output_one_line('git-write-tree')
325 cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
327 cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
329 cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
331 cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
333 cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
334 cmd += 'git-commit-tree %s' % tree_id
340 cmd += ' < .commitmsg'
342 commit_id = _output_one_line(cmd)
343 __set_head(commit_id)
344 os.remove('.commitmsg')
348 def merge(base, head1, head2):
349 """Perform a 3-way merge between base, head1 and head2 into the
352 if __run('git-read-tree -u -m', [base, head1, head2]) != 0:
353 raise GitException, 'git-read-tree failed (local changes maybe?)'
355 # this can fail if there are conflicts
356 if os.system('git-merge-cache -o -q gitmergeonefile.py -a') != 0:
357 raise GitException, 'git-merge-cache failed (possible conflicts)'
359 def status(files = [], modified = False, new = False, deleted = False,
360 conflict = False, unknown = False, noexclude = False):
361 """Show the tree status
363 cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
364 all = not (modified or new or deleted or conflict or unknown)
379 cache_files = [x for x in cache_files if x[0] in filestat]
381 for fs in cache_files:
383 print '%s %s' % (fs[0], fs[1])
387 def diff(files = [], rev1 = 'HEAD', rev2 = None, out_fd = None):
388 """Show the diff between rev1 and rev2
390 os.system('git-update-cache --refresh > /dev/null')
393 diff_str = _output(['git-diff-tree', '-p', rev1, rev2] + files)
395 diff_str = _output(['git-diff-cache', '-p', rev1] + files)
398 out_fd.write(diff_str)
402 def diffstat(files = [], rev1 = 'HEAD', rev2 = None):
403 """Return the diffstat between rev1 and rev2
406 os.system('git-update-cache --refresh > /dev/null')
407 p=popen2.Popen3('git-apply --stat')
408 diff(files, rev1, rev2, p.tochild)
410 str = p.fromchild.read().rstrip()
412 raise GitException, 'git.diffstat failed'
415 def files(rev1, rev2):
416 """Return the files modified between rev1 and rev2
418 os.system('git-update-cache --refresh > /dev/null')
421 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
422 str += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
426 def checkout(files = [], tree_id = None, force = False):
427 """Check out the given or all files
429 if tree_id and __run('git-read-tree -m', [tree_id]) != 0:
430 raise GitException, 'Failed git-read-tree -m %s' % tree_id
432 checkout_cmd = 'git-checkout-cache -q -u'
434 checkout_cmd += ' -f'
436 checkout_cmd += ' -a'
438 checkout_cmd += ' --'
440 if __run(checkout_cmd, files) != 0:
441 raise GitException, 'Failed git-checkout-cache'
444 """Switch the tree to the given id
446 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
447 raise GitException, 'git-read-tree failed (local changes maybe?)'
451 def reset(tree_id = None):
452 """Revert the tree changes relative to the given tree_id. It removes
458 cache_files = __tree_status(tree_id = tree_id)
459 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
461 checkout(tree_id = tree_id, force = True)
464 # checkout doesn't remove files
465 map(os.remove, rm_files)
467 def pull(repository = 'origin', refspec = None):
468 """Pull changes from the remote repository. At the moment, just
469 use the 'git pull' command
475 if __run('git pull', args) != 0:
476 raise GitException, 'Failed "git pull %s"' % repository
478 def apply_patch(filename = None):
479 """Apply a patch onto the current index. There must not be any
480 local changes in the tree, otherwise the command fails
482 os.system('git-update-cache --refresh > /dev/null')
485 if __run('git-apply --index', [filename]) != 0:
486 raise GitException, 'Patch does not apply cleanly'
488 _input('git-apply --index', sys.stdin)
490 def clone(repository, local_dir):
491 """Clone a remote repository. At the moment, just use the
494 if __run('git clone', [repository, local_dir]) != 0:
495 raise GitException, 'Failed "git clone %s %s"' \
496 % (repository, local_dir)