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
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')
43 """Handle the commit objects
45 def __init__(self
, id_hash
):
46 self
.__id_hash
= id_hash
47 f
= os
.popen('git-cat-file commit %s' % id_hash
, 'r')
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]
64 raise GitException
, 'Unknown commit id'
66 def get_id_hash(self
):
78 def get_committer(self
):
79 return self
.__committer
86 """Return the list of file conflicts
88 conflicts_file
= os
.path
.join(base_dir
, 'conflicts')
89 if os
.path
.isfile(conflicts_file
):
90 f
= file(conflicts_file
)
91 names
= [line
.strip() for line
in f
.readlines()]
98 f
= os
.popen(cmd
, 'r')
99 string
= f
.readline().strip()
101 raise GitException
, '%s failed' % cmd
104 def __check_base_dir():
105 return os
.path
.isdir(base_dir
)
107 def __tree_status(files
= [], tree_id
= 'HEAD', unknown
= False):
108 """Returns a list of pairs - [status, filename]
110 os
.system('git-update-cache --refresh > /dev/null')
116 exclude_file
= os
.path
.join(base_dir
, 'exclude')
118 if os
.path
.exists(exclude_file
):
119 extra_exclude
+= ' --exclude-from=%s' % exclude_file
120 fout
= os
.popen('git-ls-files --others'
121 ' --exclude="*.[ao]" --exclude=".*"'
122 ' --exclude=TAGS --exclude=tags --exclude="*~"'
123 ' --exclude="#*"' + extra_exclude
, 'r')
124 cache_files
+= [('?', line
.strip()) for line
in fout
]
127 conflicts
= get_conflicts()
130 cache_files
+= [('C', filename
) for filename
in conflicts
]
133 files_str
= reduce(lambda x
, y
: x
+ ' ' + y
, files
, '')
134 fout
= os
.popen('git-diff-cache -r %s %s' %
(tree_id
, files_str
), 'r')
136 fs
= tuple(line
.split()[4:])
137 if fs
[1] not in conflicts
:
138 cache_files
.append(fs
)
140 raise GitException
, 'git-diff-cache failed'
145 """Return true if there are local changes in the tree
147 return len(__tree_status()) != 0
150 """Returns a string representing the HEAD
152 return read_string(head_link
)
155 """Returns the name of the file pointed to by the HEAD link
158 if os
.path
.islink(head_link
) and os
.path
.isfile(head_link
):
159 return os
.path
.basename(os
.readlink(head_link
))
161 raise GitException
, 'Invalid .git/HEAD link. Git tree not initialised?'
164 """Sets the HEAD value
166 write_string(head_link
, val
)
169 """Add the files or recursively add the directory contents
171 # generate the file list
174 if not os
.path
.exists(i
):
175 raise GitException
, 'Unknown file or directory: %s' % i
178 # recursive search. We only add files
179 for root
, dirs
, local_files
in os
.walk(i
):
180 for name
in [os
.path
.join(root
, f
) for f
in local_files
]:
181 if os
.path
.isfile(name
):
182 files
.append(os
.path
.normpath(name
))
183 elif os
.path
.isfile(i
):
184 files
.append(os
.path
.normpath(i
))
186 raise GitException
, '%s is not a file or directory' % i
189 print 'Adding file %s' % f
190 if os
.system('git-update-cache --add -- %s' % f
) != 0:
191 raise GitException
, 'Unable to add %s' % f
193 def rm(files
, force
= False):
194 """Remove a file from the repository
197 git_opt
= '--force-remove'
203 print 'Removing file %s' % f
204 if os
.system('git-update-cache --force-remove -- %s' % f
) != 0:
205 raise GitException
, 'Unable to remove %s' % f
206 elif os
.path
.exists(f
):
207 raise GitException
, '%s exists. Remove it first' %f
209 print 'Removing file %s' % f
210 if os
.system('git-update-cache --remove -- %s' % f
) != 0:
211 raise GitException
, 'Unable to remove %s' % f
213 def update_cache(files
):
214 """Update the cache information for the given files
217 if os
.path
.exists(f
):
218 os
.system('git-update-cache -- %s' % f
)
220 os
.system('git-update-cache --remove -- %s' % f
)
222 def commit(message
, files
= [], parents
= [], allowempty
= False,
223 author_name
= None, author_email
= None, author_date
= None,
224 committer_name
= None, committer_email
= None):
225 """Commit the current tree to repository
227 first
= (parents
== [])
229 # Get the tree status
231 cache_files
= __tree_status(files
)
233 if not first
and len(cache_files
) == 0 and not allowempty
:
234 raise GitException
, 'No changes to commit'
236 # check for unresolved conflicts
237 if not first
and len(filter(lambda x
: x
[0] not in ['M', 'N', 'D'],
239 raise GitException
, 'Commit failed: unresolved conflicts'
241 # get the commit message
242 f
= file('.commitmsg', 'w+')
243 if message
[-1] == '\n':
251 for f
in cache_files
:
255 git_flag
= '--force-remove'
259 if os
.system('git-update-cache %s %s' %
(git_flag
, f
[1])) != 0:
260 raise GitException
, 'Failed git-update-cache -- %s' % f
[1]
262 # write the index to repository
263 tree_id
= __output('git-write-tree')
268 cmd
+= 'GIT_AUTHOR_NAME="%s" ' % author_name
270 cmd
+= 'GIT_AUTHOR_EMAIL="%s" ' % author_email
272 cmd
+= 'GIT_AUTHOR_DATE="%s" ' % author_date
274 cmd
+= 'GIT_COMMITTER_NAME="%s" ' % committer_name
276 cmd
+= 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
277 cmd
+= 'git-commit-tree %s' % tree_id
283 cmd
+= ' < .commitmsg'
285 commit_id
= __output(cmd
)
286 __set_head(commit_id
)
287 os
.remove('.commitmsg')
291 def merge(base
, head1
, head2
):
292 """Perform a 3-way merge between base, head1 and head2 into the
295 if os
.system('git-read-tree -u -m %s %s %s' %
(base
, head1
, head2
)) != 0:
296 raise GitException
, 'git-read-tree failed (local changes maybe?)'
298 # this can fail if there are conflicts
299 if os
.system('git-merge-cache -o gitmergeonefile.py -a') != 0:
300 raise GitException
, 'git-merge-cache failed (possible conflicts)'
302 # this should not fail
303 if os
.system('git-checkout-cache -f -a') != 0:
304 raise GitException
, 'Failed git-checkout-cache'
306 def status(files
= [], modified
= False, new
= False, deleted
= False,
307 conflict
= False, unknown
= False):
308 """Show the tree status
310 cache_files
= __tree_status(files
, unknown
= True)
311 all
= not (modified
or new
or deleted
or conflict
or unknown
)
325 cache_files
= filter(lambda x
: x
[0] in filestat
, cache_files
)
327 for fs
in cache_files
:
329 print '%s %s' %
(fs
[0], fs
[1])
333 def diff(files
= [], rev1
= 'HEAD', rev2
= None, output
= None,
335 """Show the diff between rev1 and rev2
337 files_str
= reduce(lambda x
, y
: x
+ ' ' + y
, files
, '')
342 extra_args
+= ' >> %s' % output
344 extra_args
+= ' > %s' % output
346 os
.system('git-update-cache --refresh > /dev/null')
349 if os
.system('git-diff-tree -p %s %s %s %s'
350 %
(rev1
, rev2
, files_str
, extra_args
)) != 0:
351 raise GitException
, 'git-diff-tree failed'
353 if os
.system('git-diff-cache -p %s %s %s'
354 %
(rev1
, files_str
, extra_args
)) != 0:
355 raise GitException
, 'git-diff-cache failed'
357 def diffstat(files
= [], rev1
= 'HEAD', rev2
= None):
358 """Return the diffstat between rev1 and rev2
360 files_str
= reduce(lambda x
, y
: x
+ ' ' + y
, files
, '')
362 os
.system('git-update-cache --refresh > /dev/null')
363 ds_cmd
= '| git-apply --stat'
366 f
= os
.popen('git-diff-tree -p %s %s %s %s'
367 %
(rev1
, rev2
, files_str
, ds_cmd
), 'r')
368 str = f
.read().rstrip()
370 raise GitException
, 'git-diff-tree failed'
372 f
= os
.popen('git-diff-cache -p %s %s %s'
373 %
(rev1
, files_str
, ds_cmd
), 'r')
374 str = f
.read().rstrip()
376 raise GitException
, 'git-diff-cache failed'
380 def files(rev1
, rev2
):
381 """Return the files modified between rev1 and rev2
383 os
.system('git-update-cache --refresh > /dev/null')
386 f
= os
.popen('git-diff-tree -r %s %s' %
(rev1
, rev2
),
389 str += '%s %s\n' %
tuple(line
.split()[4:])
391 raise GitException
, 'git-diff-tree failed'
395 def checkout(files
= [], force
= False):
396 """Check out the given or all files
404 git_flags
+= reduce(lambda x
, y
: x
+ ' ' + y
, files
, ' --')
406 if os
.system('git-checkout-cache -q -u%s' % git_flags
) != 0:
407 raise GitException
, 'Failed git-checkout-cache -q -u%s' % git_flags
410 """Switch the tree to the given id
412 to_delete
= filter(lambda x
: x
[0] == 'N', __tree_status(tree_id
= tree_id
))
414 if os
.system('git-read-tree -m %s' % tree_id
) != 0:
415 raise GitException
, 'Failed git-read-tree -m %s' % tree_id
417 checkout(force
= True)
420 # checkout doesn't remove files