tree-wide: trivial code style tweaks
[git-cola.git] / cola / gitcmds.py
blob5a478bfd4254777f46f69eabb017c3ae85962dc2
1 """Git commands and queries for Git"""
2 from __future__ import absolute_import, division, print_function, unicode_literals
3 import json
4 import os
5 import re
6 from io import StringIO
8 from . import core
9 from . import utils
10 from . import version
11 from .git import STDOUT
12 from .git import EMPTY_TREE_OID
13 from .git import OID_LENGTH
14 from .i18n import N_
15 from .interaction import Interaction
18 def add(context, items, u=False):
19 """Run "git add" while preventing argument overflow"""
20 git_add = context.git.add
21 return utils.slice_fn(
22 items, lambda paths: git_add('--', force=True, verbose=True, u=u, *paths)
26 def apply_diff(context, filename):
27 """Use "git apply" to apply the patch in `filename` to the staging area"""
28 git = context.git
29 return git.apply(filename, index=True, cached=True)
32 def apply_diff_to_worktree(context, filename):
33 """Use "git apply" to apply the patch in `filename` to the worktree"""
34 git = context.git
35 return git.apply(filename)
38 def get_branch(context, branch):
39 """Get the current branch"""
40 if branch is None:
41 branch = current_branch(context)
42 return branch
45 def upstream_remote(context, branch=None):
46 """Return the remote associated with the specified branch"""
47 config = context.cfg
48 branch = get_branch(context, branch)
49 return config.get('branch.%s.remote' % branch)
52 def remote_url(context, remote, push=False):
53 """Return the URL for the specified remote"""
54 config = context.cfg
55 url = config.get('remote.%s.url' % remote, '')
56 if push:
57 url = config.get('remote.%s.pushurl' % remote, url)
58 return url
61 def diff_index_filenames(context, ref):
62 """
63 Return a diff of filenames that have been modified relative to the index
64 """
65 git = context.git
66 out = git.diff_index(ref, name_only=True, z=True, _readonly=True)[STDOUT]
67 return _parse_diff_filenames(out)
70 def diff_filenames(context, *args):
71 """Return a list of filenames that have been modified"""
72 git = context.git
73 out = git.diff_tree(
74 name_only=True, no_commit_id=True, r=True, z=True, _readonly=True, *args
75 )[STDOUT]
76 return _parse_diff_filenames(out)
79 def listdir(context, dirname, ref='HEAD'):
80 """Get the contents of a directory according to Git
82 Query Git for the content of a directory, taking ignored
83 files into account.
85 """
86 dirs = []
87 files = []
89 # first, parse git ls-tree to get the tracked files
90 # in a list of (type, path) tuples
91 entries = ls_tree(context, dirname, ref=ref)
92 for entry in entries:
93 if entry[0][0] == 't': # tree
94 dirs.append(entry[1])
95 else:
96 files.append(entry[1])
98 # gather untracked files
99 untracked = untracked_files(context, paths=[dirname], directory=True)
100 for path in untracked:
101 if path.endswith('/'):
102 dirs.append(path[:-1])
103 else:
104 files.append(path)
106 dirs.sort()
107 files.sort()
109 return (dirs, files)
112 def diff(context, args):
113 """Return a list of filenames for the given diff arguments
115 :param args: list of arguments to pass to "git diff --name-only"
118 git = context.git
119 out = git.diff(name_only=True, z=True, _readonly=True, *args)[STDOUT]
120 return _parse_diff_filenames(out)
123 def _parse_diff_filenames(out):
124 if out:
125 return out[:-1].split('\0')
126 return []
129 def tracked_files(context, *args):
130 """Return the names of all files in the repository"""
131 git = context.git
132 out = git.ls_files('--', *args, z=True, _readonly=True)[STDOUT]
133 if out:
134 return sorted(out[:-1].split('\0'))
135 return []
138 def all_files(context, *args):
139 """Returns a sorted list of all files, including untracked files."""
140 git = context.git
141 ls_files = git.ls_files(
142 '--',
143 *args,
144 z=True,
145 cached=True,
146 others=True,
147 exclude_standard=True,
148 _readonly=True
149 )[STDOUT]
150 return sorted([f for f in ls_files.split('\0') if f])
153 class CurrentBranchCache(object):
154 """Cache for current_branch()"""
156 key = None
157 value = None
160 def reset():
161 """Reset cached value in this module (eg. the cached current branch)"""
162 CurrentBranchCache.key = None
165 def current_branch(context):
166 """Return the current branch"""
167 git = context.git
168 head = git.git_path('HEAD')
169 try:
170 key = core.stat(head).st_mtime
171 if CurrentBranchCache.key == key:
172 return CurrentBranchCache.value
173 except OSError:
174 # OSError means we can't use the stat cache
175 key = 0
177 status, data, _ = git.rev_parse('HEAD', symbolic_full_name=True, _readonly=True)
178 if status != 0:
179 # git init -- read .git/HEAD. We could do this unconditionally...
180 data = _read_git_head(context, head)
182 for refs_prefix in ('refs/heads/', 'refs/remotes/', 'refs/tags/'):
183 if data.startswith(refs_prefix):
184 value = data[len(refs_prefix):]
185 CurrentBranchCache.key = key
186 CurrentBranchCache.value = value
187 return value
188 # Detached head
189 return data
192 def _read_git_head(context, head, default='main'):
193 """Pure-python .git/HEAD reader"""
194 # Common .git/HEAD "ref: refs/heads/main" files
195 git = context.git
196 islink = core.islink(head)
197 if core.isfile(head) and not islink:
198 data = core.read(head).rstrip()
199 ref_prefix = 'ref: '
200 if data.startswith(ref_prefix):
201 return data[len(ref_prefix):]
202 # Detached head
203 return data
204 # Legacy .git/HEAD symlinks
205 if islink:
206 refs_heads = core.realpath(git.git_path('refs', 'heads'))
207 path = core.abspath(head).replace('\\', '/')
208 if path.startswith(refs_heads + '/'):
209 return path[len(refs_heads) + 1:]
211 return default
214 def branch_list(context, remote=False):
216 Return a list of local or remote branches
218 This explicitly removes HEAD from the list of remote branches.
221 if remote:
222 return for_each_ref_basename(context, 'refs/remotes')
223 return for_each_ref_basename(context, 'refs/heads')
226 def _version_sort(context, key='version:refname'):
227 if version.check_git(context, 'version-sort'):
228 sort = key
229 else:
230 sort = False
231 return sort
234 def for_each_ref_basename(context, refs):
235 """Return refs starting with 'refs'."""
236 git = context.git
237 sort = _version_sort(context)
238 _, out, _ = git.for_each_ref(refs, format='%(refname)', sort=sort, _readonly=True)
239 output = out.splitlines()
240 non_heads = [x for x in output if not x.endswith('/HEAD')]
241 offset = len(refs) + 1
242 return [x[offset:] for x in non_heads]
245 def _prefix_and_size(prefix, values):
246 """Return a tuple of (prefix, len(prefix) + 1, y) for <prefix>/ stripping"""
247 return (prefix, len(prefix) + 1, values)
250 def all_refs(context, split=False, sort_key='version:refname'):
251 """Return a tuple of (local branches, remote branches, tags)."""
252 git = context.git
253 local_branches = []
254 remote_branches = []
255 tags = []
256 query = (
257 _prefix_and_size('refs/tags', tags),
258 _prefix_and_size('refs/heads', local_branches),
259 _prefix_and_size('refs/remotes', remote_branches),
261 sort = _version_sort(context, key=sort_key)
262 _, out, _ = git.for_each_ref(format='%(refname)', sort=sort, _readonly=True)
263 for ref in out.splitlines():
264 for prefix, prefix_len, dst in query:
265 if ref.startswith(prefix) and not ref.endswith('/HEAD'):
266 dst.append(ref[prefix_len:])
267 continue
268 tags.reverse()
269 if split:
270 return local_branches, remote_branches, tags
271 return local_branches + remote_branches + tags
274 def tracked_branch(context, branch=None):
275 """Return the remote branch associated with 'branch'."""
276 if branch is None:
277 branch = current_branch(context)
278 if branch is None:
279 return None
280 config = context.cfg
281 remote = config.get('branch.%s.remote' % branch)
282 if not remote:
283 return None
284 merge_ref = config.get('branch.%s.merge' % branch)
285 if not merge_ref:
286 return None
287 refs_heads = 'refs/heads/'
288 if merge_ref.startswith(refs_heads):
289 return remote + '/' + merge_ref[len(refs_heads) :]
290 return None
293 def parse_remote_branch(branch):
294 """Split a remote branch apart into (remote, name) components"""
295 rgx = re.compile(r'^(?P<remote>[^/]+)/(?P<branch>.+)$')
296 match = rgx.match(branch)
297 remote = ''
298 branch = ''
299 if match:
300 remote = match.group('remote')
301 branch = match.group('branch')
302 return (remote, branch)
305 def untracked_files(context, paths=None, **kwargs):
306 """Returns a sorted list of untracked files."""
307 git = context.git
308 if paths is None:
309 paths = []
310 args = ['--'] + paths
311 out = git.ls_files(
312 z=True, others=True, exclude_standard=True, _readonly=True, *args, **kwargs
313 )[STDOUT]
314 if out:
315 return out[:-1].split('\0')
316 return []
319 def tag_list(context):
320 """Return a list of tags."""
321 result = for_each_ref_basename(context, 'refs/tags')
322 result.reverse()
323 return result
326 def log(git, *args, **kwargs):
327 return git.log(
328 no_color=True,
329 no_abbrev_commit=True,
330 no_ext_diff=True,
331 _readonly=True,
332 *args,
333 **kwargs
334 )[STDOUT]
337 def commit_diff(context, oid):
338 git = context.git
339 return log(git, '-1', oid, '--') + '\n\n' + oid_diff(context, oid)
342 _diff_overrides = {}
345 def update_diff_overrides(space_at_eol, space_change, all_space, function_context):
346 _diff_overrides['ignore_space_at_eol'] = space_at_eol
347 _diff_overrides['ignore_space_change'] = space_change
348 _diff_overrides['ignore_all_space'] = all_space
349 _diff_overrides['function_context'] = function_context
352 def common_diff_opts(context):
353 config = context.cfg
354 # Default to --patience when diff.algorithm is unset
355 patience = not config.get('diff.algorithm', default='')
356 submodule = version.check_git(context, 'diff-submodule')
357 opts = {
358 'patience': patience,
359 'submodule': submodule,
360 'no_color': True,
361 'no_ext_diff': True,
362 'unified': config.get('gui.diffcontext', default=3),
363 '_raw': True,
365 opts.update(_diff_overrides)
366 return opts
369 def _add_filename(args, filename):
370 if filename:
371 args.extend(['--', filename])
374 def oid_diff(context, oid, filename=None):
375 """Return the diff for an oid"""
376 # Naively "$oid^!" is what we'd like to use but that doesn't
377 # give the correct result for merges--the diff is reversed.
378 # Be explicit and compare oid against its first parent.
379 git = context.git
380 args = [oid + '~', oid]
381 opts = common_diff_opts(context)
382 _add_filename(args, filename)
383 status, out, _ = git.diff(*args, **opts)
384 if status != 0:
385 # We probably don't have "$oid~" because this is the root commit.
386 # "git show" is clever enough to handle the root commit.
387 args = [oid + '^!']
388 _add_filename(args, filename)
389 _, out, _ = git.show(pretty='format:', _readonly=True, *args, **opts)
390 out = out.lstrip()
391 return out
394 def diff_info(context, oid, filename=None):
395 git = context.git
396 decoded = log(git, '-1', oid, '--', pretty='format:%b').strip()
397 if decoded:
398 decoded += '\n\n'
399 return decoded + oid_diff(context, oid, filename=filename)
402 # pylint: disable=too-many-arguments
403 def diff_helper(
404 context,
405 commit=None,
406 ref=None,
407 endref=None,
408 filename=None,
409 cached=True,
410 deleted=False,
411 head=None,
412 amending=False,
413 with_diff_header=False,
414 suppress_header=True,
415 reverse=False,
416 untracked=False,
418 "Invokes git diff on a filepath."
419 git = context.git
420 cfg = context.cfg
421 if commit:
422 ref, endref = commit + '^', commit
423 argv = []
424 if ref and endref:
425 argv.append('%s..%s' % (ref, endref))
426 elif ref:
427 argv.extend(utils.shell_split(ref.strip()))
428 elif head and amending and cached:
429 argv.append(head)
431 encoding = None
432 if untracked:
433 argv.append('--no-index')
434 argv.append(os.devnull)
435 argv.append(filename)
436 elif filename:
437 argv.append('--')
438 if isinstance(filename, (list, tuple)):
439 argv.extend(filename)
440 else:
441 argv.append(filename)
442 encoding = cfg.file_encoding(filename)
444 status, out, _ = git.diff(
445 R=reverse,
446 M=True,
447 cached=cached,
448 _encoding=encoding,
449 *argv,
450 **common_diff_opts(context)
453 success = status == 0
455 # Diff will return 1 when comparing untracked file and it has change,
456 # therefore we will check for diff header from output to differentiate
457 # from actual error such as file not found.
458 if untracked and status == 1:
459 try:
460 _, second, _ = out.split('\n', 2)
461 except ValueError:
462 second = ''
463 success = second.startswith('new file mode ')
465 if not success:
466 # git init
467 if with_diff_header:
468 return ('', '')
469 return ''
471 result = extract_diff_header(deleted, with_diff_header, suppress_header, out)
472 return core.UStr(result, out.encoding)
475 def extract_diff_header(deleted, with_diff_header, suppress_header, diffoutput):
476 """Split a diff into a header section and payload section"""
478 if diffoutput.startswith('Submodule'):
479 if with_diff_header:
480 return ('', diffoutput)
481 return diffoutput
483 start = False
484 del_tag = 'deleted file mode '
486 output = StringIO()
487 headers = StringIO()
489 for line in diffoutput.split('\n'):
490 if not start and line[:2] == '@@' and '@@' in line[2:]:
491 start = True
492 if start or (deleted and del_tag in line):
493 output.write(line + '\n')
494 else:
495 if with_diff_header:
496 headers.write(line + '\n')
497 elif not suppress_header:
498 output.write(line + '\n')
500 output_text = output.getvalue()
501 output.close()
503 headers_text = headers.getvalue()
504 headers.close()
506 if with_diff_header:
507 return (headers_text, output_text)
508 return output_text
511 def format_patchsets(context, to_export, revs, output='patches'):
513 Group contiguous revision selection into patchsets
515 Exists to handle multi-selection.
516 Multiple disparate ranges in the revision selection
517 are grouped into continuous lists.
521 outs = []
522 errs = []
524 cur_rev = to_export[0]
525 cur_rev_idx = revs.index(cur_rev)
527 patches_to_export = [[cur_rev]]
528 patchset_idx = 0
530 # Group the patches into continuous sets
531 for rev in to_export[1:]:
532 # Limit the search to the current neighborhood for efficiency
533 try:
534 rev_idx = revs[cur_rev_idx:].index(rev)
535 rev_idx += cur_rev_idx
536 except ValueError:
537 rev_idx = revs.index(rev)
539 if rev_idx == cur_rev_idx + 1:
540 patches_to_export[patchset_idx].append(rev)
541 cur_rev_idx += 1
542 else:
543 patches_to_export.append([rev])
544 cur_rev_idx = rev_idx
545 patchset_idx += 1
547 # Export each patchsets
548 status = 0
549 for patchset in patches_to_export:
550 stat, out, err = export_patchset(
551 context,
552 patchset[0],
553 patchset[-1],
554 output=output,
555 n=len(patchset) > 1,
556 thread=True,
557 patch_with_stat=True,
559 outs.append(out)
560 if err:
561 errs.append(err)
562 status = max(stat, status)
563 return (status, '\n'.join(outs), '\n'.join(errs))
566 def export_patchset(context, start, end, output='patches', **kwargs):
567 """Export patches from start^ to end."""
568 git = context.git
569 return git.format_patch('-o', output, start + '^..' + end, **kwargs)
572 def reset_paths(context, head, items):
573 """Run "git reset" while preventing argument overflow"""
574 items = list(set(items))
575 fn = context.git.reset
576 status, out, err = utils.slice_fn(items, lambda paths: fn(head, '--', *paths))
577 return (status, out, err)
580 def unstage_paths(context, args, head='HEAD'):
581 """Unstage paths while accounting for git init"""
582 status, out, err = reset_paths(context, head, args)
583 if status == 128:
584 # handle git init: we have to use 'git rm --cached'
585 # detect this condition by checking if the file is still staged
586 return untrack_paths(context, args)
587 return (status, out, err)
590 def untrack_paths(context, args):
591 if not args:
592 return (-1, N_('Nothing to do'), '')
593 git = context.git
594 return git.update_index('--', force_remove=True, *set(args))
597 def worktree_state(
598 context, head='HEAD', update_index=False, display_untracked=True, paths=None
600 """Return a dict of files in various states of being
602 :rtype: dict, keys are staged, unstaged, untracked, unmerged,
603 changed_upstream, and submodule.
606 git = context.git
607 if update_index:
608 git.update_index(refresh=True)
610 staged, unmerged, staged_deleted, staged_submods = diff_index(
611 context, head, paths=paths
613 modified, unstaged_deleted, modified_submods = diff_worktree(context, paths)
614 if display_untracked:
615 untracked = untracked_files(context, paths=paths)
616 else:
617 untracked = []
619 # Remove unmerged paths from the modified list
620 if unmerged:
621 unmerged_set = set(unmerged)
622 modified = [path for path in modified if path not in unmerged_set]
624 # Look for upstream modified files if this is a tracking branch
625 upstream_changed = diff_upstream(context, head)
627 # Keep stuff sorted
628 staged.sort()
629 modified.sort()
630 unmerged.sort()
631 untracked.sort()
632 upstream_changed.sort()
634 return {
635 'staged': staged,
636 'modified': modified,
637 'unmerged': unmerged,
638 'untracked': untracked,
639 'upstream_changed': upstream_changed,
640 'staged_deleted': staged_deleted,
641 'unstaged_deleted': unstaged_deleted,
642 'submodules': staged_submods | modified_submods,
646 def _parse_raw_diff(out):
647 while out:
648 info, path, out = out.split('\0', 2)
649 status = info[-1]
650 is_submodule = '160000' in info[1:14]
651 yield (path, status, is_submodule)
654 def diff_index(context, head, cached=True, paths=None):
655 git = context.git
656 staged = []
657 unmerged = []
658 deleted = set()
659 submodules = set()
661 if paths is None:
662 paths = []
663 args = [head, '--'] + paths
664 status, out, _ = git.diff_index(cached=cached, z=True, _readonly=True, *args)
665 if status != 0:
666 # handle git init
667 args[0] = EMPTY_TREE_OID
668 status, out, _ = git.diff_index(cached=cached, z=True, _readonly=True, *args)
670 for path, status, is_submodule in _parse_raw_diff(out):
671 if is_submodule:
672 submodules.add(path)
673 if status in 'DAMT':
674 staged.append(path)
675 if status == 'D':
676 deleted.add(path)
677 elif status == 'U':
678 unmerged.append(path)
680 return staged, unmerged, deleted, submodules
683 def diff_worktree(context, paths=None):
684 git = context.git
685 ignore_submodules_value = context.cfg.get('diff.ignoresubmodules', 'none')
686 ignore_submodules = ignore_submodules_value in {'all', 'dirty', 'untracked'}
687 modified = []
688 deleted = set()
689 submodules = set()
691 if paths is None:
692 paths = []
693 args = ['--'] + paths
694 status, out, _ = git.diff_files(z=True, _readonly=True, *args)
695 for path, status, is_submodule in _parse_raw_diff(out):
696 if is_submodule:
697 submodules.add(path)
698 if ignore_submodules:
699 continue
700 if status in 'DAMT':
701 modified.append(path)
702 if status == 'D':
703 deleted.add(path)
705 return modified, deleted, submodules
708 def diff_upstream(context, head):
709 """Given `ref`, return $(git merge-base ref HEAD)..ref."""
710 tracked = tracked_branch(context)
711 if not tracked:
712 return []
713 base = merge_base(context, head, tracked)
714 return diff_filenames(context, base, tracked)
717 def list_submodule(context):
718 """Return submodules in the format(state, sha1, path, describe)"""
719 git = context.git
720 status, data, _ = git.submodule('status')
721 ret = []
722 if status == 0 and data:
723 data = data.splitlines()
724 # see git submodule status
725 # TODO better separation
726 for line in data:
727 state = line[0].strip()
728 sha1 = line[1 : OID_LENGTH + 1]
729 left_bracket = line.find('(', OID_LENGTH + 3)
730 if left_bracket == -1:
731 left_bracket = len(line) + 1
732 path = line[OID_LENGTH + 2 : left_bracket - 1]
733 describe = line[left_bracket + 1 : -1]
734 ret.append((state, sha1, path, describe))
735 return ret
738 def merge_base(context, head, ref):
739 """Return the merge-base of head and ref"""
740 git = context.git
741 return git.merge_base(head, ref, _readonly=True)[STDOUT]
744 def merge_base_parent(context, branch):
745 tracked = tracked_branch(context, branch=branch)
746 if tracked:
747 return tracked
748 return 'HEAD'
751 # TODO Unused?
752 def parse_ls_tree(context, rev):
753 """Return a list of (mode, type, oid, path) tuples."""
754 output = []
755 git = context.git
756 lines = git.ls_tree(rev, r=True, _readonly=True)[STDOUT].splitlines()
757 regex = re.compile(r'^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
758 for line in lines:
759 match = regex.match(line)
760 if match:
761 mode = match.group(1)
762 objtype = match.group(2)
763 oid = match.group(3)
764 filename = match.group(4)
765 output.append(
767 mode,
768 objtype,
769 oid,
770 filename,
773 return output
776 # TODO unused?
777 def ls_tree(context, path, ref='HEAD'):
778 """Return a parsed git ls-tree result for a single directory"""
779 git = context.git
780 result = []
781 status, out, _ = git.ls_tree(
782 ref, '--', path, z=True, full_tree=True, _readonly=True
784 if status == 0 and out:
785 path_offset = 6 + 1 + 4 + 1 + OID_LENGTH + 1
786 for line in out[:-1].split('\0'):
787 # 1 1 1
788 # .....6 ...4 ......................................40
789 # 040000 tree c127cde9a0c644a3a8fef449a244f47d5272dfa6 relative
790 # 100644 blob 139e42bf4acaa4927ec9be1ec55a252b97d3f1e2 relative/path
791 # 0..... 7... 12...................................... 53
792 # path_offset = 6 + 1 + 4 + 1 + OID_LENGTH(40) + 1
793 objtype = line[7:11]
794 relpath = line[path_offset:]
795 result.append((objtype, relpath))
797 return result
800 # A regex for matching the output of git(log|rev-list) --pretty=oneline
801 REV_LIST_REGEX = re.compile(r'^([0-9a-f]{40}) (.*)$')
804 def parse_rev_list(raw_revs):
805 """Parse `git log --pretty=online` output into (oid, summary) pairs."""
806 revs = []
807 for line in raw_revs.splitlines():
808 match = REV_LIST_REGEX.match(line)
809 if match:
810 rev_id = match.group(1)
811 summary = match.group(2)
812 revs.append(
814 rev_id,
815 summary,
818 return revs
821 # pylint: disable=redefined-builtin
822 def log_helper(context, all=False, extra_args=None):
823 """Return parallel arrays containing oids and summaries."""
824 revs = []
825 summaries = []
826 args = []
827 if extra_args:
828 args = extra_args
829 git = context.git
830 output = log(git, pretty='oneline', all=all, *args)
831 for line in output.splitlines():
832 match = REV_LIST_REGEX.match(line)
833 if match:
834 revs.append(match.group(1))
835 summaries.append(match.group(2))
836 return (revs, summaries)
839 def rev_list_range(context, start, end):
840 """Return (oid, summary) pairs between start and end."""
841 git = context.git
842 revrange = '%s..%s' % (start, end)
843 out = git.rev_list(revrange, pretty='oneline', _readonly=True)[STDOUT]
844 return parse_rev_list(out)
847 def commit_message_path(context):
848 """Return the path to .git/GIT_COLA_MSG"""
849 git = context.git
850 path = git.git_path('GIT_COLA_MSG')
851 if core.exists(path):
852 return path
853 return None
856 def merge_message_path(context):
857 """Return the path to .git/MERGE_MSG or .git/SQUASH_MSG."""
858 git = context.git
859 for basename in ('MERGE_MSG', 'SQUASH_MSG'):
860 path = git.git_path(basename)
861 if core.exists(path):
862 return path
863 return None
866 def prepare_commit_message_hook(context):
867 """Run the cola.preparecommitmessagehook to prepare the commit message"""
868 config = context.cfg
869 default_hook = config.hooks_path('cola-prepare-commit-msg')
870 return config.get('cola.preparecommitmessagehook', default=default_hook)
873 def cherry_pick(context, revs):
874 """Cherry-picks each revision into the current branch.
876 Returns (0, out, err) where stdout and stderr across all "git cherry-pick"
877 invocations are combined into single values when all cherry-picks succeed.
879 Returns a combined (status, out, err) of the first failing "git cherry-pick"
880 in the event of a non-zero exit status.
882 if not revs:
883 return []
884 outs = []
885 errs = []
886 status = 0
887 for rev in revs:
888 status, out, err = context.git.cherry_pick(rev)
889 if status != 0:
890 details = N_(
891 'Hint: The "Actions > Abort Cherry-Pick" menu action can be used to '
892 'cancel the current cherry-pick.'
894 output = '# git cherry-pick %s\n# %s\n\n%s' % (rev, details, out)
895 return (status, output, err)
896 outs.append(out)
897 errs.append(err)
898 return (0, '\n'.join(outs), '\n'.join(errs))
901 def abort_apply_patch(context):
902 """Abort a "git am" session."""
903 # Reset the worktree
904 git = context.git
905 status, out, err = git.am(abort=True)
906 return status, out, err
909 def abort_cherry_pick(context):
910 """Abort a cherry-pick."""
911 # Reset the worktree
912 git = context.git
913 status, out, err = git.cherry_pick(abort=True)
914 return status, out, err
917 def abort_merge(context):
918 """Abort a merge"""
919 # Reset the worktree
920 git = context.git
921 status, out, err = git.merge(abort=True)
922 return status, out, err
925 def strip_remote(remotes, remote_branch):
926 """Get branch names with the "<remote>/" prefix removed"""
927 for remote in remotes:
928 prefix = remote + '/'
929 if remote_branch.startswith(prefix):
930 return remote_branch[len(prefix):]
931 return remote_branch.split('/', 1)[-1]
934 def parse_refs(context, argv):
935 """Parse command-line arguments into object IDs"""
936 git = context.git
937 status, out, _ = git.rev_parse(_readonly=True, *argv)
938 if status == 0:
939 oids = [oid for oid in out.splitlines() if oid]
940 else:
941 oids = argv
942 return oids
945 def prev_commitmsg(context, *args):
946 """Queries git for the latest commit message."""
947 git = context.git
948 return git.log(
949 '-1', no_color=True, pretty='format:%s%n%n%b', _readonly=True, *args
950 )[STDOUT]
953 def rev_parse(context, name):
954 """Call git rev-parse and return the output"""
955 git = context.git
956 status, out, _ = git.rev_parse(name, _readonly=True)
957 if status == 0:
958 result = out.strip()
959 else:
960 result = name
961 return result
964 def write_blob(context, oid, filename):
965 """Write a blob to a temporary file and return the path
967 Modern versions of Git allow invoking filters. Older versions
968 get the object content as-is.
971 if version.check_git(context, 'cat-file-filters-path'):
972 return cat_file_to_path(context, filename, oid)
973 return cat_file_blob(context, filename, oid)
976 def cat_file_blob(context, filename, oid):
977 """Write a blob from git to the specified filename"""
978 return cat_file(context, filename, 'blob', oid)
981 def cat_file_to_path(context, filename, oid):
982 """Extract a file from an commit ref and a write it to the specified filename"""
983 return cat_file(context, filename, oid, path=filename, filters=True)
986 def cat_file(context, filename, *args, **kwargs):
987 """Redirect git cat-file output to a path"""
988 result = None
989 git = context.git
990 # Use the original filename in the suffix so that the generated filename
991 # has the correct extension, and so that it resembles the original name.
992 basename = os.path.basename(filename)
993 suffix = '-' + basename # ensures the correct filename extension
994 path = utils.tmp_filename('blob', suffix=suffix)
995 with open(path, 'wb') as tmp_file:
996 status, out, err = git.cat_file(
997 _raw=True, _readonly=True, _stdout=tmp_file, *args, **kwargs
999 Interaction.command(N_('Error'), 'git cat-file', status, out, err)
1000 if status == 0:
1001 result = path
1002 if not result:
1003 core.unlink(path)
1004 return result
1007 def write_blob_path(context, head, oid, filename):
1008 """Use write_blob() when modern git is available"""
1009 if version.check_git(context, 'cat-file-filters-path'):
1010 return write_blob(context, oid, filename)
1011 return cat_file_blob(context, filename, head + ':' + filename)
1014 def annex_path(context, head, filename):
1015 """Return the git-annex path for a filename at the specified commit"""
1016 git = context.git
1017 path = None
1018 annex_info = {}
1020 # unfortunately there's no way to filter this down to a single path
1021 # so we just have to scan all reported paths
1022 status, out, _ = git.annex('findref', '--json', head, _readonly=True)
1023 if status == 0:
1024 for line in out.splitlines():
1025 info = json.loads(line)
1026 try:
1027 annex_file = info['file']
1028 except (ValueError, KeyError):
1029 continue
1030 # we only care about this file so we can skip the rest
1031 if annex_file == filename:
1032 annex_info = info
1033 break
1034 key = annex_info.get('key', '')
1035 if key:
1036 status, out, _ = git.annex('contentlocation', key, _readonly=True)
1037 if status == 0 and os.path.exists(out):
1038 path = out
1040 return path
1043 def is_binary(context, filename):
1044 """A heustic to determine whether `filename` contains (non-text) binary content"""
1045 cfg_is_binary = context.cfg.is_binary(filename)
1046 if cfg_is_binary is not None:
1047 return cfg_is_binary
1048 # This is the same heuristic as xdiff-interface.c:buffer_is_binary().
1049 size = 8000
1050 try:
1051 result = core.read(filename, size=size, encoding='bytes')
1052 except (IOError, OSError):
1053 result = b''
1055 return b'\0' in result
1058 def is_valid_ref(context, ref):
1059 """Is the provided Git ref a valid refname?"""
1060 status, _, _ = context.git.rev_parse(ref, quiet=True, verify=True, _readonly=True)
1061 return status == 0