cola: use better config values for the font size params
[git-cola.git] / cola / models.py
blobb8b01be52beb025b408317e3b02da9e2462e746c
1 import os
2 import sys
3 import re
4 import time
5 import subprocess
6 from cStringIO import StringIO
8 from cola import git
9 from cola import utils
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()
18 GIT_COMMANDS = """
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
23 describe diff
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
34 """.split()
36 class GitCola(git.Git):
37 """GitPython throws exceptions by default.
38 We suppress exceptions in favor of return values.
39 """
40 def __init__(self):
41 self._git_dir = None
42 self._work_tree = None
43 git_dir = self.get_git_dir()
44 work_tree = self.get_work_tree()
45 if work_tree:
46 os.chdir(work_tree)
47 else:
48 print "Sorry, git-cola requires a work tree"
49 sys.exit(-1)
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):
62 if not self._git_dir:
63 self._git_dir = os.getenv('GIT_DIR')
64 if self._git_dir and self._is_git_dir(self._git_dir):
65 return self._git_dir
66 curpath = os.getcwd()
67 while curpath:
68 if self._is_git_dir(curpath):
69 self._git_dir = curpath
70 break
71 gitpath = os.path.join(curpath, '.git')
72 if self._is_git_dir(gitpath):
73 self._git_dir = gitpath
74 break
75 curpath, dummy = os.path.split(curpath)
76 if not dummy:
77 break
78 return self._git_dir
80 def _is_git_dir(self, d):
81 """ This is taken from the git setup.c:is_git_directory
82 function."""
83 if (os.path.isdir(d)
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')))
90 return False
92 class Model(model.Model):
93 """Provides a friendly wrapper for doing commit git operations."""
95 def init(self):
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.
102 self.git = GitCola()
104 # Read git config
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))
111 self.create(
112 #####################################################
113 # Used in various places
114 currentbranch = '',
115 remotes = [],
116 remotename = '',
117 local_branch = '',
118 remote_branch = '',
119 search_text = '',
120 git_version = self.git.version(),
122 #####################################################
123 # Used primarily by the main UI
124 project = os.path.basename(os.getcwd()),
125 commitmsg = '',
126 modified = [],
127 staged = [],
128 unstaged = [],
129 untracked = [],
130 window_geom = utils.parse_geom(self.get_global_cola_geometry()),
132 #####################################################
133 # Used by the create branch dialog
134 revision = '',
135 local_branches = [],
136 remote_branches = [],
137 tags = [],
139 #####################################################
140 # Used by the commit/repo browser
141 directory = '',
142 revisions = [],
143 summaries = [],
145 # These are parallel lists
146 types = [],
147 sha1s = [],
148 names = [],
150 # All items below here are re-calculated in
151 # init_browser_data()
152 directories = [],
153 directory_entries = {},
155 # These are also parallel lists
156 subtree_types = [],
157 subtree_sha1s = [],
158 subtree_names = [],
161 def __init_config_data(self):
162 """Reads git config --list and creates parameters
163 for each setting."""
164 # These parameters are saved in .gitconfig,
165 # so ideally these should be as short as possible.
167 # config items that are controllable globally
168 # and per-repository
169 self.__local_and_global_defaults = {
170 'user_name': '',
171 'user_email': '',
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 = {
180 'cola_geometry':'',
181 'cola_fontui': '',
182 'cola_fontuisize': 12,
183 'cola_fontdiff': '',
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:
201 local_dict[k]=v
202 self.set_param('local_'+k, v)
204 # Bootstrap the internal font*size variables
205 for param in ('global_cola_fontui', 'global_cola_fontdiff'):
206 setdefault = True
207 if hasattr(self, param):
208 font = self.get_param(param)
209 if font:
210 setdefault = False
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())
247 if remote:
248 remotes = []
249 for branch in branches:
250 if branch.endswith('/HEAD'):
251 continue
252 remotes.append(branch)
253 return remotes
254 return branches
256 def get_config_params(self):
257 params = []
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():
268 return
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_'):]
274 is_local = True
275 elif param.startswith('global_'):
276 param = param[len('global_'):]
277 is_local = False
278 else:
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('([^/]+)/')
308 dirs_seen = {}
309 subdirs_seen = {}
311 for idx, name in enumerate(self.names):
312 if not name.startswith(self.directory):
313 continue
314 name = name[ len(self.directory): ]
315 if name.count('/'):
316 # This is a directory...
317 match = dir_regex.match(name)
318 if not match:
319 continue
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)
330 if entry_match:
331 subdir = entry_match.group(1) + '/'
332 if subdir in subdirs_seen:
333 continue
334 subdirs_seen[subdir] = True
335 dir_entries[dirent].append(subdir)
336 else:
337 dir_entries[dirent].append(entry)
338 else:
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."""
347 if not to_process:
348 return 'No files to add or remove.'
350 to_add = []
351 to_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
362 return output
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),
403 squash=False)
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()
425 file.close()
426 self.set_commitmsg(contents)
428 def get_prev_commitmsg(self,*rest):
429 """Queries git for the latest commit message and sets it in
430 self.commitmsg."""
431 commit_msg = []
432 commit_lines = self.git.show('HEAD').split('\n')
433 for idx, msg in enumerate(commit_lines):
434 if idx < 4: continue
435 msg = msg.lstrip()
436 if msg.startswith('diff --git'):
437 commit_msg.pop()
438 break
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.
451 self.staged = []
452 self.modified = []
453 self.untracked = []
455 # Read git status items
456 (staged_items,
457 modified_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)
496 if not old_font:
497 old_font = default
498 size = self.get_param(param+'size')
499 props = old_font.split(',')
500 props[1] = str(size)
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,
511 cached=False,
512 suppress_header=False))
513 else:
514 return commit
516 def get_diff_details(self, idx, staged=True):
517 if staged:
518 filename = self.get_staged()[idx]
519 if os.path.exists(filename):
520 status = 'Staged for commit'
521 else:
522 status = 'Staged for removal'
523 diff = self.diff_helper(filename=filename,
524 cached=True)
525 else:
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,
533 cached=False)
534 else:
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)
540 else:
541 if os.path.exists(filename):
542 file = open(filename, 'r')
543 diff = file.read()
544 file.close()
545 else:
546 diff = ''
547 return diff, status, filename
549 def stage_modified(self):
550 output = self.git.add(self.get_modified())
551 self.update_status()
552 return output
554 def stage_untracked(self):
555 output = self.git.add(self.get_untracked())
556 self.update_status()
557 return output
559 def reset(self, *items):
560 output = self.git.reset('--', *items)
561 self.update_status()
562 return output
564 def unstage_all(self):
565 self.git.reset('--', *self.get_staged())
566 self.update_status()
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
574 strval = str(value)
575 if type(value) is bool:
576 # git uses "true" and "false"
577 strval = strval.lower()
578 if local:
579 argv = [ key, strval ]
580 else:
581 argv = [ '--global', key, strval ]
582 return self.git.config(*argv)
583 else:
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"""
590 kwargs = {
591 'list': True,
592 'global': not local,
594 config_lines = self.git.config(**kwargs).splitlines()
595 newdict = {}
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()))
601 try:
602 v = int(eval(v))
603 except:
604 pass
605 newdict[k]=v
606 return newdict
608 def commit_with_msg(self, msg, amend=False):
609 """Creates a git commit."""
611 if not msg.endswith('\n'):
612 msg += '\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')
620 file.write(msg)
621 file.close()
623 # Run 'git commit'
624 output = self.git.commit(F=tmpfile, amend=amend)
625 os.unlink(tmpfile)
627 return ('git commit -F %s --amend %s\n\n%s'
628 % ( tmpfile, amend, output ))
631 def diffindex(self):
632 return self.git.diff(unified=self.diff_context,
633 stat=True,
634 cached=True)
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.
653 revs = []
654 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)
659 if match:
660 revs.append(match.group(1))
661 summaries.append(match.group(2))
662 return (revs, summaries)
664 def parse_rev_list(self, raw_revs):
665 revs = []
666 for line in raw_revs.splitlines():
667 match = REV_LIST_REGEX.match(line)
668 if match:
669 rev_id = match.group(1)
670 summary = match.group(2)
671 revs.append((rev_id, summary,))
672 return revs
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,
680 commit=None,
681 branch=None,
682 filename=None,
683 color=False,
684 cached=True,
685 with_diff_header=False,
686 suppress_header=True,
687 reverse=False):
688 "Invokes git diff on a filepath."
690 argv = []
691 if commit:
692 argv.append('%s^..%s' % (commit, commit))
693 elif branch:
694 argv.append(branch)
696 if filename:
697 argv.append('--')
698 if type(filename) is list:
699 argv.extend(filename)
700 else:
701 argv.append(filename)
703 diffoutput = self.git.diff(R=reverse,
704 color=color,
705 cached=cached,
706 patch_with_raw=True,
707 unified=self.diff_context,
708 with_raw_output=True,
709 *argv)
710 diff = diffoutput.splitlines()
712 output = StringIO()
713 start = False
714 del_tag = 'deleted file mode '
716 headers = []
717 deleted = cached and not os.path.exists(filename)
718 for line in diff:
719 if not start and '@@ ' in line and ' @@' in line:
720 start = True
721 if start or(deleted and del_tag in line):
722 output.write(line + '\n')
723 else:
724 if with_diff_header:
725 headers.append(line)
726 elif not suppress_header:
727 output.write(line + '\n')
728 result = output.getvalue()
729 output.close()
730 if with_diff_header:
731 return('\n'.join(headers), result)
732 else:
733 return 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):
744 return path
745 return None
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):
752 # Reset the worktree
753 output = self.git.read_tree('HEAD', reset=True, u=True, v=True)
754 # remove MERGE_HEAD
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.
767 def eval_path(path):
768 """handles quoted paths."""
769 if path.startswith('"') and path.endswith('"'):
770 return eval(path)
771 else:
772 return path
774 MODIFIED_TAG = '# Changed but not updated:'
775 UNTRACKED_TAG = '# Untracked files:'
776 RGX_RENAMED = re.compile('(#\trenamed:\s+|#\tcopied:\s+)'
777 '(.*?)\s->\s(.*)')
778 RGX_MODIFIED = re.compile('(#\tmodified:\s+'
779 '|#\tnew file:\s+'
780 '|#\tdeleted:\s+)')
781 staged = []
782 unstaged = []
783 untracked = []
785 STAGED_MODE = 0
786 UNSTAGED_MODE = 1
787 UNTRACKED_MODE = 2
789 current_dest = staged
790 mode = STAGED_MODE
792 for status_line in self.git.status().splitlines():
793 if status_line == MODIFIED_TAG:
794 mode = UNSTAGED_MODE
795 current_dest = unstaged
796 continue
797 elif status_line == UNTRACKED_TAG:
798 mode = UNTRACKED_MODE
799 current_dest = untracked
800 continue
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)
804 if match:
805 tag = match.group(0)
806 filename = status_line.replace(tag, '')
807 current_dest.append(eval_path(filename))
808 continue
809 match = RGX_RENAMED.match(status_line)
810 if match:
811 oldname = match.group(2)
812 newname = match.group(3)
813 current_dest.append(eval_path(oldname))
814 current_dest.append(eval_path(newname))
815 continue
816 # Untracked files
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):
832 if ffwd:
833 branch_arg = '%s:%s' % ( remote_branch, local_branch )
834 else:
835 branch_arg = '+%s:%s' % ( remote_branch, local_branch )
836 args = [remote]
837 if local_branch and remote_branch:
838 args.append(branch_arg)
839 kwargs = {
840 "with_extended_output": True,
841 "tags": tags
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()
881 output = []
882 regex = re.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
883 for line in lines:
884 match = regex.match(line)
885 if match:
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,) )
891 return output
893 def format_patch_helper(self, to_export, revs, output='patches'):
894 """writes patches named by to_export to the output directory."""
896 outlines = []
898 cur_rev = to_export[0]
899 cur_master_idx = revs.index(cur_rev)
901 patches_to_export = [ [cur_rev] ]
902 patchset_idx = 0
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)
911 cur_master_idx += 1
912 continue
913 else:
914 patches_to_export.append([ rev ])
915 cur_master_idx = master_idx
916 patchset_idx += 1
918 # Export each patchsets
919 for patchset in patches_to_export:
920 cmdoutput = self.export_patchset(patchset[0],
921 patchset[-1],
922 output="patches",
923 n=len(patchset) > 1,
924 thread=True,
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)"""
949 if not revs:
950 return []
951 cherries = []
952 for rev in revs:
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()
959 if revids:
960 return [ s[:s.index(':')] for s in stashes ]
961 else:
962 return [ s[s.index(':')+1:] for s in stashes ]