6 from cStringIO
import StringIO
10 from cola
import model
12 #+-------------------------------------------------------------------------
13 #+ A regex for matching the output of git(log|rev-list) --pretty=oneline
14 REV_LIST_REGEX
= re
.compile('([0-9a-f]+)\W(.*)')
16 #+-------------------------------------------------------------------------
17 # List of functions available directly through model.command_name()
19 am annotate apply archive archive_recursive
20 bisect blame branch bundle
21 checkout checkout_index cherry cherry_pick citool
22 clean commit config count_objects
24 fast_export fetch filter_branch format_patch fsck
25 gc get_tar_commit_id grep gui
26 hard_repack imap_send init instaweb
27 log lost_found ls_files ls_remote ls_tree
28 merge mergetool mv name_rev pull push
29 read_tree rebase relink remote repack
30 request_pull reset revert rev_list rm
31 send_email shortlog show show_branch
32 show_ref stash status submodule svn
33 tag var verify_pack whatchanged
36 class GitCola(git
.Git
):
37 """GitPython throws exceptions by default.
38 We suppress exceptions in favor of return values.
42 self
._work
_tree
= None
43 git_dir
= self
.get_git_dir()
44 work_tree
= self
.get_work_tree()
48 print "Sorry, git-cola requires a work tree"
50 git
.Git
.__init
__(self
, work_tree
)
51 def execute(*args
, **kwargs
):
52 kwargs
['with_exceptions'] = False
53 return git
.Git
.execute(*args
, **kwargs
)
54 def get_work_tree(self
):
55 if not self
._work
_tree
:
56 self
._work
_tree
= os
.getenv('GIT_WORK_TREE')
57 if not self
._work
_tree
or not os
.path
.isdir(self
._work
_tree
):
58 self
._work
_tree
= os
.path
.abspath(
59 os
.path
.join(self
._git
_dir
, '..'))
60 return self
._work
_tree
61 def get_git_dir(self
):
63 self
._git
_dir
= os
.getenv('GIT_DIR')
64 if self
._git
_dir
and self
._is
_git
_dir
(self
._git
_dir
):
68 if self
._is
_git
_dir
(curpath
):
69 self
._git
_dir
= curpath
71 gitpath
= os
.path
.join(curpath
, '.git')
72 if self
._is
_git
_dir
(gitpath
):
73 self
._git
_dir
= gitpath
75 curpath
, dummy
= os
.path
.split(curpath
)
80 def _is_git_dir(self
, d
):
81 """ This is taken from the git setup.c:is_git_directory
84 and os
.path
.isdir(os
.path
.join(d
, 'objects'))
85 and os
.path
.isdir(os
.path
.join(d
, 'refs'))):
86 headref
= os
.path
.join(d
, 'HEAD')
87 return (os
.path
.isfile(headref
)
88 or (os
.path
.islink(headref
)
89 and os
.readlink(headref
).startswith('refs')))
92 class Model(model
.Model
):
93 """Provides a friendly wrapper for doing commit git operations."""
96 """Reads git repository settings and sets several methods
97 so that they refer to the git module. This object
98 encapsulates cola's interaction with git."""
100 # chdir to the root of the git tree.
101 # This keeps paths relative.
105 self
.__init
_config
_data
()
107 # Import all git commands from git.py
108 for cmd
in GIT_COMMANDS
:
109 setattr(self
, cmd
, getattr(self
.git
, cmd
))
112 #####################################################
113 # Used in various places
120 git_version
= self
.git
.version(),
122 #####################################################
123 # Used primarily by the main UI
124 project
= os
.path
.basename(os
.getcwd()),
130 window_geom
= utils
.parse_geom(self
.get_global_cola_geometry()),
132 #####################################################
133 # Used by the create branch dialog
136 remote_branches
= [],
139 #####################################################
140 # Used by the commit/repo browser
145 # These are parallel lists
150 # All items below here are re-calculated in
151 # init_browser_data()
153 directory_entries
= {},
155 # These are also parallel lists
161 def __init_config_data(self
):
162 """Reads git config --list and creates parameters
164 # These parameters are saved in .gitconfig,
165 # so ideally these should be as short as possible.
167 # config items that are controllable globally
169 self
.__local
_and
_global
_defaults
= {
172 'merge_summary': False,
173 'merge_diffstat': True,
174 'merge_verbosity': 2,
175 'gui_diffcontext': 3,
176 'gui_pruneduringfetch': False,
178 # config items that are purely git config --global settings
179 self
.__global
_defaults
= {
182 'cola_fontuisize': 12,
184 'cola_fontdiffsize': 12,
185 'cola_savewindowsettings': False,
186 'cola_editdiffreverse': False,
187 'cola_saveatexit': False,
188 'gui_editor': 'gvim',
189 'gui_diffeditor': 'xxdiff',
190 'gui_historybrowser': 'gitk',
193 local_dict
= self
.config_dict(local
=True)
194 global_dict
= self
.config_dict(local
=False)
196 for k
,v
in local_dict
.iteritems():
197 self
.set_param('local_'+k
, v
)
198 for k
,v
in global_dict
.iteritems():
199 self
.set_param('global_'+k
, v
)
200 if k
not in local_dict
:
202 self
.set_param('local_'+k
, v
)
204 # Bootstrap the internal font*size variables
205 for param
in ('global_cola_fontui', 'global_cola_fontdiff'):
207 if hasattr(self
, param
):
208 font
= self
.get_param(param
)
211 size
= int(font
.split(',')[1])
212 self
.set_param(param
+'size', size
)
213 param
= param
[len('global_'):]
214 global_dict
[param
] = font
215 global_dict
[param
+'size'] = size
217 # Load defaults for all undefined items
218 local_and_global_defaults
= self
.__local
_and
_global
_defaults
219 for k
,v
in local_and_global_defaults
.iteritems():
220 if k
not in local_dict
:
221 self
.set_param('local_'+k
, v
)
222 if k
not in global_dict
:
223 self
.set_param('global_'+k
, v
)
225 global_defaults
= self
.__global
_defaults
226 for k
,v
in global_defaults
.iteritems():
227 if k
not in global_dict
:
228 self
.set_param('global_'+k
, v
)
230 # Allow EDITOR/DIFF_EDITOR environment variable overrides
231 self
.global_gui_editor
= os
.getenv('COLA_EDITOR',
232 self
.global_gui_editor
)
233 self
.global_gui_diffeditor
= os
.getenv('COLA_DIFFEDITOR',
234 self
.global_gui_diffeditor
)
235 # Load the diff context
236 self
.diff_context
= self
.local_gui_diffcontext
238 def get_cola_config(self
, key
):
239 return getattr(self
, 'global_cola_'+key
)
241 def get_gui_config(self
, key
):
242 return getattr(self
, 'global_gui_'+key
)
244 def branch_list(self
, remote
=False):
245 branches
= map(lambda x
: x
.lstrip('* '),
246 self
.git
.branch(r
=remote
).splitlines())
249 for branch
in branches
:
250 if branch
.endswith('/HEAD'):
252 remotes
.append(branch
)
256 def get_config_params(self
):
258 params
.extend(map(lambda x
: 'local_' + x
,
259 self
.__local
_and
_global
_defaults
.keys()))
260 params
.extend(map(lambda x
: 'global_' + x
,
261 self
.__local
_and
_global
_defaults
.keys()))
262 params
.extend(map(lambda x
: 'global_' + x
,
263 self
.__global
_defaults
.keys()))
264 return [ p
for p
in params
if not p
.endswith('size') ]
266 def save_config_param(self
, param
):
267 if param
not in self
.get_config_params():
269 value
= self
.get_param(param
)
270 if param
== 'local_gui_diffcontext':
271 self
.diff_context
= value
272 if param
.startswith('local_'):
273 param
= param
[len('local_'):]
275 elif param
.startswith('global_'):
276 param
= param
[len('global_'):]
279 raise Exception("Invalid param '%s' passed to " % param
280 +'save_config_param()')
281 param
= param
.replace('_', '.') # model -> git
282 return self
.config_set(param
, value
, local
=is_local
)
284 def init_browser_data(self
):
285 """This scans over self.(names, sha1s, types) to generate
286 directories, directory_entries, and subtree_*"""
288 # Collect data for the model
289 if not self
.get_currentbranch(): return
291 self
.subtree_types
= []
292 self
.subtree_sha1s
= []
293 self
.subtree_names
= []
294 self
.directories
= []
295 self
.directory_entries
= {}
297 # Lookup the tree info
298 tree_info
= self
.parse_ls_tree(self
.get_currentbranch())
300 self
.set_types(map( lambda(x
): x
[1], tree_info
))
301 self
.set_sha1s(map( lambda(x
): x
[2], tree_info
))
302 self
.set_names(map( lambda(x
): x
[3], tree_info
))
304 if self
.directory
: self
.directories
.append('..')
306 dir_entries
= self
.directory_entries
307 dir_regex
= re
.compile('([^/]+)/')
311 for idx
, name
in enumerate(self
.names
):
312 if not name
.startswith(self
.directory
):
314 name
= name
[ len(self
.directory
): ]
316 # This is a directory...
317 match
= dir_regex
.match(name
)
320 dirent
= match
.group(1) + '/'
321 if dirent
not in self
.directory_entries
:
322 self
.directory_entries
[dirent
] = []
324 if dirent
not in dirs_seen
:
325 dirs_seen
[dirent
] = True
326 self
.directories
.append(dirent
)
328 entry
= name
.replace(dirent
, '')
329 entry_match
= dir_regex
.match(entry
)
331 subdir
= entry_match
.group(1) + '/'
332 if subdir
in subdirs_seen
:
334 subdirs_seen
[subdir
] = True
335 dir_entries
[dirent
].append(subdir
)
337 dir_entries
[dirent
].append(entry
)
339 self
.subtree_types
.append(self
.types
[idx
])
340 self
.subtree_sha1s
.append(self
.sha1s
[idx
])
341 self
.subtree_names
.append(name
)
343 def add_or_remove(self
, *to_process
):
344 """Invokes 'git add' to index the filenames in to_process that exist
345 and 'git rm' for those that do not exist."""
348 return 'No files to add or remove.'
353 for filename
in to_process
:
354 if os
.path
.exists(filename
):
355 to_add
.append(filename
)
357 output
= self
.git
.add(v
=True, *to_add
)
359 if len(to_add
) == len(to_process
):
360 # to_process only contained unremoved files --
361 # short-circuit the removal checks
364 # Process files to remote
365 for filename
in to_process
:
366 if not os
.path
.exists(filename
):
367 to_remove
.append(filename
)
368 output
+ '\n\n' + self
.git
.rm(*to_remove
)
370 def get_editor(self
):
371 return self
.get_gui_config('editor')
373 def get_diffeditor(self
):
374 return self
.get_gui_config('diffeditor')
376 def get_history_browser(self
):
377 return self
.get_gui_config('historybrowser')
379 def remember_gui_settings(self
):
380 return self
.get_cola_config('savewindowsettings')
382 def save_at_exit(self
):
383 return self
.get_cola_config('saveatexit')
385 def get_tree_node(self
, idx
):
386 return (self
.get_types()[idx
],
387 self
.get_sha1s()[idx
],
388 self
.get_names()[idx
] )
390 def get_subtree_node(self
, idx
):
391 return (self
.get_subtree_types()[idx
],
392 self
.get_subtree_sha1s()[idx
],
393 self
.get_subtree_names()[idx
] )
395 def get_all_branches(self
):
396 return (self
.get_local_branches() + self
.get_remote_branches())
398 def set_remote(self
, remote
):
399 if not remote
: return
400 self
.set_param('remote', remote
)
401 branches
= utils
.grep('%s/\S+$' % remote
,
402 self
.branch_list(remote
=True),
404 self
.set_remote_branches(branches
)
406 def add_signoff(self
,*rest
):
407 """Adds a standard Signed-off by: tag to the end
408 of the current commit message."""
410 msg
= self
.get_commitmsg()
411 signoff
=('\n\nSigned-off-by: %s <%s>\n'
412 % (self
.get_local_user_name(), self
.get_local_user_email()))
413 if signoff
not in msg
:
414 self
.set_commitmsg(msg
+ signoff
)
416 def apply_diff(self
, filename
):
417 return self
.git
.apply(filename
, index
=True, cached
=True)
419 def apply_diff_to_worktree(self
, filename
):
420 return self
.git
.apply(filename
)
422 def load_commitmsg(self
, path
):
423 file = open(path
, 'r')
424 contents
= file.read()
426 self
.set_commitmsg(contents
)
428 def get_prev_commitmsg(self
,*rest
):
429 """Queries git for the latest commit message and sets it in
432 commit_lines
= self
.git
.show('HEAD').split('\n')
433 for idx
, msg
in enumerate(commit_lines
):
436 if msg
.startswith('diff --git'):
439 commit_msg
.append(msg
)
440 self
.set_commitmsg('\n'.join(commit_msg
).rstrip())
442 def update_status(self
):
443 # This allows us to defer notification until the
444 # we finish processing data
445 notify_enabled
= self
.get_notify()
446 self
.set_notify(False)
448 # Reset the staged and unstaged model lists
449 # NOTE: the model's unstaged list is used to
450 # hold both modified and untracked files.
455 # Read git status items
458 untracked_items
) = self
.parse_status()
460 # Gather items to be committed
461 for staged
in staged_items
:
462 if staged
not in self
.get_staged():
463 self
.add_staged(staged
)
465 # Gather unindexed items
466 for modified
in modified_items
:
467 if modified
not in self
.get_modified():
468 self
.add_modified(modified
)
470 # Gather untracked items
471 for untracked
in untracked_items
:
472 if untracked
not in self
.get_untracked():
473 self
.add_untracked(untracked
)
475 self
.set_currentbranch(self
.current_branch())
476 self
.set_unstaged(self
.get_modified() + self
.get_untracked())
477 self
.set_remotes(self
.git
.remote().splitlines())
478 self
.set_remote_branches(self
.branch_list(remote
=True))
479 self
.set_local_branches(self
.branch_list(remote
=False))
480 self
.set_tags(self
.git
.tag().splitlines())
481 self
.set_revision('')
482 self
.set_local_branch('')
483 self
.set_remote_branch('')
484 # Re-enable notifications and emit changes
485 self
.set_notify(notify_enabled
)
486 self
.notify_observers('staged','unstaged')
488 def delete_branch(self
, branch
):
489 return self
.git
.branch(branch
, D
=True)
491 def get_revision_sha1(self
, idx
):
492 return self
.get_revisions()[idx
]
494 def apply_font_size(self
, param
, default
):
495 old_font
= self
.get_param(param
)
498 size
= self
.get_param(param
+'size')
499 props
= old_font
.split(',')
501 new_font
= ','.join(props
)
503 self
.set_param(param
, new_font
)
505 def get_commit_diff(self
, sha1
):
506 commit
= self
.git
.show(sha1
)
507 first_newline
= commit
.index('\n')
508 if commit
[first_newline
+1:].startswith('Merge:'):
509 return (commit
+ '\n\n'
510 + self
.diff_helper(commit
=sha1
,
512 suppress_header
=False))
516 def get_diff_details(self
, idx
, staged
=True):
518 filename
= self
.get_staged()[idx
]
519 if os
.path
.exists(filename
):
520 status
= 'Staged for commit'
522 status
= 'Staged for removal'
523 diff
= self
.diff_helper(filename
=filename
,
526 filename
= self
.get_unstaged()[idx
]
527 if os
.path
.isdir(filename
):
528 status
= 'Untracked directory'
529 diff
= '\n'.join(os
.listdir(filename
))
530 elif filename
in self
.get_modified():
531 status
= 'Modified, not staged'
532 diff
= self
.diff_helper(filename
=filename
,
535 status
= 'Untracked, not staged'
537 file_type
= utils
.run_cmd('file', '-b', filename
)
538 if 'binary' in file_type
or 'data' in file_type
:
539 diff
= utils
.run_cmd('hexdump', '-C', filename
)
541 if os
.path
.exists(filename
):
542 file = open(filename
, 'r')
547 return diff
, status
, filename
549 def stage_modified(self
):
550 output
= self
.git
.add(self
.get_modified())
554 def stage_untracked(self
):
555 output
= self
.git
.add(self
.get_untracked())
559 def reset(self
, *items
):
560 output
= self
.git
.reset('--', *items
)
564 def unstage_all(self
):
565 self
.git
.reset('--', *self
.get_staged())
568 def save_gui_settings(self
):
569 self
.config_set('cola.geometry', utils
.get_geom(), local
=False)
571 def config_set(self
, key
=None, value
=None, local
=True):
572 if key
and value
is not None:
573 # git config category.key value
575 if type(value
) is bool:
576 # git uses "true" and "false"
577 strval
= strval
.lower()
579 argv
= [ key
, strval
]
581 argv
= [ '--global', key
, strval
]
582 return self
.git
.config(*argv
)
584 msg
= "oops in config_set(key=%s,value=%s,local=%s"
585 raise Exception(msg
% (key
, value
, local
))
587 def config_dict(self
, local
=True):
588 """parses the lines from git config --list into a dictionary"""
594 config_lines
= self
.git
.config(**kwargs
).splitlines()
596 for line
in config_lines
:
597 k
, v
= line
.split('=', 1)
598 k
= k
.replace('.','_') # git -> model
599 if v
== 'true' or v
== 'false':
600 v
= bool(eval(v
.title()))
608 def commit_with_msg(self
, msg
, amend
=False):
609 """Creates a git commit."""
611 if not msg
.endswith('\n'):
613 # Sure, this is a potential "security risk," but if someone
614 # is trying to intercept/re-write commit messages on your system,
615 # then you probably have bigger problems to worry about.
616 tmpfile
= self
.get_tmp_filename()
618 # Create the commit message file
619 file = open(tmpfile
, 'w')
624 output
= self
.git
.commit(F
=tmpfile
, amend
=amend
)
627 return ('git commit -F %s --amend %s\n\n%s'
628 % ( tmpfile
, amend
, output
))
632 return self
.git
.diff(unified
=self
.diff_context
,
636 def get_tmp_dir(self
):
637 return os
.environ
.get('TMP', os
.environ
.get('TMPDIR', '/tmp'))
639 def get_tmp_file_pattern(self
):
640 return os
.path
.join(self
.get_tmp_dir(), '*.git.%s.*' % os
.getpid())
642 def get_tmp_filename(self
, prefix
=''):
643 # Allow TMPDIR/TMP with a fallback to /tmp
644 basename
= (prefix
+'.git.%s.%s'
645 % (os
.getpid(), time
.time())).replace(os
.sep
, '-')
646 return os
.path
.join(self
.get_tmp_dir(), basename
)
648 def log_helper(self
, all
=False):
650 Returns a pair of parallel arrays listing the revision sha1's
651 and commit summaries.
655 regex
= REV_LIST_REGEX
656 output
= self
.git
.log(pretty
='oneline', all
=all
)
657 for line
in output
.splitlines():
658 match
= regex
.match(line
)
660 revs
.append(match
.group(1))
661 summaries
.append(match
.group(2))
662 return (revs
, summaries
)
664 def parse_rev_list(self
, raw_revs
):
666 for line
in raw_revs
.splitlines():
667 match
= REV_LIST_REGEX
.match(line
)
669 rev_id
= match
.group(1)
670 summary
= match
.group(2)
671 revs
.append((rev_id
, summary
,))
674 def rev_list_range(self
, start
, end
):
675 range = '%s..%s' % (start
, end
)
676 raw_revs
= self
.git
.rev_list(range, pretty
='oneline')
677 return self
.parse_rev_list(raw_revs
)
679 def diff_helper(self
,
685 with_diff_header
=False,
686 suppress_header
=True,
688 "Invokes git diff on a filepath."
692 argv
.append('%s^..%s' % (commit
, commit
))
698 if type(filename
) is list:
699 argv
.extend(filename
)
701 argv
.append(filename
)
703 diffoutput
= self
.git
.diff(R
=reverse
,
707 unified
=self
.diff_context
,
708 with_raw_output
=True,
710 diff
= diffoutput
.splitlines()
714 del_tag
= 'deleted file mode '
717 deleted
= cached
and not os
.path
.exists(filename
)
719 if not start
and '@@ ' in line
and ' @@' in line
:
721 if start
or(deleted
and del_tag
in line
):
722 output
.write(line
+ '\n')
726 elif not suppress_header
:
727 output
.write(line
+ '\n')
728 result
= output
.getvalue()
731 return('\n'.join(headers
), result
)
735 def git_repo_path(self
, *subpaths
):
736 paths
= [ self
.git
.get_git_dir() ]
737 paths
.extend(subpaths
)
738 return os
.path
.realpath(os
.path
.join(*paths
))
740 def get_merge_message_path(self
):
741 for file in ('MERGE_MSG', 'SQUASH_MSG'):
742 path
= self
.git_repo_path(file)
743 if os
.path
.exists(path
):
747 def get_merge_message(self
):
748 return self
.git
.fmt_merge_msg('--file',
749 self
.git_repo_path('FETCH_HEAD'))
751 def abort_merge(self
):
753 output
= self
.git
.read_tree('HEAD', reset
=True, u
=True, v
=True)
755 merge_head
= self
.git_repo_path('MERGE_HEAD')
756 if os
.path
.exists(merge_head
):
757 os
.unlink(merge_head
)
758 # remove MERGE_MESSAGE, etc.
759 merge_msg_path
= self
.get_merge_message_path()
760 while merge_msg_path
:
761 os
.unlink(merge_msg_path
)
762 merge_msg_path
= self
.get_merge_message_path()
764 def parse_status(self
):
765 """RETURNS: A tuple of staged, unstaged and untracked file lists.
768 """handles quoted paths."""
769 if path
.startswith('"') and path
.endswith('"'):
774 MODIFIED_TAG
= '# Changed but not updated:'
775 UNTRACKED_TAG
= '# Untracked files:'
776 RGX_RENAMED
= re
.compile('(#\trenamed:\s+|#\tcopied:\s+)'
778 RGX_MODIFIED
= re
.compile('(#\tmodified:\s+'
789 current_dest
= staged
792 for status_line
in self
.git
.status().splitlines():
793 if status_line
== MODIFIED_TAG
:
795 current_dest
= unstaged
797 elif status_line
== UNTRACKED_TAG
:
798 mode
= UNTRACKED_MODE
799 current_dest
= untracked
801 # Staged/unstaged modified/renamed/deleted files
802 if mode
is STAGED_MODE
or mode
is UNSTAGED_MODE
:
803 match
= RGX_MODIFIED
.match(status_line
)
806 filename
= status_line
.replace(tag
, '')
807 current_dest
.append(eval_path(filename
))
809 match
= RGX_RENAMED
.match(status_line
)
811 oldname
= match
.group(2)
812 newname
= match
.group(3)
813 current_dest
.append(eval_path(oldname
))
814 current_dest
.append(eval_path(newname
))
817 elif mode
is UNTRACKED_MODE
:
818 if status_line
.startswith('#\t'):
819 current_dest
.append(eval_path(status_line
[2:]))
821 return( staged
, unstaged
, untracked
)
823 def reset_helper(self
, *args
, **kwargs
):
824 return self
.git
.reset('--', *args
, **kwargs
)
826 def remote_url(self
, name
):
827 return self
.git
.config('remote.%s.url' % name
, get
=True)
829 def get_remote_args(self
, remote
,
830 local_branch
='', remote_branch
='',
831 ffwd
=True, tags
=False):
833 branch_arg
= '%s:%s' % ( remote_branch
, local_branch
)
835 branch_arg
= '+%s:%s' % ( remote_branch
, local_branch
)
837 if local_branch
and remote_branch
:
838 args
.append(branch_arg
)
840 "with_extended_output": True,
843 return (args
, kwargs
)
845 def fetch_helper(self
, *args
, **kwargs
):
847 Fetches remote_branch to local_branch only if
848 remote_branch and local_branch are both supplied.
849 If either is ommitted, "git fetch <remote>" is performed instead.
850 Returns (status,output)
852 args
, kwargs
= self
.get_remote_args(*args
, **kwargs
)
853 (status
, stdout
, stderr
) = self
.git
.fetch(v
=True, *args
, **kwargs
)
854 return (status
, stdout
+ stderr
)
856 def push_helper(self
, *args
, **kwargs
):
858 Pushes local_branch to remote's remote_branch only if
859 remote_branch and local_branch both are supplied.
860 If either is ommitted, "git push <remote>" is performed instead.
861 Returns (status,output)
863 args
, kwargs
= self
.get_remote_args(*args
, **kwargs
)
864 (status
, stdout
, stderr
) = self
.git
.push(*args
, **kwargs
)
865 return (status
, stdout
+ stderr
)
867 def pull_helper(self
, *args
, **kwargs
):
869 Pushes branches. If local_branch or remote_branch is ommitted,
870 "git pull <remote>" is performed instead of
871 "git pull <remote> <remote_branch>:<local_branch>
872 Returns (status,output)
874 args
, kwargs
= self
.get_remote_args(*args
, **kwargs
)
875 (status
, stdout
, stderr
) = self
.git
.pull(v
=True, *args
, **kwargs
)
876 return (status
, stdout
+ stderr
)
878 def parse_ls_tree(self
, rev
):
879 """Returns a list of(mode, type, sha1, path) tuples."""
880 lines
= self
.git
.ls_tree(rev
, r
=True).splitlines()
882 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
884 match
= regex
.match(line
)
886 mode
= match
.group(1)
887 objtype
= match
.group(2)
888 sha1
= match
.group(3)
889 filename
= match
.group(4)
890 output
.append((mode
, objtype
, sha1
, filename
,) )
893 def format_patch_helper(self
, to_export
, revs
, output
='patches'):
894 """writes patches named by to_export to the output directory."""
898 cur_rev
= to_export
[0]
899 cur_master_idx
= revs
.index(cur_rev
)
901 patches_to_export
= [ [cur_rev
] ]
904 # Group the patches into continuous sets
905 for idx
, rev
in enumerate(to_export
[1:]):
906 # Limit the search to the current neighborhood for efficiency
907 master_idx
= revs
[ cur_master_idx
: ].index(rev
)
908 master_idx
+= cur_master_idx
909 if master_idx
== cur_master_idx
+ 1:
910 patches_to_export
[ patchset_idx
].append(rev
)
914 patches_to_export
.append([ rev
])
915 cur_master_idx
= master_idx
918 # Export each patchsets
919 for patchset
in patches_to_export
:
920 cmdoutput
= self
.export_patchset(patchset
[0],
925 patch_with_stat
=True)
926 outlines
.append(cmdoutput
)
927 return '\n'.join(outlines
)
929 def export_patchset(self
, start
, end
, output
="patches", **kwargs
):
930 revarg
= '%s^..%s' % (start
, end
)
931 return self
.git
.format_patch("-o", output
, revarg
, **kwargs
)
933 def current_branch(self
):
934 """Parses 'git branch' to find the current branch."""
935 branches
= self
.git
.branch().splitlines()
936 for branch
in branches
:
937 if branch
.startswith('* '):
938 return branch
.lstrip('* ')
939 return 'Detached HEAD'
941 def create_branch(self
, name
, base
, track
=False):
942 """Creates a branch starting from base. Pass track=True
943 to create a remote tracking branch."""
944 return self
.git
.branch(name
, base
, track
=track
)
946 def cherry_pick_list(self
, revs
, **kwargs
):
947 """Cherry-picks each revision into the current branch.
948 Returns a list of command output strings (1 per cherry pick)"""
953 cherries
.append(self
.git
.cherry_pick(rev
, **kwargs
))
954 return '\n'.join(cherries
)
956 def parse_stash_list(self
, revids
=False):
957 """Parses "git stash list" and returns a list of stashes."""
958 stashes
= self
.stash("list").splitlines()
960 return [ s
[:s
.index(':')] for s
in stashes
]
962 return [ s
[s
.index(':')+1:] for s
in stashes
]