Align branch descriptions in output of "stg branch -l"
[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
41a6d859 36
41a6d859
CM
37#
38# Classes
39#
40class Commit:
41 """Handle the commit objects
42 """
43 def __init__(self, id_hash):
44 self.__id_hash = id_hash
41a6d859 45
26dba451 46 lines = _output_lines('git-cat-file commit %s' % id_hash)
37a4d1bf 47 self.__parents = []
26dba451
BL
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':
37a4d1bf 56 self.__parents.append(field[1])
41a6d859
CM
57 if field[0] == 'author':
58 self.__author = field[1]
dad310d0 59 if field[0] == 'committer':
41a6d859 60 self.__committer = field[1]
0618ea9c 61 self.__log = ''.join(lines[i+1:])
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):
37a4d1bf
CM
70 return self.__parents[0]
71
72 def get_parents(self):
73 return self.__parents
41a6d859
CM
74
75 def get_author(self):
76 return self.__author
77
78 def get_committer(self):
79 return self.__committer
80
37a4d1bf
CM
81 def get_log(self):
82 return self.__log
83
8e29bcd2
CM
84# dictionary of Commit objects, used to avoid multiple calls to git
85__commits = dict()
41a6d859
CM
86
87#
88# Functions
89#
8e29bcd2
CM
90def get_commit(id_hash):
91 """Commit objects factory. Save/look-up them in the __commits
92 dictionary
93 """
3237b6e4
CM
94 global __commits
95
8e29bcd2
CM
96 if id_hash in __commits:
97 return __commits[id_hash]
98 else:
99 commit = Commit(id_hash)
100 __commits[id_hash] = commit
101 return commit
102
41a6d859
CM
103def get_conflicts():
104 """Return the list of file conflicts
105 """
106 conflicts_file = os.path.join(base_dir, 'conflicts')
107 if os.path.isfile(conflicts_file):
108 f = file(conflicts_file)
109 names = [line.strip() for line in f.readlines()]
110 f.close()
111 return names
112 else:
113 return None
114
0d2cd1e4 115def _input(cmd, file_desc):
741f2784 116 p = popen2.Popen3(cmd, True)
6fe6b1bd
CM
117 while True:
118 line = file_desc.readline()
119 if not line:
120 break
0d2cd1e4
CM
121 p.tochild.write(line)
122 p.tochild.close()
123 if p.wait():
124 raise GitException, '%s failed' % str(cmd)
125
26dba451 126def _output(cmd):
741f2784 127 p=popen2.Popen3(cmd, True)
7cc615f3 128 output = p.fromchild.read()
26dba451
BL
129 if p.wait():
130 raise GitException, '%s failed' % str(cmd)
7cc615f3 131 return output
26dba451 132
d3cf7d86 133def _output_one_line(cmd, file_desc = None):
741f2784 134 p=popen2.Popen3(cmd, True)
d3cf7d86
PBG
135 if file_desc != None:
136 for line in file_desc:
137 p.tochild.write(line)
138 p.tochild.close()
7cc615f3 139 output = p.fromchild.readline().strip()
26dba451
BL
140 if p.wait():
141 raise GitException, '%s failed' % str(cmd)
7cc615f3 142 return output
41a6d859 143
26dba451 144def _output_lines(cmd):
741f2784 145 p=popen2.Popen3(cmd, True)
26dba451
BL
146 lines = p.fromchild.readlines()
147 if p.wait():
148 raise GitException, '%s failed' % str(cmd)
149 return lines
150
151def __run(cmd, args=None):
152 """__run: runs cmd using spawnvp.
153
154 Runs cmd using spawnvp. The shell is avoided so it won't mess up
155 our arguments. If args is very large, the command is run multiple
156 times; args is split xargs style: cmd is passed on each
157 invocation. Unlike xargs, returns immediately if any non-zero
158 return code is received.
159 """
160
161 args_l=cmd.split()
162 if args is None:
163 args = []
164 for i in range(0, len(args)+1, 100):
165 r=os.spawnvp(os.P_WAIT, args_l[0], args_l + args[i:min(i+100, len(args))])
166 if r:
167 return r
168 return 0
169
41a6d859
CM
170def __check_base_dir():
171 return os.path.isdir(base_dir)
172
9216b602 173def __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:
be24d874
CM
185 exclude_file = os.path.join(base_dir, 'info', 'exclude')
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
7c09df84 208 for line in _output_lines(['git-diff-index', '-r', 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
215def 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 223def 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
232def 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
237def 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
246def __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
256def __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
263def refresh_index():
264 """Refresh index with stat() information from the working directory.
265 """
266 __run('git-update-index -q --unmerged --refresh')
267
d1eb3f85 268def 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
CL
276def branch_exists(branch):
277 """Existance check for the named branch
278 """
279 for line in _output_lines(['git-rev-parse', '--symbolic', '--all']):
280 if line.strip() == branch:
281 return True
282 return False
283
284def 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
CL
298
299 if os.path.isfile(os.path.join(base_dir, 'MERGE_HEAD')):
300 os.remove(os.path.join(base_dir, 'MERGE_HEAD'))
301
982b9697
CL
302def 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
319 if os.path.isfile(os.path.join(base_dir, 'MERGE_HEAD')):
320 os.remove(os.path.join(base_dir, 'MERGE_HEAD'))
321
6f48e5f8
CL
322def 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
328 os.remove(os.path.join(base_dir, branch_head))
329
72594233
CL
330def 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
CL
341 set_head_file(to_head)
342 os.rename(os.path.join(base_dir, from_head), os.path.join(base_dir, to_head))
72594233 343
41a6d859
CM
344def add(names):
345 """Add the files or recursively add the directory contents
346 """
347 # generate the file list
348 files = []
349 for i in names:
350 if not os.path.exists(i):
351 raise GitException, 'Unknown file or directory: %s' % i
352
353 if os.path.isdir(i):
354 # recursive search. We only add files
355 for root, dirs, local_files in os.walk(i):
356 for name in [os.path.join(root, f) for f in local_files]:
357 if os.path.isfile(name):
358 files.append(os.path.normpath(name))
359 elif os.path.isfile(i):
360 files.append(os.path.normpath(i))
361 else:
362 raise GitException, '%s is not a file or directory' % i
363
26dba451 364 if files:
7c09df84 365 if __run('git-update-index --add --', files):
26dba451 366 raise GitException, 'Unable to add file'
41a6d859
CM
367
368def rm(files, force = False):
369 """Remove a file from the repository
370 """
26dba451
BL
371 if not force:
372 for f in files:
373 if os.path.exists(f):
374 raise GitException, '%s exists. Remove it first' %f
375 if files:
7c09df84 376 __run('git-update-index --remove --', files)
26dba451
BL
377 else:
378 if files:
7c09df84 379 __run('git-update-index --force-remove --', files)
41a6d859 380
9216b602 381def update_cache(files = None, force = False):
cfafb945
CM
382 """Update the cache information for the given files
383 """
9216b602
CL
384 if not files:
385 files = []
386
402ad990 387 cache_files = __tree_status(files)
26dba451 388
402ad990
CM
389 # everything is up-to-date
390 if len(cache_files) == 0:
391 return False
392
393 # check for unresolved conflicts
394 if not force and [x for x in cache_files
395 if x[0] not in ['M', 'N', 'A', 'D']]:
396 raise GitException, 'Updating cache failed: unresolved conflicts'
26dba451 397
402ad990
CM
398 # update the cache
399 add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
400 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
401 m_files = [x[1] for x in cache_files if x[0] in ['M']]
402
7c09df84
JH
403 if add_files and __run('git-update-index --add --', add_files) != 0:
404 raise GitException, 'Failed git-update-index --add'
405 if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
406 raise GitException, 'Failed git-update-index --rm'
407 if m_files and __run('git-update-index --', m_files) != 0:
408 raise GitException, 'Failed git-update-index'
402ad990
CM
409
410 return True
cfafb945 411
9216b602 412def commit(message, files = None, parents = None, allowempty = False,
d3cf7d86 413 cache_update = True, tree_id = None,
41a6d859
CM
414 author_name = None, author_email = None, author_date = None,
415 committer_name = None, committer_email = None):
416 """Commit the current tree to repository
417 """
9216b602
CL
418 if not files:
419 files = []
420 if not parents:
421 parents = []
422
41a6d859 423 # Get the tree status
402ad990
CM
424 if cache_update and parents != []:
425 changes = update_cache(files)
426 if not changes and not allowempty:
427 raise GitException, 'No changes to commit'
41a6d859
CM
428
429 # get the commit message
d3cf7d86
PBG
430 if message[-1:] != '\n':
431 message += '\n'
41a6d859 432
d3cf7d86 433 must_switch = True
41a6d859 434 # write the index to repository
d3cf7d86
PBG
435 if tree_id == None:
436 tree_id = _output_one_line('git-write-tree')
437 else:
438 must_switch = False
41a6d859
CM
439
440 # the commit
441 cmd = ''
442 if author_name:
443 cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
444 if author_email:
445 cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
446 if author_date:
447 cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
448 if committer_name:
449 cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
450 if committer_email:
451 cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
452 cmd += 'git-commit-tree %s' % tree_id
453
454 # get the parents
455 for p in parents:
456 cmd += ' -p %s' % p
457
d3cf7d86
PBG
458 commit_id = _output_one_line(cmd, message)
459 if must_switch:
460 __set_head(commit_id)
41a6d859
CM
461
462 return commit_id
463
575a7e7c
CM
464def apply_diff(rev1, rev2):
465 """Apply the diff between rev1 and rev2 onto the current
466 index. This function doesn't need to raise an exception since it
467 is only used for fast-pushing a patch. If this operation fails,
468 the pushing would fall back to the three-way merge.
469 """
470 return os.system('git-diff-tree -p %s %s | git-apply --index 2> /dev/null'
471 % (rev1, rev2)) == 0
472
41a6d859
CM
473def merge(base, head1, head2):
474 """Perform a 3-way merge between base, head1 and head2 into the
475 local tree
476 """
f8fb5747 477 refresh_index()
26dba451 478 if __run('git-read-tree -u -m', [base, head1, head2]) != 0:
41a6d859
CM
479 raise GitException, 'git-read-tree failed (local changes maybe?)'
480
481 # this can fail if there are conflicts
962ef1c7 482 if __run('git-merge-index -o -q gitmergeonefile.py -a') != 0:
547eb28f 483 raise GitException, 'git-merge-index failed (possible conflicts)'
41a6d859 484
9216b602 485def status(files = None, modified = False, new = False, deleted = False,
4d4c0e3a 486 conflict = False, unknown = False, noexclude = False):
41a6d859
CM
487 """Show the tree status
488 """
9216b602
CL
489 if not files:
490 files = []
491
4d4c0e3a 492 cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
41a6d859
CM
493 all = not (modified or new or deleted or conflict or unknown)
494
495 if not all:
496 filestat = []
497 if modified:
498 filestat.append('M')
499 if new:
7371951a 500 filestat.append('A')
41a6d859
CM
501 filestat.append('N')
502 if deleted:
503 filestat.append('D')
504 if conflict:
505 filestat.append('C')
506 if unknown:
507 filestat.append('?')
402ad990 508 cache_files = [x for x in cache_files if x[0] in filestat]
41a6d859
CM
509
510 for fs in cache_files:
511 if all:
512 print '%s %s' % (fs[0], fs[1])
513 else:
514 print '%s' % fs[1]
515
9216b602 516def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None):
41a6d859
CM
517 """Show the diff between rev1 and rev2
518 """
9216b602
CL
519 if not files:
520 files = []
41a6d859 521
fcc1ad70 522 if rev1 and rev2:
b4bddc06 523 diff_str = _output(['git-diff-tree', '-p', rev1, rev2] + files)
fcc1ad70 524 elif rev1 or rev2:
f8fb5747 525 refresh_index()
fcc1ad70
CL
526 if rev2:
527 diff_str = _output(['git-diff-index', '-p', '-R', rev2] + files)
528 else:
529 diff_str = _output(['git-diff-index', '-p', rev1] + files)
530 else:
531 diff_str = ''
b4bddc06
CM
532
533 if out_fd:
534 out_fd.write(diff_str)
535 else:
536 return diff_str
41a6d859 537
9216b602 538def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
41a6d859
CM
539 """Return the diffstat between rev1 and rev2
540 """
9216b602
CL
541 if not files:
542 files = []
41a6d859 543
26dba451
BL
544 p=popen2.Popen3('git-apply --stat')
545 diff(files, rev1, rev2, p.tochild)
546 p.tochild.close()
7cc615f3 547 diff_str = p.fromchild.read().rstrip()
26dba451
BL
548 if p.wait():
549 raise GitException, 'git.diffstat failed'
7cc615f3 550 return diff_str
41a6d859
CM
551
552def files(rev1, rev2):
553 """Return the files modified between rev1 and rev2
554 """
41a6d859 555
7cc615f3 556 result = ''
26dba451 557 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
7cc615f3 558 result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
41a6d859 559
7cc615f3 560 return result.rstrip()
41a6d859 561
faed6770
PBG
562def barefiles(rev1, rev2):
563 """Return the files modified between rev1 and rev2, without status info
564 """
565
7cc615f3 566 result = ''
faed6770 567 for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
7cc615f3 568 result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
faed6770 569
7cc615f3 570 return result.rstrip()
faed6770 571
9216b602 572def checkout(files = None, tree_id = None, force = False):
41a6d859
CM
573 """Check out the given or all files
574 """
9216b602 575 if not files:
97310762 576 files = []
9216b602 577
1008fbce
CM
578 if tree_id and __run('git-read-tree -m', [tree_id]) != 0:
579 raise GitException, 'Failed git-read-tree -m %s' % tree_id
580
7c09df84 581 checkout_cmd = 'git-checkout-index -q -u'
41a6d859 582 if force:
1008fbce 583 checkout_cmd += ' -f'
41a6d859 584 if len(files) == 0:
1008fbce 585 checkout_cmd += ' -a'
41a6d859 586 else:
1008fbce 587 checkout_cmd += ' --'
41a6d859 588
1008fbce 589 if __run(checkout_cmd, files) != 0:
7c09df84 590 raise GitException, 'Failed git-checkout-index'
41a6d859
CM
591
592def switch(tree_id):
593 """Switch the tree to the given id
594 """
f8fb5747 595 refresh_index()
a5b29a1c
CM
596 if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
597 raise GitException, 'git-read-tree failed (local changes maybe?)'
41a6d859 598
41a6d859
CM
599 __set_head(tree_id)
600
49e316b9 601def reset(files = None, tree_id = None):
05d593c0
CM
602 """Revert the tree changes relative to the given tree_id. It removes
603 any local changes
604 """
605 if not tree_id:
606 tree_id = get_head()
607
49e316b9 608 cache_files = __tree_status(files, tree_id)
05d593c0
CM
609 rm_files = [x[1] for x in cache_files if x[0] in ['D']]
610
49e316b9 611 checkout(files, tree_id, True)
05d593c0
CM
612 # checkout doesn't remove files
613 map(os.remove, rm_files)
614
49e316b9
CM
615 # if the reset refers to the whole tree, switch the HEAD as well
616 if not files:
617 __set_head(tree_id)
618
1f5e9148
CM
619def pull(repository = 'origin', refspec = None):
620 """Pull changes from the remote repository. At the moment, just
621 use the 'git pull' command
f338c3c0 622 """
ba1a4550
CM
623 # 'git pull' updates the HEAD
624 __clear_head_cache()
625
1f5e9148
CM
626 args = [repository]
627 if refspec:
628 args.append(refspec)
f338c3c0 629
ddbbfd84 630 if __run('git pull', args) != 0:
1f5e9148 631 raise GitException, 'Failed "git pull %s"' % repository
f338c3c0 632
84fcbc3b
CM
633def apply_patch(filename = None, base = None):
634 """Apply a patch onto the current or given index. There must not
635 be any local changes in the tree, otherwise the command fails
0d2cd1e4 636 """
84fcbc3b
CM
637 def __apply_patch():
638 if filename:
639 return __run('git-apply --index', [filename]) == 0
640 else:
641 try:
642 _input('git-apply --index', sys.stdin)
643 except GitException:
644 return False
645 return True
646
84fcbc3b
CM
647 if base:
648 orig_head = get_head()
649 switch(base)
e1db88d0
CL
650 else:
651 refresh_index() # needed since __apply_patch() doesn't do it
84fcbc3b
CM
652
653 if not __apply_patch():
654 if base:
655 switch(orig_head)
656 raise GitException, 'Patch does not apply cleanly'
657 elif base:
658 top = commit(message = 'temporary commit used for applying a patch',
659 parents = [base])
660 switch(orig_head)
661 merge(base, orig_head, top)
1008fbce
CM
662
663def clone(repository, local_dir):
664 """Clone a remote repository. At the moment, just use the
665 'git clone' script
666 """
667 if __run('git clone', [repository, local_dir]) != 0:
668 raise GitException, 'Failed "git clone %s %s"' \
669 % (repository, local_dir)
cd25e03d
CM
670
671def modifying_revs(files, base_rev):
672 """Return the revisions from the list modifying the given files
673 """
674 cmd = ['git-rev-list', '%s..' % base_rev, '--']
675 revs = [line.strip() for line in _output_lines(cmd + files)]
676
677 return revs