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.
23 self
._work
_tree
= None
24 self
._has
_worktree
= True
25 git_dir
= self
.get_git_dir()
26 work_tree
= self
.get_work_tree()
29 git
.Git
.__init
__(self
, work_tree
)
30 def execute(*args
, **kwargs
):
31 kwargs
['with_exceptions'] = False
32 return git
.Git
.execute(*args
, **kwargs
)
33 def get_work_tree(self
):
34 if self
._work
_tree
or not self
._has
_worktree
:
35 return self
._work
_tree
37 self
._git
_dir
= self
.get_git_dir()
38 # Handle bare repositories
39 if (len(os
.path
.basename(self
._git
_dir
)) > 4
40 and self
._git
_dir
.endswith('.git')):
41 self
._has
_worktree
= False
42 return self
._work
_tree
43 self
._work
_tree
= os
.getenv('GIT_WORK_TREE')
44 if not self
._work
_tree
or not os
.path
.isdir(self
._work
_tree
):
45 self
._work
_tree
= os
.path
.abspath(
46 os
.path
.join(os
.path
.abspath(self
._git
_dir
), '..'))
47 return self
._work
_tree
48 def get_git_dir(self
):
51 self
._git
_dir
= os
.getenv('GIT_DIR')
52 if self
._git
_dir
and self
._is
_git
_dir
(self
._git
_dir
):
54 curpath
= os
.path
.abspath(os
.getcwd())
55 # Search for a .git directory
57 if self
._is
_git
_dir
(curpath
):
58 self
._git
_dir
= curpath
60 gitpath
= os
.path
.join(curpath
, '.git')
61 if self
._is
_git
_dir
(gitpath
):
62 self
._git
_dir
= gitpath
64 curpath
, dummy
= os
.path
.split(curpath
)
68 sys
.stderr
.write("oops, %s is not a git project.\n"
73 def _is_git_dir(self
, d
):
74 """ This is taken from the git setup.c:is_git_directory
77 and os
.path
.isdir(os
.path
.join(d
, 'objects'))
78 and os
.path
.isdir(os
.path
.join(d
, 'refs'))):
79 headref
= os
.path
.join(d
, 'HEAD')
80 return (os
.path
.isfile(headref
)
81 or (os
.path
.islink(headref
)
82 and os
.readlink(headref
).startswith('refs')))
85 class Model(model
.Model
):
86 """Provides a friendly wrapper for doing commit git operations."""
89 """Reads git repository settings and sets several methods
90 so that they refer to the git module. This object
91 encapsulates cola's interaction with git."""
93 # Initialize the git command object
97 self
.__init
_config
_data
()
100 #####################################################
101 # Used in various places
108 git_version
= self
.git
.version(),
110 #####################################################
111 # Used primarily by the main UI
112 project
= os
.path
.basename(os
.getcwd()),
119 window_geom
= utils
.parse_geom(self
.get_cola_config('geometry')),
121 #####################################################
122 # Used by the create branch dialog
125 remote_branches
= [],
128 #####################################################
129 # Used by the commit/repo browser
134 # These are parallel lists
139 # All items below here are re-calculated in
140 # init_browser_data()
142 directory_entries
= {},
144 # These are also parallel lists
150 def __init_config_data(self
):
151 """Reads git config --list and creates parameters
153 # These parameters are saved in .gitconfig,
154 # so ideally these should be as short as possible.
156 # config items that are controllable globally
158 self
.__local
_and
_global
_defaults
= {
161 'merge_summary': False,
162 'merge_diffstat': True,
163 'merge_verbosity': 2,
164 'gui_diffcontext': 3,
165 'gui_pruneduringfetch': False,
167 # config items that are purely git config --global settings
168 self
.__global
_defaults
= {
171 'cola_fontuisize': 12,
173 'cola_fontdiffsize': 12,
174 'cola_savewindowsettings': False,
175 'merge_keepbackup': True,
176 'merge_tool': os
.getenv('MERGETOOL', 'xxdiff'),
177 'gui_editor': os
.getenv('EDITOR', 'gvim'),
178 'gui_historybrowser': 'gitk',
181 local_dict
= self
.config_dict(local
=True)
182 global_dict
= self
.config_dict(local
=False)
184 for k
,v
in local_dict
.iteritems():
185 self
.set_param('local_'+k
, v
)
186 for k
,v
in global_dict
.iteritems():
187 self
.set_param('global_'+k
, v
)
188 if k
not in local_dict
:
190 self
.set_param('local_'+k
, v
)
192 # Bootstrap the internal font*size variables
193 for param
in ('global_cola_fontui', 'global_cola_fontdiff'):
195 if hasattr(self
, param
):
196 font
= self
.get_param(param
)
199 size
= int(font
.split(',')[1])
200 self
.set_param(param
+'size', size
)
201 param
= param
[len('global_'):]
202 global_dict
[param
] = font
203 global_dict
[param
+'size'] = size
205 # Load defaults for all undefined items
206 local_and_global_defaults
= self
.__local
_and
_global
_defaults
207 for k
,v
in local_and_global_defaults
.iteritems():
208 if k
not in local_dict
:
209 self
.set_param('local_'+k
, v
)
210 if k
not in global_dict
:
211 self
.set_param('global_'+k
, v
)
213 global_defaults
= self
.__global
_defaults
214 for k
,v
in global_defaults
.iteritems():
215 if k
not in global_dict
:
216 self
.set_param('global_'+k
, v
)
218 # Load the diff context
219 self
.diff_context
= self
.local_gui_diffcontext
221 def get_global_config(self
, key
):
222 return getattr(self
, 'global_'+key
.replace('.', '_'))
224 def get_cola_config(self
, key
):
225 return getattr(self
, 'global_cola_'+key
)
227 def get_gui_config(self
, key
):
228 return getattr(self
, 'global_gui_'+key
)
230 def branch_list(self
, remote
=False):
231 branches
= map(lambda x
: x
.lstrip('* '),
232 self
.git
.branch(r
=remote
).splitlines())
235 for branch
in branches
:
236 if branch
.endswith('/HEAD'):
238 remotes
.append(branch
)
242 def get_config_params(self
):
244 params
.extend(map(lambda x
: 'local_' + x
,
245 self
.__local
_and
_global
_defaults
.keys()))
246 params
.extend(map(lambda x
: 'global_' + x
,
247 self
.__local
_and
_global
_defaults
.keys()))
248 params
.extend(map(lambda x
: 'global_' + x
,
249 self
.__global
_defaults
.keys()))
250 return [ p
for p
in params
if not p
.endswith('size') ]
252 def save_config_param(self
, param
):
253 if param
not in self
.get_config_params():
255 value
= self
.get_param(param
)
256 if param
== 'local_gui_diffcontext':
257 self
.diff_context
= value
258 if param
.startswith('local_'):
259 param
= param
[len('local_'):]
261 elif param
.startswith('global_'):
262 param
= param
[len('global_'):]
265 raise Exception("Invalid param '%s' passed to " % param
266 +'save_config_param()')
267 param
= param
.replace('_', '.') # model -> git
268 return self
.config_set(param
, value
, local
=is_local
)
270 def init_browser_data(self
):
271 """This scans over self.(names, sha1s, types) to generate
272 directories, directory_entries, and subtree_*"""
274 # Collect data for the model
275 if not self
.get_currentbranch(): return
277 self
.subtree_types
= []
278 self
.subtree_sha1s
= []
279 self
.subtree_names
= []
280 self
.directories
= []
281 self
.directory_entries
= {}
283 # Lookup the tree info
284 tree_info
= self
.parse_ls_tree(self
.get_currentbranch())
286 self
.set_types(map( lambda(x
): x
[1], tree_info
))
287 self
.set_sha1s(map( lambda(x
): x
[2], tree_info
))
288 self
.set_names(map( lambda(x
): x
[3], tree_info
))
290 if self
.directory
: self
.directories
.append('..')
292 dir_entries
= self
.directory_entries
293 dir_regex
= re
.compile('([^/]+)/')
297 for idx
, name
in enumerate(self
.names
):
298 if not name
.startswith(self
.directory
):
300 name
= name
[ len(self
.directory
): ]
302 # This is a directory...
303 match
= dir_regex
.match(name
)
306 dirent
= match
.group(1) + '/'
307 if dirent
not in self
.directory_entries
:
308 self
.directory_entries
[dirent
] = []
310 if dirent
not in dirs_seen
:
311 dirs_seen
[dirent
] = True
312 self
.directories
.append(dirent
)
314 entry
= name
.replace(dirent
, '')
315 entry_match
= dir_regex
.match(entry
)
317 subdir
= entry_match
.group(1) + '/'
318 if subdir
in subdirs_seen
:
320 subdirs_seen
[subdir
] = True
321 dir_entries
[dirent
].append(subdir
)
323 dir_entries
[dirent
].append(entry
)
325 self
.subtree_types
.append(self
.types
[idx
])
326 self
.subtree_sha1s
.append(self
.sha1s
[idx
])
327 self
.subtree_names
.append(name
)
329 def add_or_remove(self
, *to_process
):
330 """Invokes 'git add' to index the filenames in to_process that exist
331 and 'git rm' for those that do not exist."""
334 return 'No files to add or remove.'
339 for filename
in to_process
:
340 encfilename
= filename
.encode('utf-8')
341 if os
.path
.exists(encfilename
):
342 to_add
.append(filename
)
344 output
= self
.git
.add(v
=True, *to_add
)
346 if len(to_add
) == len(to_process
):
347 # to_process only contained unremoved files --
348 # short-circuit the removal checks
351 # Process files to remote
352 for filename
in to_process
:
353 if not os
.path
.exists(filename
):
354 to_remove
.append(filename
)
355 output
+ '\n\n' + self
.git
.rm(*to_remove
)
357 def get_editor(self
):
358 return self
.get_gui_config('editor')
360 def get_mergetool(self
):
361 return self
.get_global_config('merge.tool')
363 def get_history_browser(self
):
364 return self
.get_gui_config('historybrowser')
366 def remember_gui_settings(self
):
367 return self
.get_cola_config('savewindowsettings')
369 def get_tree_node(self
, idx
):
370 return (self
.get_types()[idx
],
371 self
.get_sha1s()[idx
],
372 self
.get_names()[idx
] )
374 def get_subtree_node(self
, idx
):
375 return (self
.get_subtree_types()[idx
],
376 self
.get_subtree_sha1s()[idx
],
377 self
.get_subtree_names()[idx
] )
379 def get_all_branches(self
):
380 return (self
.get_local_branches() + self
.get_remote_branches())
382 def set_remote(self
, remote
):
385 self
.set_param('remote', remote
)
386 branches
= utils
.grep('%s/\S+$' % remote
,
387 self
.branch_list(remote
=True),
389 self
.set_remote_branches(branches
)
391 def add_signoff(self
,*rest
):
392 """Adds a standard Signed-off by: tag to the end
393 of the current commit message."""
394 msg
= self
.get_commitmsg()
395 signoff
=('\n\nSigned-off-by: %s <%s>\n'
396 % (self
.get_local_user_name(), self
.get_local_user_email()))
397 if signoff
not in msg
:
398 self
.set_commitmsg(msg
+ signoff
)
400 def apply_diff(self
, filename
):
401 return self
.git
.apply(filename
, index
=True, cached
=True)
403 def apply_diff_to_worktree(self
, filename
):
404 return self
.git
.apply(filename
)
406 def load_commitmsg(self
, path
):
407 file = open(path
, 'r')
408 contents
= file.read().decode('utf-8')
410 self
.set_commitmsg(contents
)
412 def get_prev_commitmsg(self
,*rest
):
413 """Queries git for the latest commit message and sets it in
416 commit_lines
= self
.git
.show('HEAD').split('\n')
417 for idx
, msg
in enumerate(commit_lines
):
420 if msg
.startswith('diff --git'):
423 commit_msg
.append(msg
)
424 self
.set_commitmsg('\n'.join(commit_msg
).rstrip())
426 def load_commitmsg_template(self
):
428 template
= self
.get_global_config('commit.template')
429 except AttributeError:
431 self
.load_commitmsg(template
)
433 def update_status(self
, amend
=False):
434 # This allows us to defer notification until the
435 # we finish processing data
436 notify_enabled
= self
.get_notify()
437 self
.set_notify(False)
439 # Reset the staged and unstaged model lists
440 # NOTE: the model's unstaged list is used to
441 # hold both modified and untracked files.
446 # Read git status items
450 unmerged_items
) = self
.get_workdir_state(amend
=amend
)
452 # Gather items to be committed
453 for staged
in staged_items
:
454 if staged
not in self
.get_staged():
455 self
.add_staged(staged
)
457 # Gather unindexed items
458 for modified
in modified_items
:
459 if modified
not in self
.get_modified():
460 self
.add_modified(modified
)
462 # Gather untracked items
463 for untracked
in untracked_items
:
464 if untracked
not in self
.get_untracked():
465 self
.add_untracked(untracked
)
467 # Gather unmerged items
468 for unmerged
in unmerged_items
:
469 if unmerged
not in self
.get_unmerged():
470 self
.add_unmerged(unmerged
)
472 self
.set_currentbranch(self
.current_branch())
473 self
.set_unstaged(self
.get_modified() + self
.get_untracked() + self
.get_unmerged())
474 self
.set_remotes(self
.git
.remote().splitlines())
475 self
.set_remote_branches(self
.branch_list(remote
=True))
476 self
.set_local_branches(self
.branch_list(remote
=False))
477 self
.set_tags(self
.git
.tag().splitlines())
478 self
.set_revision('')
479 self
.set_local_branch('')
480 self
.set_remote_branch('')
481 # Re-enable notifications and emit changes
482 self
.set_notify(notify_enabled
)
483 self
.notify_observers('staged','unstaged')
485 def delete_branch(self
, branch
):
486 return self
.git
.branch(branch
, D
=True)
488 def get_revision_sha1(self
, idx
):
489 return self
.get_revisions()[idx
]
491 def apply_font_size(self
, param
, default
):
492 old_font
= self
.get_param(param
)
495 size
= self
.get_param(param
+'size')
496 props
= old_font
.split(',')
498 new_font
= ','.join(props
)
500 self
.set_param(param
, new_font
)
502 def get_commit_diff(self
, sha1
):
503 commit
= self
.git
.show(sha1
)
504 first_newline
= commit
.index('\n')
505 if commit
[first_newline
+1:].startswith('Merge:'):
506 return (commit
+ '\n\n'
507 + self
.diff_helper(commit
=sha1
,
509 suppress_header
=False))
513 def get_filename(self
, idx
, staged
=True):
516 return self
.get_staged()[idx
]
518 return self
.get_unstaged()[idx
]
522 def get_diff_details(self
, idx
, ref
, staged
=True):
523 filename
= self
.get_filename(idx
, staged
=staged
)
525 return (None, None, None)
526 encfilename
= filename
.encode('utf-8')
528 if os
.path
.exists(encfilename
):
529 status
= 'Staged for commit'
531 status
= 'Staged for removal'
532 diff
= self
.diff_helper(filename
=filename
,
536 if os
.path
.isdir(encfilename
):
537 status
= 'Untracked directory'
538 diff
= '\n'.join(os
.listdir(filename
))
540 elif filename
in self
.get_unmerged():
542 diff
= ('@@@+-+-+-+-+-+-+-+-+-+-+ UNMERGED +-+-+-+-+-+-+-+-+-+-+@@@\n\n'
543 '>>> %s is unmerged.\n' % filename
+
544 'Right-click on the filename '
545 'to launch "git mergetool".\n\n\n')
546 diff
+= self
.diff_helper(filename
=filename
,
548 patch_with_raw
=False)
549 elif filename
in self
.get_modified():
550 status
= 'Modified, not staged'
551 diff
= self
.diff_helper(filename
=filename
,
554 status
= 'Untracked, not staged'
555 diff
= 'SHA1: ' + self
.git
.hash_object(filename
)
556 return diff
, status
, filename
558 def stage_modified(self
):
559 output
= self
.git
.add(v
=True, *self
.get_modified())
563 def stage_untracked(self
):
564 output
= self
.git
.add(self
.get_untracked())
568 def reset(self
, *items
):
569 output
= self
.git
.reset('--', *items
)
573 def unstage_all(self
):
574 self
.git
.reset('--', *self
.get_staged())
577 def save_gui_settings(self
):
578 self
.config_set('cola.geometry', utils
.get_geom(), local
=False)
580 def config_set(self
, key
=None, value
=None, local
=True):
581 if key
and value
is not None:
582 # git config category.key value
583 strval
= unicode(value
).encode('utf-8')
584 if type(value
) is bool:
585 # git uses "true" and "false"
586 strval
= strval
.lower()
588 argv
= [ key
, strval
]
590 argv
= [ '--global', key
, strval
]
591 return self
.git
.config(*argv
)
593 msg
= "oops in config_set(key=%s,value=%s,local=%s"
594 raise Exception(msg
% (key
, value
, local
))
596 def config_dict(self
, local
=True):
597 """parses the lines from git config --list into a dictionary"""
603 config_lines
= self
.git
.config(**kwargs
).splitlines()
605 for line
in config_lines
:
606 k
, v
= line
.split('=', 1)
607 k
= k
.replace('.','_') # git -> model
608 if v
== 'true' or v
== 'false':
609 v
= bool(eval(v
.title()))
617 def commit_with_msg(self
, msg
, amend
=False):
618 """Creates a git commit."""
620 if not msg
.endswith('\n'):
622 # Sure, this is a potential "security risk," but if someone
623 # is trying to intercept/re-write commit messages on your system,
624 # then you probably have bigger problems to worry about.
625 tmpfile
= self
.get_tmp_filename()
627 # Create the commit message file
628 fh
= open(tmpfile
, 'w')
633 (status
, stdout
, stderr
) = self
.git
.commit(v
=True,
636 with_extended_output
=True)
639 return (status
, stdout
+stderr
)
643 return self
.git
.diff(unified
=self
.diff_context
,
647 def get_tmp_dir(self
):
648 return os
.environ
.get('TMP', os
.environ
.get('TMPDIR', '/tmp'))
650 def get_tmp_file_pattern(self
):
651 return os
.path
.join(self
.get_tmp_dir(), '*.git.%s.*' % os
.getpid())
653 def get_tmp_filename(self
, prefix
=''):
654 # Allow TMPDIR/TMP with a fallback to /tmp
655 basename
= (prefix
+'.git.%s.%s'
656 % (os
.getpid(), time
.time())).replace(os
.sep
, '-')
657 return os
.path
.join(self
.get_tmp_dir(), basename
)
659 def log_helper(self
, all
=False):
661 Returns a pair of parallel arrays listing the revision sha1's
662 and commit summaries.
666 regex
= REV_LIST_REGEX
667 output
= self
.git
.log(pretty
='oneline', all
=all
)
668 for line
in output
.splitlines():
669 match
= regex
.match(line
)
671 revs
.append(match
.group(1))
672 summaries
.append(match
.group(2))
673 return (revs
, summaries
)
675 def parse_rev_list(self
, raw_revs
):
677 for line
in raw_revs
.splitlines():
678 match
= REV_LIST_REGEX
.match(line
)
680 rev_id
= match
.group(1)
681 summary
= match
.group(2)
682 revs
.append((rev_id
, summary
,))
685 def rev_list_range(self
, start
, end
):
686 range = '%s..%s' % (start
, end
)
687 raw_revs
= self
.git
.rev_list(range, pretty
='oneline')
688 return self
.parse_rev_list(raw_revs
)
690 def diff_helper(self
,
697 with_diff_header
=False,
698 suppress_header
=True,
700 patch_with_raw
=True):
701 "Invokes git diff on a filepath."
703 ref
, endref
= commit
+'^', commit
706 argv
.append('%s..%s' % (ref
, endref
))
714 if type(filename
) is list:
715 argv
.extend(filename
)
717 argv
.append(filename
)
721 del_tag
= 'deleted file mode '
724 deleted
= cached
and not os
.path
.exists(filename
.encode('utf-8'))
726 diffoutput
= self
.git
.diff(R
=reverse
,
728 patch_with_raw
=patch_with_raw
,
729 unified
=self
.diff_context
,
730 with_raw_output
=True,
732 diff
= diffoutput
.splitlines()
734 line
= unicode(line
).encode('utf-8')
735 if not start
and '@@' == line
[:2] and '@@' in line
[2:]:
737 if start
or(deleted
and del_tag
in line
):
738 output
.write(line
+ '\n')
742 elif not suppress_header
:
743 output
.write(line
+ '\n')
745 result
= output
.getvalue().decode('utf-8')
749 return('\n'.join(headers
), result
)
753 def git_repo_path(self
, *subpaths
):
754 paths
= [ self
.git
.get_git_dir() ]
755 paths
.extend(subpaths
)
756 return os
.path
.realpath(os
.path
.join(*paths
))
758 def get_merge_message_path(self
):
759 for file in ('MERGE_MSG', 'SQUASH_MSG'):
760 path
= self
.git_repo_path(file)
761 if os
.path
.exists(path
):
765 def get_merge_message(self
):
766 return self
.git
.fmt_merge_msg('--file',
767 self
.git_repo_path('FETCH_HEAD'))
769 def abort_merge(self
):
771 output
= self
.git
.read_tree('HEAD', reset
=True, u
=True, v
=True)
773 merge_head
= self
.git_repo_path('MERGE_HEAD')
774 if os
.path
.exists(merge_head
):
775 os
.unlink(merge_head
)
776 # remove MERGE_MESSAGE, etc.
777 merge_msg_path
= self
.get_merge_message_path()
778 while merge_msg_path
:
779 os
.unlink(merge_msg_path
)
780 merge_msg_path
= self
.get_merge_message_path()
782 def get_workdir_state(self
, amend
=False):
783 """RETURNS: A tuple of staged, unstaged untracked, and unmerged
787 """handles quoted paths."""
788 if path
.startswith('"') and path
.endswith('"'):
789 return eval(path
).decode('utf-8')
796 (staged
, unstaged
, unmerged
, untracked
) = ([], [], [], [])
798 for idx
, line
in enumerate(self
.git
.diff_index(head
).splitlines()):
799 rest
, name
= line
.split('\t')
801 name
= eval_path(name
)
802 if status
== 'M' or status
== 'D':
803 unstaged
.append(name
)
805 for idx
, line
in enumerate(self
.git
.diff_index(head
, cached
=True)
807 rest
, name
= line
.split('\t')
809 name
= eval_path(name
)
812 # is this file partially staged?
813 diff
= self
.git
.diff('--', name
, name_only
=True, z
=True)
815 unstaged
.remove(name
)
820 unstaged
.remove(name
)
822 unmerged
.append(name
)
824 for line
in self
.git
.ls_files(others
=True, exclude_standard
=True,
827 untracked
.append(line
)
829 return (staged
, unstaged
, untracked
, unmerged
)
831 def reset_helper(self
, *args
, **kwargs
):
832 return self
.git
.reset('--', *args
, **kwargs
)
834 def remote_url(self
, name
):
835 return self
.git
.config('remote.%s.url' % name
, get
=True)
837 def get_remote_args(self
, remote
,
838 local_branch
='', remote_branch
='',
839 ffwd
=True, tags
=False):
841 branch_arg
= '%s:%s' % ( remote_branch
, local_branch
)
843 branch_arg
= '+%s:%s' % ( remote_branch
, local_branch
)
845 if local_branch
and remote_branch
:
846 args
.append(branch_arg
)
848 "with_extended_output": True,
851 return (args
, kwargs
)
853 def fetch_helper(self
, *args
, **kwargs
):
855 Fetches remote_branch to local_branch only if
856 remote_branch and local_branch are both supplied.
857 If either is ommitted, "git fetch <remote>" is performed instead.
858 Returns (status,output)
860 args
, kwargs
= self
.get_remote_args(*args
, **kwargs
)
861 (status
, stdout
, stderr
) = self
.git
.fetch(v
=True, *args
, **kwargs
)
862 return (status
, stdout
+ stderr
)
864 def push_helper(self
, *args
, **kwargs
):
866 Pushes local_branch to remote's remote_branch only if
867 remote_branch and local_branch both are supplied.
868 If either is ommitted, "git push <remote>" is performed instead.
869 Returns (status,output)
871 args
, kwargs
= self
.get_remote_args(*args
, **kwargs
)
872 (status
, stdout
, stderr
) = self
.git
.push(*args
, **kwargs
)
873 return (status
, stdout
+ stderr
)
875 def pull_helper(self
, *args
, **kwargs
):
877 Pushes branches. If local_branch or remote_branch is ommitted,
878 "git pull <remote>" is performed instead of
879 "git pull <remote> <remote_branch>:<local_branch>
880 Returns (status,output)
882 args
, kwargs
= self
.get_remote_args(*args
, **kwargs
)
883 (status
, stdout
, stderr
) = self
.git
.pull(v
=True, *args
, **kwargs
)
884 return (status
, stdout
+ stderr
)
886 def parse_ls_tree(self
, rev
):
887 """Returns a list of(mode, type, sha1, path) tuples."""
888 lines
= self
.git
.ls_tree(rev
, r
=True).splitlines()
890 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
892 match
= regex
.match(line
)
894 mode
= match
.group(1)
895 objtype
= match
.group(2)
896 sha1
= match
.group(3)
897 filename
= match
.group(4)
898 output
.append((mode
, objtype
, sha1
, filename
,) )
901 def format_patch_helper(self
, to_export
, revs
, output
='patches'):
902 """writes patches named by to_export to the output directory."""
906 cur_rev
= to_export
[0]
907 cur_master_idx
= revs
.index(cur_rev
)
909 patches_to_export
= [ [cur_rev
] ]
912 # Group the patches into continuous sets
913 for idx
, rev
in enumerate(to_export
[1:]):
914 # Limit the search to the current neighborhood for efficiency
915 master_idx
= revs
[ cur_master_idx
: ].index(rev
)
916 master_idx
+= cur_master_idx
917 if master_idx
== cur_master_idx
+ 1:
918 patches_to_export
[ patchset_idx
].append(rev
)
922 patches_to_export
.append([ rev
])
923 cur_master_idx
= master_idx
926 # Export each patchsets
927 for patchset
in patches_to_export
:
928 cmdoutput
= self
.export_patchset(patchset
[0],
933 patch_with_stat
=True)
934 outlines
.append(cmdoutput
)
935 return '\n'.join(outlines
)
937 def export_patchset(self
, start
, end
, output
="patches", **kwargs
):
938 revarg
= '%s^..%s' % (start
, end
)
939 return self
.git
.format_patch("-o", output
, revarg
, **kwargs
)
941 def current_branch(self
):
942 """Parses 'git branch' to find the current branch."""
943 branches
= self
.git
.branch().splitlines()
944 for branch
in branches
:
945 if branch
.startswith('* '):
946 return branch
.lstrip('* ')
947 return 'Detached HEAD'
949 def create_branch(self
, name
, base
, track
=False):
950 """Creates a branch starting from base. Pass track=True
951 to create a remote tracking branch."""
952 return self
.git
.branch(name
, base
, track
=track
)
954 def cherry_pick_list(self
, revs
, **kwargs
):
955 """Cherry-picks each revision into the current branch.
956 Returns a list of command output strings (1 per cherry pick)"""
961 cherries
.append(self
.git
.cherry_pick(rev
, **kwargs
))
962 return '\n'.join(cherries
)
964 def parse_stash_list(self
, revids
=False):
965 """Parses "git stash list" and returns a list of stashes."""
966 stashes
= self
.git
.stash("list").splitlines()
968 return [ s
[:s
.index(':')] for s
in stashes
]
970 return [ s
[s
.index(':')+1:] for s
in stashes
]
973 return self
.git
.diff(
975 unified
=self
.diff_context
,
978 def pad(self
, pstr
, num
=22):
979 topad
= num
-len(pstr
)
981 return pstr
+ ' '*topad
985 def describe(self
, revid
, descr
):
986 version
= self
.git
.describe(revid
, tags
=True, always
=True,
988 return version
+ ' - ' + descr
990 def update_revision_lists(self
, filename
=None, show_versions
=False):
991 num_results
= self
.get_num_results()
993 rev_list
= self
.git
.log('--', filename
,
994 max_count
=num_results
,
997 rev_list
= self
.git
.log(max_count
=num_results
,
998 pretty
='oneline', all
=True)
1000 commit_list
= self
.parse_rev_list(rev_list
)
1001 commit_list
.reverse()
1002 commits
= map(lambda x
: x
[0], commit_list
)
1003 descriptions
= map(lambda x
: x
[1], commit_list
)
1005 fancy_descr_list
= map(lambda x
: self
.describe(*x
), commit_list
)
1006 self
.set_descriptions_start(fancy_descr_list
)
1007 self
.set_descriptions_end(fancy_descr_list
)
1009 self
.set_descriptions_start(descriptions
)
1010 self
.set_descriptions_end(descriptions
)
1012 self
.set_revisions_start(commits
)
1013 self
.set_revisions_end(commits
)
1017 def get_changed_files(self
, start
, end
):
1018 zfiles_str
= self
.git
.diff('%s..%s' % (start
, end
),
1019 name_only
=True, z
=True)
1020 zfiles_str
= zfiles_str
.strip('\0')
1021 files
= zfiles_str
.split('\0')
1024 def get_renamed_files(self
, start
, end
):
1026 difflines
= self
.git
.diff('%s..%s' % (start
, end
), M
=True).splitlines()
1027 for line
in difflines
:
1028 if line
.startswith('rename from '):
1029 files
.append(line
[12:].rstrip())