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
)
117 line
= file_desc
.readline()
120 p
.tochild
.write(line
)
123 raise GitException
, '%s failed' % str
(cmd
)
127 string
= p
.fromchild
.read()
129 raise GitException
, '%s failed' % str
(cmd
)
132 def _output_one_line(cmd
, file_desc
= None):
134 if file_desc
!= None:
135 for line
in file_desc
:
136 p
.tochild
.write(line
)
138 string
= p
.fromchild
.readline().strip()
140 raise GitException
, '%s failed' % str
(cmd
)
143 def _output_lines(cmd
):
145 lines
= p
.fromchild
.readlines()
147 raise GitException
, '%s failed' % str
(cmd
)
150 def __run(cmd
, args
=None):
151 """__run: runs cmd using spawnvp.
153 Runs cmd using spawnvp. The shell is avoided so it won't mess up
154 our arguments. If args is very large, the command is run multiple
155 times; args is split xargs style: cmd is passed on each
156 invocation. Unlike xargs, returns immediately if any non-zero
157 return code is received.
163 for i
in range(0, len(args
)+1, 100):
164 r
=os
.spawnvp(os
.P_WAIT
, args_l
[0], args_l
+ args
[i
:min(i
+100, len(args
))])
169 def __check_base_dir():
170 return os
.path
.isdir(base_dir
)
172 def __tree_status(files
= [], tree_id
= 'HEAD', unknown
= False,
174 """Returns a list of pairs - [status, filename]
176 os
.system('git-update-index --refresh > /dev/null')
182 exclude_file
= os
.path
.join(base_dir
, 'info', 'exclude')
183 base_exclude
= ['--exclude=%s' % s for s in
184 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
185 base_exclude
.append('--exclude-per-directory=.gitignore')
187 if os
.path
.exists(exclude_file
):
188 extra_exclude
= ['--exclude-from=%s' % exclude_file
]
192 extra_exclude
= base_exclude
= []
194 lines
= _output_lines(['git-ls-files', '--others'] + base_exclude
196 cache_files
+= [('?', line
.strip()) for line
in lines
]
199 conflicts
= get_conflicts()
202 cache_files
+= [('C', filename
) for filename
in conflicts
]
205 for line
in _output_lines(['git-diff-index', '-r', tree_id
] + files
):
206 fs
= tuple(line
.rstrip().split(' ',4)[-1].split('\t',1))
207 if fs
[1] not in conflicts
:
208 cache_files
.append(fs
)
213 """Return true if there are local changes in the tree
215 return len(__tree_status()) != 0
218 """Returns a string representing the HEAD
220 return read_string(head_link
)
223 """Returns the name of the file pointed to by the HEAD link
226 if os
.path
.islink(head_link
) and os
.path
.isfile(head_link
):
227 return os
.path
.basename(os
.readlink(head_link
))
229 raise GitException
, 'Invalid .git/HEAD link. Git tree not initialised?'
232 """Sets the HEAD value
234 write_string(head_link
, val
)
237 """Add the files or recursively add the directory contents
239 # generate the file list
242 if not os
.path
.exists(i
):
243 raise GitException
, 'Unknown file or directory: %s' % i
246 # recursive search. We only add files
247 for root
, dirs
, local_files
in os
.walk(i
):
248 for name
in [os
.path
.join(root
, f
) for f
in local_files
]:
249 if os
.path
.isfile(name
):
250 files
.append(os
.path
.normpath(name
))
251 elif os
.path
.isfile(i
):
252 files
.append(os
.path
.normpath(i
))
254 raise GitException
, '%s is not a file or directory' % i
257 if __run('git-update-index --add --', files
):
258 raise GitException
, 'Unable to add file'
260 def rm(files
, force
= False):
261 """Remove a file from the repository
264 git_opt
= '--force-remove'
270 if os
.path
.exists(f
):
271 raise GitException
, '%s exists. Remove it first' %f
273 __run('git-update-index --remove --', files
)
276 __run('git-update-index --force-remove --', files
)
278 def update_cache(files
= [], force
= False):
279 """Update the cache information for the given files
281 cache_files
= __tree_status(files
)
283 # everything is up-to-date
284 if len(cache_files
) == 0:
287 # check for unresolved conflicts
288 if not force
and [x
for x
in cache_files
289 if x
[0] not in ['M', 'N', 'A', 'D']]:
290 raise GitException
, 'Updating cache failed: unresolved conflicts'
293 add_files
= [x
[1] for x
in cache_files
if x
[0] in ['N', 'A']]
294 rm_files
= [x
[1] for x
in cache_files
if x
[0] in ['D']]
295 m_files
= [x
[1] for x
in cache_files
if x
[0] in ['M']]
297 if add_files
and __run('git-update-index --add --', add_files
) != 0:
298 raise GitException
, 'Failed git-update-index --add'
299 if rm_files
and __run('git-update-index --force-remove --', rm_files
) != 0:
300 raise GitException
, 'Failed git-update-index --rm'
301 if m_files
and __run('git-update-index --', m_files
) != 0:
302 raise GitException
, 'Failed git-update-index'
306 def commit(message
, files
= [], parents
= [], allowempty
= False,
307 cache_update
= True, tree_id
= None,
308 author_name
= None, author_email
= None, author_date
= None,
309 committer_name
= None, committer_email
= None):
310 """Commit the current tree to repository
312 # Get the tree status
313 if cache_update
and parents
!= []:
314 changes
= update_cache(files
)
315 if not changes
and not allowempty
:
316 raise GitException
, 'No changes to commit'
318 # get the commit message
319 if message
[-1:] != '\n':
323 # write the index to repository
325 tree_id
= _output_one_line('git-write-tree')
332 cmd
+= 'GIT_AUTHOR_NAME="%s" ' % author_name
334 cmd
+= 'GIT_AUTHOR_EMAIL="%s" ' % author_email
336 cmd
+= 'GIT_AUTHOR_DATE="%s" ' % author_date
338 cmd
+= 'GIT_COMMITTER_NAME="%s" ' % committer_name
340 cmd
+= 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
341 cmd
+= 'git-commit-tree %s' % tree_id
347 commit_id
= _output_one_line(cmd
, message
)
349 __set_head(commit_id
)
353 def merge(base
, head1
, head2
):
354 """Perform a 3-way merge between base, head1 and head2 into the
357 if __run('git-read-tree -u -m', [base
, head1
, head2
]) != 0:
358 raise GitException
, 'git-read-tree failed (local changes maybe?)'
360 # this can fail if there are conflicts
361 if os
.system('git-merge-index -o -q gitmergeonefile.py -a') != 0:
362 raise GitException
, 'git-merge-cache failed (possible conflicts)'
364 def status(files
= [], modified
= False, new
= False, deleted
= False,
365 conflict
= False, unknown
= False, noexclude
= False):
366 """Show the tree status
368 cache_files
= __tree_status(files
, unknown
= True, noexclude
= noexclude
)
369 all
= not (modified
or new
or deleted
or conflict
or unknown
)
384 cache_files
= [x
for x
in cache_files
if x
[0] in filestat
]
386 for fs
in cache_files
:
388 print '%s %s' %
(fs
[0], fs
[1])
392 def diff(files
= [], rev1
= 'HEAD', rev2
= None, out_fd
= None):
393 """Show the diff between rev1 and rev2
397 diff_str
= _output(['git-diff-tree', '-p', rev1
, rev2
] + files
)
399 os
.system('git-update-index --refresh > /dev/null')
400 diff_str
= _output(['git-diff-index', '-p', rev1
] + files
)
403 out_fd
.write(diff_str
)
407 def diffstat(files
= [], rev1
= 'HEAD', rev2
= None):
408 """Return the diffstat between rev1 and rev2
411 p
=popen2
.Popen3('git-apply --stat')
412 diff(files
, rev1
, rev2
, p
.tochild
)
414 str = p
.fromchild
.read().rstrip()
416 raise GitException
, 'git.diffstat failed'
419 def files(rev1
, rev2
):
420 """Return the files modified between rev1 and rev2
424 for line
in _output_lines('git-diff-tree -r %s %s' %
(rev1
, rev2
)):
425 str += '%s %s\n' %
tuple(line
.rstrip().split(' ',4)[-1].split('\t',1))
429 def barefiles(rev1
, rev2
):
430 """Return the files modified between rev1 and rev2, without status info
434 for line
in _output_lines('git-diff-tree -r %s %s' %
(rev1
, rev2
)):
435 str += '%s\n' % line
.rstrip().split(' ',4)[-1].split('\t',1)[-1]
439 def checkout(files
= [], tree_id
= None, force
= False):
440 """Check out the given or all files
442 if tree_id
and __run('git-read-tree -m', [tree_id
]) != 0:
443 raise GitException
, 'Failed git-read-tree -m %s' % tree_id
445 checkout_cmd
= 'git-checkout-index -q -u'
447 checkout_cmd
+= ' -f'
449 checkout_cmd
+= ' -a'
451 checkout_cmd
+= ' --'
453 if __run(checkout_cmd
, files
) != 0:
454 raise GitException
, 'Failed git-checkout-index'
457 """Switch the tree to the given id
459 if __run('git-read-tree -u -m', [get_head(), tree_id
]) != 0:
460 raise GitException
, 'git-read-tree failed (local changes maybe?)'
464 def reset(tree_id
= None):
465 """Revert the tree changes relative to the given tree_id. It removes
471 cache_files
= __tree_status(tree_id
= tree_id
)
472 rm_files
= [x
[1] for x
in cache_files
if x
[0] in ['D']]
474 checkout(tree_id
= tree_id
, force
= True)
477 # checkout doesn't remove files
478 map(os
.remove
, rm_files
)
480 def pull(repository
= 'origin', refspec
= None):
481 """Pull changes from the remote repository. At the moment, just
482 use the 'git pull' command
488 if __run('git pull', args
) != 0:
489 raise GitException
, 'Failed "git pull %s"' % repository
491 def apply_patch(filename
= None):
492 """Apply a patch onto the current index. There must not be any
493 local changes in the tree, otherwise the command fails
495 os
.system('git-update-index --refresh > /dev/null')
498 if __run('git-apply --index', [filename
]) != 0:
499 raise GitException
, 'Patch does not apply cleanly'
501 _input('git-apply --index', sys
.stdin
)
503 def clone(repository
, local_dir
):
504 """Clone a remote repository. At the moment, just use the
507 if __run('git clone', [repository
, local_dir
]) != 0:
508 raise GitException
, 'Failed "git clone %s %s"' \
509 %
(repository
, local_dir
)