Cancel the commit cherry-picking in import
[stgit] / stgit / git.py
CommitLineData
41a6d859
CM
1"""Python GIT interface
2"""
3
4__copyright__ = """
5Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
6
7This program is free software; you can redistribute it and/or modify
8it under the terms of the GNU General Public License version 2 as
9published by the Free Software Foundation.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
15
16You should have received a copy of the GNU General Public License
17along with this program; if not, write to the Free Software
18Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19"""
20
26dba451 21import sys, os, glob, popen2
41a6d859
CM
22
23from stgit.utils import *
24
25# git exception class
26class GitException(Exception):
27 pass
28
29
30# Different start-up variables read from the environment
31if 'GIT_DIR' in os.environ:
32 base_dir = os.environ['GIT_DIR']
33else:
34 base_dir = '.git'
35
36head_link = os.path.join(base_dir, 'HEAD')
37
41a6d859
CM
38#
39# Classes
40#
41class Commit:
42 """Handle the commit objects
43 """
44 def __init__(self, id_hash):
45 self.__id_hash = id_hash
41a6d859 46
26dba451 47 lines = _output_lines('git-cat-file commit %s' % id_hash)
37a4d1bf 48 self.__parents = []
26dba451
BL
49 for i in range(len(lines)):
50 line = lines[i]
41a6d859
CM
51 if line == '\n':
52 break
53 field = line.strip().split(' ', 1)
54 if field[0] == 'tree':
55 self.__tree = field[1]
56 elif field[0] == 'parent':
37a4d1bf 57 self.__parents.append(field[1])
41a6d859
CM
58 if field[0] == 'author':
59 self.__author = field[1]
dad310d0 60 if field[0] == 'committer':
41a6d859 61 self.__committer = field[1]
26dba451 62 self.__log = ''.join(lines[i:])
41a6d859
CM
63
64 def get_id_hash(self):
65 return self.__id_hash
66
67 def get_tree(self):
68 return self.__tree
69
70 def get_parent(self):
37a4d1bf
CM
71 return self.__parents[0]
72
73 def get_parents(self):
74 return self.__parents
41a6d859
CM
75
76 def get_author(self):
77 return self.__author
78
79 def get_committer(self):
80 return self.__committer
81
37a4d1bf
CM
82 def get_log(self):
83 return self.__log
84
8e29bcd2
CM
85# dictionary of Commit objects, used to avoid multiple calls to git
86__commits = dict()
41a6d859
CM
87
88#
89# Functions
90#
8e29bcd2
CM
91def get_commit(id_hash):
92 """Commit objects factory. Save/look-up them in the __commits
93 dictionary
94 """
95 if id_hash in __commits:
96 return __commits[id_hash]
97 else:
98 commit = Commit(id_hash)
99 __commits[id_hash] = commit
100 return commit
101
41a6d859
CM
102def get_conflicts():
103 """Return the list of file conflicts
104 """
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()]
109 f.close()
110 return names
111 else:
112 return None
113
0d2cd1e4
CM
114def _input(cmd, file_desc):
115 p = popen2.Popen3(cmd)
6fe6b1bd
CM
116 while True:
117 line = file_desc.readline()
118 if not line:
119 break
0d2cd1e4
CM
120 p.tochild.write(line)
121 p.tochild.close()
122 if p.wait():
123 raise GitException, '%s failed' % str(cmd)
124
26dba451
BL
125def _output(cmd):
126 p=popen2.Popen3(cmd)
127 string = p.fromchild.read()
128 if p.wait():
129 raise GitException, '%s failed' % str(cmd)
130 return string
131
d3cf7d86 132def _output_one_line(cmd, file_desc = None):
26dba451 133 p=popen2.Popen3(cmd)
d3cf7d86
PBG
134 if file_desc != None:
135 for line in file_desc:
136 p.tochild.write(line)
137 p.tochild.close()
26dba451
BL
138 string = p.fromchild.readline().strip()
139 if p.wait():
140 raise GitException, '%s failed' % str(cmd)
41a6d859
CM
141 return string
142
26dba451
BL
143def _output_lines(cmd):
144 p=popen2.Popen3(cmd)
145 lines = p.fromchild.readlines()
146 if p.wait():
147 raise GitException, '%s failed' % str(cmd)
148 return lines
149
150def __run(cmd, args=None):
151 """__run: runs cmd using spawnvp.
152
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.
158 """
159
160 args_l=cmd.split()
161 if args is None:
162 args = []
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))])
165 if r:
166 return r
167 return 0
168
41a6d859
CM
169def __check_base_dir():
170 return os.path.isdir(base_dir)
171
be24d874
CM
172def __tree_status(files = [], tree_id = 'HEAD', unknown = False,
173 noexclude = True):
41a6d859
CM
174 """Returns a list of pairs - [status, filename]
175 """
7c09df84 176 os.system('git-update-index --refresh > /dev/null')
41a6d859
CM
177
178 cache_files = []
179
180 # unknown files
181 if unknown:
be24d874
CM
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')
186
41a6d859 187 if os.path.exists(exclude_file):
3c6fbd2c 188 extra_exclude = ['--exclude-from=%s' % exclude_file]
be24d874
CM
189 else:
190 extra_exclude = []
4d4c0e3a
PBG
191 if noexclude:
192 extra_exclude = base_exclude = []
be24d874
CM
193
194 lines = _output_lines(['git-ls-files', '--others'] + base_exclude
4d4c0e3a 195 + extra_exclude)
26dba451 196 cache_files += [('?', line.strip()) for line in lines]
41a6d859
CM
197
198 # conflicted files
199 conflicts = get_conflicts()
200 if not conflicts:
201 conflicts = []
202 cache_files += [('C', filename) for filename in conflicts]
203
204 # the rest
7c09df84 205 for line in _output_lines(['git-diff-index', '-r', tree_id] + files):
26dba451 206 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
41a6d859
CM
207 if fs[1] not in conflicts:
208 cache_files.append(fs)
41a6d859
CM
209
210 return cache_files
211
212def local_changes():
213 """Return true if there are local changes in the tree
214 """
215 return len(__tree_status()) != 0
216
217def get_head():
218 """Returns a string representing the HEAD
219 """
220 return read_string(head_link)
221
222def get_head_file():
223 """Returns the name of the file pointed to by the HEAD link
224 """
225 # valid link
226 if os.path.islink(head_link) and os.path.isfile(head_link):
227 return os.path.basename(os.readlink(head_link))
228 else:
229 raise GitException, 'Invalid .git/HEAD link. Git tree not initialised?'
230
231def __set_head(val):
232 """Sets the HEAD value
233 """
234 write_string(head_link, val)
235
d1eb3f85
CM
236def rev_parse(git_id):
237 """Parse the string and return an SHA1 id
238 """
a2dcde71 239 return _output(['git-rev-parse', git_id]).strip()
d1eb3f85 240
41a6d859
CM
241def add(names):
242 """Add the files or recursively add the directory contents
243 """
244 # generate the file list
245 files = []
246 for i in names:
247 if not os.path.exists(i):
248 raise GitException, 'Unknown file or directory: %s' % i
249
250 if os.path.isdir(i):
251 # recursive search. We only add files
252 for root, dirs, local_files in os.walk(i):
253 for name in [os.path.join(root, f) for f in local_files]:
254 if os.path.isfile(name):
255 files.append(os.path.normpath(name))
256 elif os.path.isfile(i):
257 files.append(os.path.normpath(i))
258 else:
259 raise GitException, '%s is not a file or directory' % i
260
26dba451 261 if files:
7c09df84 262 if __run('git-update-index --add --', files):
26dba451 263 raise GitException, 'Unable to add file'
41a6d859
CM
264
265def rm(files, force = False):
266 """Remove a file from the repository
267 """
268 if force:
269 git_opt = '--force-remove'
270 else:
271 git_opt = '--remove'
272
26dba451
BL
273 if not force:
274 for f in files:
275 if os.path.exists(f):
276 raise GitException, '%s exists. Remove it first' %f
277 if files:
7c09df84 278 __run('git-update-index --remove --', files)
26dba451
BL
279 else:
280 if files:
7c09df84 281 __run('git-update-index --force-remove --', files)
41a6d859 282
402ad990 283def update_cache(files = [], force = False):
cfafb945
CM
284 """Update the cache information for the given files
285 """
402ad990 286 cache_files = __tree_status(files)
26dba451 287
402ad990
CM
288 # everything is up-to-date
289 if len(cache_files) == 0:
290 return False
291
292 # check for unresolved conflicts
293 if not force and [x for x in cache_files
294 if x[0] not in ['M', 'N', 'A', 'D']]:
295 raise GitException, 'Updating cache failed: unresolved conflicts'
26dba451 296
402ad990
CM
297 # update the cache
298 add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
299 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
300 m_files = [x[1] for x in cache_files if x[0] in ['M']]
301
7c09df84
JH
302 if add_files and __run('git-update-index --add --', add_files) != 0:
303 raise GitException, 'Failed git-update-index --add'
304 if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
305 raise GitException, 'Failed git-update-index --rm'
306 if m_files and __run('git-update-index --', m_files) != 0:
307 raise GitException, 'Failed git-update-index'
402ad990
CM
308
309 return True
cfafb945 310
41a6d859 311def commit(message, files = [], parents = [], allowempty = False,
d3cf7d86 312 cache_update = True, tree_id = None,
41a6d859
CM
313 author_name = None, author_email = None, author_date = None,
314 committer_name = None, committer_email = None):
315 """Commit the current tree to repository
316 """
41a6d859 317 # Get the tree status
402ad990
CM
318 if cache_update and parents != []:
319 changes = update_cache(files)
320 if not changes and not allowempty:
321 raise GitException, 'No changes to commit'
41a6d859
CM
322
323 # get the commit message
d3cf7d86
PBG
324 if message[-1:] != '\n':
325 message += '\n'
41a6d859 326
d3cf7d86 327 must_switch = True
41a6d859 328 # write the index to repository
d3cf7d86
PBG
329 if tree_id == None:
330 tree_id = _output_one_line('git-write-tree')
331 else:
332 must_switch = False
41a6d859
CM
333
334 # the commit
335 cmd = ''
336 if author_name:
337 cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
338 if author_email:
339 cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
340 if author_date:
341 cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
342 if committer_name:
343 cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
344 if committer_email:
345 cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
346 cmd += 'git-commit-tree %s' % tree_id
347
348 # get the parents
349 for p in parents:
350 cmd += ' -p %s' % p
351
d3cf7d86
PBG
352 commit_id = _output_one_line(cmd, message)
353 if must_switch:
354 __set_head(commit_id)
41a6d859
CM
355
356 return commit_id
357
358def merge(base, head1, head2):
359 """Perform a 3-way merge between base, head1 and head2 into the
360 local tree
361 """
26dba451 362 if __run('git-read-tree -u -m', [base, head1, head2]) != 0:
41a6d859
CM
363 raise GitException, 'git-read-tree failed (local changes maybe?)'
364
365 # this can fail if there are conflicts
7c09df84 366 if os.system('git-merge-index -o -q gitmergeonefile.py -a') != 0:
41a6d859
CM
367 raise GitException, 'git-merge-cache failed (possible conflicts)'
368
41a6d859 369def status(files = [], modified = False, new = False, deleted = False,
4d4c0e3a 370 conflict = False, unknown = False, noexclude = False):
41a6d859
CM
371 """Show the tree status
372 """
4d4c0e3a 373 cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
41a6d859
CM
374 all = not (modified or new or deleted or conflict or unknown)
375
376 if not all:
377 filestat = []
378 if modified:
379 filestat.append('M')
380 if new:
7371951a 381 filestat.append('A')
41a6d859
CM
382 filestat.append('N')
383 if deleted:
384 filestat.append('D')
385 if conflict:
386 filestat.append('C')
387 if unknown:
388 filestat.append('?')
402ad990 389 cache_files = [x for x in cache_files if x[0] in filestat]
41a6d859
CM
390
391 for fs in cache_files:
392 if all:
393 print '%s %s' % (fs[0], fs[1])
394 else:
395 print '%s' % fs[1]
396
b4bddc06 397def diff(files = [], rev1 = 'HEAD', rev2 = None, out_fd = None):
41a6d859
CM
398 """Show the diff between rev1 and rev2
399 """
41a6d859
CM
400
401 if rev2:
b4bddc06 402 diff_str = _output(['git-diff-tree', '-p', rev1, rev2] + files)
41a6d859 403 else:
7c09df84
JH
404 os.system('git-update-index --refresh > /dev/null')
405 diff_str = _output(['git-diff-index', '-p', rev1] + files)
b4bddc06
CM
406
407 if out_fd:
408 out_fd.write(diff_str)
409 else:
410 return diff_str
41a6d859
CM
411
412def diffstat(files = [], rev1 = 'HEAD', rev2 = None):
413 """Return the diffstat between rev1 and rev2
414 """
41a6d859 415
26dba451
BL
416 p=popen2.Popen3('git-apply --stat')
417 diff(files, rev1, rev2, p.tochild)
418 p.tochild.close()
419 str = p.fromchild.read().rstrip()
420 if p.wait():
421 raise GitException, 'git.diffstat failed'
41a6d859
CM
422 return str
423
424def files(rev1, rev2):
425 """Return the files modified between rev1 and rev2
426 """
41a6d859
CM
427
428 str = ''
26dba451
BL
429 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
430 str += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
41a6d859
CM
431
432 return str.rstrip()
433
faed6770
PBG
434def barefiles(rev1, rev2):
435 """Return the files modified between rev1 and rev2, without status info
436 """
437
438 str = ''
439 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
440 str += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
441
442 return str.rstrip()
443
1008fbce 444def checkout(files = [], tree_id = None, force = False):
41a6d859
CM
445 """Check out the given or all files
446 """
1008fbce
CM
447 if tree_id and __run('git-read-tree -m', [tree_id]) != 0:
448 raise GitException, 'Failed git-read-tree -m %s' % tree_id
449
7c09df84 450 checkout_cmd = 'git-checkout-index -q -u'
41a6d859 451 if force:
1008fbce 452 checkout_cmd += ' -f'
41a6d859 453 if len(files) == 0:
1008fbce 454 checkout_cmd += ' -a'
41a6d859 455 else:
1008fbce 456 checkout_cmd += ' --'
41a6d859 457
1008fbce 458 if __run(checkout_cmd, files) != 0:
7c09df84 459 raise GitException, 'Failed git-checkout-index'
41a6d859
CM
460
461def switch(tree_id):
462 """Switch the tree to the given id
463 """
a5b29a1c
CM
464 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
465 raise GitException, 'git-read-tree failed (local changes maybe?)'
41a6d859 466
41a6d859
CM
467 __set_head(tree_id)
468
05d593c0
CM
469def reset(tree_id = None):
470 """Revert the tree changes relative to the given tree_id. It removes
471 any local changes
472 """
473 if not tree_id:
474 tree_id = get_head()
475
476 cache_files = __tree_status(tree_id = tree_id)
477 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
478
479 checkout(tree_id = tree_id, force = True)
480 __set_head(tree_id)
481
482 # checkout doesn't remove files
483 map(os.remove, rm_files)
484
1f5e9148
CM
485def pull(repository = 'origin', refspec = None):
486 """Pull changes from the remote repository. At the moment, just
487 use the 'git pull' command
f338c3c0 488 """
1f5e9148
CM
489 args = [repository]
490 if refspec:
491 args.append(refspec)
f338c3c0 492
ddbbfd84 493 if __run('git pull', args) != 0:
1f5e9148 494 raise GitException, 'Failed "git pull %s"' % repository
f338c3c0 495
0d2cd1e4
CM
496def apply_patch(filename = None):
497 """Apply a patch onto the current index. There must not be any
498 local changes in the tree, otherwise the command fails
499 """
7c09df84 500 os.system('git-update-index --refresh > /dev/null')
0d2cd1e4
CM
501
502 if filename:
503 if __run('git-apply --index', [filename]) != 0:
504 raise GitException, 'Patch does not apply cleanly'
505 else:
506 _input('git-apply --index', sys.stdin)
1008fbce
CM
507
508def clone(repository, local_dir):
509 """Clone a remote repository. At the moment, just use the
510 'git clone' script
511 """
512 if __run('git clone', [repository, local_dir]) != 0:
513 raise GitException, 'Failed "git clone %s %s"' \
514 % (repository, local_dir)