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
151 show_untracked
= True,
153 #####################################################
154 # Used by the create branch dialog
157 remote_branches
= [],
160 #####################################################
161 # Used by the commit/repo browser
166 # These are parallel lists
171 # All items below here are re-calculated in
172 # init_browser_data()
174 directory_entries
= {},
176 # These are also parallel lists
182 def __init_config_data(self
):
183 """Reads git config --list and creates parameters
185 # These parameters are saved in .gitconfig,
186 # so ideally these should be as short as possible.
188 # config items that are controllable globally
190 self
.__local
_and
_global
_defaults
= {
193 'merge_summary': False,
194 'merge_diffstat': True,
195 'merge_verbosity': 2,
196 'gui_diffcontext': 3,
197 'gui_pruneduringfetch': False,
199 # config items that are purely git config --global settings
200 self
.__global
_defaults
= {
203 'cola_fontuisize': 12,
205 'cola_fontdiffsize': 12,
206 'cola_savewindowsettings': False,
207 'merge_keepbackup': True,
208 'merge_tool': os
.getenv('MERGETOOL', 'xxdiff'),
209 'gui_editor': os
.getenv('EDITOR', 'gvim'),
210 'gui_historybrowser': 'gitk',
213 local_dict
= self
.config_dict(local
=True)
214 global_dict
= self
.config_dict(local
=False)
216 for k
,v
in local_dict
.iteritems():
217 self
.set_param('local_'+k
, v
)
218 for k
,v
in global_dict
.iteritems():
219 self
.set_param('global_'+k
, v
)
220 if k
not in local_dict
:
222 self
.set_param('local_'+k
, v
)
224 # Bootstrap the internal font*size variables
225 for param
in ('global_cola_fontui', 'global_cola_fontdiff'):
227 if hasattr(self
, param
):
228 font
= self
.get_param(param
)
231 size
= int(font
.split(',')[1])
232 self
.set_param(param
+'size', size
)
233 param
= param
[len('global_'):]
234 global_dict
[param
] = font
235 global_dict
[param
+'size'] = size
237 # Load defaults for all undefined items
238 local_and_global_defaults
= self
.__local
_and
_global
_defaults
239 for k
,v
in local_and_global_defaults
.iteritems():
240 if k
not in local_dict
:
241 self
.set_param('local_'+k
, v
)
242 if k
not in global_dict
:
243 self
.set_param('global_'+k
, v
)
245 global_defaults
= self
.__global
_defaults
246 for k
,v
in global_defaults
.iteritems():
247 if k
not in global_dict
:
248 self
.set_param('global_'+k
, v
)
250 # Load the diff context
251 self
.diff_context
= self
.local_gui_diffcontext
253 def get_global_config(self
, key
):
254 return getattr(self
, 'global_'+key
.replace('.', '_'))
256 def get_cola_config(self
, key
):
257 return getattr(self
, 'global_cola_'+key
)
259 def get_gui_config(self
, key
):
260 return getattr(self
, 'global_gui_'+key
)
262 def get_default_remote(self
):
263 branch
= self
.get_currentbranch()
264 branchconfig
= 'local_branch_%s_remote' % branch
265 if branchconfig
in self
.get_param_names():
266 remote
= self
.get_param(branchconfig
)
271 def get_corresponding_remote_ref(self
):
272 remote
= self
.get_default_remote()
273 branch
= self
.get_currentbranch()
274 best_match
= '%s/%s' % (remote
, branch
)
275 remote_branches
= self
.get_remote_branches()
276 if not remote_branches
:
278 for rb
in remote_branches
:
281 return remote_branches
[0]
283 def get_diff_filenames(self
, arg
):
284 diff_zstr
= self
.git
.diff(arg
, name_only
=True, z
=True).rstrip('\0')
285 return [ f
.decode('utf-8') for f
in diff_zstr
.split('\0') if f
]
287 def branch_list(self
, remote
=False):
288 branches
= map(lambda x
: x
.lstrip('* '),
289 self
.git
.branch(r
=remote
).splitlines())
292 for branch
in branches
:
293 if branch
.endswith('/HEAD'):
295 remotes
.append(branch
)
299 def get_config_params(self
):
301 params
.extend(map(lambda x
: 'local_' + x
,
302 self
.__local
_and
_global
_defaults
.keys()))
303 params
.extend(map(lambda x
: 'global_' + x
,
304 self
.__local
_and
_global
_defaults
.keys()))
305 params
.extend(map(lambda x
: 'global_' + x
,
306 self
.__global
_defaults
.keys()))
307 return [ p
for p
in params
if not p
.endswith('size') ]
309 def save_config_param(self
, param
):
310 if param
not in self
.get_config_params():
312 value
= self
.get_param(param
)
313 if param
== 'local_gui_diffcontext':
314 self
.diff_context
= value
315 if param
.startswith('local_'):
316 param
= param
[len('local_'):]
318 elif param
.startswith('global_'):
319 param
= param
[len('global_'):]
322 raise Exception("Invalid param '%s' passed to " % param
323 +'save_config_param()')
324 param
= param
.replace('_', '.') # model -> git
325 return self
.config_set(param
, value
, local
=is_local
)
327 def init_browser_data(self
):
328 """This scans over self.(names, sha1s, types) to generate
329 directories, directory_entries, and subtree_*"""
331 # Collect data for the model
332 if not self
.get_currentbranch(): return
334 self
.subtree_types
= []
335 self
.subtree_sha1s
= []
336 self
.subtree_names
= []
337 self
.directories
= []
338 self
.directory_entries
= {}
340 # Lookup the tree info
341 tree_info
= self
.parse_ls_tree(self
.get_currentbranch())
343 self
.set_types(map( lambda(x
): x
[1], tree_info
))
344 self
.set_sha1s(map( lambda(x
): x
[2], tree_info
))
345 self
.set_names(map( lambda(x
): x
[3], tree_info
))
347 if self
.directory
: self
.directories
.append('..')
349 dir_entries
= self
.directory_entries
350 dir_regex
= re
.compile('([^/]+)/')
354 for idx
, name
in enumerate(self
.names
):
355 if not name
.startswith(self
.directory
):
357 name
= name
[ len(self
.directory
): ]
359 # This is a directory...
360 match
= dir_regex
.match(name
)
363 dirent
= match
.group(1) + '/'
364 if dirent
not in self
.directory_entries
:
365 self
.directory_entries
[dirent
] = []
367 if dirent
not in dirs_seen
:
368 dirs_seen
[dirent
] = True
369 self
.directories
.append(dirent
)
371 entry
= name
.replace(dirent
, '')
372 entry_match
= dir_regex
.match(entry
)
374 subdir
= entry_match
.group(1) + '/'
375 if subdir
in subdirs_seen
:
377 subdirs_seen
[subdir
] = True
378 dir_entries
[dirent
].append(subdir
)
380 dir_entries
[dirent
].append(entry
)
382 self
.subtree_types
.append(self
.types
[idx
])
383 self
.subtree_sha1s
.append(self
.sha1s
[idx
])
384 self
.subtree_names
.append(name
)
386 def add_or_remove(self
, *to_process
):
387 """Invokes 'git add' to index the filenames in to_process that exist
388 and 'git rm' for those that do not exist."""
391 return 'No files to add or remove.'
396 for filename
in to_process
:
397 encfilename
= filename
.encode('utf-8')
398 if os
.path
.exists(encfilename
):
399 to_add
.append(filename
)
402 output
= self
.git
.add(v
=True, *to_add
)
406 if len(to_add
) == len(to_process
):
407 # to_process only contained unremoved files --
408 # short-circuit the removal checks
411 # Process files to remote
412 for filename
in to_process
:
413 if not os
.path
.exists(filename
):
414 to_remove
.append(filename
)
415 output
+ '\n\n' + self
.git
.rm(*to_remove
)
417 def get_editor(self
):
418 return self
.get_gui_config('editor')
420 def get_mergetool(self
):
421 return self
.get_global_config('merge.tool')
423 def get_history_browser(self
):
424 return self
.get_gui_config('historybrowser')
426 def remember_gui_settings(self
):
427 return self
.get_cola_config('savewindowsettings')
429 def get_tree_node(self
, idx
):
430 return (self
.get_types()[idx
],
431 self
.get_sha1s()[idx
],
432 self
.get_names()[idx
] )
434 def get_subtree_node(self
, idx
):
435 return (self
.get_subtree_types()[idx
],
436 self
.get_subtree_sha1s()[idx
],
437 self
.get_subtree_names()[idx
] )
439 def get_all_branches(self
):
440 return (self
.get_local_branches() + self
.get_remote_branches())
442 def set_remote(self
, remote
):
445 self
.set_param('remote', remote
)
446 branches
= utils
.grep('%s/\S+$' % remote
,
447 self
.branch_list(remote
=True),
449 self
.set_remote_branches(branches
)
451 def add_signoff(self
,*rest
):
452 """Adds a standard Signed-off by: tag to the end
453 of the current commit message."""
454 msg
= self
.get_commitmsg()
455 signoff
=('\n\nSigned-off-by: %s <%s>\n'
456 % (self
.get_local_user_name(), self
.get_local_user_email()))
457 if signoff
not in msg
:
458 self
.set_commitmsg(msg
+ signoff
)
460 def apply_diff(self
, filename
):
461 return self
.git
.apply(filename
, index
=True, cached
=True)
463 def apply_diff_to_worktree(self
, filename
):
464 return self
.git
.apply(filename
)
466 def load_commitmsg(self
, path
):
467 file = open(path
, 'r')
468 contents
= file.read().decode('utf-8')
470 self
.set_commitmsg(contents
)
472 def get_prev_commitmsg(self
,*rest
):
473 """Queries git for the latest commit message and sets it in
476 commit_lines
= self
.git
.show('HEAD').decode('utf-8').split('\n')
477 for idx
, msg
in enumerate(commit_lines
):
481 if msg
.startswith('diff --git'):
484 commit_msg
.append(msg
)
485 self
.set_commitmsg('\n'.join(commit_msg
).rstrip())
487 def load_commitmsg_template(self
):
489 template
= self
.get_global_config('commit.template')
490 except AttributeError:
492 self
.load_commitmsg(template
)
494 def update_status(self
, amend
=False):
495 # This allows us to defer notification until the
496 # we finish processing data
497 notify_enabled
= self
.get_notify()
498 self
.set_notify(False)
500 # Reset the staged and unstaged model lists
501 # NOTE: the model's unstaged list is used to
502 # hold both modified and untracked files.
507 # Read git status items
511 unmerged_items
) = self
.get_workdir_state(amend
=amend
)
513 # Gather items to be committed
514 for staged
in staged_items
:
515 if staged
not in self
.get_staged():
516 self
.add_staged(staged
)
518 # Gather unindexed items
519 for modified
in modified_items
:
520 if modified
not in self
.get_modified():
521 self
.add_modified(modified
)
523 # Gather untracked items
524 for untracked
in untracked_items
:
525 if untracked
not in self
.get_untracked():
526 self
.add_untracked(untracked
)
528 # Gather unmerged items
529 for unmerged
in unmerged_items
:
530 if unmerged
not in self
.get_unmerged():
531 self
.add_unmerged(unmerged
)
533 self
.set_currentbranch(self
.current_branch())
534 if self
.get_show_untracked():
535 self
.set_unstaged(self
.get_modified() + self
.get_unmerged() +
536 self
.get_untracked())
538 self
.set_unstaged(self
.get_modified() + self
.get_unmerged())
539 self
.set_remotes(self
.git
.remote().splitlines())
540 self
.set_remote_branches(self
.branch_list(remote
=True))
541 self
.set_local_branches(self
.branch_list(remote
=False))
542 self
.set_tags(self
.git
.tag().splitlines())
543 self
.set_revision('')
544 self
.set_local_branch('')
545 self
.set_remote_branch('')
546 # Re-enable notifications and emit changes
547 self
.set_notify(notify_enabled
)
548 self
.notify_observers('staged','unstaged')
550 def delete_branch(self
, branch
):
551 return self
.git
.branch(branch
, D
=True)
553 def get_revision_sha1(self
, idx
):
554 return self
.get_revisions()[idx
]
556 def apply_font_size(self
, param
, default
):
557 old_font
= self
.get_param(param
)
560 size
= self
.get_param(param
+'size')
561 props
= old_font
.split(',')
563 new_font
= ','.join(props
)
565 self
.set_param(param
, new_font
)
567 def get_commit_diff(self
, sha1
):
568 commit
= self
.git
.show(sha1
)
569 first_newline
= commit
.index('\n')
570 if commit
[first_newline
+1:].startswith('Merge:'):
571 return (commit
+ '\n\n'
572 + self
.diff_helper(commit
=sha1
,
574 suppress_header
=False))
578 def get_filename(self
, idx
, staged
=True):
581 return self
.get_staged()[idx
]
583 return self
.get_unstaged()[idx
]
587 def get_diff_details(self
, idx
, ref
, staged
=True):
588 filename
= self
.get_filename(idx
, staged
=staged
)
590 return (None, None, None)
591 encfilename
= filename
.encode('utf-8')
593 if os
.path
.exists(encfilename
):
594 status
= 'Staged for commit'
596 status
= 'Staged for removal'
597 diff
= self
.diff_helper(filename
=filename
,
601 if os
.path
.isdir(encfilename
):
602 status
= 'Untracked directory'
603 diff
= '\n'.join(os
.listdir(filename
))
605 elif filename
in self
.get_unmerged():
607 diff
= ('@@@+-+-+-+-+-+-+-+-+-+-+ UNMERGED +-+-+-+-+-+-+-+-+-+-+@@@\n\n'
608 '>>> %s is unmerged.\n' % filename
+
609 'Right-click on the filename '
610 'to launch "git mergetool".\n\n\n')
611 diff
+= self
.diff_helper(filename
=filename
,
613 patch_with_raw
=False)
614 elif filename
in self
.get_modified():
615 status
= 'Modified, not staged'
616 diff
= self
.diff_helper(filename
=filename
,
619 status
= 'Untracked, not staged'
620 diff
= 'SHA1: ' + self
.git
.hash_object(filename
)
621 return diff
, status
, filename
623 def stage_modified(self
):
624 output
= self
.git
.add(v
=True, *self
.get_modified())
628 def stage_untracked(self
):
629 output
= self
.git
.add(self
.get_untracked())
633 def reset(self
, *items
):
634 output
= self
.git
.reset('--', *items
)
638 def unstage_all(self
):
639 self
.git
.reset('--', *self
.get_staged())
642 def save_gui_settings(self
):
643 self
.config_set('cola.geometry', utils
.get_geom(), local
=False)
645 def config_set(self
, key
=None, value
=None, local
=True):
646 if key
and value
is not None:
647 # git config category.key value
648 strval
= unicode(value
).encode('utf-8')
649 if type(value
) is bool:
650 # git uses "true" and "false"
651 strval
= strval
.lower()
653 argv
= [ key
, strval
]
655 argv
= [ '--global', key
, strval
]
656 return self
.git
.config(*argv
)
658 msg
= "oops in config_set(key=%s,value=%s,local=%s"
659 raise Exception(msg
% (key
, value
, local
))
661 def config_dict(self
, local
=True):
662 """parses the lines from git config --list into a dictionary"""
666 'global': not local
, # global is a python keyword
668 config_lines
= self
.git
.config(**kwargs
).splitlines()
670 for line
in config_lines
:
671 k
, v
= line
.split('=', 1)
672 k
= k
.replace('.','_') # git -> model
673 if v
== 'true' or v
== 'false':
674 v
= bool(eval(v
.title()))
682 def commit_with_msg(self
, msg
, amend
=False):
683 """Creates a git commit."""
685 if not msg
.endswith('\n'):
687 # Sure, this is a potential "security risk," but if someone
688 # is trying to intercept/re-write commit messages on your system,
689 # then you probably have bigger problems to worry about.
690 tmpfile
= self
.get_tmp_filename()
692 # Create the commit message file
693 fh
= open(tmpfile
, 'w')
698 (status
, stdout
, stderr
) = self
.git
.commit(v
=True,
701 with_extended_output
=True)
704 return (status
, stdout
+stderr
)
708 return self
.git
.diff(unified
=self
.diff_context
,
712 def get_tmp_dir(self
):
713 return os
.environ
.get('TMP', os
.environ
.get('TMPDIR', '/tmp'))
715 def get_tmp_file_pattern(self
):
716 return os
.path
.join(self
.get_tmp_dir(), '*.git.%s.*' % os
.getpid())
718 def get_tmp_filename(self
, prefix
=''):
719 # Allow TMPDIR/TMP with a fallback to /tmp
720 basename
= (prefix
+'.git.%s.%s'
721 % (os
.getpid(), time
.time())).replace(os
.sep
, '-')
722 return os
.path
.join(self
.get_tmp_dir(), basename
)
724 def log_helper(self
, all
=False):
726 Returns a pair of parallel arrays listing the revision sha1's
727 and commit summaries.
731 regex
= REV_LIST_REGEX
732 output
= self
.git
.log(pretty
='oneline', all
=all
)
733 for line
in output
.splitlines():
734 match
= regex
.match(line
)
736 revs
.append(match
.group(1))
737 summaries
.append(match
.group(2))
738 return (revs
, summaries
)
740 def parse_rev_list(self
, raw_revs
):
742 for line
in raw_revs
.splitlines():
743 match
= REV_LIST_REGEX
.match(line
)
745 rev_id
= match
.group(1)
746 summary
= match
.group(2)
747 revs
.append((rev_id
, summary
,))
750 def rev_list_range(self
, start
, end
):
751 range = '%s..%s' % (start
, end
)
752 raw_revs
= self
.git
.rev_list(range, pretty
='oneline')
753 return self
.parse_rev_list(raw_revs
)
755 def diff_helper(self
,
762 with_diff_header
=False,
763 suppress_header
=True,
765 patch_with_raw
=True):
766 "Invokes git diff on a filepath."
768 ref
, endref
= commit
+'^', commit
771 argv
.append('%s..%s' % (ref
, endref
))
779 if type(filename
) is list:
780 argv
.extend(filename
)
782 argv
.append(filename
)
786 del_tag
= 'deleted file mode '
789 deleted
= cached
and not os
.path
.exists(filename
.encode('utf-8'))
791 diffoutput
= self
.git
.diff(R
=reverse
,
793 patch_with_raw
=patch_with_raw
,
794 unified
=self
.diff_context
,
795 with_raw_output
=True,
797 diff
= diffoutput
.splitlines()
799 line
= unicode(line
.decode('utf-8'))
800 if not start
and '@@' == line
[:2] and '@@' in line
[2:]:
802 if start
or(deleted
and del_tag
in line
):
803 output
.write(line
.encode('utf-8') + '\n')
807 elif not suppress_header
:
808 output
.write(line
.encode('utf-8') + '\n')
810 result
= output
.getvalue().decode('utf-8')
814 return('\n'.join(headers
), result
)
818 def git_repo_path(self
, *subpaths
):
819 paths
= [ self
.git
.get_git_dir() ]
820 paths
.extend(subpaths
)
821 return os
.path
.realpath(os
.path
.join(*paths
))
823 def get_merge_message_path(self
):
824 for file in ('MERGE_MSG', 'SQUASH_MSG'):
825 path
= self
.git_repo_path(file)
826 if os
.path
.exists(path
):
830 def get_merge_message(self
):
831 return self
.git
.fmt_merge_msg('--file',
832 self
.git_repo_path('FETCH_HEAD'))
834 def abort_merge(self
):
836 output
= self
.git
.read_tree('HEAD', reset
=True, u
=True, v
=True)
838 merge_head
= self
.git_repo_path('MERGE_HEAD')
839 if os
.path
.exists(merge_head
):
840 os
.unlink(merge_head
)
841 # remove MERGE_MESSAGE, etc.
842 merge_msg_path
= self
.get_merge_message_path()
843 while merge_msg_path
:
844 os
.unlink(merge_msg_path
)
845 merge_msg_path
= self
.get_merge_message_path()
847 def get_workdir_state(self
, amend
=False):
848 """RETURNS: A tuple of staged, unstaged untracked, and unmerged
851 self
.partially_staged
= set()
855 (staged
, unstaged
, unmerged
, untracked
) = ([], [], [], [])
857 for idx
, line
in enumerate(self
.git
.diff_index(head
).splitlines()):
858 rest
, name
= line
.split('\t')
860 name
= eval_path(name
)
861 if status
== 'M' or status
== 'D':
862 unstaged
.append(name
)
864 for idx
, line
in enumerate(self
.git
.diff_index(head
, cached
=True)
866 rest
, name
= line
.split('\t')
868 name
= eval_path(name
)
871 # is this file partially staged?
872 diff
= self
.git
.diff('--', name
, name_only
=True, z
=True)
874 unstaged
.remove(name
)
876 self
.partially_staged
.add(name
)
881 unstaged
.remove(name
)
883 unmerged
.append(name
)
885 for line
in self
.git
.ls_files(others
=True, exclude_standard
=True,
888 untracked
.append(line
.decode('utf-8'))
890 return (staged
, unstaged
, untracked
, unmerged
)
892 def reset_helper(self
, *args
, **kwargs
):
893 return self
.git
.reset('--', *args
, **kwargs
)
895 def remote_url(self
, name
):
896 return self
.git
.config('remote.%s.url' % name
, get
=True)
898 def get_remote_args(self
, remote
,
899 local_branch
='', remote_branch
='',
900 ffwd
=True, tags
=False):
902 branch_arg
= '%s:%s' % ( remote_branch
, local_branch
)
904 branch_arg
= '+%s:%s' % ( remote_branch
, local_branch
)
906 if local_branch
and remote_branch
:
907 args
.append(branch_arg
)
912 return (args
, kwargs
)
914 def gen_remote_helper(self
, gitaction
):
915 """Generates a closure that calls git fetch, push or pull
917 def remote_helper(remote
, **kwargs
):
918 args
, kwargs
= self
.get_remote_args(remote
, **kwargs
)
919 return gitaction(*args
, **kwargs
)
922 def parse_ls_tree(self
, rev
):
923 """Returns a list of(mode, type, sha1, path) tuples."""
924 lines
= self
.git
.ls_tree(rev
, r
=True).splitlines()
926 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
928 match
= regex
.match(line
)
930 mode
= match
.group(1)
931 objtype
= match
.group(2)
932 sha1
= match
.group(3)
933 filename
= match
.group(4)
934 output
.append((mode
, objtype
, sha1
, filename
,) )
937 def format_patch_helper(self
, to_export
, revs
, output
='patches'):
938 """writes patches named by to_export to the output directory."""
942 cur_rev
= to_export
[0]
943 cur_master_idx
= revs
.index(cur_rev
)
945 patches_to_export
= [ [cur_rev
] ]
948 # Group the patches into continuous sets
949 for idx
, rev
in enumerate(to_export
[1:]):
950 # Limit the search to the current neighborhood for efficiency
951 master_idx
= revs
[ cur_master_idx
: ].index(rev
)
952 master_idx
+= cur_master_idx
953 if master_idx
== cur_master_idx
+ 1:
954 patches_to_export
[ patchset_idx
].append(rev
)
958 patches_to_export
.append([ rev
])
959 cur_master_idx
= master_idx
962 # Export each patchsets
963 for patchset
in patches_to_export
:
964 cmdoutput
= self
.export_patchset(patchset
[0],
969 patch_with_stat
=True)
970 outlines
.append(cmdoutput
)
971 return '\n'.join(outlines
)
973 def export_patchset(self
, start
, end
, output
="patches", **kwargs
):
974 revarg
= '%s^..%s' % (start
, end
)
975 return self
.git
.format_patch("-o", output
, revarg
, **kwargs
)
977 def current_branch(self
):
978 """Parses 'git branch' to find the current branch."""
979 branches
= self
.git
.branch().splitlines()
980 for branch
in branches
:
981 if branch
.startswith('* '):
982 return branch
.lstrip('* ')
983 return 'Detached HEAD'
985 def create_branch(self
, name
, base
, track
=False):
986 """Creates a branch starting from base. Pass track=True
987 to create a remote tracking branch."""
988 return self
.git
.branch(name
, base
, track
=track
)
990 def cherry_pick_list(self
, revs
, **kwargs
):
991 """Cherry-picks each revision into the current branch.
992 Returns a list of command output strings (1 per cherry pick)"""
997 cherries
.append(self
.git
.cherry_pick(rev
, **kwargs
))
998 return '\n'.join(cherries
)
1000 def parse_stash_list(self
, revids
=False):
1001 """Parses "git stash list" and returns a list of stashes."""
1002 stashes
= self
.git
.stash("list").splitlines()
1004 return [ s
[:s
.index(':')] for s
in stashes
]
1006 return [ s
[s
.index(':')+1:] for s
in stashes
]
1009 return self
.git
.diff(
1011 unified
=self
.diff_context
,
1014 def pad(self
, pstr
, num
=22):
1015 topad
= num
-len(pstr
)
1017 return pstr
+ ' '*topad
1021 def describe(self
, revid
, descr
):
1022 version
= self
.git
.describe(revid
, tags
=True, always
=True,
1024 return version
+ ' - ' + descr
1026 def update_revision_lists(self
, filename
=None, show_versions
=False):
1027 num_results
= self
.get_num_results()
1029 rev_list
= self
.git
.log('--', filename
,
1030 max_count
=num_results
,
1033 rev_list
= self
.git
.log(max_count
=num_results
,
1034 pretty
='oneline', all
=True)
1036 commit_list
= self
.parse_rev_list(rev_list
)
1037 commit_list
.reverse()
1038 commits
= map(lambda x
: x
[0], commit_list
)
1039 descriptions
= map(lambda x
: x
[1], commit_list
)
1041 fancy_descr_list
= map(lambda x
: self
.describe(*x
), commit_list
)
1042 self
.set_descriptions_start(fancy_descr_list
)
1043 self
.set_descriptions_end(fancy_descr_list
)
1045 self
.set_descriptions_start(descriptions
)
1046 self
.set_descriptions_end(descriptions
)
1048 self
.set_revisions_start(commits
)
1049 self
.set_revisions_end(commits
)
1053 def get_changed_files(self
, start
, end
):
1054 zfiles_str
= self
.git
.diff('%s..%s' % (start
, end
),
1055 name_only
=True, z
=True).strip('\0')
1056 return [ enc
.decode('utf-8')
1057 for enc
in zfiles_str
.split('\0') if enc
]
1059 def get_renamed_files(self
, start
, end
):
1061 difflines
= self
.git
.diff('%s..%s' % (start
, end
), M
=True).splitlines()
1062 return [ eval_path(r
[12:].rstrip())
1063 for r
in difflines
if r
.startswith('rename from ') ]