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()]
96 string = p.fromchild.read()
98 raise GitException, '%s failed' % str(cmd)
101 def _output_one_line(cmd):
103 string = p.fromchild.readline().strip()
105 raise GitException, '%s failed' % str(cmd)
108 def _output_lines(cmd):
110 lines = p.fromchild.readlines()
112 raise GitException, '%s failed' % str(cmd)
115 def __run(cmd, args=None):
116 """__run: runs cmd using spawnvp.
118 Runs cmd using spawnvp. The shell is avoided so it won't mess up
119 our arguments. If args is very large, the command is run multiple
120 times; args is split xargs style: cmd is passed on each
121 invocation. Unlike xargs, returns immediately if any non-zero
122 return code is received.
128 for i in range(0, len(args)+1, 100):
129 r=os.spawnvp(os.P_WAIT, args_l[0], args_l + args[i:min(i+100, len(args))])
134 def __check_base_dir():
135 return os.path.isdir(base_dir)
137 def __tree_status(files = [], tree_id = 'HEAD', unknown = False):
138 """Returns a list of pairs - [status, filename]
140 os.system('git-update-cache --refresh > /dev/null')
146 exclude_file = os.path.join(base_dir, 'exclude')
148 if os.path.exists(exclude_file):
149 extra_exclude.append('--exclude-from=%s' % exclude_file)
150 lines = _output_lines(['git-ls-files', '--others',
151 '--exclude=*.[ao]', '--exclude=.*'
152 '--exclude=TAGS', '--exclude=tags', '--exclude=*~',
153 '--exclude=#*'] + extra_exclude)
154 cache_files += [('?', line.strip()) for line in lines]
157 conflicts = get_conflicts()
160 cache_files += [('C', filename) for filename in conflicts]
163 for line in _output_lines(['git-diff-cache', '-r', tree_id] + files):
164 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
165 if fs[1] not in conflicts:
166 cache_files.append(fs)
171 """Return true if there are local changes in the tree
173 return len(__tree_status()) != 0
176 """Returns a string representing the HEAD
178 return read_string(head_link)
181 """Returns the name of the file pointed to by the HEAD link
184 if os.path.islink(head_link) and os.path.isfile(head_link):
185 return os.path.basename(os.readlink(head_link))
187 raise GitException, 'Invalid .git/HEAD link. Git tree not initialised?'
190 """Sets the HEAD value
192 write_string(head_link, val)
195 """Add the files or recursively add the directory contents
197 # generate the file list
200 if not os.path.exists(i):
201 raise GitException, 'Unknown file or directory: %s' % i
204 # recursive search. We only add files
205 for root, dirs, local_files in os.walk(i):
206 for name in [os.path.join(root, f) for f in local_files]:
207 if os.path.isfile(name):
208 files.append(os.path.normpath(name))
209 elif os.path.isfile(i):
210 files.append(os.path.normpath(i))
212 raise GitException, '%s is not a file or directory' % i
215 if __run('git-update-cache --add --', files):
216 raise GitException, 'Unable to add file'
218 def rm(files, force = False):
219 """Remove a file from the repository
222 git_opt = '--force-remove'
228 if os.path.exists(f):
229 raise GitException, '%s exists. Remove it first' %f
231 __run('git-update-cache --remove --', files)
234 __run('git-update-cache --force-remove --', files)
236 def update_cache(files):
237 """Update the cache information for the given files
243 if os.path.exists(f):
249 __run('git-update-cache --', files_here)
251 __run('git-update-cache --remove --', files_gone)
253 def commit(message, files = [], parents = [], allowempty = False,
254 author_name = None, author_email = None, author_date = None,
255 committer_name = None, committer_email = None):
256 """Commit the current tree to repository
258 first = (parents == [])
260 # Get the tree status
262 cache_files = __tree_status(files)
264 if not first and len(cache_files) == 0 and not allowempty:
265 raise GitException, 'No changes to commit'
267 # check for unresolved conflicts
268 if not first and len(filter(lambda x: x[0] not in ['M', 'N', 'D'],
270 raise GitException, 'Commit failed: unresolved conflicts'
272 # get the commit message
273 f = file('.commitmsg', 'w+')
274 if message[-1] == '\n':
285 for f in cache_files:
287 add_files.append(f[1])
289 rm_files.append(f[1])
294 if __run('git-update-cache --add --', add_files):
295 raise GitException, 'Failed git-update-cache --add'
297 if __run('git-update-cache --force-remove --', rm_files):
298 raise GitException, 'Failed git-update-cache --rm'
300 if __run('git-update-cache --', m_files):
301 raise GitException, 'Failed git-update-cache'
303 # write the index to repository
304 tree_id = _output_one_line('git-write-tree')
309 cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
311 cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
313 cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
315 cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
317 cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
318 cmd += 'git-commit-tree %s' % tree_id
324 cmd += ' < .commitmsg'
326 commit_id = _output_one_line(cmd)
327 __set_head(commit_id)
328 os.remove('.commitmsg')
332 def merge(base, head1, head2):
333 """Perform a 3-way merge between base, head1 and head2 into the
336 if __run('git-read-tree -u -m', [base, head1, head2]) != 0:
337 raise GitException, 'git-read-tree failed (local changes maybe?)'
339 # this can fail if there are conflicts
340 if os.system('git-merge-cache -o gitmergeonefile.py -a') != 0:
341 raise GitException, 'git-merge-cache failed (possible conflicts)'
343 # this should not fail
344 if os.system('git-checkout-cache -f -a') != 0:
345 raise GitException, 'Failed git-checkout-cache'
347 def status(files = [], modified = False, new = False, deleted = False,
348 conflict = False, unknown = False):
349 """Show the tree status
351 cache_files = __tree_status(files, unknown = True)
352 all = not (modified or new or deleted or conflict or unknown)
366 cache_files = filter(lambda x: x[0] in filestat, cache_files)
368 for fs in cache_files:
370 print '%s %s' % (fs[0], fs[1])
374 def diff(files = [], rev1 = 'HEAD', rev2 = None, out_fd = None):
375 """Show the diff between rev1 and rev2
377 os.system('git-update-cache --refresh > /dev/null')
380 diff_str = _output(['git-diff-tree', '-p', rev1, rev2] + files)
382 diff_str = _output(['git-diff-cache', '-p', rev1] + files)
385 out_fd.write(diff_str)
389 def diffstat(files = [], rev1 = 'HEAD', rev2 = None):
390 """Return the diffstat between rev1 and rev2
393 os.system('git-update-cache --refresh > /dev/null')
394 p=popen2.Popen3('git-apply --stat')
395 diff(files, rev1, rev2, p.tochild)
397 str = p.fromchild.read().rstrip()
399 raise GitException, 'git.diffstat failed'
402 def files(rev1, rev2):
403 """Return the files modified between rev1 and rev2
405 os.system('git-update-cache --refresh > /dev/null')
408 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
409 str += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
413 def checkout(files = [], force = False):
414 """Check out the given or all files
416 git_flags = 'git-checkout-cache -q -u'
424 if __run(git_flags, files) != 0:
425 raise GitException, 'Failed git-checkout-cache'
428 """Switch the tree to the given id
430 to_delete = filter(lambda x: x[0] == 'N', __tree_status(tree_id = tree_id))
432 if __run('git-read-tree -m', [tree_id]) != 0:
433 raise GitException, 'Failed git-read-tree -m %s' % tree_id
435 checkout(force = True)
438 # checkout doesn't remove files