Print the git version when running the "stg version" command
[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
e030aa4a 21import sys, os, 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]
0618ea9c 62 self.__log = ''.join(lines[i+1:])
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 """
3237b6e4
CM
95 global __commits
96
8e29bcd2
CM
97 if id_hash in __commits:
98 return __commits[id_hash]
99 else:
100 commit = Commit(id_hash)
101 __commits[id_hash] = commit
102 return commit
103
41a6d859
CM
104def get_conflicts():
105 """Return the list of file conflicts
106 """
107 conflicts_file = os.path.join(base_dir, 'conflicts')
108 if os.path.isfile(conflicts_file):
109 f = file(conflicts_file)
110 names = [line.strip() for line in f.readlines()]
111 f.close()
112 return names
113 else:
114 return None
115
0d2cd1e4
CM
116def _input(cmd, file_desc):
117 p = popen2.Popen3(cmd)
6fe6b1bd
CM
118 while True:
119 line = file_desc.readline()
120 if not line:
121 break
0d2cd1e4
CM
122 p.tochild.write(line)
123 p.tochild.close()
124 if p.wait():
125 raise GitException, '%s failed' % str(cmd)
126
26dba451
BL
127def _output(cmd):
128 p=popen2.Popen3(cmd)
129 string = p.fromchild.read()
130 if p.wait():
131 raise GitException, '%s failed' % str(cmd)
132 return string
133
d3cf7d86 134def _output_one_line(cmd, file_desc = None):
26dba451 135 p=popen2.Popen3(cmd)
d3cf7d86
PBG
136 if file_desc != None:
137 for line in file_desc:
138 p.tochild.write(line)
139 p.tochild.close()
26dba451
BL
140 string = p.fromchild.readline().strip()
141 if p.wait():
142 raise GitException, '%s failed' % str(cmd)
41a6d859
CM
143 return string
144
26dba451
BL
145def _output_lines(cmd):
146 p=popen2.Popen3(cmd)
147 lines = p.fromchild.readlines()
148 if p.wait():
149 raise GitException, '%s failed' % str(cmd)
150 return lines
151
152def __run(cmd, args=None):
153 """__run: runs cmd using spawnvp.
154
155 Runs cmd using spawnvp. The shell is avoided so it won't mess up
156 our arguments. If args is very large, the command is run multiple
157 times; args is split xargs style: cmd is passed on each
158 invocation. Unlike xargs, returns immediately if any non-zero
159 return code is received.
160 """
161
162 args_l=cmd.split()
163 if args is None:
164 args = []
165 for i in range(0, len(args)+1, 100):
166 r=os.spawnvp(os.P_WAIT, args_l[0], args_l + args[i:min(i+100, len(args))])
167 if r:
168 return r
169 return 0
170
41a6d859
CM
171def __check_base_dir():
172 return os.path.isdir(base_dir)
173
be24d874
CM
174def __tree_status(files = [], tree_id = 'HEAD', unknown = False,
175 noexclude = True):
41a6d859
CM
176 """Returns a list of pairs - [status, filename]
177 """
7c09df84 178 os.system('git-update-index --refresh > /dev/null')
41a6d859
CM
179
180 cache_files = []
181
182 # unknown files
183 if unknown:
be24d874
CM
184 exclude_file = os.path.join(base_dir, 'info', 'exclude')
185 base_exclude = ['--exclude=%s' % s for s in
186 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
187 base_exclude.append('--exclude-per-directory=.gitignore')
188
41a6d859 189 if os.path.exists(exclude_file):
3c6fbd2c 190 extra_exclude = ['--exclude-from=%s' % exclude_file]
be24d874
CM
191 else:
192 extra_exclude = []
4d4c0e3a
PBG
193 if noexclude:
194 extra_exclude = base_exclude = []
be24d874
CM
195
196 lines = _output_lines(['git-ls-files', '--others'] + base_exclude
4d4c0e3a 197 + extra_exclude)
26dba451 198 cache_files += [('?', line.strip()) for line in lines]
41a6d859
CM
199
200 # conflicted files
201 conflicts = get_conflicts()
202 if not conflicts:
203 conflicts = []
204 cache_files += [('C', filename) for filename in conflicts]
205
206 # the rest
7c09df84 207 for line in _output_lines(['git-diff-index', '-r', tree_id] + files):
26dba451 208 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
41a6d859
CM
209 if fs[1] not in conflicts:
210 cache_files.append(fs)
41a6d859
CM
211
212 return cache_files
213
214def local_changes():
215 """Return true if there are local changes in the tree
216 """
217 return len(__tree_status()) != 0
218
aa01a285
CM
219# HEAD value cached
220__head = None
221
41a6d859 222def get_head():
3097799d 223 """Verifies the HEAD and returns the SHA1 id that represents it
41a6d859 224 """
aa01a285
CM
225 global __head
226
227 if not __head:
228 __head = rev_parse('HEAD')
229 return __head
41a6d859
CM
230
231def get_head_file():
232 """Returns the name of the file pointed to by the HEAD link
233 """
3097799d 234 return os.path.basename(_output_one_line('git-symbolic-ref HEAD'))
41a6d859 235
b99a02b0
CL
236def set_head_file(ref):
237 """Resets HEAD to point to a new ref
238 """
24eede72
CM
239 # head cache flushing is needed since we might have a different value
240 # in the new head
241 __clear_head_cache()
b99a02b0
CL
242 if __run('git-symbolic-ref HEAD', [ref]) != 0:
243 raise GitException, 'Could not set head to "%s"' % ref
244
41a6d859
CM
245def __set_head(val):
246 """Sets the HEAD value
247 """
aa01a285
CM
248 global __head
249
ba1a4550
CM
250 if not __head or __head != val:
251 if __run('git-update-ref HEAD', [val]) != 0:
252 raise GitException, 'Could not update HEAD to "%s".' % val
253 __head = val
254
255def __clear_head_cache():
256 """Sets the __head to None so that a re-read is forced
257 """
258 global __head
259
260 __head = None
41a6d859 261
d1eb3f85 262def rev_parse(git_id):
3097799d 263 """Parse the string and return a verified SHA1 id
d1eb3f85 264 """
84fcbc3b
CM
265 try:
266 return _output_one_line(['git-rev-parse', '--verify', git_id])
267 except GitException:
268 raise GitException, 'Unknown revision: %s' % git_id
d1eb3f85 269
2b4a8aa5
CL
270def branch_exists(branch):
271 """Existance check for the named branch
272 """
273 for line in _output_lines(['git-rev-parse', '--symbolic', '--all']):
274 if line.strip() == branch:
275 return True
276 return False
277
278def create_branch(new_branch, tree_id = None):
279 """Create a new branch in the git repository
280 """
281 new_head = os.path.join('refs', 'heads', new_branch)
282 if branch_exists(new_head):
283 raise GitException, 'Branch "%s" already exists' % new_branch
284
285 current_head = get_head()
286 set_head_file(new_head)
287 __set_head(current_head)
288
289 # a checkout isn't needed if new branch points to the current head
290 if tree_id:
2bc93640 291 switch(tree_id)
2b4a8aa5
CL
292
293 if os.path.isfile(os.path.join(base_dir, 'MERGE_HEAD')):
294 os.remove(os.path.join(base_dir, 'MERGE_HEAD'))
295
982b9697
CL
296def switch_branch(name):
297 """Switch to a git branch
298 """
98d6e2c5
CL
299 global __head
300
982b9697
CL
301 new_head = os.path.join('refs', 'heads', name)
302 if not branch_exists(new_head):
303 raise GitException, 'Branch "%s" does not exist' % name
304
305 tree_id = rev_parse(new_head + '^0')
306 if tree_id != get_head():
307 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
308 raise GitException, 'git-read-tree failed (local changes maybe?)'
309 __head = tree_id
310 set_head_file(new_head)
311
312 if os.path.isfile(os.path.join(base_dir, 'MERGE_HEAD')):
313 os.remove(os.path.join(base_dir, 'MERGE_HEAD'))
314
6f48e5f8
CL
315def delete_branch(name):
316 """Delete a git branch
317 """
318 branch_head = os.path.join('refs', 'heads', name)
319 if not branch_exists(branch_head):
320 raise GitException, 'Branch "%s" does not exist' % name
321 os.remove(os.path.join(base_dir, branch_head))
322
72594233
CL
323def rename_branch(from_name, to_name):
324 """Rename a git branch
325 """
c47501f9 326 from_head = os.path.join('refs', 'heads', from_name)
72594233
CL
327 if not branch_exists(from_head):
328 raise GitException, 'Branch "%s" does not exist' % from_name
c47501f9 329 to_head = os.path.join('refs', 'heads', to_name)
72594233
CL
330 if branch_exists(to_head):
331 raise GitException, 'Branch "%s" already exists' % to_name
332
333 if get_head_file() == from_name:
c47501f9
CL
334 set_head_file(to_head)
335 os.rename(os.path.join(base_dir, from_head), os.path.join(base_dir, to_head))
72594233 336
41a6d859
CM
337def add(names):
338 """Add the files or recursively add the directory contents
339 """
340 # generate the file list
341 files = []
342 for i in names:
343 if not os.path.exists(i):
344 raise GitException, 'Unknown file or directory: %s' % i
345
346 if os.path.isdir(i):
347 # recursive search. We only add files
348 for root, dirs, local_files in os.walk(i):
349 for name in [os.path.join(root, f) for f in local_files]:
350 if os.path.isfile(name):
351 files.append(os.path.normpath(name))
352 elif os.path.isfile(i):
353 files.append(os.path.normpath(i))
354 else:
355 raise GitException, '%s is not a file or directory' % i
356
26dba451 357 if files:
7c09df84 358 if __run('git-update-index --add --', files):
26dba451 359 raise GitException, 'Unable to add file'
41a6d859
CM
360
361def rm(files, force = False):
362 """Remove a file from the repository
363 """
26dba451
BL
364 if not force:
365 for f in files:
366 if os.path.exists(f):
367 raise GitException, '%s exists. Remove it first' %f
368 if files:
7c09df84 369 __run('git-update-index --remove --', files)
26dba451
BL
370 else:
371 if files:
7c09df84 372 __run('git-update-index --force-remove --', files)
41a6d859 373
402ad990 374def update_cache(files = [], force = False):
cfafb945
CM
375 """Update the cache information for the given files
376 """
402ad990 377 cache_files = __tree_status(files)
26dba451 378
402ad990
CM
379 # everything is up-to-date
380 if len(cache_files) == 0:
381 return False
382
383 # check for unresolved conflicts
384 if not force and [x for x in cache_files
385 if x[0] not in ['M', 'N', 'A', 'D']]:
386 raise GitException, 'Updating cache failed: unresolved conflicts'
26dba451 387
402ad990
CM
388 # update the cache
389 add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
390 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
391 m_files = [x[1] for x in cache_files if x[0] in ['M']]
392
7c09df84
JH
393 if add_files and __run('git-update-index --add --', add_files) != 0:
394 raise GitException, 'Failed git-update-index --add'
395 if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
396 raise GitException, 'Failed git-update-index --rm'
397 if m_files and __run('git-update-index --', m_files) != 0:
398 raise GitException, 'Failed git-update-index'
402ad990
CM
399
400 return True
cfafb945 401
41a6d859 402def commit(message, files = [], parents = [], allowempty = False,
d3cf7d86 403 cache_update = True, tree_id = None,
41a6d859
CM
404 author_name = None, author_email = None, author_date = None,
405 committer_name = None, committer_email = None):
406 """Commit the current tree to repository
407 """
41a6d859 408 # Get the tree status
402ad990
CM
409 if cache_update and parents != []:
410 changes = update_cache(files)
411 if not changes and not allowempty:
412 raise GitException, 'No changes to commit'
41a6d859
CM
413
414 # get the commit message
d3cf7d86
PBG
415 if message[-1:] != '\n':
416 message += '\n'
41a6d859 417
d3cf7d86 418 must_switch = True
41a6d859 419 # write the index to repository
d3cf7d86
PBG
420 if tree_id == None:
421 tree_id = _output_one_line('git-write-tree')
422 else:
423 must_switch = False
41a6d859
CM
424
425 # the commit
426 cmd = ''
427 if author_name:
428 cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
429 if author_email:
430 cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
431 if author_date:
432 cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
433 if committer_name:
434 cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
435 if committer_email:
436 cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
437 cmd += 'git-commit-tree %s' % tree_id
438
439 # get the parents
440 for p in parents:
441 cmd += ' -p %s' % p
442
d3cf7d86
PBG
443 commit_id = _output_one_line(cmd, message)
444 if must_switch:
445 __set_head(commit_id)
41a6d859
CM
446
447 return commit_id
448
575a7e7c
CM
449def apply_diff(rev1, rev2):
450 """Apply the diff between rev1 and rev2 onto the current
451 index. This function doesn't need to raise an exception since it
452 is only used for fast-pushing a patch. If this operation fails,
453 the pushing would fall back to the three-way merge.
454 """
455 return os.system('git-diff-tree -p %s %s | git-apply --index 2> /dev/null'
456 % (rev1, rev2)) == 0
457
41a6d859
CM
458def merge(base, head1, head2):
459 """Perform a 3-way merge between base, head1 and head2 into the
460 local tree
461 """
26dba451 462 if __run('git-read-tree -u -m', [base, head1, head2]) != 0:
41a6d859
CM
463 raise GitException, 'git-read-tree failed (local changes maybe?)'
464
465 # this can fail if there are conflicts
7c09df84 466 if os.system('git-merge-index -o -q gitmergeonefile.py -a') != 0:
547eb28f 467 raise GitException, 'git-merge-index failed (possible conflicts)'
41a6d859 468
41a6d859 469def status(files = [], modified = False, new = False, deleted = False,
4d4c0e3a 470 conflict = False, unknown = False, noexclude = False):
41a6d859
CM
471 """Show the tree status
472 """
4d4c0e3a 473 cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
41a6d859
CM
474 all = not (modified or new or deleted or conflict or unknown)
475
476 if not all:
477 filestat = []
478 if modified:
479 filestat.append('M')
480 if new:
7371951a 481 filestat.append('A')
41a6d859
CM
482 filestat.append('N')
483 if deleted:
484 filestat.append('D')
485 if conflict:
486 filestat.append('C')
487 if unknown:
488 filestat.append('?')
402ad990 489 cache_files = [x for x in cache_files if x[0] in filestat]
41a6d859
CM
490
491 for fs in cache_files:
492 if all:
493 print '%s %s' % (fs[0], fs[1])
494 else:
495 print '%s' % fs[1]
496
b4bddc06 497def diff(files = [], rev1 = 'HEAD', rev2 = None, out_fd = None):
41a6d859
CM
498 """Show the diff between rev1 and rev2
499 """
41a6d859
CM
500
501 if rev2:
b4bddc06 502 diff_str = _output(['git-diff-tree', '-p', rev1, rev2] + files)
41a6d859 503 else:
7c09df84
JH
504 os.system('git-update-index --refresh > /dev/null')
505 diff_str = _output(['git-diff-index', '-p', rev1] + files)
b4bddc06
CM
506
507 if out_fd:
508 out_fd.write(diff_str)
509 else:
510 return diff_str
41a6d859
CM
511
512def diffstat(files = [], rev1 = 'HEAD', rev2 = None):
513 """Return the diffstat between rev1 and rev2
514 """
41a6d859 515
26dba451
BL
516 p=popen2.Popen3('git-apply --stat')
517 diff(files, rev1, rev2, p.tochild)
518 p.tochild.close()
519 str = p.fromchild.read().rstrip()
520 if p.wait():
521 raise GitException, 'git.diffstat failed'
41a6d859
CM
522 return str
523
524def files(rev1, rev2):
525 """Return the files modified between rev1 and rev2
526 """
41a6d859
CM
527
528 str = ''
26dba451
BL
529 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
530 str += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
41a6d859
CM
531
532 return str.rstrip()
533
faed6770
PBG
534def barefiles(rev1, rev2):
535 """Return the files modified between rev1 and rev2, without status info
536 """
537
538 str = ''
539 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
540 str += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
541
542 return str.rstrip()
543
1008fbce 544def checkout(files = [], tree_id = None, force = False):
41a6d859
CM
545 """Check out the given or all files
546 """
1008fbce
CM
547 if tree_id and __run('git-read-tree -m', [tree_id]) != 0:
548 raise GitException, 'Failed git-read-tree -m %s' % tree_id
549
7c09df84 550 checkout_cmd = 'git-checkout-index -q -u'
41a6d859 551 if force:
1008fbce 552 checkout_cmd += ' -f'
41a6d859 553 if len(files) == 0:
1008fbce 554 checkout_cmd += ' -a'
41a6d859 555 else:
1008fbce 556 checkout_cmd += ' --'
41a6d859 557
1008fbce 558 if __run(checkout_cmd, files) != 0:
7c09df84 559 raise GitException, 'Failed git-checkout-index'
41a6d859
CM
560
561def switch(tree_id):
562 """Switch the tree to the given id
563 """
a5b29a1c
CM
564 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
565 raise GitException, 'git-read-tree failed (local changes maybe?)'
41a6d859 566
41a6d859
CM
567 __set_head(tree_id)
568
05d593c0
CM
569def reset(tree_id = None):
570 """Revert the tree changes relative to the given tree_id. It removes
571 any local changes
572 """
573 if not tree_id:
574 tree_id = get_head()
575
576 cache_files = __tree_status(tree_id = tree_id)
577 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
578
579 checkout(tree_id = tree_id, force = True)
580 __set_head(tree_id)
581
582 # checkout doesn't remove files
583 map(os.remove, rm_files)
584
1f5e9148
CM
585def pull(repository = 'origin', refspec = None):
586 """Pull changes from the remote repository. At the moment, just
587 use the 'git pull' command
f338c3c0 588 """
ba1a4550
CM
589 # 'git pull' updates the HEAD
590 __clear_head_cache()
591
1f5e9148
CM
592 args = [repository]
593 if refspec:
594 args.append(refspec)
f338c3c0 595
ddbbfd84 596 if __run('git pull', args) != 0:
1f5e9148 597 raise GitException, 'Failed "git pull %s"' % repository
f338c3c0 598
84fcbc3b
CM
599def apply_patch(filename = None, base = None):
600 """Apply a patch onto the current or given index. There must not
601 be any local changes in the tree, otherwise the command fails
0d2cd1e4 602 """
84fcbc3b
CM
603 def __apply_patch():
604 if filename:
605 return __run('git-apply --index', [filename]) == 0
606 else:
607 try:
608 _input('git-apply --index', sys.stdin)
609 except GitException:
610 return False
611 return True
612
7c09df84 613 os.system('git-update-index --refresh > /dev/null')
0d2cd1e4 614
84fcbc3b
CM
615 if base:
616 orig_head = get_head()
617 switch(base)
618
619 if not __apply_patch():
620 if base:
621 switch(orig_head)
622 raise GitException, 'Patch does not apply cleanly'
623 elif base:
624 top = commit(message = 'temporary commit used for applying a patch',
625 parents = [base])
626 switch(orig_head)
627 merge(base, orig_head, top)
1008fbce
CM
628
629def clone(repository, local_dir):
630 """Clone a remote repository. At the moment, just use the
631 'git clone' script
632 """
633 if __run('git clone', [repository, local_dir]) != 0:
634 raise GitException, 'Failed "git clone %s %s"' \
635 % (repository, local_dir)