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