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 get_work_tree(self
):
32 return self
._work
_tree
35 curdir
= self
._git
_dir
39 if self
._is
_git
_dir
(os
.path
.join(curdir
, '.git')):
42 # Handle bare repositories
43 if (len(os
.path
.basename(curdir
)) > 4
44 and curdir
.endswith('.git')):
46 if 'GIT_WORK_TREE' in os
.environ
:
47 self
._work
_tree
= os
.getenv('GIT_WORK_TREE')
48 if not self
._work
_tree
or not os
.path
.isdir(self
._work
_tree
):
50 gitparent
= os
.path
.join(os
.path
.abspath(self
._git
_dir
), '..')
51 self
._work
_tree
= os
.path
.abspath(gitparent
)
52 self
.set_cwd(self
._work
_tree
)
53 return self
._work
_tree
56 return self
._git
_dir
and self
._is
_git
_dir
(self
._git
_dir
)
58 def get_git_dir(self
):
61 if 'GIT_DIR' in os
.environ
:
62 self
._git
_dir
= os
.getenv('GIT_DIR')
64 curpath
= os
.path
.abspath(self
._git
_dir
)
66 curpath
= os
.path
.abspath(os
.getcwd())
67 # Search for a .git directory
69 if self
._is
_git
_dir
(curpath
):
70 self
._git
_dir
= curpath
72 gitpath
= os
.path
.join(curpath
, '.git')
73 if self
._is
_git
_dir
(gitpath
):
74 self
._git
_dir
= gitpath
76 curpath
, dummy
= os
.path
.split(curpath
)
81 def _is_git_dir(self
, d
):
82 """ This is taken from the git setup.c:is_git_directory
85 and os
.path
.isdir(os
.path
.join(d
, 'objects'))
86 and os
.path
.isdir(os
.path
.join(d
, 'refs'))):
87 headref
= os
.path
.join(d
, 'HEAD')
88 return (os
.path
.isfile(headref
)
89 or (os
.path
.islink(headref
)
90 and os
.readlink(headref
).startswith('refs')))
94 """handles quoted paths."""
95 if path
.startswith('"') and path
.endswith('"'):
96 return eval(path
).decode('utf-8')
100 class Model(model
.Model
):
101 """Provides a friendly wrapper for doing commit git operations."""
104 worktree
= self
.git
.get_work_tree()
105 clone
= model
.Model
.clone(self
)
106 clone
.use_worktree(worktree
)
109 def use_worktree(self
, worktree
):
110 self
.git
.load_worktree(worktree
)
111 is_valid
= self
.git
.is_valid()
113 self
.__init
_config
_data
()
117 """Reads git repository settings and sets several methods
118 so that they refer to the git module. This object
119 encapsulates cola's interaction with git."""
121 # Initialize the git command object
123 self
.partially_staged
= set()
125 self
.fetch_helper
= self
.gen_remote_helper(self
.git
.fetch
)
126 self
.push_helper
= self
.gen_remote_helper(self
.git
.push
)
127 self
.pull_helper
= self
.gen_remote_helper(self
.git
.pull
)
130 #####################################################
131 # Used in various places
139 #####################################################
140 # Used primarily by the main UI
147 show_untracked
= True,
149 #####################################################
150 # Used by the create branch dialog
153 remote_branches
= [],
156 #####################################################
157 # Used by the commit/repo browser
162 # These are parallel lists
167 # All items below here are re-calculated in
168 # init_browser_data()
170 directory_entries
= {},
172 # These are also parallel lists
178 def __init_config_data(self
):
179 """Reads git config --list and creates parameters
181 # These parameters are saved in .gitconfig,
182 # so ideally these should be as short as possible.
184 # config items that are controllable globally
186 self
.__local
_and
_global
_defaults
= {
189 'merge_summary': False,
190 'merge_diffstat': True,
191 'merge_verbosity': 2,
192 'gui_diffcontext': 3,
193 'gui_pruneduringfetch': False,
195 # config items that are purely git config --global settings
196 self
.__global
_defaults
= {
199 'cola_fontuisize': 12,
201 'cola_fontdiffsize': 12,
202 'cola_savewindowsettings': False,
203 'merge_keepbackup': True,
204 'merge_tool': os
.getenv('MERGETOOL', 'xxdiff'),
205 'gui_editor': os
.getenv('EDITOR', 'gvim'),
206 'gui_historybrowser': 'gitk',
209 local_dict
= self
.config_dict(local
=True)
210 global_dict
= self
.config_dict(local
=False)
212 for k
,v
in local_dict
.iteritems():
213 self
.set_param('local_'+k
, v
)
214 for k
,v
in global_dict
.iteritems():
215 self
.set_param('global_'+k
, v
)
216 if k
not in local_dict
:
218 self
.set_param('local_'+k
, v
)
220 # Bootstrap the internal font*size variables
221 for param
in ('global_cola_fontui', 'global_cola_fontdiff'):
223 if hasattr(self
, param
):
224 font
= self
.get_param(param
)
227 size
= int(font
.split(',')[1])
228 self
.set_param(param
+'size', size
)
229 param
= param
[len('global_'):]
230 global_dict
[param
] = font
231 global_dict
[param
+'size'] = size
233 # Load defaults for all undefined items
234 local_and_global_defaults
= self
.__local
_and
_global
_defaults
235 for k
,v
in local_and_global_defaults
.iteritems():
236 if k
not in local_dict
:
237 self
.set_param('local_'+k
, v
)
238 if k
not in global_dict
:
239 self
.set_param('global_'+k
, v
)
241 global_defaults
= self
.__global
_defaults
242 for k
,v
in global_defaults
.iteritems():
243 if k
not in global_dict
:
244 self
.set_param('global_'+k
, v
)
246 # Load the diff context
247 self
.diff_context
= self
.local_gui_diffcontext
249 def get_global_config(self
, key
):
250 return getattr(self
, 'global_'+key
.replace('.', '_'))
252 def get_cola_config(self
, key
):
253 return getattr(self
, 'global_cola_'+key
)
255 def get_gui_config(self
, key
):
256 return getattr(self
, 'global_gui_'+key
)
258 def get_default_remote(self
):
259 branch
= self
.get_currentbranch()
260 branchconfig
= 'local_branch_%s_remote' % branch
261 if branchconfig
in self
.get_param_names():
262 remote
= self
.get_param(branchconfig
)
267 def get_corresponding_remote_ref(self
):
268 remote
= self
.get_default_remote()
269 branch
= self
.get_currentbranch()
270 best_match
= '%s/%s' % (remote
, branch
)
271 remote_branches
= self
.get_remote_branches()
272 if not remote_branches
:
274 for rb
in remote_branches
:
277 return remote_branches
[0]
279 def get_diff_filenames(self
, arg
):
280 diff_zstr
= self
.git
.diff(arg
, name_only
=True, z
=True).rstrip('\0')
281 return [ f
.decode('utf-8') for f
in diff_zstr
.split('\0') if f
]
283 def branch_list(self
, remote
=False):
284 branches
= map(lambda x
: x
.lstrip('* '),
285 self
.git
.branch(r
=remote
).splitlines())
288 for branch
in branches
:
289 if branch
.endswith('/HEAD'):
291 remotes
.append(branch
)
295 def get_config_params(self
):
297 params
.extend(map(lambda x
: 'local_' + x
,
298 self
.__local
_and
_global
_defaults
.keys()))
299 params
.extend(map(lambda x
: 'global_' + x
,
300 self
.__local
_and
_global
_defaults
.keys()))
301 params
.extend(map(lambda x
: 'global_' + x
,
302 self
.__global
_defaults
.keys()))
303 return [ p
for p
in params
if not p
.endswith('size') ]
305 def save_config_param(self
, param
):
306 if param
not in self
.get_config_params():
308 value
= self
.get_param(param
)
309 if param
== 'local_gui_diffcontext':
310 self
.diff_context
= value
311 if param
.startswith('local_'):
312 param
= param
[len('local_'):]
314 elif param
.startswith('global_'):
315 param
= param
[len('global_'):]
318 raise Exception("Invalid param '%s' passed to " % param
319 +'save_config_param()')
320 param
= param
.replace('_', '.') # model -> git
321 return self
.config_set(param
, value
, local
=is_local
)
323 def init_browser_data(self
):
324 """This scans over self.(names, sha1s, types) to generate
325 directories, directory_entries, and subtree_*"""
327 # Collect data for the model
328 if not self
.get_currentbranch(): return
330 self
.subtree_types
= []
331 self
.subtree_sha1s
= []
332 self
.subtree_names
= []
333 self
.directories
= []
334 self
.directory_entries
= {}
336 # Lookup the tree info
337 tree_info
= self
.parse_ls_tree(self
.get_currentbranch())
339 self
.set_types(map( lambda(x
): x
[1], tree_info
))
340 self
.set_sha1s(map( lambda(x
): x
[2], tree_info
))
341 self
.set_names(map( lambda(x
): x
[3], tree_info
))
343 if self
.directory
: self
.directories
.append('..')
345 dir_entries
= self
.directory_entries
346 dir_regex
= re
.compile('([^/]+)/')
350 for idx
, name
in enumerate(self
.names
):
351 if not name
.startswith(self
.directory
):
353 name
= name
[ len(self
.directory
): ]
355 # This is a directory...
356 match
= dir_regex
.match(name
)
359 dirent
= match
.group(1) + '/'
360 if dirent
not in self
.directory_entries
:
361 self
.directory_entries
[dirent
] = []
363 if dirent
not in dirs_seen
:
364 dirs_seen
[dirent
] = True
365 self
.directories
.append(dirent
)
367 entry
= name
.replace(dirent
, '')
368 entry_match
= dir_regex
.match(entry
)
370 subdir
= entry_match
.group(1) + '/'
371 if subdir
in subdirs_seen
:
373 subdirs_seen
[subdir
] = True
374 dir_entries
[dirent
].append(subdir
)
376 dir_entries
[dirent
].append(entry
)
378 self
.subtree_types
.append(self
.types
[idx
])
379 self
.subtree_sha1s
.append(self
.sha1s
[idx
])
380 self
.subtree_names
.append(name
)
382 def add_or_remove(self
, *to_process
):
383 """Invokes 'git add' to index the filenames in to_process that exist
384 and 'git rm' for those that do not exist."""
387 return 'No files to add or remove.'
392 for filename
in to_process
:
393 encfilename
= filename
.encode('utf-8')
394 if os
.path
.exists(encfilename
):
395 to_add
.append(filename
)
398 output
= self
.git
.add(v
=True, *to_add
)
402 if len(to_add
) == len(to_process
):
403 # to_process only contained unremoved files --
404 # short-circuit the removal checks
407 # Process files to remote
408 for filename
in to_process
:
409 if not os
.path
.exists(filename
):
410 to_remove
.append(filename
)
411 output
+ '\n\n' + self
.git
.rm(*to_remove
)
413 def get_editor(self
):
414 return self
.get_gui_config('editor')
416 def get_mergetool(self
):
417 return self
.get_global_config('merge.tool')
419 def get_history_browser(self
):
420 return self
.get_gui_config('historybrowser')
422 def remember_gui_settings(self
):
423 return self
.get_cola_config('savewindowsettings')
425 def get_tree_node(self
, idx
):
426 return (self
.get_types()[idx
],
427 self
.get_sha1s()[idx
],
428 self
.get_names()[idx
] )
430 def get_subtree_node(self
, idx
):
431 return (self
.get_subtree_types()[idx
],
432 self
.get_subtree_sha1s()[idx
],
433 self
.get_subtree_names()[idx
] )
435 def get_all_branches(self
):
436 return (self
.get_local_branches() + self
.get_remote_branches())
438 def set_remote(self
, remote
):
441 self
.set_param('remote', remote
)
442 branches
= utils
.grep('%s/\S+$' % remote
,
443 self
.branch_list(remote
=True),
445 self
.set_remote_branches(branches
)
447 def add_signoff(self
,*rest
):
448 """Adds a standard Signed-off by: tag to the end
449 of the current commit message."""
450 msg
= self
.get_commitmsg()
451 signoff
=('\n\nSigned-off-by: %s <%s>\n'
452 % (self
.get_local_user_name(), self
.get_local_user_email()))
453 if signoff
not in msg
:
454 self
.set_commitmsg(msg
+ signoff
)
456 def apply_diff(self
, filename
):
457 return self
.git
.apply(filename
, index
=True, cached
=True)
459 def apply_diff_to_worktree(self
, filename
):
460 return self
.git
.apply(filename
)
462 def load_commitmsg(self
, path
):
463 file = open(path
, 'r')
464 contents
= file.read().decode('utf-8')
466 self
.set_commitmsg(contents
)
468 def get_prev_commitmsg(self
,*rest
):
469 """Queries git for the latest commit message and sets it in
472 commit_lines
= self
.git
.show('HEAD').decode('utf-8').split('\n')
473 for idx
, msg
in enumerate(commit_lines
):
477 if msg
.startswith('diff --git'):
480 commit_msg
.append(msg
)
481 self
.set_commitmsg('\n'.join(commit_msg
).rstrip())
483 def load_commitmsg_template(self
):
485 template
= self
.get_global_config('commit.template')
486 except AttributeError:
488 self
.load_commitmsg(template
)
490 def update_status(self
, amend
=False):
491 # This allows us to defer notification until the
492 # we finish processing data
493 notify_enabled
= self
.get_notify()
494 self
.set_notify(False)
496 # Reset the staged and unstaged model lists
497 # NOTE: the model's unstaged list is used to
498 # hold both modified and untracked files.
503 # Read git status items
507 unmerged_items
) = self
.get_workdir_state(amend
=amend
)
509 # Gather items to be committed
510 for staged
in staged_items
:
511 if staged
not in self
.get_staged():
512 self
.add_staged(staged
)
514 # Gather unindexed items
515 for modified
in modified_items
:
516 if modified
not in self
.get_modified():
517 self
.add_modified(modified
)
519 # Gather untracked items
520 for untracked
in untracked_items
:
521 if untracked
not in self
.get_untracked():
522 self
.add_untracked(untracked
)
524 # Gather unmerged items
525 for unmerged
in unmerged_items
:
526 if unmerged
not in self
.get_unmerged():
527 self
.add_unmerged(unmerged
)
529 self
.set_currentbranch(self
.current_branch())
530 if self
.get_show_untracked():
531 self
.set_unstaged(self
.get_modified() + self
.get_unmerged() +
532 self
.get_untracked())
534 self
.set_unstaged(self
.get_modified() + self
.get_unmerged())
535 self
.set_remotes(self
.git
.remote().splitlines())
536 self
.set_remote_branches(self
.branch_list(remote
=True))
537 self
.set_local_branches(self
.branch_list(remote
=False))
538 self
.set_tags(self
.git
.tag().splitlines())
539 self
.set_revision('')
540 self
.set_local_branch('')
541 self
.set_remote_branch('')
542 # Re-enable notifications and emit changes
543 self
.set_notify(notify_enabled
)
544 self
.notify_observers('staged','unstaged')
546 def delete_branch(self
, branch
):
547 return self
.git
.branch(branch
, D
=True)
549 def get_revision_sha1(self
, idx
):
550 return self
.get_revisions()[idx
]
552 def apply_font_size(self
, param
, default
):
553 old_font
= self
.get_param(param
)
556 size
= self
.get_param(param
+'size')
557 props
= old_font
.split(',')
559 new_font
= ','.join(props
)
561 self
.set_param(param
, new_font
)
563 def get_commit_diff(self
, sha1
):
564 commit
= self
.git
.show(sha1
)
565 first_newline
= commit
.index('\n')
566 if commit
[first_newline
+1:].startswith('Merge:'):
567 return (commit
+ '\n\n'
568 + self
.diff_helper(commit
=sha1
,
570 suppress_header
=False))
574 def get_filename(self
, idx
, staged
=True):
577 return self
.get_staged()[idx
]
579 return self
.get_unstaged()[idx
]
583 def get_diff_details(self
, idx
, ref
, staged
=True):
584 filename
= self
.get_filename(idx
, staged
=staged
)
586 return (None, None, None)
587 encfilename
= filename
.encode('utf-8')
589 if os
.path
.exists(encfilename
):
590 status
= 'Staged for commit'
592 status
= 'Staged for removal'
593 diff
= self
.diff_helper(filename
=filename
,
597 if os
.path
.isdir(encfilename
):
598 status
= 'Untracked directory'
599 diff
= '\n'.join(os
.listdir(filename
))
601 elif filename
in self
.get_unmerged():
603 diff
= ('@@@+-+-+-+-+-+-+-+-+-+-+ UNMERGED +-+-+-+-+-+-+-+-+-+-+@@@\n\n'
604 '>>> %s is unmerged.\n' % filename
+
605 'Right-click on the filename '
606 'to launch "git mergetool".\n\n\n')
607 diff
+= self
.diff_helper(filename
=filename
,
609 patch_with_raw
=False)
610 elif filename
in self
.get_modified():
611 status
= 'Modified, not staged'
612 diff
= self
.diff_helper(filename
=filename
,
615 status
= 'Untracked, not staged'
616 diff
= 'SHA1: ' + self
.git
.hash_object(filename
)
617 return diff
, status
, filename
619 def stage_modified(self
):
620 output
= self
.git
.add(v
=True, *self
.get_modified())
624 def stage_untracked(self
):
625 output
= self
.git
.add(self
.get_untracked())
629 def reset(self
, *items
):
630 output
= self
.git
.reset('--', *items
)
634 def unstage_all(self
):
635 self
.git
.reset('--', *self
.get_staged())
638 def save_gui_settings(self
):
639 self
.config_set('cola.geometry', utils
.get_geom(), local
=False)
641 def config_set(self
, key
=None, value
=None, local
=True):
642 if key
and value
is not None:
643 # git config category.key value
644 strval
= unicode(value
)
645 if type(value
) is bool:
646 # git uses "true" and "false"
647 strval
= strval
.lower()
649 argv
= [ key
, strval
]
651 argv
= [ '--global', key
, strval
]
652 return self
.git
.config(*argv
)
654 msg
= "oops in config_set(key=%s,value=%s,local=%s"
655 raise Exception(msg
% (key
, value
, local
))
657 def config_dict(self
, local
=True):
658 """parses the lines from git config --list into a dictionary"""
662 'global': not local
, # global is a python keyword
664 config_lines
= self
.git
.config(**kwargs
).splitlines()
666 for line
in config_lines
:
667 k
, v
= line
.split('=', 1)
668 v
= v
.decode('utf-8')
669 k
= k
.replace('.','_') # git -> model
670 if v
== 'true' or v
== 'false':
671 v
= bool(eval(v
.title()))
679 def commit_with_msg(self
, msg
, amend
=False):
680 """Creates a git commit."""
682 if not msg
.endswith('\n'):
684 # Sure, this is a potential "security risk," but if someone
685 # is trying to intercept/re-write commit messages on your system,
686 # then you probably have bigger problems to worry about.
687 tmpfile
= self
.get_tmp_filename()
689 # Create the commit message file
690 fh
= open(tmpfile
, 'w')
695 (status
, stdout
, stderr
) = self
.git
.commit(F
=tmpfile
,
698 with_extended_output
=True)
701 return (status
, stdout
+stderr
)
705 return self
.git
.diff(unified
=self
.diff_context
,
709 def get_tmp_dir(self
):
710 # Allow TMPDIR/TMP with a fallback to /tmp
711 return os
.environ
.get('TMP', os
.environ
.get('TMPDIR', '/tmp'))
713 def get_tmp_file_pattern(self
):
714 return os
.path
.join(self
.get_tmp_dir(), '*.git-cola.%s.*' % os
.getpid())
716 def get_tmp_filename(self
, prefix
=''):
717 basename
= ((prefix
+'.git-cola.%s.%s'
718 % (os
.getpid(), time
.time())))
719 basename
= basename
.replace('/', '-')
720 basename
= basename
.replace('\\', '-')
721 tmpdir
= self
.get_tmp_dir()
722 return os
.path
.join(tmpdir
, 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 symbolic-ref' to find the current branch."""
979 headref
= self
.git
.symbolic_ref('HEAD')
980 if headref
.startswith('refs/heads/'):
982 elif headref
.startswith('fatal: '):
983 return 'Not currently on any branch'
986 def create_branch(self
, name
, base
, track
=False):
987 """Creates a branch starting from base. Pass track=True
988 to create a remote tracking branch."""
989 return self
.git
.branch(name
, base
, track
=track
)
991 def cherry_pick_list(self
, revs
, **kwargs
):
992 """Cherry-picks each revision into the current branch.
993 Returns a list of command output strings (1 per cherry pick)"""
998 cherries
.append(self
.git
.cherry_pick(rev
, **kwargs
))
999 return '\n'.join(cherries
)
1001 def parse_stash_list(self
, revids
=False):
1002 """Parses "git stash list" and returns a list of stashes."""
1003 stashes
= self
.git
.stash("list").splitlines()
1005 return [ s
[:s
.index(':')] for s
in stashes
]
1007 return [ s
[s
.index(':')+1:] for s
in stashes
]
1010 return self
.git
.diff(
1012 unified
=self
.diff_context
,
1015 def pad(self
, pstr
, num
=22):
1016 topad
= num
-len(pstr
)
1018 return pstr
+ ' '*topad
1022 def describe(self
, revid
, descr
):
1023 version
= self
.git
.describe(revid
, tags
=True, always
=True,
1025 return version
+ ' - ' + descr
1027 def update_revision_lists(self
, filename
=None, show_versions
=False):
1028 num_results
= self
.get_num_results()
1030 rev_list
= self
.git
.log('--', filename
,
1031 max_count
=num_results
,
1034 rev_list
= self
.git
.log(max_count
=num_results
,
1035 pretty
='oneline', all
=True)
1037 commit_list
= self
.parse_rev_list(rev_list
)
1038 commit_list
.reverse()
1039 commits
= map(lambda x
: x
[0], commit_list
)
1040 descriptions
= map(lambda x
: x
[1].decode('utf-8'), commit_list
)
1042 fancy_descr_list
= map(lambda x
: self
.describe(*x
), commit_list
)
1043 self
.set_descriptions_start(fancy_descr_list
)
1044 self
.set_descriptions_end(fancy_descr_list
)
1046 self
.set_descriptions_start(descriptions
)
1047 self
.set_descriptions_end(descriptions
)
1049 self
.set_revisions_start(commits
)
1050 self
.set_revisions_end(commits
)
1054 def get_changed_files(self
, start
, end
):
1055 zfiles_str
= self
.git
.diff('%s..%s' % (start
, end
),
1056 name_only
=True, z
=True).strip('\0')
1057 return [ enc
.decode('utf-8')
1058 for enc
in zfiles_str
.split('\0') if enc
]
1060 def get_renamed_files(self
, start
, end
):
1062 difflines
= self
.git
.diff('%s..%s' % (start
, end
), M
=True).splitlines()
1063 return [ eval_path(r
[12:].rstrip())
1064 for r
in difflines
if r
.startswith('rename from ') ]