gitmodel: implement fetch/push/pull_helper()
[ugit.git] / ugit / models.py
blob377fb019628e2447feae92fa9da7f3618da42a69
1 import os
2 import sys
3 import re
4 import time
5 import subprocess
6 from cStringIO import StringIO
8 # GitPython http://gitorious.org/projects/git-python
9 import git
11 from ugit import utils
12 from ugit import model
14 #+-------------------------------------------------------------------------
15 #+ A regex for matching the output of git(log|rev-list) --pretty=oneline
16 REV_LIST_REGEX = re.compile('([0-9a-f]+)\W(.*)')
18 #+-------------------------------------------------------------------------
19 # List of functions available directly through model.command_name()
20 GIT_COMMANDS = """
21 am annotate apply archive archive_recursive
22 bisect blame branch bundle
23 checkout checkout_index cherry cherry_pick citool
24 clean commit config count_objects
25 describe diff
26 fast_export fetch filter_branch format_patch fsck
27 gc get_tar_commit_id grep gui
28 hard_repack imap_send init instaweb
29 log lost_found ls_files ls_remote ls_tree
30 merge mergetool mv name_rev pull push
31 read_tree rebase relink remote repack
32 request_pull reset revert rev_list rm
33 send_email shortlog show show_branch
34 show_ref stash status submodule svn
35 tag var verify_pack whatchanged
36 """.split()
38 class Model(model.Model):
39 """Provides a friendly wrapper for doing commit git operations."""
41 def init(self):
42 """Reads git repository settings and sets several methods
43 so that they refer to the git module. This object is
44 encapsulates ugit's interaction with git."""
46 # chdir to the root of the git tree.
47 # This keeps paths relative.
48 self.git = git.Git()
49 os.chdir( self.git.git_dir )
51 # Read git config
52 self.__init_config_data()
54 # Import all git commands from git.py
55 for cmd in GIT_COMMANDS:
56 setattr(self, cmd, getattr(self.git, cmd))
58 self.create(
59 #####################################################
60 # Used in various places
61 currentbranch = '',
62 remotes = [],
63 remotename = '',
64 local_branch = '',
65 remote_branch = '',
66 search_text = '',
67 git_version = self.git.version(),
69 #####################################################
70 # Used primarily by the main UI
71 project = os.path.basename(os.getcwd()),
72 commitmsg = '',
73 modified = [],
74 staged = [],
75 unstaged = [],
76 untracked = [],
77 window_geom = utils.parse_geom(
78 self.get_global_ugit_geometry()),
80 #####################################################
81 # Used by the create branch dialog
82 revision = '',
83 local_branches = [],
84 remote_branches = [],
85 tags = [],
87 #####################################################
88 # Used by the commit/repo browser
89 directory = '',
90 revisions = [],
91 summaries = [],
93 # These are parallel lists
94 types = [],
95 sha1s = [],
96 names = [],
98 # All items below here are re-calculated in
99 # init_browser_data()
100 directories = [],
101 directory_entries = {},
103 # These are also parallel lists
104 subtree_types = [],
105 subtree_sha1s = [],
106 subtree_names = [],
109 def __init_config_data(self):
110 """Reads git config --list and creates parameters
111 for each setting."""
112 # These parameters are saved in .gitconfig,
113 # so ideally these should be as short as possible.
115 # config items that are controllable globally
116 # and per-repository
117 self.__local_and_global_defaults = {
118 'user_name': '',
119 'user_email': '',
120 'merge_summary': False,
121 'merge_diffstat': True,
122 'merge_verbosity': 2,
123 'gui_diffcontext': 3,
124 'gui_pruneduringfetch': False,
126 # config items that are purely git config --global settings
127 self.__global_defaults = {
128 'ugit_geometry':'',
129 'ugit_fontui': '',
130 'ugit_fontui_size':12,
131 'ugit_fontdiff': '',
132 'ugit_fontdiff_size':12,
133 'ugit_savewindowsettings': False,
134 'ugit_saveatexit': False,
135 'gui_editor': 'gvim',
136 'gui_diffeditor': 'xxdiff',
137 'gui_historybrowser': 'gitk',
140 local_dict = self.config_dict(local=True)
141 global_dict = self.config_dict(local=False)
143 for k,v in local_dict.iteritems():
144 self.set_param('local_'+k, v)
145 for k,v in global_dict.iteritems():
146 self.set_param('global_'+k, v)
147 if k not in local_dict:
148 local_dict[k]=v
149 self.set_param('local_'+k, v)
151 # Bootstrap the internal font*_size variables
152 for param in ('global_ugit_fontui', 'global_ugit_fontdiff'):
153 if hasattr(self, param):
154 font = self.get_param(param)
155 if font:
156 size = int(font.split(',')[1])
157 self.set_param(param+'_size', size)
158 param = param[len('global_'):]
159 global_dict[param] = font
160 global_dict[param+'_size'] = size
162 # Load defaults for all undefined items
163 local_and_global_defaults = self.__local_and_global_defaults
164 for k,v in local_and_global_defaults.iteritems():
165 if k not in local_dict:
166 self.set_param('local_'+k, v)
167 if k not in global_dict:
168 self.set_param('global_'+k, v)
170 global_defaults = self.__global_defaults
171 for k,v in global_defaults.iteritems():
172 if k not in global_dict:
173 self.set_param('global_'+k, v)
175 # Allow EDITOR/DIFF_EDITOR environment variable overrides
176 self.global_gui_editor = os.getenv("GUI_EDITOR", self.global_gui_editor)
177 self.global_gui_diffeditor = os.getenv("DIFF_EDITOR", self.global_gui_diffeditor)
179 # Load the diff context
180 self.diff_context = self.local_gui_diffcontext
182 def branch_list(self, remote=False):
183 branches = map(lambda x: x.lstrip('* '),
184 self.git.branch(r=remote).splitlines())
185 if remote:
186 remotes = []
187 for branch in branches:
188 if branch.endswith('/HEAD'):
189 continue
190 remotes.append(branch)
191 return remotes
192 return branches
194 def get_config_params(self):
195 params = []
196 params.extend(map(lambda x: 'local_' + x,
197 self.__local_and_global_defaults.keys()))
198 params.extend(map(lambda x: 'global_' + x,
199 self.__local_and_global_defaults.keys()))
200 params.extend(map(lambda x: 'global_' + x,
201 self.__global_defaults.keys()))
202 return params
204 def save_config_param(self, param):
205 if param not in self.get_config_params():
206 return
207 value = self.get_param(param)
208 if param == 'local_gui_diffcontext':
209 self.diff_context = value
210 if param.startswith('local_'):
211 param = param[len('local_'):]
212 is_local = True
213 elif param.startswith('global_'):
214 param = param[len('global_'):]
215 is_local = False
216 else:
217 raise Exception("Invalid param '%s' passed to " % param
218 + "save_config_param()")
219 param = param.replace('_','.') # model -> git
220 return self.config_set(param, value, local=is_local)
222 def init_browser_data(self):
223 '''This scans over self.(names, sha1s, types) to generate
224 directories, directory_entries, and subtree_*'''
226 # Collect data for the model
227 if not self.get_currentbranch(): return
229 self.subtree_types = []
230 self.subtree_sha1s = []
231 self.subtree_names = []
232 self.directories = []
233 self.directory_entries = {}
235 # Lookup the tree info
236 tree_info = self.parse_ls_tree(self.get_currentbranch())
238 self.set_types(map( lambda(x): x[1], tree_info ))
239 self.set_sha1s(map( lambda(x): x[2], tree_info ))
240 self.set_names(map( lambda(x): x[3], tree_info ))
242 if self.directory: self.directories.append('..')
244 dir_entries = self.directory_entries
245 dir_regex = re.compile('([^/]+)/')
246 dirs_seen = {}
247 subdirs_seen = {}
249 for idx, name in enumerate(self.names):
251 if not name.startswith(self.directory): continue
252 name = name[ len(self.directory): ]
254 if name.count('/'):
255 # This is a directory...
256 match = dir_regex.match(name)
257 if not match: continue
259 dirent = match.group(1) + '/'
260 if dirent not in self.directory_entries:
261 self.directory_entries[dirent] = []
263 if dirent not in dirs_seen:
264 dirs_seen[dirent] = True
265 self.directories.append(dirent)
267 entry = name.replace(dirent, '')
268 entry_match = dir_regex.match(entry)
269 if entry_match:
270 subdir = entry_match.group(1) + '/'
271 if subdir in subdirs_seen: continue
272 subdirs_seen[subdir] = True
273 dir_entries[dirent].append(subdir)
274 else:
275 dir_entries[dirent].append(entry)
276 else:
277 self.subtree_types.append(self.types[idx])
278 self.subtree_sha1s.append(self.sha1s[idx])
279 self.subtree_names.append(name)
281 def add_or_remove(self, *to_process):
282 """Invokes 'git add' to index the filenames in to_process that exist
283 and 'git rm' for those that do not exist."""
285 if not to_process:
286 return 'No files to add or remove.'
288 to_add = []
289 to_remove = []
291 for filename in to_process:
292 if os.path.exists(filename):
293 to_add.append(filename)
295 output = self.git.add(verbose=True, *to_add)
297 if len(to_add) == len(to_process):
298 # to_process only contained unremoved files --
299 # short-circuit the removal checks
300 return output
302 # Process files to remote
303 for filename in to_process:
304 if not os.path.exists(filename):
305 to_remove.append(filename)
306 output + '\n\n' + self.git.rm(*to_remove)
308 def get_editor(self):
309 return self.global_gui_editor
311 def get_diffeditor(self):
312 return self.global_gui_diffeditor
314 def get_history_browser(self):
315 return self.global_gui_historybrowser
317 def remember_gui_settings(self):
318 return self.global_ugit_savewindowsettings
320 def save_at_exit(self):
321 return self.global_ugit_saveatexit
323 def get_tree_node(self, idx):
324 return (self.get_types()[idx],
325 self.get_sha1s()[idx],
326 self.get_names()[idx] )
328 def get_subtree_node(self, idx):
329 return (self.get_subtree_types()[idx],
330 self.get_subtree_sha1s()[idx],
331 self.get_subtree_names()[idx] )
333 def get_all_branches(self):
334 return (self.get_local_branches() + self.get_remote_branches())
336 def set_remote(self, remote):
337 if not remote: return
338 self.set_param('remote', remote)
339 branches = utils.grep( '%s/\S+$' % remote,
340 self.branch_list(remote=True), squash=False)
341 self.set_remote_branches(branches)
343 def add_signoff(self,*rest):
344 '''Adds a standard Signed-off by: tag to the end
345 of the current commit message.'''
347 msg = self.get_commitmsg()
348 signoff =('\n\nSigned-off-by: %s <%s>\n' % (
349 self.get_local_user_name(),
350 self.get_local_user_email()))
352 if signoff not in msg:
353 self.set_commitmsg(msg + signoff)
355 def apply_diff(self, filename):
356 return self.git.apply(filename, index=True, cached=True)
358 def load_commitmsg(self, path):
359 file = open(path, 'r')
360 contents = file.read()
361 file.close()
362 self.set_commitmsg(contents)
364 def get_prev_commitmsg(self,*rest):
365 '''Queries git for the latest commit message and sets it in
366 self.commitmsg.'''
367 commit_msg = []
368 commit_lines = self.git.show('HEAD').split('\n')
369 for idx, msg in enumerate(commit_lines):
370 if idx < 4: continue
371 msg = msg.lstrip()
372 if msg.startswith('diff --git'):
373 commit_msg.pop()
374 break
375 commit_msg.append(msg)
376 self.set_commitmsg('\n'.join(commit_msg).rstrip())
378 def update_status(self):
379 # This allows us to defer notification until the
380 # we finish processing data
381 notify_enabled = self.get_notify()
382 self.set_notify(False)
384 # Reset the staged and unstaged model lists
385 # NOTE: the model's unstaged list is used to
386 # hold both modified and untracked files.
387 self.staged = []
388 self.modified = []
389 self.untracked = []
391 # Read git status items
392 ( staged_items,
393 modified_items,
394 untracked_items ) = self.parse_status()
396 # Gather items to be committed
397 for staged in staged_items:
398 if staged not in self.get_staged():
399 self.add_staged(staged)
401 # Gather unindexed items
402 for modified in modified_items:
403 if modified not in self.get_modified():
404 self.add_modified(modified)
406 # Gather untracked items
407 for untracked in untracked_items:
408 if untracked not in self.get_untracked():
409 self.add_untracked(untracked)
411 self.set_currentbranch(self.current_branch())
412 self.set_unstaged(self.get_modified() + self.get_untracked())
413 self.set_remotes(self.git.remote().splitlines())
414 self.set_remote_branches(self.branch_list(remote=True))
415 self.set_local_branches(self.branch_list(remote=False))
416 self.set_tags(self.git.tag().splitlines())
417 self.set_revision('')
418 self.set_local_branch('')
419 self.set_remote_branch('')
420 # Re-enable notifications and emit changes
421 self.set_notify(notify_enabled)
422 self.notify_observers('staged','unstaged')
424 def delete_branch(self, branch):
425 return self.git.branch(branch, D=True)
427 def get_revision_sha1(self, idx):
428 return self.get_revisions()[idx]
430 def apply_font_size(self, param, default):
431 old_font = self.get_param(param)
432 if not old_font:
433 old_font = default
435 size = self.get_param(param+'_size')
436 props = old_font.split(',')
437 props[1] = str(size)
438 new_font = ','.join(props)
440 self.set_param(param, new_font)
442 def read_font_size(self, param, new_font):
443 new_size = int(new_font.split(',')[1])
444 self.set_param(param, new_size)
446 def get_commit_diff(self, sha1):
447 commit = self.git.show(sha1)
448 first_newline = commit.index('\n')
449 if commit[first_newline+1:].startswith('Merge:'):
450 return (commit
451 + '\n\n'
452 + self.diff_helper(
453 commit=sha1,
454 cached=False,
455 suppress_header=False,
458 else:
459 return commit
461 def get_diff_and_status(self, idx, staged=True):
462 if staged:
463 filename = self.get_staged()[idx]
464 if os.path.exists(filename):
465 status = 'Staged for commit'
466 else:
467 status = 'Staged for removal'
468 diff = self.diff_helper(
469 filename=filename,
470 cached=True,
472 else:
473 filename = self.get_unstaged()[idx]
474 if os.path.isdir(filename):
475 status = 'Untracked directory'
476 diff = '\n'.join(os.listdir(filename))
477 elif filename in self.get_modified():
478 status = 'Modified, not staged'
479 diff = self.diff_helper(
480 filename=filename,
481 cached=False,
483 else:
484 status = 'Untracked, not staged'
486 file_type = utils.run_cmd('file', '-b', filename)
487 if 'binary' in file_type or 'data' in file_type:
488 diff = utils.run_cmd('hexdump', '-C', filename)
489 else:
490 if os.path.exists(filename):
491 file = open(filename, 'r')
492 diff = file.read()
493 file.close()
494 else:
495 diff = ''
496 return diff, status
498 def stage_modified(self):
499 output = self.git.add(self.get_modified())
500 self.update_status()
501 return output
503 def stage_untracked(self):
504 output = self.git.add(self.get_untracked())
505 self.update_status()
506 return output
508 def reset(self, *items):
509 output = self.git.reset('--', *items)
510 self.update_status()
511 return output
513 def unstage_all(self):
514 self.git.reset('--', *self.get_staged())
515 self.update_status()
517 def save_gui_settings(self):
518 self.config_set('ugit.geometry', utils.get_geom(), local=False)
520 def config_set(self, key=None, value=None, local=True):
521 if key and value is not None:
522 # git config category.key value
523 strval = str(value)
524 if type(value) is bool:
525 # git uses "true" and "false"
526 strval = strval.lower()
527 if local:
528 argv = [ key, strval ]
529 else:
530 argv = [ '--global', key, strval ]
531 return self.git.config(*argv)
532 else:
533 msg = "oops in config_set(key=%s,value=%s,local=%s"
534 raise Exception(msg % (key, value, local))
536 def config_dict(self, local=True):
537 """parses the lines from git config --list into a dictionary"""
539 kwargs = {
540 'list': True,
541 'global': not local,
543 config_lines = self.git.config(**kwargs).splitlines()
544 newdict = {}
545 for line in config_lines:
546 k, v = line.split('=', 1)
547 k = k.replace('.','_') # git -> model
548 if v == 'true' or v == 'false':
549 v = bool(eval(v.title()))
550 try:
551 v = int(eval(v))
552 except:
553 pass
554 newdict[k]=v
555 return newdict
557 def commit_with_msg(self, msg, amend=False):
558 """Creates a git commit."""
560 if not msg.endswith('\n'):
561 msg += '\n'
562 # Sure, this is a potential "security risk," but if someone
563 # is trying to intercept/re-write commit messages on your system,
564 # then you probably have bigger problems to worry about.
565 tmpfile = self.get_tmp_filename()
567 # Create the commit message file
568 file = open(tmpfile, 'w')
569 file.write(msg)
570 file.close()
572 # Run 'git commit'
573 output = self.git.commit(F=tmpfile, amend=amend)
574 os.unlink(tmpfile)
576 return ('git commit -F %s --amend %s\n\n%s'
577 % ( tmpfile, amend, output ))
580 def diffindex(self):
581 return self.git.diff(
582 unified=self.diff_context,
583 stat=True,
584 cached=True
587 def get_tmp_filename(self):
588 # Allow TMPDIR/TMP with a fallback to /tmp
589 env = os.environ
590 basename = '.git.%s.%s' % ( os.getpid(), time.time() )
591 tmpdir = env.get('TMP', env.get('TMPDIR', '/tmp'))
592 return os.path.join( tmpdir, basename )
594 def log_helper(self, all=False):
595 """Returns a pair of parallel arrays listing the revision sha1's
596 and commit summaries."""
597 revs = []
598 summaries = []
599 regex = REV_LIST_REGEX
600 output = self.git.log(pretty='oneline', all=all)
601 for line in output.splitlines():
602 match = regex.match(line)
603 if match:
604 revs.append(match.group(1))
605 summaries.append(match.group(2))
606 return( revs, summaries )
608 def parse_rev_list(self, raw_revs):
609 revs = []
610 for line in raw_revs.splitlines():
611 match = REV_LIST_REGEX.match(line)
612 if match:
613 rev_id = match.group(1)
614 summary = match.group(2)
615 revs.append((rev_id, summary,) )
616 return revs
618 def rev_list_range(self, start, end):
619 range = '%s..%s' % ( start, end )
620 raw_revs = self.git.rev_list(range, pretty='oneline')
621 return self.parse_rev_list(raw_revs)
623 def diff_helper(self,
624 commit=None,
625 filename=None,
626 color=False,
627 cached=True,
628 with_diff_header=False,
629 suppress_header=True,
630 reverse=False):
631 "Invokes git diff on a filepath."
633 argv = []
634 if commit:
635 argv.append('%s^..%s' % (commit, commit))
637 if filename:
638 argv.append('--')
639 if type(filename) is list:
640 argv.extend(filename)
641 else:
642 argv.append(filename)
644 diff = self.git.diff(
645 R=reverse,
646 color=color,
647 cached=cached,
648 patch_with_raw=True,
649 unified=self.diff_context,
650 *argv
651 ).splitlines()
653 output = StringIO()
654 start = False
655 del_tag = 'deleted file mode '
657 headers = []
658 deleted = cached and not os.path.exists(filename)
659 for line in diff:
660 if not start and '@@ ' in line and ' @@' in line:
661 start = True
662 if start or(deleted and del_tag in line):
663 output.write(line + '\n')
664 else:
665 if with_diff_header:
666 headers.append(line)
667 elif not suppress_header:
668 output.write(line + '\n')
669 result = output.getvalue()
670 output.close()
671 if with_diff_header:
672 return('\n'.join(headers), result)
673 else:
674 return result
676 def git_repo_path(self, *subpaths):
677 paths = [ self.git.rev_parse(git_dir=True) ]
678 paths.extend(subpaths)
679 return os.path.realpath(os.path.join(*paths))
681 def get_merge_message_path(self):
682 for file in ('MERGE_MSG', 'SQUASH_MSG'):
683 path = self.git_repo_path(file)
684 if os.path.exists(path):
685 return path
686 return None
688 def get_merge_message(self):
689 return self.git.fmt_merge_msg(
690 '--file', self.git_repo_path('FETCH_HEAD')
693 def abort_merge(self):
694 # Reset the worktree
695 output = self.git.read_tree("HEAD", reset=True, u=True, v=True)
696 # remove MERGE_HEAD
697 merge_head = self.git_repo_path('MERGE_HEAD')
698 if os.path.exists(merge_head):
699 os.unlink(merge_head)
700 # remove MERGE_MESSAGE, etc.
701 merge_msg_path = self.get_merge_message_path()
702 while merge_msg_path:
703 os.unlink(merge_msg_path)
704 merge_msg_path = self.get_merge_message_path()
707 def parse_status(self):
708 """RETURNS: A tuple of staged, unstaged and untracked file lists.
710 def eval_path(path):
711 """handles quoted paths."""
712 if path.startswith('"') and path.endswith('"'):
713 return eval(path)
714 else:
715 return path
717 MODIFIED_TAG = '# Changed but not updated:'
718 UNTRACKED_TAG = '# Untracked files:'
719 RGX_RENAMED = re.compile(
720 '(#\trenamed:\s+)'
721 '(.*?)\s->\s(.*)'
723 RGX_MODIFIED = re.compile(
724 '(#\tmodified:\s+'
725 '|#\tnew file:\s+'
726 '|#\tdeleted:\s+)'
728 staged = []
729 unstaged = []
730 untracked = []
732 STAGED_MODE = 0
733 UNSTAGED_MODE = 1
734 UNTRACKED_MODE = 2
736 current_dest = staged
737 mode = STAGED_MODE
739 for status_line in self.git.status().splitlines():
740 if status_line == MODIFIED_TAG:
741 mode = UNSTAGED_MODE
742 current_dest = unstaged
743 continue
744 elif status_line == UNTRACKED_TAG:
745 mode = UNTRACKED_MODE
746 current_dest = untracked
747 continue
748 # Staged/unstaged modified/renamed/deleted files
749 if mode is STAGED_MODE or mode is UNSTAGED_MODE:
750 match = RGX_MODIFIED.match(status_line)
751 if match:
752 tag = match.group(0)
753 filename = status_line.replace(tag, '')
754 current_dest.append(eval_path(filename))
755 continue
756 match = RGX_RENAMED.match(status_line)
757 if match:
758 oldname = match.group(2)
759 newname = match.group(3)
760 current_dest.append(eval_path(oldname))
761 current_dest.append(eval_path(newname))
762 continue
763 # Untracked files
764 elif mode is UNTRACKED_MODE:
765 if status_line.startswith('#\t'):
766 current_dest.append(eval_path(status_line[2:]))
768 return( staged, unstaged, untracked )
770 def reset_helper(self, *args, **kwargs):
771 return self.git.reset('--', *args, **kwargs)
773 def remote_url(self, name):
774 return self.git.config('remote.%s.url' % name, get=True)
776 def get_remote_args(self, remote,
777 local_branch='', remote_branch='',
778 ffwd=True, tags=False):
779 if ffwd:
780 branch_arg = '%s:%s' % ( remote_branch, local_branch )
781 else:
782 branch_arg = '+%s:%s' % ( remote_branch, local_branch )
783 args = [remote]
784 if local_branch and remote_branch:
785 args.append(branch_arg)
786 kwargs = { "with_stderr": True, "with_status": True, "tags": tags }
787 return (args, kwargs)
789 def fetch_helper(self, *args, **kwargs):
791 Fetches remote_branch to local_branch only if
792 remote_branch and local_branch are both supplied.
793 If either is ommitted, "git fetch <remote>" is performed instead.
794 Returns (status,output)
796 args, kwargs = self.get_remote_args(*args, **kwargs)
797 return self.git.fetch(v=True, *args, **kwargs)
799 def push_helper(self, *args, **kwargs):
801 Pushes local_branch to remote's remote_branch only if
802 remote_branch and local_branch both are supplied.
803 If either is ommitted, "git push <remote>" is performed instead.
804 Returns (status,output)
806 args, kwargs = self.get_remote_args(*args, **kwargs)
807 return self.git.push(*args, **kwargs)
809 def pull_helper(self, *args, **kwargs):
811 Pushes branches. If local_branch or remote_branch is ommitted,
812 "git pull <remote>" is performed instead of
813 "git pull <remote> <remote_branch>:<local_branch>
814 Returns (status,output)
816 args, kwargs = self.get_remote_args(*args, **kwargs)
817 return self.git.pull(*args, **kwargs)
820 def parse_ls_tree(self, rev):
821 """Returns a list of(mode, type, sha1, path) tuples."""
822 lines = self.git.ls_tree(rev, r=True).splitlines()
823 output = []
824 regex = re.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
825 for line in lines:
826 match = regex.match(line)
827 if match:
828 mode = match.group(1)
829 objtype = match.group(2)
830 sha1 = match.group(3)
831 filename = match.group(4)
832 output.append((mode, objtype, sha1, filename,) )
833 return output
835 def format_patch_helper(self, to_export, revs, output='patches'):
836 """writes patches named by to_export to the output directory."""
838 outlines = []
840 cur_rev = to_export[0]
841 cur_master_idx = revs.index(cur_rev)
843 patches_to_export = [ [cur_rev] ]
844 patchset_idx = 0
846 # Group the patches into continuous sets
847 for idx, rev in enumerate(to_export[1:]):
848 # Limit the search to the current neighborhood for efficiency
849 master_idx = revs[ cur_master_idx: ].index(rev)
850 master_idx += cur_master_idx
851 if master_idx == cur_master_idx + 1:
852 patches_to_export[ patchset_idx ].append(rev)
853 cur_master_idx += 1
854 continue
855 else:
856 patches_to_export.append([ rev ])
857 cur_master_idx = master_idx
858 patchset_idx += 1
860 # Export each patchsets
861 for patchset in patches_to_export:
862 cmdoutput = self.export_patchset(
863 patchset[0],
864 patchset[-1],
865 output="patches",
866 n=len(patchset) > 1,
867 thread=True,
868 patch_with_stat=True,
870 outlines.append(cmdoutput)
871 return '\n'.join(outlines)
873 def export_patchset(self, start, end, output="patches", **kwargs):
874 revarg = '%s^..%s' % (start, end)
875 return self.git.format_patch("-o", output, revarg, **kwargs)
877 def current_branch(self):
878 """Parses 'git branch' to find the current branch."""
879 branches = self.git.branch().splitlines()
880 for branch in branches:
881 if branch.startswith('* '):
882 return branch.lstrip('* ')
883 return 'Detached HEAD'
885 def create_branch(self, name, base, track=False):
886 """Creates a branch starting from base. Pass track=True
887 to create a remote tracking branch."""
888 return self.git.branch(name, base, track=track)
890 def cherry_pick_list(self, revs, **kwargs):
891 """Cherry-picks each revision into the current branch.
892 Returns a list of command output strings (1 per cherry pick)"""
893 if not revs:
894 return []
895 cherries = []
896 for rev in revs:
897 cherries.append(self.git.cherry_pick(rev, **kwargs))
898 return '\n'.join(cherries)
900 def parse_stash_list(self, revids=False):
901 """Parses "git stash list" and returns a list of stashes."""
902 stashes = self.stash("list").splitlines()
903 if revids:
904 return [ s[:s.index(':')] for s in stashes ]
905 else:
906 return [ s[s.index(':')+1:] for s in stashes ]