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