9ad6f0d18241893750954bb1f7f440880d09406f
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] == 'committer':
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
, file_desc
= None):
131 if file_desc
!= None:
132 for line
in file_desc
:
133 p
.tochild
.write(line
)
135 string
= p
.fromchild
.readline().strip()
137 raise GitException
, '%s failed' % str
(cmd
)
140 def _output_lines(cmd
):
142 lines
= p
.fromchild
.readlines()
144 raise GitException
, '%s failed' % str
(cmd
)
147 def __run(cmd
, args
=None):
148 """__run: runs cmd using spawnvp.
150 Runs cmd using spawnvp. The shell is avoided so it won't mess up
151 our arguments. If args is very large, the command is run multiple
152 times; args is split xargs style: cmd is passed on each
153 invocation. Unlike xargs, returns immediately if any non-zero
154 return code is received.
160 for i
in range(0, len(args
)+1, 100):
161 r
=os
.spawnvp(os
.P_WAIT
, args_l
[0], args_l
+ args
[i
:min(i
+100, len(args
))])
166 def __check_base_dir():
167 return os
.path
.isdir(base_dir
)
169 def __tree_status(files
= [], tree_id
= 'HEAD', unknown
= False,
171 """Returns a list of pairs - [status, filename]
173 os
.system('git-update-cache --refresh > /dev/null')
179 exclude_file
= os
.path
.join(base_dir
, 'info', 'exclude')
180 base_exclude
= ['--exclude=%s' % s for s in
181 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
182 base_exclude
.append('--exclude-per-directory=.gitignore')
184 if os
.path
.exists(exclude_file
):
185 extra_exclude
= '--exclude-from=%s' % exclude_file
189 extra_exclude
= base_exclude
= []
191 lines
= _output_lines(['git-ls-files', '--others'] + base_exclude
193 cache_files
+= [('?', line
.strip()) for line
in lines
]
196 conflicts
= get_conflicts()
199 cache_files
+= [('C', filename
) for filename
in conflicts
]
202 for line
in _output_lines(['git-diff-cache', '-r', tree_id
] + files
):
203 fs
= tuple(line
.rstrip().split(' ',4)[-1].split('\t',1))
204 if fs
[1] not in conflicts
:
205 cache_files
.append(fs
)
210 """Return true if there are local changes in the tree
212 return len(__tree_status()) != 0
215 """Returns a string representing the HEAD
217 return read_string(head_link
)
220 """Returns the name of the file pointed to by the HEAD link
223 if os
.path
.islink(head_link
) and os
.path
.isfile(head_link
):
224 return os
.path
.basename(os
.readlink(head_link
))
226 raise GitException
, 'Invalid .git/HEAD link. Git tree not initialised?'
229 """Sets the HEAD value
231 write_string(head_link
, val
)
234 """Add the files or recursively add the directory contents
236 # generate the file list
239 if not os
.path
.exists(i
):
240 raise GitException
, 'Unknown file or directory: %s' % i
243 # recursive search. We only add files
244 for root
, dirs
, local_files
in os
.walk(i
):
245 for name
in [os
.path
.join(root
, f
) for f
in local_files
]:
246 if os
.path
.isfile(name
):
247 files
.append(os
.path
.normpath(name
))
248 elif os
.path
.isfile(i
):
249 files
.append(os
.path
.normpath(i
))
251 raise GitException
, '%s is not a file or directory' % i
254 if __run('git-update-cache --add --', files
):
255 raise GitException
, 'Unable to add file'
257 def rm(files
, force
= False):
258 """Remove a file from the repository
261 git_opt
= '--force-remove'
267 if os
.path
.exists(f
):
268 raise GitException
, '%s exists. Remove it first' %f
270 __run('git-update-cache --remove --', files
)
273 __run('git-update-cache --force-remove --', files
)
275 def update_cache(files
= [], force
= False):
276 """Update the cache information for the given files
278 cache_files
= __tree_status(files
)
280 # everything is up-to-date
281 if len(cache_files
) == 0:
284 # check for unresolved conflicts
285 if not force
and [x
for x
in cache_files
286 if x
[0] not in ['M', 'N', 'A', 'D']]:
287 raise GitException
, 'Updating cache failed: unresolved conflicts'
290 add_files
= [x
[1] for x
in cache_files
if x
[0] in ['N', 'A']]
291 rm_files
= [x
[1] for x
in cache_files
if x
[0] in ['D']]
292 m_files
= [x
[1] for x
in cache_files
if x
[0] in ['M']]
294 if add_files
and __run('git-update-cache --add --', add_files
) != 0:
295 raise GitException
, 'Failed git-update-cache --add'
296 if rm_files
and __run('git-update-cache --force-remove --', rm_files
) != 0:
297 raise GitException
, 'Failed git-update-cache --rm'
298 if m_files
and __run('git-update-cache --', m_files
) != 0:
299 raise GitException
, 'Failed git-update-cache'
303 def commit(message
, files
= [], parents
= [], allowempty
= False,
304 cache_update
= True, tree_id
= None,
305 author_name
= None, author_email
= None, author_date
= None,
306 committer_name
= None, committer_email
= None):
307 """Commit the current tree to repository
309 # Get the tree status
310 if cache_update
and parents
!= []:
311 changes
= update_cache(files
)
312 if not changes
and not allowempty
:
313 raise GitException
, 'No changes to commit'
315 # get the commit message
316 if message
[-1:] != '\n':
320 # write the index to repository
322 tree_id
= _output_one_line('git-write-tree')
329 cmd
+= 'GIT_AUTHOR_NAME="%s" ' % author_name
331 cmd
+= 'GIT_AUTHOR_EMAIL="%s" ' % author_email
333 cmd
+= 'GIT_AUTHOR_DATE="%s" ' % author_date
335 cmd
+= 'GIT_COMMITTER_NAME="%s" ' % committer_name
337 cmd
+= 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
338 cmd
+= 'git-commit-tree %s' % tree_id
344 commit_id
= _output_one_line(cmd
, message
)
346 __set_head(commit_id
)
350 def merge(base
, head1
, head2
):
351 """Perform a 3-way merge between base, head1 and head2 into the
354 if __run('git-read-tree -u -m', [base
, head1
, head2
]) != 0:
355 raise GitException
, 'git-read-tree failed (local changes maybe?)'
357 # this can fail if there are conflicts
358 if os
.system('git-merge-cache -o -q gitmergeonefile.py -a') != 0:
359 raise GitException
, 'git-merge-cache failed (possible conflicts)'
361 def status(files
= [], modified
= False, new
= False, deleted
= False,
362 conflict
= False, unknown
= False, noexclude
= False):
363 """Show the tree status
365 cache_files
= __tree_status(files
, unknown
= True, noexclude
= noexclude
)
366 all
= not (modified
or new
or deleted
or conflict
or unknown
)
381 cache_files
= [x
for x
in cache_files
if x
[0] in filestat
]
383 for fs
in cache_files
:
385 print '%s %s' %
(fs
[0], fs
[1])
389 def diff(files
= [], rev1
= 'HEAD', rev2
= None, out_fd
= None):
390 """Show the diff between rev1 and rev2
394 diff_str
= _output(['git-diff-tree', '-p', rev1
, rev2
] + files
)
396 os
.system('git-update-cache --refresh > /dev/null')
397 diff_str
= _output(['git-diff-cache', '-p', rev1
] + files
)
400 out_fd
.write(diff_str
)
404 def diffstat(files
= [], rev1
= 'HEAD', rev2
= None):
405 """Return the diffstat between rev1 and rev2
408 p
=popen2
.Popen3('git-apply --stat')
409 diff(files
, rev1
, rev2
, p
.tochild
)
411 str = p
.fromchild
.read().rstrip()
413 raise GitException
, 'git.diffstat failed'
416 def files(rev1
, rev2
):
417 """Return the files modified between rev1 and rev2
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 barefiles(rev1
, rev2
):
427 """Return the files modified between rev1 and rev2, without status info
431 for line
in _output_lines('git-diff-tree -r %s %s' %
(rev1
, rev2
)):
432 str += '%s\n' % line
.rstrip().split(' ',4)[-1].split('\t',1)[-1]
436 def checkout(files
= [], tree_id
= None, force
= False):
437 """Check out the given or all files
439 if tree_id
and __run('git-read-tree -m', [tree_id
]) != 0:
440 raise GitException
, 'Failed git-read-tree -m %s' % tree_id
442 checkout_cmd
= 'git-checkout-cache -q -u'
444 checkout_cmd
+= ' -f'
446 checkout_cmd
+= ' -a'
448 checkout_cmd
+= ' --'
450 if __run(checkout_cmd
, files
) != 0:
451 raise GitException
, 'Failed git-checkout-cache'
454 """Switch the tree to the given id
456 if __run('git-read-tree -u -m', [get_head(), tree_id
]) != 0:
457 raise GitException
, 'git-read-tree failed (local changes maybe?)'
461 def reset(tree_id
= None):
462 """Revert the tree changes relative to the given tree_id. It removes
468 cache_files
= __tree_status(tree_id
= tree_id
)
469 rm_files
= [x
[1] for x
in cache_files
if x
[0] in ['D']]
471 checkout(tree_id
= tree_id
, force
= True)
474 # checkout doesn't remove files
475 map(os
.remove
, rm_files
)
477 def pull(repository
= 'origin', refspec
= None):
478 """Pull changes from the remote repository. At the moment, just
479 use the 'git pull' command
485 if __run('git pull', args
) != 0:
486 raise GitException
, 'Failed "git pull %s"' % repository
488 def apply_patch(filename
= None):
489 """Apply a patch onto the current index. There must not be any
490 local changes in the tree, otherwise the command fails
492 os
.system('git-update-cache --refresh > /dev/null')
495 if __run('git-apply --index', [filename
]) != 0:
496 raise GitException
, 'Patch does not apply cleanly'
498 _input('git-apply --index', sys
.stdin
)
500 def clone(repository
, local_dir
):
501 """Clone a remote repository. At the moment, just use the
504 if __run('git clone', [repository
, local_dir
]) != 0:
505 raise GitException
, 'Failed "git clone %s %s"' \
506 %
(repository
, local_dir
)