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
:
101 raise GitException
, '%s failed' % str
(cmd
)
105 string
= p
.fromchild
.read()
107 raise GitException
, '%s failed' % str
(cmd
)
110 def _output_one_line(cmd
):
112 string
= p
.fromchild
.readline().strip()
114 raise GitException
, '%s failed' % str
(cmd
)
117 def _output_lines(cmd
):
119 lines
= p
.fromchild
.readlines()
121 raise GitException
, '%s failed' % str
(cmd
)
124 def __run(cmd
, args
=None):
125 """__run: runs cmd using spawnvp.
127 Runs cmd using spawnvp. The shell is avoided so it won't mess up
128 our arguments. If args is very large, the command is run multiple
129 times; args is split xargs style: cmd is passed on each
130 invocation. Unlike xargs, returns immediately if any non-zero
131 return code is received.
137 for i
in range(0, len(args
)+1, 100):
138 r
=os
.spawnvp(os
.P_WAIT
, args_l
[0], args_l
+ args
[i
:min(i
+100, len(args
))])
143 def __check_base_dir():
144 return os
.path
.isdir(base_dir
)
146 def __tree_status(files
= [], tree_id
= 'HEAD', unknown
= False):
147 """Returns a list of pairs - [status, filename]
149 os
.system('git-update-cache --refresh > /dev/null')
155 exclude_file
= os
.path
.join(base_dir
, 'exclude')
157 if os
.path
.exists(exclude_file
):
158 extra_exclude
.append('--exclude-from=%s' % exclude_file
)
159 lines
= _output_lines(['git-ls-files', '--others',
160 '--exclude=*.[ao]', '--exclude=.*'
161 '--exclude=TAGS', '--exclude=tags', '--exclude=*~',
162 '--exclude=#*'] + extra_exclude
)
163 cache_files
+= [('?', line
.strip()) for line
in lines
]
166 conflicts
= get_conflicts()
169 cache_files
+= [('C', filename
) for filename
in conflicts
]
172 for line
in _output_lines(['git-diff-cache', '-r', tree_id
] + files
):
173 fs
= tuple(line
.rstrip().split(' ',4)[-1].split('\t',1))
174 if fs
[1] not in conflicts
:
175 cache_files
.append(fs
)
180 """Return true if there are local changes in the tree
182 return len(__tree_status()) != 0
185 """Returns a string representing the HEAD
187 return read_string(head_link
)
190 """Returns the name of the file pointed to by the HEAD link
193 if os
.path
.islink(head_link
) and os
.path
.isfile(head_link
):
194 return os
.path
.basename(os
.readlink(head_link
))
196 raise GitException
, 'Invalid .git/HEAD link. Git tree not initialised?'
199 """Sets the HEAD value
201 write_string(head_link
, val
)
204 """Add the files or recursively add the directory contents
206 # generate the file list
209 if not os
.path
.exists(i
):
210 raise GitException
, 'Unknown file or directory: %s' % i
213 # recursive search. We only add files
214 for root
, dirs
, local_files
in os
.walk(i
):
215 for name
in [os
.path
.join(root
, f
) for f
in local_files
]:
216 if os
.path
.isfile(name
):
217 files
.append(os
.path
.normpath(name
))
218 elif os
.path
.isfile(i
):
219 files
.append(os
.path
.normpath(i
))
221 raise GitException
, '%s is not a file or directory' % i
224 if __run('git-update-cache --add --', files
):
225 raise GitException
, 'Unable to add file'
227 def rm(files
, force
= False):
228 """Remove a file from the repository
231 git_opt
= '--force-remove'
237 if os
.path
.exists(f
):
238 raise GitException
, '%s exists. Remove it first' %f
240 __run('git-update-cache --remove --', files
)
243 __run('git-update-cache --force-remove --', files
)
245 def update_cache(files
):
246 """Update the cache information for the given files
252 if os
.path
.exists(f
):
258 __run('git-update-cache --', files_here
)
260 __run('git-update-cache --remove --', files_gone
)
262 def commit(message
, files
= [], parents
= [], allowempty
= False,
263 author_name
= None, author_email
= None, author_date
= None,
264 committer_name
= None, committer_email
= None):
265 """Commit the current tree to repository
267 first
= (parents
== [])
269 # Get the tree status
271 cache_files
= __tree_status(files
)
273 if not first
and len(cache_files
) == 0 and not allowempty
:
274 raise GitException
, 'No changes to commit'
276 # check for unresolved conflicts
277 if not first
and len(filter(lambda x
: x
[0] not in ['M', 'N', 'A', 'D'],
279 raise GitException
, 'Commit failed: unresolved conflicts'
281 # get the commit message
282 f
= file('.commitmsg', 'w+')
283 if message
[-1] == '\n':
294 for f
in cache_files
:
295 if f
[0] in ['N', 'A']:
296 add_files
.append(f
[1])
298 rm_files
.append(f
[1])
303 if __run('git-update-cache --add --', add_files
):
304 raise GitException
, 'Failed git-update-cache --add'
306 if __run('git-update-cache --force-remove --', rm_files
):
307 raise GitException
, 'Failed git-update-cache --rm'
309 if __run('git-update-cache --', m_files
):
310 raise GitException
, 'Failed git-update-cache'
312 # write the index to repository
313 tree_id
= _output_one_line('git-write-tree')
318 cmd
+= 'GIT_AUTHOR_NAME="%s" ' % author_name
320 cmd
+= 'GIT_AUTHOR_EMAIL="%s" ' % author_email
322 cmd
+= 'GIT_AUTHOR_DATE="%s" ' % author_date
324 cmd
+= 'GIT_COMMITTER_NAME="%s" ' % committer_name
326 cmd
+= 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
327 cmd
+= 'git-commit-tree %s' % tree_id
333 cmd
+= ' < .commitmsg'
335 commit_id
= _output_one_line(cmd
)
336 __set_head(commit_id
)
337 os
.remove('.commitmsg')
341 def merge(base
, head1
, head2
):
342 """Perform a 3-way merge between base, head1 and head2 into the
345 if __run('git-read-tree -u -m', [base
, head1
, head2
]) != 0:
346 raise GitException
, 'git-read-tree failed (local changes maybe?)'
348 # this can fail if there are conflicts
349 if os
.system('git-merge-cache -o gitmergeonefile.py -a') != 0:
350 raise GitException
, 'git-merge-cache failed (possible conflicts)'
352 # this should not fail
353 if os
.system('git-checkout-cache -f -a') != 0:
354 raise GitException
, 'Failed git-checkout-cache'
356 def status(files
= [], modified
= False, new
= False, deleted
= False,
357 conflict
= False, unknown
= False):
358 """Show the tree status
360 cache_files
= __tree_status(files
, unknown
= True)
361 all
= not (modified
or new
or deleted
or conflict
or unknown
)
376 cache_files
= filter(lambda x
: x
[0] in filestat
, cache_files
)
378 for fs
in cache_files
:
380 print '%s %s' %
(fs
[0], fs
[1])
384 def diff(files
= [], rev1
= 'HEAD', rev2
= None, out_fd
= None):
385 """Show the diff between rev1 and rev2
387 os
.system('git-update-cache --refresh > /dev/null')
390 diff_str
= _output(['git-diff-tree', '-p', rev1
, rev2
] + files
)
392 diff_str
= _output(['git-diff-cache', '-p', rev1
] + files
)
395 out_fd
.write(diff_str
)
399 def diffstat(files
= [], rev1
= 'HEAD', rev2
= None):
400 """Return the diffstat between rev1 and rev2
403 os
.system('git-update-cache --refresh > /dev/null')
404 p
=popen2
.Popen3('git-apply --stat')
405 diff(files
, rev1
, rev2
, p
.tochild
)
407 str = p
.fromchild
.read().rstrip()
409 raise GitException
, 'git.diffstat failed'
412 def files(rev1
, rev2
):
413 """Return the files modified between rev1 and rev2
415 os
.system('git-update-cache --refresh > /dev/null')
418 for line
in _output_lines('git-diff-tree -r %s %s' %
(rev1
, rev2
)):
419 str += '%s %s\n' %
tuple(line
.rstrip().split(' ',4)[-1].split('\t',1))
423 def checkout(files
= [], tree_id
= None, force
= False):
424 """Check out the given or all files
426 if tree_id
and __run('git-read-tree -m', [tree_id
]) != 0:
427 raise GitException
, 'Failed git-read-tree -m %s' % tree_id
429 checkout_cmd
= 'git-checkout-cache -q -u'
431 checkout_cmd
+= ' -f'
433 checkout_cmd
+= ' -a'
435 checkout_cmd
+= ' --'
437 if __run(checkout_cmd
, files
) != 0:
438 raise GitException
, 'Failed git-checkout-cache'
441 """Switch the tree to the given id
443 to_delete
= filter(lambda x
: x
[0] in ['N', 'A'],
444 __tree_status(tree_id
= tree_id
))
446 checkout(tree_id
= tree_id
, force
= True)
449 # checkout doesn't remove files
453 def fetch(location
, head
= None, tag
= None):
454 """Fetch changes from the remote repository. At the moment, just
455 use the 'git fetch' scripts
463 if __run('git fetch', args
) != 0:
464 raise GitException
, 'Failed "git fetch %s"' % location
466 return read_string(os
.path
.join(base_dir
, 'FETCH_HEAD'))
468 def apply_patch(filename
= None):
469 """Apply a patch onto the current index. There must not be any
470 local changes in the tree, otherwise the command fails
472 os
.system('git-update-cache --refresh > /dev/null')
475 if __run('git-apply --index', [filename
]) != 0:
476 raise GitException
, 'Patch does not apply cleanly'
478 _input('git-apply --index', sys
.stdin
)
480 def clone(repository
, local_dir
):
481 """Clone a remote repository. At the moment, just use the
484 if __run('git clone', [repository
, local_dir
]) != 0:
485 raise GitException
, 'Failed "git clone %s %s"' \
486 %
(repository
, local_dir
)