Fix the push_patch function to not always call refresh_patch
[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
BL
47 lines = _output_lines('git-cat-file commit %s' % id_hash)
48 for i in range(len(lines)):
49 line = lines[i]
41a6d859
CM
50 if line == '\n':
51 break
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]
26dba451 61 self.__log = ''.join(lines[i:])
41a6d859
CM
62
63 def get_id_hash(self):
64 return self.__id_hash
65
66 def get_tree(self):
67 return self.__tree
68
69 def get_parent(self):
70 return self.__parent
71
72 def get_author(self):
73 return self.__author
74
75 def get_committer(self):
76 return self.__committer
77
8e29bcd2
CM
78# dictionary of Commit objects, used to avoid multiple calls to git
79__commits = dict()
41a6d859
CM
80
81#
82# Functions
83#
8e29bcd2
CM
84def get_commit(id_hash):
85 """Commit objects factory. Save/look-up them in the __commits
86 dictionary
87 """
88 if id_hash in __commits:
89 return __commits[id_hash]
90 else:
91 commit = Commit(id_hash)
92 __commits[id_hash] = commit
93 return commit
94
41a6d859
CM
95def get_conflicts():
96 """Return the list of file conflicts
97 """
98 conflicts_file = os.path.join(base_dir, 'conflicts')
99 if os.path.isfile(conflicts_file):
100 f = file(conflicts_file)
101 names = [line.strip() for line in f.readlines()]
102 f.close()
103 return names
104 else:
105 return None
106
0d2cd1e4
CM
107def _input(cmd, file_desc):
108 p = popen2.Popen3(cmd)
109 for line in file_desc:
0d2cd1e4
CM
110 p.tochild.write(line)
111 p.tochild.close()
112 if p.wait():
113 raise GitException, '%s failed' % str(cmd)
114
26dba451
BL
115def _output(cmd):
116 p=popen2.Popen3(cmd)
117 string = p.fromchild.read()
118 if p.wait():
119 raise GitException, '%s failed' % str(cmd)
120 return string
121
122def _output_one_line(cmd):
123 p=popen2.Popen3(cmd)
124 string = p.fromchild.readline().strip()
125 if p.wait():
126 raise GitException, '%s failed' % str(cmd)
41a6d859
CM
127 return string
128
26dba451
BL
129def _output_lines(cmd):
130 p=popen2.Popen3(cmd)
131 lines = p.fromchild.readlines()
132 if p.wait():
133 raise GitException, '%s failed' % str(cmd)
134 return lines
135
136def __run(cmd, args=None):
137 """__run: runs cmd using spawnvp.
138
139 Runs cmd using spawnvp. The shell is avoided so it won't mess up
140 our arguments. If args is very large, the command is run multiple
141 times; args is split xargs style: cmd is passed on each
142 invocation. Unlike xargs, returns immediately if any non-zero
143 return code is received.
144 """
145
146 args_l=cmd.split()
147 if args is None:
148 args = []
149 for i in range(0, len(args)+1, 100):
150 r=os.spawnvp(os.P_WAIT, args_l[0], args_l + args[i:min(i+100, len(args))])
151 if r:
152 return r
153 return 0
154
41a6d859
CM
155def __check_base_dir():
156 return os.path.isdir(base_dir)
157
158def __tree_status(files = [], tree_id = 'HEAD', unknown = False):
159 """Returns a list of pairs - [status, filename]
160 """
161 os.system('git-update-cache --refresh > /dev/null')
162
163 cache_files = []
164
165 # unknown files
166 if unknown:
167 exclude_file = os.path.join(base_dir, 'exclude')
26dba451 168 extra_exclude = []
41a6d859 169 if os.path.exists(exclude_file):
26dba451
BL
170 extra_exclude.append('--exclude-from=%s' % exclude_file)
171 lines = _output_lines(['git-ls-files', '--others',
172 '--exclude=*.[ao]', '--exclude=.*'
173 '--exclude=TAGS', '--exclude=tags', '--exclude=*~',
174 '--exclude=#*'] + extra_exclude)
175 cache_files += [('?', line.strip()) for line in lines]
41a6d859
CM
176
177 # conflicted files
178 conflicts = get_conflicts()
179 if not conflicts:
180 conflicts = []
181 cache_files += [('C', filename) for filename in conflicts]
182
183 # the rest
26dba451
BL
184 for line in _output_lines(['git-diff-cache', '-r', tree_id] + files):
185 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
41a6d859
CM
186 if fs[1] not in conflicts:
187 cache_files.append(fs)
41a6d859
CM
188
189 return cache_files
190
191def local_changes():
192 """Return true if there are local changes in the tree
193 """
194 return len(__tree_status()) != 0
195
196def get_head():
197 """Returns a string representing the HEAD
198 """
199 return read_string(head_link)
200
201def get_head_file():
202 """Returns the name of the file pointed to by the HEAD link
203 """
204 # valid link
205 if os.path.islink(head_link) and os.path.isfile(head_link):
206 return os.path.basename(os.readlink(head_link))
207 else:
208 raise GitException, 'Invalid .git/HEAD link. Git tree not initialised?'
209
210def __set_head(val):
211 """Sets the HEAD value
212 """
213 write_string(head_link, val)
214
215def add(names):
216 """Add the files or recursively add the directory contents
217 """
218 # generate the file list
219 files = []
220 for i in names:
221 if not os.path.exists(i):
222 raise GitException, 'Unknown file or directory: %s' % i
223
224 if os.path.isdir(i):
225 # recursive search. We only add files
226 for root, dirs, local_files in os.walk(i):
227 for name in [os.path.join(root, f) for f in local_files]:
228 if os.path.isfile(name):
229 files.append(os.path.normpath(name))
230 elif os.path.isfile(i):
231 files.append(os.path.normpath(i))
232 else:
233 raise GitException, '%s is not a file or directory' % i
234
26dba451
BL
235 if files:
236 if __run('git-update-cache --add --', files):
237 raise GitException, 'Unable to add file'
41a6d859
CM
238
239def rm(files, force = False):
240 """Remove a file from the repository
241 """
242 if force:
243 git_opt = '--force-remove'
244 else:
245 git_opt = '--remove'
246
26dba451
BL
247 if not force:
248 for f in files:
249 if os.path.exists(f):
250 raise GitException, '%s exists. Remove it first' %f
251 if files:
252 __run('git-update-cache --remove --', files)
253 else:
254 if files:
255 __run('git-update-cache --force-remove --', files)
41a6d859 256
cfafb945
CM
257def update_cache(files):
258 """Update the cache information for the given files
259 """
26dba451
BL
260 files_here = []
261 files_gone = []
262
cfafb945
CM
263 for f in files:
264 if os.path.exists(f):
26dba451 265 files_here.append(f)
cfafb945 266 else:
26dba451
BL
267 files_gone.append(f)
268
269 if files_here:
270 __run('git-update-cache --', files_here)
271 if files_gone:
272 __run('git-update-cache --remove --', files_gone)
cfafb945 273
41a6d859
CM
274def commit(message, files = [], parents = [], allowempty = False,
275 author_name = None, author_email = None, author_date = None,
276 committer_name = None, committer_email = None):
277 """Commit the current tree to repository
278 """
279 first = (parents == [])
280
281 # Get the tree status
282 if not first:
283 cache_files = __tree_status(files)
284
285 if not first and len(cache_files) == 0 and not allowempty:
286 raise GitException, 'No changes to commit'
287
288 # check for unresolved conflicts
c4a1503a 289 if not first and len(filter(lambda x: x[0] not in ['M', 'N', 'A', 'D'],
41a6d859
CM
290 cache_files)) != 0:
291 raise GitException, 'Commit failed: unresolved conflicts'
292
293 # get the commit message
294 f = file('.commitmsg', 'w+')
295 if message[-1] == '\n':
296 f.write(message)
297 else:
298 print >> f, message
299 f.close()
300
301 # update the cache
302 if not first:
26dba451
BL
303 add_files=[]
304 rm_files=[]
305 m_files=[]
41a6d859 306 for f in cache_files:
c4a1503a 307 if f[0] in ['N', 'A']:
26dba451 308 add_files.append(f[1])
41a6d859 309 elif f[0] == 'D':
26dba451 310 rm_files.append(f[1])
41a6d859 311 else:
26dba451
BL
312 m_files.append(f[1])
313
314 if add_files:
315 if __run('git-update-cache --add --', add_files):
316 raise GitException, 'Failed git-update-cache --add'
317 if rm_files:
318 if __run('git-update-cache --force-remove --', rm_files):
319 raise GitException, 'Failed git-update-cache --rm'
320 if m_files:
321 if __run('git-update-cache --', m_files):
322 raise GitException, 'Failed git-update-cache'
41a6d859
CM
323
324 # write the index to repository
26dba451 325 tree_id = _output_one_line('git-write-tree')
41a6d859
CM
326
327 # the commit
328 cmd = ''
329 if author_name:
330 cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
331 if author_email:
332 cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
333 if author_date:
334 cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
335 if committer_name:
336 cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
337 if committer_email:
338 cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
339 cmd += 'git-commit-tree %s' % tree_id
340
341 # get the parents
342 for p in parents:
343 cmd += ' -p %s' % p
344
345 cmd += ' < .commitmsg'
346
26dba451 347 commit_id = _output_one_line(cmd)
41a6d859
CM
348 __set_head(commit_id)
349 os.remove('.commitmsg')
350
351 return commit_id
352
353def merge(base, head1, head2):
354 """Perform a 3-way merge between base, head1 and head2 into the
355 local tree
356 """
26dba451 357 if __run('git-read-tree -u -m', [base, head1, head2]) != 0:
41a6d859
CM
358 raise GitException, 'git-read-tree failed (local changes maybe?)'
359
360 # this can fail if there are conflicts
361 if os.system('git-merge-cache -o gitmergeonefile.py -a') != 0:
362 raise GitException, 'git-merge-cache failed (possible conflicts)'
363
364 # this should not fail
365 if os.system('git-checkout-cache -f -a') != 0:
366 raise GitException, 'Failed git-checkout-cache'
367
368def status(files = [], modified = False, new = False, deleted = False,
369 conflict = False, unknown = False):
370 """Show the tree status
371 """
372 cache_files = __tree_status(files, unknown = True)
373 all = not (modified or new or deleted or conflict or unknown)
374
375 if not all:
376 filestat = []
377 if modified:
378 filestat.append('M')
379 if new:
7371951a 380 filestat.append('A')
41a6d859
CM
381 filestat.append('N')
382 if deleted:
383 filestat.append('D')
384 if conflict:
385 filestat.append('C')
386 if unknown:
387 filestat.append('?')
388 cache_files = filter(lambda x: x[0] in filestat, cache_files)
389
390 for fs in cache_files:
391 if all:
392 print '%s %s' % (fs[0], fs[1])
393 else:
394 print '%s' % fs[1]
395
b4bddc06 396def diff(files = [], rev1 = 'HEAD', rev2 = None, out_fd = None):
41a6d859
CM
397 """Show the diff between rev1 and rev2
398 """
41a6d859
CM
399 os.system('git-update-cache --refresh > /dev/null')
400
401 if rev2:
b4bddc06 402 diff_str = _output(['git-diff-tree', '-p', rev1, rev2] + files)
41a6d859 403 else:
b4bddc06
CM
404 diff_str = _output(['git-diff-cache', '-p', rev1] + files)
405
406 if out_fd:
407 out_fd.write(diff_str)
408 else:
409 return diff_str
41a6d859
CM
410
411def diffstat(files = [], rev1 = 'HEAD', rev2 = None):
412 """Return the diffstat between rev1 and rev2
413 """
41a6d859
CM
414
415 os.system('git-update-cache --refresh > /dev/null')
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 """
427 os.system('git-update-cache --refresh > /dev/null')
428
429 str = ''
26dba451
BL
430 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
431 str += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
41a6d859
CM
432
433 return str.rstrip()
434
1008fbce 435def checkout(files = [], tree_id = None, force = False):
41a6d859
CM
436 """Check out the given or all files
437 """
1008fbce
CM
438 if tree_id and __run('git-read-tree -m', [tree_id]) != 0:
439 raise GitException, 'Failed git-read-tree -m %s' % tree_id
440
441 checkout_cmd = 'git-checkout-cache -q -u'
41a6d859 442 if force:
1008fbce 443 checkout_cmd += ' -f'
41a6d859 444 if len(files) == 0:
1008fbce 445 checkout_cmd += ' -a'
41a6d859 446 else:
1008fbce 447 checkout_cmd += ' --'
41a6d859 448
1008fbce 449 if __run(checkout_cmd, files) != 0:
26dba451 450 raise GitException, 'Failed git-checkout-cache'
41a6d859
CM
451
452def switch(tree_id):
453 """Switch the tree to the given id
454 """
c4a1503a
PO
455 to_delete = filter(lambda x: x[0] in ['N', 'A'],
456 __tree_status(tree_id = tree_id))
41a6d859 457
1008fbce 458 checkout(tree_id = tree_id, force = True)
41a6d859
CM
459 __set_head(tree_id)
460
461 # checkout doesn't remove files
462 for fs in to_delete:
463 os.remove(fs[1])
f338c3c0 464
ddbbfd84 465def pull(location, head = None, tag = None):
f338c3c0
CM
466 """Fetch changes from the remote repository. At the moment, just
467 use the 'git fetch' scripts
468 """
469 args = [location]
470 if head:
471 args += [head]
472 elif tag:
473 args += ['tag', tag]
474
ddbbfd84 475 if __run('git pull', args) != 0:
f338c3c0
CM
476 raise GitException, 'Failed "git fetch %s"' % location
477
0d2cd1e4
CM
478def apply_patch(filename = None):
479 """Apply a patch onto the current index. There must not be any
480 local changes in the tree, otherwise the command fails
481 """
482 os.system('git-update-cache --refresh > /dev/null')
483
484 if filename:
485 if __run('git-apply --index', [filename]) != 0:
486 raise GitException, 'Patch does not apply cleanly'
487 else:
488 _input('git-apply --index', sys.stdin)
1008fbce
CM
489
490def clone(repository, local_dir):
491 """Clone a remote repository. At the moment, just use the
492 'git clone' script
493 """
494 if __run('git clone', [repository, local_dir]) != 0:
495 raise GitException, 'Failed "git clone %s %s"' \
496 % (repository, local_dir)