gitcmds: fix pylint violations
[git-cola.git] / cola / gitcmds.py
blob0f4346447dad86e4a00bad6252148a43156cc41f
1 """Git commands and queries for Git"""
2 from __future__ import division, absolute_import, 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 class InvalidRepositoryError(Exception):
19 pass
22 def add(context, items, u=False):
23 """Run "git add" while preventing argument overflow"""
24 fn = context.git.add
25 return utils.slice_fn(
26 items, lambda paths: fn('--', force=True, verbose=True, u=u, *paths)
30 def apply_diff(context, filename):
31 git = context.git
32 return git.apply(filename, index=True, cached=True)
35 def apply_diff_to_worktree(context, filename):
36 git = context.git
37 return git.apply(filename)
40 def get_branch(context, branch):
41 if branch is None:
42 branch = current_branch(context)
43 return branch
46 def upstream_remote(context, branch=None):
47 """Return the remote associated with the specified branch"""
48 config = context.cfg
49 branch = get_branch(context, branch)
50 return config.get('branch.%s.remote' % branch)
53 def remote_url(context, remote, push=False):
54 """Return the URL for the specified remote"""
55 config = context.cfg
56 url = config.get('remote.%s.url' % remote, '')
57 if push:
58 url = config.get('remote.%s.pushurl' % remote, url)
59 return url
62 def diff_index_filenames(context, ref):
63 """
64 Return a diff of filenames that have been modified relative to the index
65 """
66 git = context.git
67 out = git.diff_index(ref, name_only=True, z=True)[STDOUT]
68 return _parse_diff_filenames(out)
71 def diff_filenames(context, *args):
72 """Return a list of filenames that have been modified"""
73 git = context.git
74 out = git.diff_tree(
75 name_only=True, no_commit_id=True, r=True, z=True, _readonly=True, *args
76 )[STDOUT]
77 return _parse_diff_filenames(out)
80 def listdir(context, dirname, ref='HEAD'):
81 """Get the contents of a directory according to Git
83 Query Git for the content of a directory, taking ignored
84 files into account.
86 """
87 dirs = []
88 files = []
90 # first, parse git ls-tree to get the tracked files
91 # in a list of (type, path) tuples
92 entries = ls_tree(context, dirname, ref=ref)
93 for entry in entries:
94 if entry[0][0] == 't': # tree
95 dirs.append(entry[1])
96 else:
97 files.append(entry[1])
99 # gather untracked files
100 untracked = untracked_files(context, paths=[dirname], directory=True)
101 for path in untracked:
102 if path.endswith('/'):
103 dirs.append(path[:-1])
104 else:
105 files.append(path)
107 dirs.sort()
108 files.sort()
110 return (dirs, files)
113 def diff(context, args):
114 """Return a list of filenames for the given diff arguments
116 :param args: list of arguments to pass to "git diff --name-only"
119 git = context.git
120 out = git.diff(name_only=True, z=True, *args)[STDOUT]
121 return _parse_diff_filenames(out)
124 def _parse_diff_filenames(out):
125 if out:
126 return out[:-1].split('\0')
127 return []
130 def tracked_files(context, *args):
131 """Return the names of all files in the repository"""
132 git = context.git
133 out = git.ls_files('--', *args, z=True)[STDOUT]
134 if out:
135 return sorted(out[:-1].split('\0'))
136 return []
139 def all_files(context, *args):
140 """Returns a sorted list of all files, including untracked files."""
141 git = context.git
142 ls_files = git.ls_files(
143 '--', *args, z=True, cached=True, others=True, exclude_standard=True
144 )[STDOUT]
145 return sorted([f for f in ls_files.split('\0') if f])
148 class _current_branch(object):
149 """Cache for current_branch()"""
151 key = None
152 value = None
155 def reset():
156 _current_branch.key = None
159 def current_branch(context):
160 """Return the current branch"""
161 git = context.git
162 head = git.git_path('HEAD')
163 try:
164 key = core.stat(head).st_mtime
165 if _current_branch.key == key:
166 return _current_branch.value
167 except OSError:
168 # OSError means we can't use the stat cache
169 key = 0
171 status, data, _ = git.rev_parse('HEAD', symbolic_full_name=True)
172 if status != 0:
173 # git init -- read .git/HEAD. We could do this unconditionally...
174 data = _read_git_head(context, head)
176 for refs_prefix in ('refs/heads/', 'refs/remotes/', 'refs/tags/'):
177 if data.startswith(refs_prefix):
178 value = data[len(refs_prefix) :]
179 _current_branch.key = key
180 _current_branch.value = value
181 return value
182 # Detached head
183 return data
186 def _read_git_head(context, head, default='main'):
187 """Pure-python .git/HEAD reader"""
188 # Common .git/HEAD "ref: refs/heads/main" files
189 git = context.git
190 islink = core.islink(head)
191 if core.isfile(head) and not islink:
192 data = core.read(head).rstrip()
193 ref_prefix = 'ref: '
194 if data.startswith(ref_prefix):
195 return data[len(ref_prefix) :]
196 # Detached head
197 return data
198 # Legacy .git/HEAD symlinks
199 elif islink:
200 refs_heads = core.realpath(git.git_path('refs', 'heads'))
201 path = core.abspath(head).replace('\\', '/')
202 if path.startswith(refs_heads + '/'):
203 return path[len(refs_heads) + 1 :]
205 return default
208 def branch_list(context, remote=False):
210 Return a list of local or remote branches
212 This explicitly removes HEAD from the list of remote branches.
215 if remote:
216 return for_each_ref_basename(context, 'refs/remotes')
217 return for_each_ref_basename(context, 'refs/heads')
220 def _version_sort(context, key='version:refname'):
221 if version.check_git(context, 'version-sort'):
222 sort = key
223 else:
224 sort = False
225 return sort
228 def for_each_ref_basename(context, refs):
229 """Return refs starting with 'refs'."""
230 git = context.git
231 sort = _version_sort(context)
232 _, out, _ = git.for_each_ref(refs, format='%(refname)', sort=sort, _readonly=True)
233 output = out.splitlines()
234 non_heads = [x for x in output if not x.endswith('/HEAD')]
235 offset = len(refs) + 1
236 return [x[offset:] for x in non_heads]
239 def _triple(x, y):
240 return (x, len(x) + 1, y)
243 def all_refs(context, split=False, sort_key='version:refname'):
244 """Return a tuple of (local branches, remote branches, tags)."""
245 git = context.git
246 local_branches = []
247 remote_branches = []
248 tags = []
249 triple = _triple
250 query = (
251 triple('refs/tags', tags),
252 triple('refs/heads', local_branches),
253 triple('refs/remotes', remote_branches),
255 sort = _version_sort(context, key=sort_key)
256 _, out, _ = git.for_each_ref(format='%(refname)', sort=sort, _readonly=True)
257 for ref in out.splitlines():
258 for prefix, prefix_len, dst in query:
259 if ref.startswith(prefix) and not ref.endswith('/HEAD'):
260 dst.append(ref[prefix_len:])
261 continue
262 tags.reverse()
263 if split:
264 return local_branches, remote_branches, tags
265 return local_branches + remote_branches + tags
268 def tracked_branch(context, branch=None):
269 """Return the remote branch associated with 'branch'."""
270 if branch is None:
271 branch = current_branch(context)
272 if branch is None:
273 return None
274 config = context.cfg
275 remote = config.get('branch.%s.remote' % branch)
276 if not remote:
277 return None
278 merge_ref = config.get('branch.%s.merge' % branch)
279 if not merge_ref:
280 return None
281 refs_heads = 'refs/heads/'
282 if merge_ref.startswith(refs_heads):
283 return remote + '/' + merge_ref[len(refs_heads) :]
284 return None
287 def parse_remote_branch(branch):
288 """Split a remote branch apart into (remote, name) components"""
289 rgx = re.compile(r'^(?P<remote>[^/]+)/(?P<branch>.+)$')
290 match = rgx.match(branch)
291 remote = ''
292 branch = ''
293 if match:
294 remote = match.group('remote')
295 branch = match.group('branch')
296 return (remote, branch)
299 def untracked_files(context, paths=None, **kwargs):
300 """Returns a sorted list of untracked files."""
301 git = context.git
302 if paths is None:
303 paths = []
304 args = ['--'] + paths
305 out = git.ls_files(z=True, others=True, exclude_standard=True, *args, **kwargs)[
306 STDOUT
308 if out:
309 return out[:-1].split('\0')
310 return []
313 def tag_list(context):
314 """Return a list of tags."""
315 result = for_each_ref_basename(context, 'refs/tags')
316 result.reverse()
317 return result
320 def log(git, *args, **kwargs):
321 return git.log(
322 no_color=True,
323 no_abbrev_commit=True,
324 no_ext_diff=True,
325 _readonly=True,
326 *args,
327 **kwargs
328 )[STDOUT]
331 def commit_diff(context, oid):
332 git = context.git
333 return log(git, '-1', oid, '--') + '\n\n' + oid_diff(context, oid)
336 _diff_overrides = {}
339 def update_diff_overrides(space_at_eol, space_change, all_space, function_context):
340 _diff_overrides['ignore_space_at_eol'] = space_at_eol
341 _diff_overrides['ignore_space_change'] = space_change
342 _diff_overrides['ignore_all_space'] = all_space
343 _diff_overrides['function_context'] = function_context
346 def common_diff_opts(context):
347 config = context.cfg
348 # Default to --patience when diff.algorithm is unset
349 patience = not config.get('diff.algorithm', default='')
350 submodule = version.check_git(context, 'diff-submodule')
351 opts = {
352 'patience': patience,
353 'submodule': submodule,
354 'no_color': True,
355 'no_ext_diff': True,
356 'unified': config.get('gui.diffcontext', default=3),
357 '_raw': True,
359 opts.update(_diff_overrides)
360 return opts
363 def _add_filename(args, filename):
364 if filename:
365 args.extend(['--', filename])
368 def oid_diff(context, oid, filename=None):
369 """Return the diff for an oid"""
370 # Naively "$oid^!" is what we'd like to use but that doesn't
371 # give the correct result for merges--the diff is reversed.
372 # Be explicit and compare oid against its first parent.
373 git = context.git
374 args = [oid + '~', oid]
375 opts = common_diff_opts(context)
376 _add_filename(args, filename)
377 status, out, _ = git.diff(*args, **opts)
378 if status != 0:
379 # We probably don't have "$oid~" because this is the root commit.
380 # "git show" is clever enough to handle the root commit.
381 args = [oid + '^!']
382 _add_filename(args, filename)
383 _, out, _ = git.show(pretty='format:', _readonly=True, *args, **opts)
384 out = out.lstrip()
385 return out
388 def diff_info(context, oid, filename=None):
389 git = context.git
390 decoded = log(git, '-1', oid, '--', pretty='format:%b').strip()
391 if decoded:
392 decoded += '\n\n'
393 return decoded + oid_diff(context, oid, filename=filename)
396 # pylint: disable=too-many-arguments
397 def diff_helper(
398 context,
399 commit=None,
400 ref=None,
401 endref=None,
402 filename=None,
403 cached=True,
404 deleted=False,
405 head=None,
406 amending=False,
407 with_diff_header=False,
408 suppress_header=True,
409 reverse=False,
410 untracked=False,
412 "Invokes git diff on a filepath."
413 git = context.git
414 cfg = context.cfg
415 if commit:
416 ref, endref = commit + '^', commit
417 argv = []
418 if ref and endref:
419 argv.append('%s..%s' % (ref, endref))
420 elif ref:
421 for r in utils.shell_split(ref.strip()):
422 argv.append(r)
423 elif head and amending and cached:
424 argv.append(head)
426 encoding = None
427 if untracked:
428 argv.append('--no-index')
429 argv.append(os.devnull)
430 argv.append(filename)
431 elif filename:
432 argv.append('--')
433 if isinstance(filename, (list, tuple)):
434 argv.extend(filename)
435 else:
436 argv.append(filename)
437 encoding = cfg.file_encoding(filename)
439 status, out, _ = git.diff(
440 R=reverse,
441 M=True,
442 cached=cached,
443 _encoding=encoding,
444 *argv,
445 **common_diff_opts(context)
448 success = status == 0
450 # Diff will return 1 when comparing untracked file and it has change,
451 # therefore we will check for diff header from output to differentiate
452 # from actual error such as file not found.
453 if untracked and status == 1:
454 try:
455 _, second, _ = out.split('\n', 2)
456 except ValueError:
457 second = ''
458 success = second.startswith('new file mode ')
460 if not success:
461 # git init
462 if with_diff_header:
463 return ('', '')
464 return ''
466 result = extract_diff_header(deleted, with_diff_header, suppress_header, out)
467 return core.UStr(result, out.encoding)
470 def extract_diff_header(deleted, with_diff_header, suppress_header, diffoutput):
471 """Split a diff into a header section and payload section"""
473 if diffoutput.startswith('Submodule'):
474 if with_diff_header:
475 return ('', diffoutput)
476 return diffoutput
478 start = False
479 del_tag = 'deleted file mode '
481 output = StringIO()
482 headers = StringIO()
484 for line in diffoutput.split('\n'):
485 if not start and line[:2] == '@@' and '@@' in line[2:]:
486 start = True
487 if start or (deleted and del_tag in line):
488 output.write(line + '\n')
489 else:
490 if with_diff_header:
491 headers.write(line + '\n')
492 elif not suppress_header:
493 output.write(line + '\n')
495 output_text = output.getvalue()
496 output.close()
498 headers_text = headers.getvalue()
499 headers.close()
501 if with_diff_header:
502 return (headers_text, output_text)
503 return output_text
506 def format_patchsets(context, to_export, revs, output='patches'):
508 Group contiguous revision selection into patchsets
510 Exists to handle multi-selection.
511 Multiple disparate ranges in the revision selection
512 are grouped into continuous lists.
516 outs = []
517 errs = []
519 cur_rev = to_export[0]
520 cur_rev_idx = revs.index(cur_rev)
522 patches_to_export = [[cur_rev]]
523 patchset_idx = 0
525 # Group the patches into continuous sets
526 for rev in to_export[1:]:
527 # Limit the search to the current neighborhood for efficiency
528 try:
529 rev_idx = revs[cur_rev_idx:].index(rev)
530 rev_idx += cur_rev_idx
531 except ValueError:
532 rev_idx = revs.index(rev)
534 if rev_idx == cur_rev_idx + 1:
535 patches_to_export[patchset_idx].append(rev)
536 cur_rev_idx += 1
537 else:
538 patches_to_export.append([rev])
539 cur_rev_idx = rev_idx
540 patchset_idx += 1
542 # Export each patchsets
543 status = 0
544 for patchset in patches_to_export:
545 stat, out, err = export_patchset(
546 context,
547 patchset[0],
548 patchset[-1],
549 output=output,
550 n=len(patchset) > 1,
551 thread=True,
552 patch_with_stat=True,
554 outs.append(out)
555 if err:
556 errs.append(err)
557 status = max(stat, status)
558 return (status, '\n'.join(outs), '\n'.join(errs))
561 def export_patchset(context, start, end, output='patches', **kwargs):
562 """Export patches from start^ to end."""
563 git = context.git
564 return git.format_patch('-o', output, start + '^..' + end, **kwargs)
567 def reset_paths(context, head, items):
568 """Run "git reset" while preventing argument overflow"""
569 items = list(set(items))
570 fn = context.git.reset
571 status, out, err = utils.slice_fn(items, lambda paths: fn(head, '--', *paths))
572 return (status, out, err)
575 def unstage_paths(context, args, head='HEAD'):
576 """Unstage paths while accounting for git init"""
577 status, out, err = reset_paths(context, head, args)
578 if status == 128:
579 # handle git init: we have to use 'git rm --cached'
580 # detect this condition by checking if the file is still staged
581 return untrack_paths(context, args)
582 return (status, out, err)
585 def untrack_paths(context, args):
586 if not args:
587 return (-1, N_('Nothing to do'), '')
588 git = context.git
589 return git.update_index('--', force_remove=True, *set(args))
592 def worktree_state(
593 context, head='HEAD', update_index=False, display_untracked=True, paths=None
595 """Return a dict of files in various states of being
597 :rtype: dict, keys are staged, unstaged, untracked, unmerged,
598 changed_upstream, and submodule.
601 git = context.git
602 if update_index:
603 git.update_index(refresh=True)
605 staged, unmerged, staged_deleted, staged_submods = diff_index(
606 context, head, paths=paths
608 modified, unstaged_deleted, modified_submods = diff_worktree(context, paths)
609 if display_untracked:
610 untracked = untracked_files(context, paths=paths)
611 else:
612 untracked = []
614 # Remove unmerged paths from the modified list
615 if unmerged:
616 unmerged_set = set(unmerged)
617 modified = [path for path in modified if path not in unmerged_set]
619 # Look for upstream modified files if this is a tracking branch
620 upstream_changed = diff_upstream(context, head)
622 # Keep stuff sorted
623 staged.sort()
624 modified.sort()
625 unmerged.sort()
626 untracked.sort()
627 upstream_changed.sort()
629 return {
630 'staged': staged,
631 'modified': modified,
632 'unmerged': unmerged,
633 'untracked': untracked,
634 'upstream_changed': upstream_changed,
635 'staged_deleted': staged_deleted,
636 'unstaged_deleted': unstaged_deleted,
637 'submodules': staged_submods | modified_submods,
641 def _parse_raw_diff(out):
642 while out:
643 info, path, out = out.split('\0', 2)
644 status = info[-1]
645 is_submodule = '160000' in info[1:14]
646 yield (path, status, is_submodule)
649 def diff_index(context, head, cached=True, paths=None):
650 git = context.git
651 staged = []
652 unmerged = []
653 deleted = set()
654 submodules = set()
656 if paths is None:
657 paths = []
658 args = [head, '--'] + paths
659 status, out, _ = git.diff_index(cached=cached, z=True, *args)
660 if status != 0:
661 # handle git init
662 args[0] = EMPTY_TREE_OID
663 status, out, _ = git.diff_index(cached=cached, z=True, *args)
665 for path, status, is_submodule in _parse_raw_diff(out):
666 if is_submodule:
667 submodules.add(path)
668 if status in 'DAMT':
669 staged.append(path)
670 if status == 'D':
671 deleted.add(path)
672 elif status == 'U':
673 unmerged.append(path)
675 return staged, unmerged, deleted, submodules
678 def diff_worktree(context, paths=None):
679 git = context.git
680 modified = []
681 deleted = set()
682 submodules = set()
684 if paths is None:
685 paths = []
686 args = ['--'] + paths
687 status, out, _ = git.diff_files(z=True, *args)
688 for path, status, is_submodule in _parse_raw_diff(out):
689 if is_submodule:
690 submodules.add(path)
691 if status in 'DAMT':
692 modified.append(path)
693 if status == 'D':
694 deleted.add(path)
696 return modified, deleted, submodules
699 def diff_upstream(context, head):
700 """Given `ref`, return $(git merge-base ref HEAD)..ref."""
701 tracked = tracked_branch(context)
702 if not tracked:
703 return []
704 base = merge_base(context, head, tracked)
705 return diff_filenames(context, base, tracked)
708 def list_submodule(context):
709 """Return submodules in the format(state, sha1, path, describe)"""
710 git = context.git
711 status, data, _ = git.submodule('status')
712 ret = []
713 if status == 0 and data:
714 data = data.splitlines()
715 # see git submodule status
716 # TODO better separation
717 for line in data:
718 state = line[0].strip()
719 sha1 = line[1 : OID_LENGTH + 1]
720 left_bracket = line.find('(', OID_LENGTH + 3)
721 if left_bracket == -1:
722 left_bracket = len(line) + 1
723 path = line[OID_LENGTH + 2 : left_bracket - 1]
724 describe = line[left_bracket + 1 : -1]
725 ret.append((state, sha1, path, describe))
726 return ret
729 def merge_base(context, head, ref):
730 """Return the merge-base of head and ref"""
731 git = context.git
732 return git.merge_base(head, ref, _readonly=True)[STDOUT]
735 def merge_base_parent(context, branch):
736 tracked = tracked_branch(context, branch=branch)
737 if tracked:
738 return tracked
739 return 'HEAD'
742 # TODO Unused?
743 def parse_ls_tree(context, rev):
744 """Return a list of (mode, type, oid, path) tuples."""
745 output = []
746 git = context.git
747 lines = git.ls_tree(rev, r=True, _readonly=True)[STDOUT].splitlines()
748 regex = re.compile(r'^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
749 for line in lines:
750 match = regex.match(line)
751 if match:
752 mode = match.group(1)
753 objtype = match.group(2)
754 oid = match.group(3)
755 filename = match.group(4)
756 output.append(
758 mode,
759 objtype,
760 oid,
761 filename,
764 return output
767 # TODO unused?
768 def ls_tree(context, path, ref='HEAD'):
769 """Return a parsed git ls-tree result for a single directory"""
770 git = context.git
771 result = []
772 status, out, _ = git.ls_tree(ref, '--', path, z=True, full_tree=True)
773 if status == 0 and out:
774 path_offset = 6 + 1 + 4 + 1 + OID_LENGTH + 1
775 for line in out[:-1].split('\0'):
776 # 1 1 1
777 # .....6 ...4 ......................................40
778 # 040000 tree c127cde9a0c644a3a8fef449a244f47d5272dfa6 relative
779 # 100644 blob 139e42bf4acaa4927ec9be1ec55a252b97d3f1e2 relative/path
780 # 0..... 7... 12...................................... 53
781 # path_offset = 6 + 1 + 4 + 1 + OID_LENGTH(40) + 1
782 objtype = line[7:11]
783 relpath = line[path_offset:]
784 result.append((objtype, relpath))
786 return result
789 # A regex for matching the output of git(log|rev-list) --pretty=oneline
790 REV_LIST_REGEX = re.compile(r'^([0-9a-f]{40}) (.*)$')
793 def parse_rev_list(raw_revs):
794 """Parse `git log --pretty=online` output into (oid, summary) pairs."""
795 revs = []
796 for line in raw_revs.splitlines():
797 match = REV_LIST_REGEX.match(line)
798 if match:
799 rev_id = match.group(1)
800 summary = match.group(2)
801 revs.append(
803 rev_id,
804 summary,
807 return revs
810 # pylint: disable=redefined-builtin
811 def log_helper(context, all=False, extra_args=None):
812 """Return parallel arrays containing oids and summaries."""
813 revs = []
814 summaries = []
815 args = []
816 if extra_args:
817 args = extra_args
818 git = context.git
819 output = log(git, pretty='oneline', all=all, *args)
820 for line in output.splitlines():
821 match = REV_LIST_REGEX.match(line)
822 if match:
823 revs.append(match.group(1))
824 summaries.append(match.group(2))
825 return (revs, summaries)
828 def rev_list_range(context, start, end):
829 """Return (oid, summary) pairs between start and end."""
830 git = context.git
831 revrange = '%s..%s' % (start, end)
832 out = git.rev_list(revrange, pretty='oneline')[STDOUT]
833 return parse_rev_list(out)
836 def commit_message_path(context):
837 """Return the path to .git/GIT_COLA_MSG"""
838 git = context.git
839 path = git.git_path('GIT_COLA_MSG')
840 if core.exists(path):
841 return path
842 return None
845 def merge_message_path(context):
846 """Return the path to .git/MERGE_MSG or .git/SQUASH_MSG."""
847 git = context.git
848 for basename in ('MERGE_MSG', 'SQUASH_MSG'):
849 path = git.git_path(basename)
850 if core.exists(path):
851 return path
852 return None
855 def prepare_commit_message_hook(context):
856 """Run the cola.preparecommitmessagehook to prepare the commit message"""
857 config = context.cfg
858 default_hook = config.hooks_path('cola-prepare-commit-msg')
859 return config.get('cola.preparecommitmessagehook', default=default_hook)
862 def abort_merge(context):
863 """Abort a merge by reading the tree at HEAD."""
864 # Reset the worktree
865 git = context.git
866 status, out, err = git.read_tree('HEAD', reset=True, u=True, v=True)
867 # remove MERGE_HEAD
868 merge_head = git.git_path('MERGE_HEAD')
869 if core.exists(merge_head):
870 core.unlink(merge_head)
871 # remove MERGE_MESSAGE, etc.
872 merge_msg_path = merge_message_path(context)
873 while merge_msg_path:
874 core.unlink(merge_msg_path)
875 merge_msg_path = merge_message_path(context)
876 return status, out, err
879 def strip_remote(remotes, remote_branch):
880 for remote in remotes:
881 prefix = remote + '/'
882 if remote_branch.startswith(prefix):
883 return remote_branch[len(prefix) :]
884 return remote_branch.split('/', 1)[-1]
887 def parse_refs(context, argv):
888 """Parse command-line arguments into object IDs"""
889 git = context.git
890 status, out, _ = git.rev_parse(*argv)
891 if status == 0:
892 oids = [oid for oid in out.splitlines() if oid]
893 else:
894 oids = argv
895 return oids
898 def prev_commitmsg(context, *args):
899 """Queries git for the latest commit message."""
900 git = context.git
901 return git.log('-1', no_color=True, pretty='format:%s%n%n%b', *args)[STDOUT]
904 def rev_parse(context, name):
905 """Call git rev-parse and return the output"""
906 git = context.git
907 status, out, _ = git.rev_parse(name)
908 if status == 0:
909 result = out.strip()
910 else:
911 result = name
912 return result
915 def write_blob(context, oid, filename):
916 """Write a blob to a temporary file and return the path
918 Modern versions of Git allow invoking filters. Older versions
919 get the object content as-is.
922 if version.check_git(context, 'cat-file-filters-path'):
923 return cat_file_to_path(context, filename, oid)
924 return cat_file_blob(context, filename, oid)
927 def cat_file_blob(context, filename, oid):
928 return cat_file(context, filename, 'blob', oid)
931 def cat_file_to_path(context, filename, oid):
932 return cat_file(context, filename, oid, path=filename, filters=True)
935 def cat_file(context, filename, *args, **kwargs):
936 """Redirect git cat-file output to a path"""
937 result = None
938 git = context.git
939 # Use the original filename in the suffix so that the generated filename
940 # has the correct extension, and so that it resembles the original name.
941 basename = os.path.basename(filename)
942 suffix = '-' + basename # ensures the correct filename extension
943 path = utils.tmp_filename('blob', suffix=suffix)
944 with open(path, 'wb') as fp:
945 status, out, err = git.cat_file(
946 _raw=True, _readonly=True, _stdout=fp, *args, **kwargs
948 Interaction.command(N_('Error'), 'git cat-file', status, out, err)
949 if status == 0:
950 result = path
951 if not result:
952 core.unlink(path)
953 return result
956 def write_blob_path(context, head, oid, filename):
957 """Use write_blob() when modern git is available"""
958 if version.check_git(context, 'cat-file-filters-path'):
959 return write_blob(context, oid, filename)
960 return cat_file_blob(context, filename, head + ':' + filename)
963 def annex_path(context, head, filename):
964 """Return the git-annex path for a filename at the specified commit"""
965 git = context.git
966 path = None
967 annex_info = {}
969 # unfortunately there's no way to filter this down to a single path
970 # so we just have to scan all reported paths
971 status, out, _ = git.annex('findref', '--json', head)
972 if status == 0:
973 for line in out.splitlines():
974 info = json.loads(line)
975 try:
976 annex_file = info['file']
977 except (ValueError, KeyError):
978 continue
979 # we only care about this file so we can skip the rest
980 if annex_file == filename:
981 annex_info = info
982 break
983 key = annex_info.get('key', '')
984 if key:
985 status, out, _ = git.annex('contentlocation', key)
986 if status == 0 and os.path.exists(out):
987 path = out
989 return path
992 def is_binary(context, filename):
993 cfg_is_binary = context.cfg.is_binary(filename)
994 if cfg_is_binary is not None:
995 return cfg_is_binary
996 # This is the same heuristic as xdiff-interface.c:buffer_is_binary().
997 size = 8000
998 try:
999 result = core.read(filename, size=size, encoding='bytes')
1000 except (IOError, OSError):
1001 result = b''
1003 return b'\0' in result