1 # Copyright (c) 2008 David Aguilar
7 from cStringIO
import StringIO
10 from cola
import utils
11 from cola
import model
13 #+-------------------------------------------------------------------------
14 #+ A regex for matching the output of git(log|rev-list) --pretty=oneline
15 REV_LIST_REGEX
= re
.compile('([0-9a-f]+)\W(.*)')
17 class GitCola(git
.Git
):
18 """GitPython throws exceptions by default.
19 We suppress exceptions in favor of return values.
22 git
.Git
.__init
__(self
)
23 self
.load_worktree(os
.getcwd())
25 def load_worktree(self
, path
):
27 self
._work
_tree
= None
30 def execute(*args
, **kwargs
):
31 kwargs
['with_exceptions'] = False
32 return git
.Git
.execute(*args
, **kwargs
)
34 def get_work_tree(self
):
36 return self
._work
_tree
39 curdir
= self
._git
_dir
43 if self
._is
_git
_dir
(os
.path
.join(curdir
, '.git')):
46 # Handle bare repositories
47 if (len(os
.path
.basename(curdir
)) > 4
48 and curdir
.endswith('.git')):
50 if 'GIT_WORK_TREE' in os
.environ
:
51 self
._work
_tree
= os
.getenv('GIT_WORK_TREE')
52 if not self
._work
_tree
or not os
.path
.isdir(self
._work
_tree
):
54 gitparent
= os
.path
.join(os
.path
.abspath(self
._git
_dir
), '..')
55 self
._work
_tree
= os
.path
.abspath(gitparent
)
56 self
.set_cwd(self
._work
_tree
)
57 return self
._work
_tree
60 return self
._git
_dir
and self
._is
_git
_dir
(self
._git
_dir
)
62 def get_git_dir(self
):
65 if 'GIT_DIR' in os
.environ
:
66 self
._git
_dir
= os
.getenv('GIT_DIR')
68 curpath
= os
.path
.abspath(self
._git
_dir
)
70 curpath
= os
.path
.abspath(os
.getcwd())
71 # Search for a .git directory
73 if self
._is
_git
_dir
(curpath
):
74 self
._git
_dir
= curpath
76 gitpath
= os
.path
.join(curpath
, '.git')
77 if self
._is
_git
_dir
(gitpath
):
78 self
._git
_dir
= gitpath
80 curpath
, dummy
= os
.path
.split(curpath
)
85 def _is_git_dir(self
, d
):
86 """ This is taken from the git setup.c:is_git_directory
89 and os
.path
.isdir(os
.path
.join(d
, 'objects'))
90 and os
.path
.isdir(os
.path
.join(d
, 'refs'))):
91 headref
= os
.path
.join(d
, 'HEAD')
92 return (os
.path
.isfile(headref
)
93 or (os
.path
.islink(headref
)
94 and os
.readlink(headref
).startswith('refs')))
98 """handles quoted paths."""
99 if path
.startswith('"') and path
.endswith('"'):
100 return eval(path
).decode('utf-8')
104 class Model(model
.Model
):
105 """Provides a friendly wrapper for doing commit git operations."""
108 worktree
= self
.git
.get_work_tree()
109 clone
= model
.Model
.clone(self
)
110 clone
.use_worktree(worktree
)
113 def use_worktree(self
, worktree
):
114 self
.git
.load_worktree(worktree
)
115 is_valid
= self
.git
.is_valid()
117 self
.__init
_config
_data
()
121 """Reads git repository settings and sets several methods
122 so that they refer to the git module. This object
123 encapsulates cola's interaction with git."""
125 # Initialize the git command object
127 self
.partially_staged
= set()
129 self
.fetch_helper
= self
.gen_remote_helper(self
.git
.fetch
)
130 self
.push_helper
= self
.gen_remote_helper(self
.git
.push
)
131 self
.pull_helper
= self
.gen_remote_helper(self
.git
.pull
)
134 #####################################################
135 # Used in various places
143 #####################################################
144 # Used primarily by the main UI
152 #####################################################
153 # Used by the create branch dialog
156 remote_branches
= [],
159 #####################################################
160 # Used by the commit/repo browser
165 # These are parallel lists
170 # All items below here are re-calculated in
171 # init_browser_data()
173 directory_entries
= {},
175 # These are also parallel lists
181 def __init_config_data(self
):
182 """Reads git config --list and creates parameters
184 # These parameters are saved in .gitconfig,
185 # so ideally these should be as short as possible.
187 # config items that are controllable globally
189 self
.__local
_and
_global
_defaults
= {
192 'merge_summary': False,
193 'merge_diffstat': True,
194 'merge_verbosity': 2,
195 'gui_diffcontext': 3,
196 'gui_pruneduringfetch': False,
198 # config items that are purely git config --global settings
199 self
.__global
_defaults
= {
202 'cola_fontuisize': 12,
204 'cola_fontdiffsize': 12,
205 'cola_savewindowsettings': False,
206 'merge_keepbackup': True,
207 'merge_tool': os
.getenv('MERGETOOL', 'xxdiff'),
208 'gui_editor': os
.getenv('EDITOR', 'gvim'),
209 'gui_historybrowser': 'gitk',
212 local_dict
= self
.config_dict(local
=True)
213 global_dict
= self
.config_dict(local
=False)
215 for k
,v
in local_dict
.iteritems():
216 self
.set_param('local_'+k
, v
)
217 for k
,v
in global_dict
.iteritems():
218 self
.set_param('global_'+k
, v
)
219 if k
not in local_dict
:
221 self
.set_param('local_'+k
, v
)
223 # Bootstrap the internal font*size variables
224 for param
in ('global_cola_fontui', 'global_cola_fontdiff'):
226 if hasattr(self
, param
):
227 font
= self
.get_param(param
)
230 size
= int(font
.split(',')[1])
231 self
.set_param(param
+'size', size
)
232 param
= param
[len('global_'):]
233 global_dict
[param
] = font
234 global_dict
[param
+'size'] = size
236 # Load defaults for all undefined items
237 local_and_global_defaults
= self
.__local
_and
_global
_defaults
238 for k
,v
in local_and_global_defaults
.iteritems():
239 if k
not in local_dict
:
240 self
.set_param('local_'+k
, v
)
241 if k
not in global_dict
:
242 self
.set_param('global_'+k
, v
)
244 global_defaults
= self
.__global
_defaults
245 for k
,v
in global_defaults
.iteritems():
246 if k
not in global_dict
:
247 self
.set_param('global_'+k
, v
)
249 # Load the diff context
250 self
.diff_context
= self
.local_gui_diffcontext
252 def get_global_config(self
, key
):
253 return getattr(self
, 'global_'+key
.replace('.', '_'))
255 def get_cola_config(self
, key
):
256 return getattr(self
, 'global_cola_'+key
)
258 def get_gui_config(self
, key
):
259 return getattr(self
, 'global_gui_'+key
)
261 def get_default_remote(self
):
262 branch
= self
.get_currentbranch()
263 branchconfig
= 'local_branch_%s_remote' % branch
264 if branchconfig
in self
.get_param_names():
265 remote
= self
.get_param(branchconfig
)
270 def get_corresponding_remote_ref(self
):
271 remote
= self
.get_default_remote()
272 branch
= self
.get_currentbranch()
273 best_match
= '%s/%s' % (remote
, branch
)
274 remote_branches
= self
.get_remote_branches()
275 if not remote_branches
:
277 for rb
in remote_branches
:
280 return remote_branches
[0]
282 def get_diff_filenames(self
, arg
):
283 diff_zstr
= self
.git
.diff(arg
, name_only
=True, z
=True).rstrip('\0')
284 return [ f
.decode('utf-8') for f
in diff_zstr
.split('\0') if f
]
286 def branch_list(self
, remote
=False):
287 branches
= map(lambda x
: x
.lstrip('* '),
288 self
.git
.branch(r
=remote
).splitlines())
291 for branch
in branches
:
292 if branch
.endswith('/HEAD'):
294 remotes
.append(branch
)
298 def get_config_params(self
):
300 params
.extend(map(lambda x
: 'local_' + x
,
301 self
.__local
_and
_global
_defaults
.keys()))
302 params
.extend(map(lambda x
: 'global_' + x
,
303 self
.__local
_and
_global
_defaults
.keys()))
304 params
.extend(map(lambda x
: 'global_' + x
,
305 self
.__global
_defaults
.keys()))
306 return [ p
for p
in params
if not p
.endswith('size') ]
308 def save_config_param(self
, param
):
309 if param
not in self
.get_config_params():
311 value
= self
.get_param(param
)
312 if param
== 'local_gui_diffcontext':
313 self
.diff_context
= value
314 if param
.startswith('local_'):
315 param
= param
[len('local_'):]
317 elif param
.startswith('global_'):
318 param
= param
[len('global_'):]
321 raise Exception("Invalid param '%s' passed to " % param
322 +'save_config_param()')
323 param
= param
.replace('_', '.') # model -> git
324 return self
.config_set(param
, value
, local
=is_local
)
326 def init_browser_data(self
):
327 """This scans over self.(names, sha1s, types) to generate
328 directories, directory_entries, and subtree_*"""
330 # Collect data for the model
331 if not self
.get_currentbranch(): return
333 self
.subtree_types
= []
334 self
.subtree_sha1s
= []
335 self
.subtree_names
= []
336 self
.directories
= []
337 self
.directory_entries
= {}
339 # Lookup the tree info
340 tree_info
= self
.parse_ls_tree(self
.get_currentbranch())
342 self
.set_types(map( lambda(x
): x
[1], tree_info
))
343 self
.set_sha1s(map( lambda(x
): x
[2], tree_info
))
344 self
.set_names(map( lambda(x
): x
[3], tree_info
))
346 if self
.directory
: self
.directories
.append('..')
348 dir_entries
= self
.directory_entries
349 dir_regex
= re
.compile('([^/]+)/')
353 for idx
, name
in enumerate(self
.names
):
354 if not name
.startswith(self
.directory
):
356 name
= name
[ len(self
.directory
): ]
358 # This is a directory...
359 match
= dir_regex
.match(name
)
362 dirent
= match
.group(1) + '/'
363 if dirent
not in self
.directory_entries
:
364 self
.directory_entries
[dirent
] = []
366 if dirent
not in dirs_seen
:
367 dirs_seen
[dirent
] = True
368 self
.directories
.append(dirent
)
370 entry
= name
.replace(dirent
, '')
371 entry_match
= dir_regex
.match(entry
)
373 subdir
= entry_match
.group(1) + '/'
374 if subdir
in subdirs_seen
:
376 subdirs_seen
[subdir
] = True
377 dir_entries
[dirent
].append(subdir
)
379 dir_entries
[dirent
].append(entry
)
381 self
.subtree_types
.append(self
.types
[idx
])
382 self
.subtree_sha1s
.append(self
.sha1s
[idx
])
383 self
.subtree_names
.append(name
)
385 def add_or_remove(self
, *to_process
):
386 """Invokes 'git add' to index the filenames in to_process that exist
387 and 'git rm' for those that do not exist."""
390 return 'No files to add or remove.'
395 for filename
in to_process
:
396 encfilename
= filename
.encode('utf-8')
397 if os
.path
.exists(encfilename
):
398 to_add
.append(filename
)
401 output
= self
.git
.add(v
=True, *to_add
)
405 if len(to_add
) == len(to_process
):
406 # to_process only contained unremoved files --
407 # short-circuit the removal checks
410 # Process files to remote
411 for filename
in to_process
:
412 if not os
.path
.exists(filename
):
413 to_remove
.append(filename
)
414 output
+ '\n\n' + self
.git
.rm(*to_remove
)
416 def get_editor(self
):
417 return self
.get_gui_config('editor')
419 def get_mergetool(self
):
420 return self
.get_global_config('merge.tool')
422 def get_history_browser(self
):
423 return self
.get_gui_config('historybrowser')
425 def remember_gui_settings(self
):
426 return self
.get_cola_config('savewindowsettings')
428 def get_tree_node(self
, idx
):
429 return (self
.get_types()[idx
],
430 self
.get_sha1s()[idx
],
431 self
.get_names()[idx
] )
433 def get_subtree_node(self
, idx
):
434 return (self
.get_subtree_types()[idx
],
435 self
.get_subtree_sha1s()[idx
],
436 self
.get_subtree_names()[idx
] )
438 def get_all_branches(self
):
439 return (self
.get_local_branches() + self
.get_remote_branches())
441 def set_remote(self
, remote
):
444 self
.set_param('remote', remote
)
445 branches
= utils
.grep('%s/\S+$' % remote
,
446 self
.branch_list(remote
=True),
448 self
.set_remote_branches(branches
)
450 def add_signoff(self
,*rest
):
451 """Adds a standard Signed-off by: tag to the end
452 of the current commit message."""
453 msg
= self
.get_commitmsg()
454 signoff
=('\n\nSigned-off-by: %s <%s>\n'
455 % (self
.get_local_user_name(), self
.get_local_user_email()))
456 if signoff
not in msg
:
457 self
.set_commitmsg(msg
+ signoff
)
459 def apply_diff(self
, filename
):
460 return self
.git
.apply(filename
, index
=True, cached
=True)
462 def apply_diff_to_worktree(self
, filename
):
463 return self
.git
.apply(filename
)
465 def load_commitmsg(self
, path
):
466 file = open(path
, 'r')
467 contents
= file.read().decode('utf-8')
469 self
.set_commitmsg(contents
)
471 def get_prev_commitmsg(self
,*rest
):
472 """Queries git for the latest commit message and sets it in
475 commit_lines
= self
.git
.show('HEAD').split('\n')
476 for idx
, msg
in enumerate(commit_lines
):
479 if msg
.startswith('diff --git'):
482 commit_msg
.append(msg
)
483 self
.set_commitmsg('\n'.join(commit_msg
).rstrip())
485 def load_commitmsg_template(self
):
487 template
= self
.get_global_config('commit.template')
488 except AttributeError:
490 self
.load_commitmsg(template
)
492 def update_status(self
, amend
=False):
493 # This allows us to defer notification until the
494 # we finish processing data
495 notify_enabled
= self
.get_notify()
496 self
.set_notify(False)
498 # Reset the staged and unstaged model lists
499 # NOTE: the model's unstaged list is used to
500 # hold both modified and untracked files.
505 # Read git status items
509 unmerged_items
) = self
.get_workdir_state(amend
=amend
)
511 # Gather items to be committed
512 for staged
in staged_items
:
513 if staged
not in self
.get_staged():
514 self
.add_staged(staged
)
516 # Gather unindexed items
517 for modified
in modified_items
:
518 if modified
not in self
.get_modified():
519 self
.add_modified(modified
)
521 # Gather untracked items
522 for untracked
in untracked_items
:
523 if untracked
not in self
.get_untracked():
524 self
.add_untracked(untracked
)
526 # Gather unmerged items
527 for unmerged
in unmerged_items
:
528 if unmerged
not in self
.get_unmerged():
529 self
.add_unmerged(unmerged
)
531 self
.set_currentbranch(self
.current_branch())
532 self
.set_unstaged(self
.get_modified() + self
.get_untracked() + self
.get_unmerged())
533 self
.set_remotes(self
.git
.remote().splitlines())
534 self
.set_remote_branches(self
.branch_list(remote
=True))
535 self
.set_local_branches(self
.branch_list(remote
=False))
536 self
.set_tags(self
.git
.tag().splitlines())
537 self
.set_revision('')
538 self
.set_local_branch('')
539 self
.set_remote_branch('')
540 # Re-enable notifications and emit changes
541 self
.set_notify(notify_enabled
)
542 self
.notify_observers('staged','unstaged')
544 def delete_branch(self
, branch
):
545 return self
.git
.branch(branch
, D
=True)
547 def get_revision_sha1(self
, idx
):
548 return self
.get_revisions()[idx
]
550 def apply_font_size(self
, param
, default
):
551 old_font
= self
.get_param(param
)
554 size
= self
.get_param(param
+'size')
555 props
= old_font
.split(',')
557 new_font
= ','.join(props
)
559 self
.set_param(param
, new_font
)
561 def get_commit_diff(self
, sha1
):
562 commit
= self
.git
.show(sha1
)
563 first_newline
= commit
.index('\n')
564 if commit
[first_newline
+1:].startswith('Merge:'):
565 return (commit
+ '\n\n'
566 + self
.diff_helper(commit
=sha1
,
568 suppress_header
=False))
572 def get_filename(self
, idx
, staged
=True):
575 return self
.get_staged()[idx
]
577 return self
.get_unstaged()[idx
]
581 def get_diff_details(self
, idx
, ref
, staged
=True):
582 filename
= self
.get_filename(idx
, staged
=staged
)
584 return (None, None, None)
585 encfilename
= filename
.encode('utf-8')
587 if os
.path
.exists(encfilename
):
588 status
= 'Staged for commit'
590 status
= 'Staged for removal'
591 diff
= self
.diff_helper(filename
=filename
,
595 if os
.path
.isdir(encfilename
):
596 status
= 'Untracked directory'
597 diff
= '\n'.join(os
.listdir(filename
))
599 elif filename
in self
.get_unmerged():
601 diff
= ('@@@+-+-+-+-+-+-+-+-+-+-+ UNMERGED +-+-+-+-+-+-+-+-+-+-+@@@\n\n'
602 '>>> %s is unmerged.\n' % filename
+
603 'Right-click on the filename '
604 'to launch "git mergetool".\n\n\n')
605 diff
+= self
.diff_helper(filename
=filename
,
607 patch_with_raw
=False)
608 elif filename
in self
.get_modified():
609 status
= 'Modified, not staged'
610 diff
= self
.diff_helper(filename
=filename
,
613 status
= 'Untracked, not staged'
614 diff
= 'SHA1: ' + self
.git
.hash_object(filename
)
615 return diff
, status
, filename
617 def stage_modified(self
):
618 output
= self
.git
.add(v
=True, *self
.get_modified())
622 def stage_untracked(self
):
623 output
= self
.git
.add(self
.get_untracked())
627 def reset(self
, *items
):
628 output
= self
.git
.reset('--', *items
)
632 def unstage_all(self
):
633 self
.git
.reset('--', *self
.get_staged())
636 def save_gui_settings(self
):
637 self
.config_set('cola.geometry', utils
.get_geom(), local
=False)
639 def config_set(self
, key
=None, value
=None, local
=True):
640 if key
and value
is not None:
641 # git config category.key value
642 strval
= unicode(value
).encode('utf-8')
643 if type(value
) is bool:
644 # git uses "true" and "false"
645 strval
= strval
.lower()
647 argv
= [ key
, strval
]
649 argv
= [ '--global', key
, strval
]
650 return self
.git
.config(*argv
)
652 msg
= "oops in config_set(key=%s,value=%s,local=%s"
653 raise Exception(msg
% (key
, value
, local
))
655 def config_dict(self
, local
=True):
656 """parses the lines from git config --list into a dictionary"""
660 'global': not local
, # global is a python keyword
662 config_lines
= self
.git
.config(**kwargs
).splitlines()
664 for line
in config_lines
:
665 k
, v
= line
.split('=', 1)
666 k
= k
.replace('.','_') # git -> model
667 if v
== 'true' or v
== 'false':
668 v
= bool(eval(v
.title()))
676 def commit_with_msg(self
, msg
, amend
=False):
677 """Creates a git commit."""
679 if not msg
.endswith('\n'):
681 # Sure, this is a potential "security risk," but if someone
682 # is trying to intercept/re-write commit messages on your system,
683 # then you probably have bigger problems to worry about.
684 tmpfile
= self
.get_tmp_filename()
686 # Create the commit message file
687 fh
= open(tmpfile
, 'w')
692 (status
, stdout
, stderr
) = self
.git
.commit(v
=True,
695 with_extended_output
=True)
698 return (status
, stdout
+stderr
)
702 return self
.git
.diff(unified
=self
.diff_context
,
706 def get_tmp_dir(self
):
707 return os
.environ
.get('TMP', os
.environ
.get('TMPDIR', '/tmp'))
709 def get_tmp_file_pattern(self
):
710 return os
.path
.join(self
.get_tmp_dir(), '*.git.%s.*' % os
.getpid())
712 def get_tmp_filename(self
, prefix
=''):
713 # Allow TMPDIR/TMP with a fallback to /tmp
714 basename
= (prefix
+'.git.%s.%s'
715 % (os
.getpid(), time
.time())).replace(os
.sep
, '-')
716 return os
.path
.join(self
.get_tmp_dir(), basename
)
718 def log_helper(self
, all
=False):
720 Returns a pair of parallel arrays listing the revision sha1's
721 and commit summaries.
725 regex
= REV_LIST_REGEX
726 output
= self
.git
.log(pretty
='oneline', all
=all
)
727 for line
in output
.splitlines():
728 match
= regex
.match(line
)
730 revs
.append(match
.group(1))
731 summaries
.append(match
.group(2))
732 return (revs
, summaries
)
734 def parse_rev_list(self
, raw_revs
):
736 for line
in raw_revs
.splitlines():
737 match
= REV_LIST_REGEX
.match(line
)
739 rev_id
= match
.group(1)
740 summary
= match
.group(2)
741 revs
.append((rev_id
, summary
,))
744 def rev_list_range(self
, start
, end
):
745 range = '%s..%s' % (start
, end
)
746 raw_revs
= self
.git
.rev_list(range, pretty
='oneline')
747 return self
.parse_rev_list(raw_revs
)
749 def diff_helper(self
,
756 with_diff_header
=False,
757 suppress_header
=True,
759 patch_with_raw
=True):
760 "Invokes git diff on a filepath."
762 ref
, endref
= commit
+'^', commit
765 argv
.append('%s..%s' % (ref
, endref
))
773 if type(filename
) is list:
774 argv
.extend(filename
)
776 argv
.append(filename
)
780 del_tag
= 'deleted file mode '
783 deleted
= cached
and not os
.path
.exists(filename
.encode('utf-8'))
785 diffoutput
= self
.git
.diff(R
=reverse
,
787 patch_with_raw
=patch_with_raw
,
788 unified
=self
.diff_context
,
789 with_raw_output
=True,
791 diff
= diffoutput
.splitlines()
793 line
= unicode(line
.decode('utf-8'))
794 if not start
and '@@' == line
[:2] and '@@' in line
[2:]:
796 if start
or(deleted
and del_tag
in line
):
797 output
.write(line
.encode('utf-8') + '\n')
801 elif not suppress_header
:
802 output
.write(line
.encode('utf-8') + '\n')
804 result
= output
.getvalue().decode('utf-8')
808 return('\n'.join(headers
), result
)
812 def git_repo_path(self
, *subpaths
):
813 paths
= [ self
.git
.get_git_dir() ]
814 paths
.extend(subpaths
)
815 return os
.path
.realpath(os
.path
.join(*paths
))
817 def get_merge_message_path(self
):
818 for file in ('MERGE_MSG', 'SQUASH_MSG'):
819 path
= self
.git_repo_path(file)
820 if os
.path
.exists(path
):
824 def get_merge_message(self
):
825 return self
.git
.fmt_merge_msg('--file',
826 self
.git_repo_path('FETCH_HEAD'))
828 def abort_merge(self
):
830 output
= self
.git
.read_tree('HEAD', reset
=True, u
=True, v
=True)
832 merge_head
= self
.git_repo_path('MERGE_HEAD')
833 if os
.path
.exists(merge_head
):
834 os
.unlink(merge_head
)
835 # remove MERGE_MESSAGE, etc.
836 merge_msg_path
= self
.get_merge_message_path()
837 while merge_msg_path
:
838 os
.unlink(merge_msg_path
)
839 merge_msg_path
= self
.get_merge_message_path()
841 def get_workdir_state(self
, amend
=False):
842 """RETURNS: A tuple of staged, unstaged untracked, and unmerged
845 self
.partially_staged
= set()
849 (staged
, unstaged
, unmerged
, untracked
) = ([], [], [], [])
851 for idx
, line
in enumerate(self
.git
.diff_index(head
).splitlines()):
852 rest
, name
= line
.split('\t')
854 name
= eval_path(name
)
855 if status
== 'M' or status
== 'D':
856 unstaged
.append(name
)
858 for idx
, line
in enumerate(self
.git
.diff_index(head
, cached
=True)
860 rest
, name
= line
.split('\t')
862 name
= eval_path(name
)
865 # is this file partially staged?
866 diff
= self
.git
.diff('--', name
, name_only
=True, z
=True)
868 unstaged
.remove(name
)
870 self
.partially_staged
.add(name
)
875 unstaged
.remove(name
)
877 unmerged
.append(name
)
879 for line
in self
.git
.ls_files(others
=True, exclude_standard
=True,
882 untracked
.append(line
.decode('utf-8'))
884 return (staged
, unstaged
, untracked
, unmerged
)
886 def reset_helper(self
, *args
, **kwargs
):
887 return self
.git
.reset('--', *args
, **kwargs
)
889 def remote_url(self
, name
):
890 return self
.git
.config('remote.%s.url' % name
, get
=True)
892 def get_remote_args(self
, remote
,
893 local_branch
='', remote_branch
='',
894 ffwd
=True, tags
=False):
896 branch_arg
= '%s:%s' % ( remote_branch
, local_branch
)
898 branch_arg
= '+%s:%s' % ( remote_branch
, local_branch
)
900 if local_branch
and remote_branch
:
901 args
.append(branch_arg
)
906 return (args
, kwargs
)
908 def gen_remote_helper(self
, gitaction
):
909 """Generates a closure that calls git fetch, push or pull
911 def remote_helper(remote
, **kwargs
):
912 args
, kwargs
= self
.get_remote_args(remote
, **kwargs
)
913 return gitaction(*args
, **kwargs
)
916 def parse_ls_tree(self
, rev
):
917 """Returns a list of(mode, type, sha1, path) tuples."""
918 lines
= self
.git
.ls_tree(rev
, r
=True).splitlines()
920 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
922 match
= regex
.match(line
)
924 mode
= match
.group(1)
925 objtype
= match
.group(2)
926 sha1
= match
.group(3)
927 filename
= match
.group(4)
928 output
.append((mode
, objtype
, sha1
, filename
,) )
931 def format_patch_helper(self
, to_export
, revs
, output
='patches'):
932 """writes patches named by to_export to the output directory."""
936 cur_rev
= to_export
[0]
937 cur_master_idx
= revs
.index(cur_rev
)
939 patches_to_export
= [ [cur_rev
] ]
942 # Group the patches into continuous sets
943 for idx
, rev
in enumerate(to_export
[1:]):
944 # Limit the search to the current neighborhood for efficiency
945 master_idx
= revs
[ cur_master_idx
: ].index(rev
)
946 master_idx
+= cur_master_idx
947 if master_idx
== cur_master_idx
+ 1:
948 patches_to_export
[ patchset_idx
].append(rev
)
952 patches_to_export
.append([ rev
])
953 cur_master_idx
= master_idx
956 # Export each patchsets
957 for patchset
in patches_to_export
:
958 cmdoutput
= self
.export_patchset(patchset
[0],
963 patch_with_stat
=True)
964 outlines
.append(cmdoutput
)
965 return '\n'.join(outlines
)
967 def export_patchset(self
, start
, end
, output
="patches", **kwargs
):
968 revarg
= '%s^..%s' % (start
, end
)
969 return self
.git
.format_patch("-o", output
, revarg
, **kwargs
)
971 def current_branch(self
):
972 """Parses 'git branch' to find the current branch."""
973 branches
= self
.git
.branch().splitlines()
974 for branch
in branches
:
975 if branch
.startswith('* '):
976 return branch
.lstrip('* ')
977 return 'Detached HEAD'
979 def create_branch(self
, name
, base
, track
=False):
980 """Creates a branch starting from base. Pass track=True
981 to create a remote tracking branch."""
982 return self
.git
.branch(name
, base
, track
=track
)
984 def cherry_pick_list(self
, revs
, **kwargs
):
985 """Cherry-picks each revision into the current branch.
986 Returns a list of command output strings (1 per cherry pick)"""
991 cherries
.append(self
.git
.cherry_pick(rev
, **kwargs
))
992 return '\n'.join(cherries
)
994 def parse_stash_list(self
, revids
=False):
995 """Parses "git stash list" and returns a list of stashes."""
996 stashes
= self
.git
.stash("list").splitlines()
998 return [ s
[:s
.index(':')] for s
in stashes
]
1000 return [ s
[s
.index(':')+1:] for s
in stashes
]
1003 return self
.git
.diff(
1005 unified
=self
.diff_context
,
1008 def pad(self
, pstr
, num
=22):
1009 topad
= num
-len(pstr
)
1011 return pstr
+ ' '*topad
1015 def describe(self
, revid
, descr
):
1016 version
= self
.git
.describe(revid
, tags
=True, always
=True,
1018 return version
+ ' - ' + descr
1020 def update_revision_lists(self
, filename
=None, show_versions
=False):
1021 num_results
= self
.get_num_results()
1023 rev_list
= self
.git
.log('--', filename
,
1024 max_count
=num_results
,
1027 rev_list
= self
.git
.log(max_count
=num_results
,
1028 pretty
='oneline', all
=True)
1030 commit_list
= self
.parse_rev_list(rev_list
)
1031 commit_list
.reverse()
1032 commits
= map(lambda x
: x
[0], commit_list
)
1033 descriptions
= map(lambda x
: x
[1], commit_list
)
1035 fancy_descr_list
= map(lambda x
: self
.describe(*x
), commit_list
)
1036 self
.set_descriptions_start(fancy_descr_list
)
1037 self
.set_descriptions_end(fancy_descr_list
)
1039 self
.set_descriptions_start(descriptions
)
1040 self
.set_descriptions_end(descriptions
)
1042 self
.set_revisions_start(commits
)
1043 self
.set_revisions_end(commits
)
1047 def get_changed_files(self
, start
, end
):
1048 zfiles_str
= self
.git
.diff('%s..%s' % (start
, end
),
1049 name_only
=True, z
=True).strip('\0')
1050 return [ enc
.decode('utf-8')
1051 for enc
in zfiles_str
.split('\0') if enc
]
1053 def get_renamed_files(self
, start
, end
):
1055 difflines
= self
.git
.diff('%s..%s' % (start
, end
), M
=True).splitlines()
1056 return [ eval_path(r
[12:].rstrip())
1057 for r
in difflines
if r
.startswith('rename from ') ]