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)
48 for i in range(len(lines)):
52 field = line.strip().split(' ', 1)
53 if field[0] == 'tree':
54 self.__tree = field[1]
55 elif field[0] == 'parent':
56 self.__parent = field[1]
57 if field[0] == 'author':
58 self.__author = field[1]
59 if field[0] == 'comitter':
60 self.__committer = field[1]
61 self.__log = ''.join(lines[i:])
63 def get_id_hash(self):
75 def get_committer(self):
76 return self.__committer
83 """Return the list of file conflicts
85 conflicts_file = os.path.join(base_dir, 'conflicts')
86 if os.path.isfile(conflicts_file):
87 f = file(conflicts_file)
88 names = [line.strip() for line in f.readlines()]
94 def _input(cmd, file_desc):
95 p = popen2.Popen3(cmd)
96 for line in file_desc:
100 raise GitException, '%s failed' % str(cmd)
104 string = p.fromchild.read()
106 raise GitException, '%s failed' % str(cmd)
109 def _output_one_line(cmd):
111 string = p.fromchild.readline().strip()
113 raise GitException, '%s failed' % str(cmd)
116 def _output_lines(cmd):
118 lines = p.fromchild.readlines()
120 raise GitException, '%s failed' % str(cmd)
123 def __run(cmd, args=None):
124 """__run: runs cmd using spawnvp.
126 Runs cmd using spawnvp. The shell is avoided so it won't mess up
127 our arguments. If args is very large, the command is run multiple
128 times; args is split xargs style: cmd is passed on each
129 invocation. Unlike xargs, returns immediately if any non-zero
130 return code is received.
136 for i in range(0, len(args)+1, 100):
137 r=os.spawnvp(os.P_WAIT, args_l[0], args_l + args[i:min(i+100, len(args))])
142 def __check_base_dir():
143 return os.path.isdir(base_dir)
145 def __tree_status(files = [], tree_id = 'HEAD', unknown = False):
146 """Returns a list of pairs - [status, filename]
148 os.system('git-update-cache --refresh > /dev/null')
154 exclude_file = os.path.join(base_dir, 'exclude')
156 if os.path.exists(exclude_file):
157 extra_exclude.append('--exclude-from=%s' % exclude_file)
158 lines = _output_lines(['git-ls-files', '--others',
159 '--exclude=*.[ao]', '--exclude=.*'
160 '--exclude=TAGS', '--exclude=tags', '--exclude=*~',
161 '--exclude=#*'] + extra_exclude)
162 cache_files += [('?', line.strip()) for line in lines]
165 conflicts = get_conflicts()
168 cache_files += [('C', filename) for filename in conflicts]
171 for line in _output_lines(['git-diff-cache', '-r', tree_id] + files):
172 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
173 if fs[1] not in conflicts:
174 cache_files.append(fs)
179 """Return true if there are local changes in the tree
181 return len(__tree_status()) != 0
184 """Returns a string representing the HEAD
186 return read_string(head_link)
189 """Returns the name of the file pointed to by the HEAD link
192 if os.path.islink(head_link) and os.path.isfile(head_link):
193 return os.path.basename(os.readlink(head_link))
195 raise GitException, 'Invalid .git/HEAD link. Git tree not initialised?'
198 """Sets the HEAD value
200 write_string(head_link, val)
203 """Add the files or recursively add the directory contents
205 # generate the file list
208 if not os.path.exists(i):
209 raise GitException, 'Unknown file or directory: %s' % i
212 # recursive search. We only add files
213 for root, dirs, local_files in os.walk(i):
214 for name in [os.path.join(root, f) for f in local_files]:
215 if os.path.isfile(name):
216 files.append(os.path.normpath(name))
217 elif os.path.isfile(i):
218 files.append(os.path.normpath(i))
220 raise GitException, '%s is not a file or directory' % i
223 if __run('git-update-cache --add --', files):
224 raise GitException, 'Unable to add file'
226 def rm(files, force = False):
227 """Remove a file from the repository
230 git_opt = '--force-remove'
236 if os.path.exists(f):
237 raise GitException, '%s exists. Remove it first' %f
239 __run('git-update-cache --remove --', files)
242 __run('git-update-cache --force-remove --', files)
244 def update_cache(files):
245 """Update the cache information for the given files
251 if os.path.exists(f):
257 __run('git-update-cache --', files_here)
259 __run('git-update-cache --remove --', files_gone)
261 def commit(message, files = [], parents = [], allowempty = False,
262 author_name = None, author_email = None, author_date = None,
263 committer_name = None, committer_email = None):
264 """Commit the current tree to repository
266 first = (parents == [])
268 # Get the tree status
270 cache_files = __tree_status(files)
272 if not first and len(cache_files) == 0 and not allowempty:
273 raise GitException, 'No changes to commit'
275 # check for unresolved conflicts
276 if not first and len(filter(lambda x: x[0] not in ['M', 'N', 'A', 'D'],
278 raise GitException, 'Commit failed: unresolved conflicts'
280 # get the commit message
281 f = file('.commitmsg', 'w+')
282 if message[-1] == '\n':
293 for f in cache_files:
294 if f[0] in ['N', 'A']:
295 add_files.append(f[1])
297 rm_files.append(f[1])
302 if __run('git-update-cache --add --', add_files):
303 raise GitException, 'Failed git-update-cache --add'
305 if __run('git-update-cache --force-remove --', rm_files):
306 raise GitException, 'Failed git-update-cache --rm'
308 if __run('git-update-cache --', m_files):
309 raise GitException, 'Failed git-update-cache'
311 # write the index to repository
312 tree_id = _output_one_line('git-write-tree')
317 cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
319 cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
321 cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
323 cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
325 cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
326 cmd += 'git-commit-tree %s' % tree_id
332 cmd += ' < .commitmsg'
334 commit_id = _output_one_line(cmd)
335 __set_head(commit_id)
336 os.remove('.commitmsg')
340 def merge(base, head1, head2):
341 """Perform a 3-way merge between base, head1 and head2 into the
344 if __run('git-read-tree -u -m', [base, head1, head2]) != 0:
345 raise GitException, 'git-read-tree failed (local changes maybe?)'
347 # this can fail if there are conflicts
348 if os.system('git-merge-cache -o gitmergeonefile.py -a') != 0:
349 raise GitException, 'git-merge-cache failed (possible conflicts)'
351 # this should not fail
352 if os.system('git-checkout-cache -f -a') != 0:
353 raise GitException, 'Failed git-checkout-cache'
355 def status(files = [], modified = False, new = False, deleted = False,
356 conflict = False, unknown = False):
357 """Show the tree status
359 cache_files = __tree_status(files, unknown = True)
360 all = not (modified or new or deleted or conflict or unknown)
375 cache_files = filter(lambda x: x[0] in filestat, cache_files)
377 for fs in cache_files:
379 print '%s %s' % (fs[0], fs[1])
383 def diff(files = [], rev1 = 'HEAD', rev2 = None, out_fd = None):
384 """Show the diff between rev1 and rev2
386 os.system('git-update-cache --refresh > /dev/null')
389 diff_str = _output(['git-diff-tree', '-p', rev1, rev2] + files)
391 diff_str = _output(['git-diff-cache', '-p', rev1] + files)
394 out_fd.write(diff_str)
398 def diffstat(files = [], rev1 = 'HEAD', rev2 = None):
399 """Return the diffstat between rev1 and rev2
402 os.system('git-update-cache --refresh > /dev/null')
403 p=popen2.Popen3('git-apply --stat')
404 diff(files, rev1, rev2, p.tochild)
406 str = p.fromchild.read().rstrip()
408 raise GitException, 'git.diffstat failed'
411 def files(rev1, rev2):
412 """Return the files modified between rev1 and rev2
414 os.system('git-update-cache --refresh > /dev/null')
417 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
418 str += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
422 def checkout(files = [], tree_id = None, force = False):
423 """Check out the given or all files
425 if tree_id and __run('git-read-tree -m', [tree_id]) != 0:
426 raise GitException, 'Failed git-read-tree -m %s' % tree_id
428 checkout_cmd = 'git-checkout-cache -q -u'
430 checkout_cmd += ' -f'
432 checkout_cmd += ' -a'
434 checkout_cmd += ' --'
436 if __run(checkout_cmd, files) != 0:
437 raise GitException, 'Failed git-checkout-cache'
440 """Switch the tree to the given id
442 to_delete = filter(lambda x: x[0] in ['N', 'A'],
443 __tree_status(tree_id = tree_id))
445 checkout(tree_id = tree_id, force = True)
448 # checkout doesn't remove files
452 def fetch(location, head = None, tag = None):
453 """Fetch changes from the remote repository. At the moment, just
454 use the 'git fetch' scripts
462 if __run('git fetch', args) != 0:
463 raise GitException, 'Failed "git fetch %s"' % location
465 return read_string(os.path.join(base_dir, 'FETCH_HEAD'))
467 def apply_patch(filename = None):
468 """Apply a patch onto the current index. There must not be any
469 local changes in the tree, otherwise the command fails
471 os.system('git-update-cache --refresh > /dev/null')
474 if __run('git-apply --index', [filename]) != 0:
475 raise GitException, 'Patch does not apply cleanly'
477 _input('git-apply --index', sys.stdin)
479 def clone(repository, local_dir):
480 """Clone a remote repository. At the moment, just use the
483 if __run('git clone', [repository, local_dir]) != 0:
484 raise GitException, 'Failed "git clone %s %s"' \
485 % (repository, local_dir)