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