1 """Git commands and queries for Git"""
5 from io
import StringIO
10 from .git
import STDOUT
11 from .git
import EMPTY_TREE_OID
12 from .git
import OID_LENGTH
14 from .interaction
import Interaction
15 from .models
import prefs
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_func(
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"""
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"""
35 return git
.apply(filename
)
38 def get_branch(context
, branch
):
39 """Get the current branch"""
41 branch
= current_branch(context
)
45 def upstream_remote(context
, branch
=None):
46 """Return the remote associated with the specified branch"""
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"""
55 url
= config
.get('remote.%s.url' % remote
, '')
57 url
= config
.get('remote.%s.pushurl' % remote
, url
)
61 def diff_index_filenames(context
, ref
):
63 Return a diff of filenames that have been modified relative to the index
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"""
73 out
= diff_tree(context
, *args
)[STDOUT
]
74 return _parse_diff_filenames(out
)
77 def changed_files(context
, oid
):
78 """Return the list of filenames that changed in a given commit oid"""
79 status
, out
, _
= diff_tree(context
, oid
+ '~', oid
)
82 status
, out
, _
= diff_tree(context
, EMPTY_TREE_OID
, oid
)
84 result
= _parse_diff_filenames(out
)
90 def diff_tree(context
, *args
):
91 """Return a list of filenames that have been modified"""
93 return git_diff_tree(git
, *args
)
96 def git_diff_tree(git
, *args
):
98 name_only
=True, no_commit_id
=True, r
=True, z
=True, _readonly
=True, *args
102 def listdir(context
, dirname
, ref
='HEAD'):
103 """Get the contents of a directory according to Git
105 Query Git for the content of a directory, taking ignored
112 # first, parse git ls-tree to get the tracked files
113 # in a list of (type, path) tuples
114 entries
= ls_tree(context
, dirname
, ref
=ref
)
115 for entry
in entries
:
116 if entry
[0][0] == 't': # tree
117 dirs
.append(entry
[1])
119 files
.append(entry
[1])
121 # gather untracked files
122 untracked
= untracked_files(context
, paths
=[dirname
], directory
=True)
123 for path
in untracked
:
124 if path
.endswith('/'):
125 dirs
.append(path
[:-1])
135 def diff(context
, args
):
136 """Return a list of filenames for the given diff arguments
138 :param args: list of arguments to pass to "git diff --name-only"
142 out
= git
.diff(name_only
=True, z
=True, _readonly
=True, *args
)[STDOUT
]
143 return _parse_diff_filenames(out
)
146 def _parse_diff_filenames(out
):
148 return out
[:-1].split('\0')
152 def tracked_files(context
, *args
):
153 """Return the names of all files in the repository"""
155 out
= git
.ls_files('--', *args
, z
=True, _readonly
=True)[STDOUT
]
157 return sorted(out
[:-1].split('\0'))
161 def all_files(context
, *args
):
162 """Returns a sorted list of all files, including untracked files."""
164 ls_files
= git
.ls_files(
170 exclude_standard
=True,
173 return sorted([f
for f
in ls_files
.split('\0') if f
])
176 class CurrentBranchCache
:
177 """Cache for current_branch()"""
184 """Reset cached value in this module (e.g. the cached current branch)"""
185 CurrentBranchCache
.key
= None
188 def current_branch(context
):
189 """Return the current branch"""
191 head
= git
.git_path('HEAD')
193 key
= core
.stat(head
).st_mtime
194 if CurrentBranchCache
.key
== key
:
195 return CurrentBranchCache
.value
197 # OSError means we can't use the stat cache
200 status
, data
, _
= git
.rev_parse('HEAD', symbolic_full_name
=True, _readonly
=True)
202 # git init -- read .git/HEAD. We could do this unconditionally...
203 data
= _read_git_head(context
, head
)
205 for refs_prefix
in ('refs/heads/', 'refs/remotes/', 'refs/tags/'):
206 if data
.startswith(refs_prefix
):
207 value
= data
[len(refs_prefix
) :]
208 CurrentBranchCache
.key
= key
209 CurrentBranchCache
.value
= value
215 def _read_git_head(context
, head
, default
='main'):
216 """Pure-python .git/HEAD reader"""
217 # Common .git/HEAD "ref: refs/heads/main" files
219 islink
= core
.islink(head
)
220 if core
.isfile(head
) and not islink
:
221 data
= core
.read(head
).rstrip()
223 if data
.startswith(ref_prefix
):
224 return data
[len(ref_prefix
) :]
227 # Legacy .git/HEAD symlinks
229 refs_heads
= core
.realpath(git
.git_path('refs', 'heads'))
230 path
= core
.abspath(head
).replace('\\', '/')
231 if path
.startswith(refs_heads
+ '/'):
232 return path
[len(refs_heads
) + 1 :]
237 def branch_list(context
, remote
=False):
239 Return a list of local or remote branches
241 This explicitly removes HEAD from the list of remote branches.
245 return for_each_ref_basename(context
, 'refs/remotes')
246 return for_each_ref_basename(context
, 'refs/heads')
249 def _version_sort(context
, key
='version:refname'):
250 if version
.check_git(context
, 'version-sort'):
257 def for_each_ref_basename(context
, refs
):
258 """Return refs starting with 'refs'."""
260 sort
= _version_sort(context
)
261 _
, out
, _
= git
.for_each_ref(refs
, format
='%(refname)', sort
=sort
, _readonly
=True)
262 output
= out
.splitlines()
263 non_heads
= [x
for x
in output
if not x
.endswith('/HEAD')]
264 offset
= len(refs
) + 1
265 return [x
[offset
:] for x
in non_heads
]
268 def _prefix_and_size(prefix
, values
):
269 """Return a tuple of (prefix, len(prefix) + 1, y) for <prefix>/ stripping"""
270 return (prefix
, len(prefix
) + 1, values
)
273 def all_refs(context
, split
=False, sort_key
='version:refname'):
274 """Return a tuple of (local branches, remote branches, tags)."""
280 _prefix_and_size('refs/tags', tags
),
281 _prefix_and_size('refs/heads', local_branches
),
282 _prefix_and_size('refs/remotes', remote_branches
),
284 sort
= _version_sort(context
, key
=sort_key
)
285 _
, out
, _
= git
.for_each_ref(format
='%(refname)', sort
=sort
, _readonly
=True)
286 for ref
in out
.splitlines():
287 for prefix
, prefix_len
, dst
in query
:
288 if ref
.startswith(prefix
) and not ref
.endswith('/HEAD'):
289 dst
.append(ref
[prefix_len
:])
293 return local_branches
, remote_branches
, tags
294 return local_branches
+ remote_branches
+ tags
297 def tracked_branch(context
, branch
=None):
298 """Return the remote branch associated with 'branch'."""
300 branch
= current_branch(context
)
304 remote
= config
.get('branch.%s.remote' % branch
)
307 merge_ref
= config
.get('branch.%s.merge' % branch
)
310 refs_heads
= 'refs/heads/'
311 if merge_ref
.startswith(refs_heads
):
312 return remote
+ '/' + merge_ref
[len(refs_heads
) :]
316 def parse_remote_branch(branch
):
317 """Split a remote branch apart into (remote, name) components"""
318 rgx
= re
.compile(r
'^(?P<remote>[^/]+)/(?P<branch>.+)$')
319 match
= rgx
.match(branch
)
323 remote
= match
.group('remote')
324 branch
= match
.group('branch')
325 return (remote
, branch
)
328 def untracked_files(context
, paths
=None, **kwargs
):
329 """Returns a sorted list of untracked files."""
333 args
= ['--'] + paths
335 z
=True, others
=True, exclude_standard
=True, _readonly
=True, *args
, **kwargs
338 return out
[:-1].split('\0')
342 def tag_list(context
):
343 """Return a list of tags."""
344 result
= for_each_ref_basename(context
, 'refs/tags')
349 def log(git
, *args
, **kwargs
):
352 no_abbrev_commit
=True,
360 def commit_diff(context
, oid
):
362 return log(git
, '-1', oid
, '--') + '\n\n' + oid_diff(context
, oid
)
368 def update_diff_overrides(space_at_eol
, space_change
, all_space
, function_context
):
369 _diff_overrides
['ignore_space_at_eol'] = space_at_eol
370 _diff_overrides
['ignore_space_change'] = space_change
371 _diff_overrides
['ignore_all_space'] = all_space
372 _diff_overrides
['function_context'] = function_context
375 def common_diff_opts(context
):
377 # Default to --patience when diff.algorithm is unset
378 patience
= not config
.get('diff.algorithm', default
='')
379 submodule
= version
.check_git(context
, 'diff-submodule')
381 'patience': patience
,
382 'submodule': submodule
,
385 'unified': config
.get('gui.diffcontext', default
=3),
389 opts
.update(_diff_overrides
)
393 def _add_filename(args
, filename
):
395 args
.extend(['--', filename
])
398 def oid_diff(context
, oid
, filename
=None):
399 """Return the diff for an oid"""
400 # Naively "$oid^!" is what we'd like to use but that doesn't
401 # give the correct result for merges--the diff is reversed.
402 # Be explicit and compare oid against its first parent.
403 return oid_diff_range(context
, oid
+ '~', oid
, filename
=filename
)
406 def oid_diff_range(context
, start
, end
, filename
=None):
407 """Return the diff for a commit range"""
410 opts
= common_diff_opts(context
)
411 _add_filename(args
, filename
)
412 status
, out
, _
= git
.diff(*args
, **opts
)
414 # We probably don't have "$oid~" because this is the root commit.
415 # "git show" is clever enough to handle the root commit.
417 _add_filename(args
, filename
)
418 _
, out
, _
= git
.show(pretty
='format:', *args
, **opts
)
423 def diff_info(context
, oid
, filename
=None):
424 """Return the diff for the specified oid"""
425 return diff_range(context
, oid
+ '~', oid
, filename
=filename
)
428 def diff_range(context
, start
, end
, filename
=None):
429 """Return the diff for the specified commit range"""
431 decoded
= log(git
, '-1', end
, '--', pretty
='format:%b').strip()
434 return decoded
+ oid_diff_range(context
, start
, end
, filename
=filename
)
437 # pylint: disable=too-many-arguments
448 with_diff_header
=False,
449 suppress_header
=True,
453 """Invoke git diff on a path"""
457 ref
, endref
= commit
+ '^', commit
460 argv
.append(f
'{ref}..{endref}')
462 argv
.extend(utils
.shell_split(ref
.strip()))
463 elif head
and amending
and cached
:
468 argv
.append('--no-index')
469 argv
.append(os
.devnull
)
470 argv
.append(filename
)
473 if isinstance(filename
, (list, tuple)):
474 argv
.extend(filename
)
476 argv
.append(filename
)
477 encoding
= cfg
.file_encoding(filename
)
479 status
, out
, _
= git
.diff(
485 **common_diff_opts(context
),
488 success
= status
== 0
490 # Diff will return 1 when comparing untracked file and it has change,
491 # therefore we will check for diff header from output to differentiate
492 # from actual error such as file not found.
493 if untracked
and status
== 1:
495 _
, second
, _
= out
.split('\n', 2)
498 success
= second
.startswith('new file mode ')
506 result
= extract_diff_header(deleted
, with_diff_header
, suppress_header
, out
)
507 return core
.UStr(result
, out
.encoding
)
510 def extract_diff_header(deleted
, with_diff_header
, suppress_header
, diffoutput
):
511 """Split a diff into a header section and payload section"""
513 if diffoutput
.startswith('Submodule'):
515 return ('', diffoutput
)
519 del_tag
= 'deleted file mode '
524 for line
in diffoutput
.split('\n'):
525 if not start
and line
[:2] == '@@' and '@@' in line
[2:]:
527 if start
or (deleted
and del_tag
in line
):
528 output
.write(line
+ '\n')
531 headers
.write(line
+ '\n')
532 elif not suppress_header
:
533 output
.write(line
+ '\n')
535 output_text
= output
.getvalue()
538 headers_text
= headers
.getvalue()
542 return (headers_text
, output_text
)
546 def format_patchsets(context
, to_export
, revs
, output
='patches'):
548 Group contiguous revision selection into patch sets
550 Exists to handle multi-selection.
551 Multiple disparate ranges in the revision selection
552 are grouped into continuous lists.
559 cur_rev
= to_export
[0]
560 cur_rev_idx
= revs
.index(cur_rev
)
562 patches_to_export
= [[cur_rev
]]
565 # Group the patches into continuous sets
566 for rev
in to_export
[1:]:
567 # Limit the search to the current neighborhood for efficiency
569 rev_idx
= revs
[cur_rev_idx
:].index(rev
)
570 rev_idx
+= cur_rev_idx
572 rev_idx
= revs
.index(rev
)
574 if rev_idx
== cur_rev_idx
+ 1:
575 patches_to_export
[patchset_idx
].append(rev
)
578 patches_to_export
.append([rev
])
579 cur_rev_idx
= rev_idx
582 # Export each patch set
584 for patchset
in patches_to_export
:
585 stat
, out
, err
= export_patchset(
592 patch_with_stat
=True,
597 status
= max(stat
, status
)
598 return (status
, '\n'.join(outs
), '\n'.join(errs
))
601 def export_patchset(context
, start
, end
, output
='patches', **kwargs
):
602 """Export patches from start^ to end."""
604 return git
.format_patch('-o', output
, start
+ '^..' + end
, **kwargs
)
607 def reset_paths(context
, head
, items
):
608 """Run "git reset" while preventing argument overflow"""
609 items
= list(set(items
))
610 func
= context
.git
.reset
611 status
, out
, err
= utils
.slice_func(items
, lambda paths
: func(head
, '--', *paths
))
612 return (status
, out
, err
)
615 def unstage_paths(context
, args
, head
='HEAD'):
616 """Unstage paths while accounting for git init"""
617 status
, out
, err
= reset_paths(context
, head
, args
)
619 # handle git init: we have to use 'git rm --cached'
620 # detect this condition by checking if the file is still staged
621 return untrack_paths(context
, args
)
622 return (status
, out
, err
)
625 def untrack_paths(context
, args
):
627 return (-1, N_('Nothing to do'), '')
629 return git
.update_index('--', force_remove
=True, *set(args
))
633 context
, head
='HEAD', update_index
=False, display_untracked
=True, paths
=None
635 """Return a dict of files in various states of being
637 :rtype: dict, keys are staged, unstaged, untracked, unmerged,
638 changed_upstream, and submodule.
642 git
.update_index(refresh
=True)
644 staged
, unmerged
, staged_deleted
, staged_submods
= diff_index(
645 context
, head
, paths
=paths
647 modified
, unstaged_deleted
, modified_submods
= diff_worktree(context
, paths
)
648 if display_untracked
:
649 untracked
= untracked_files(context
, paths
=paths
)
653 # Remove unmerged paths from the modified list
655 unmerged_set
= set(unmerged
)
656 modified
= [path
for path
in modified
if path
not in unmerged_set
]
658 # Look for upstream modified files if this is a tracking branch
659 upstream_changed
= diff_upstream(context
, head
)
666 upstream_changed
.sort()
670 'modified': modified
,
671 'unmerged': unmerged
,
672 'untracked': untracked
,
673 'upstream_changed': upstream_changed
,
674 'staged_deleted': staged_deleted
,
675 'unstaged_deleted': unstaged_deleted
,
676 'submodules': staged_submods | modified_submods
,
680 def _parse_raw_diff(out
):
682 info
, path
, out
= out
.split('\0', 2)
684 is_submodule
= '160000' in info
[1:14]
685 yield (path
, status
, is_submodule
)
688 def diff_index(context
, head
, cached
=True, paths
=None):
697 args
= [head
, '--'] + paths
698 status
, out
, _
= git
.diff_index(cached
=cached
, z
=True, _readonly
=True, *args
)
701 args
[0] = EMPTY_TREE_OID
702 status
, out
, _
= git
.diff_index(cached
=cached
, z
=True, _readonly
=True, *args
)
704 for path
, status
, is_submodule
in _parse_raw_diff(out
):
712 unmerged
.append(path
)
714 return staged
, unmerged
, deleted
, submodules
717 def diff_worktree(context
, paths
=None):
719 ignore_submodules_value
= context
.cfg
.get('diff.ignoresubmodules', 'none')
720 ignore_submodules
= ignore_submodules_value
in {'all', 'dirty', 'untracked'}
727 args
= ['--'] + paths
728 status
, out
, _
= git
.diff_files(z
=True, _readonly
=True, *args
)
729 for path
, status
, is_submodule
in _parse_raw_diff(out
):
732 if ignore_submodules
:
735 modified
.append(path
)
739 return modified
, deleted
, submodules
742 def diff_upstream(context
, head
):
743 """Given `ref`, return $(git merge-base ref HEAD)..ref."""
744 tracked
= tracked_branch(context
)
747 base
= merge_base(context
, head
, tracked
)
748 return diff_filenames(context
, base
, tracked
)
751 def list_submodule(context
):
752 """Return submodules in the format(state, sha_1, path, describe)"""
754 status
, data
, _
= git
.submodule('status')
756 if status
== 0 and data
:
757 data
= data
.splitlines()
758 # see git submodule status
760 state
= line
[0].strip()
761 sha1
= line
[1 : OID_LENGTH
+ 1]
762 left_bracket
= line
.find('(', OID_LENGTH
+ 3)
763 if left_bracket
== -1:
764 left_bracket
= len(line
) + 1
765 path
= line
[OID_LENGTH
+ 2 : left_bracket
- 1]
766 describe
= line
[left_bracket
+ 1 : -1]
767 ret
.append((state
, sha1
, path
, describe
))
771 def merge_base(context
, head
, ref
):
772 """Return the merge-base of head and ref"""
774 return git
.merge_base(head
, ref
, _readonly
=True)[STDOUT
]
777 def merge_base_parent(context
, branch
):
778 tracked
= tracked_branch(context
, branch
=branch
)
784 def ls_tree(context
, path
, ref
='HEAD'):
785 """Return a parsed git ls-tree result for a single directory"""
788 status
, out
, _
= git
.ls_tree(
789 ref
, '--', path
, z
=True, full_tree
=True, _readonly
=True
791 if status
== 0 and out
:
792 path_offset
= 6 + 1 + 4 + 1 + OID_LENGTH
+ 1
793 for line
in out
[:-1].split('\0'):
795 # .....6 ...4 ......................................40
796 # 040000 tree c127cde9a0c644a3a8fef449a244f47d5272dfa6 relative
797 # 100644 blob 139e42bf4acaa4927ec9be1ec55a252b97d3f1e2 relative/path
798 # 0..... 7... 12...................................... 53
799 # path_offset = 6 + 1 + 4 + 1 + OID_LENGTH(40) + 1
801 relpath
= line
[path_offset
:]
802 result
.append((objtype
, relpath
))
807 # A regex for matching the output of git(log|rev-list) --pretty=oneline
808 REV_LIST_REGEX
= re
.compile(r
'^([0-9a-f]{40}) (.*)$')
811 def parse_rev_list(raw_revs
):
812 """Parse `git log --pretty=online` output into (oid, summary) pairs."""
814 for line
in raw_revs
.splitlines():
815 match
= REV_LIST_REGEX
.match(line
)
817 rev_id
= match
.group(1)
818 summary
= match
.group(2)
826 # pylint: disable=redefined-builtin
827 def log_helper(context
, all
=False, extra_args
=None):
828 """Return parallel arrays containing oids and summaries."""
835 output
= log(git
, pretty
='oneline', all
=all
, *args
)
836 for line
in output
.splitlines():
837 match
= REV_LIST_REGEX
.match(line
)
839 revs
.append(match
.group(1))
840 summaries
.append(match
.group(2))
841 return (revs
, summaries
)
844 def rev_list_range(context
, start
, end
):
845 """Return (oid, summary) pairs between start and end."""
847 revrange
= f
'{start}..{end}'
848 out
= git
.rev_list(revrange
, pretty
='oneline', _readonly
=True)[STDOUT
]
849 return parse_rev_list(out
)
852 def commit_message_path(context
):
853 """Return the path to .git/GIT_COLA_MSG"""
855 path
= git
.git_path('GIT_COLA_MSG')
856 if core
.exists(path
):
861 def merge_message_path(context
):
862 """Return the path to .git/MERGE_MSG or .git/SQUASH_MSG."""
864 for basename
in ('MERGE_MSG', 'SQUASH_MSG'):
865 path
= git
.git_path(basename
)
866 if core
.exists(path
):
871 def read_merge_commit_message(context
, path
):
872 """Read a merge commit message from disk while stripping commentary"""
873 content
= core
.read(path
)
874 cleanup_mode
= prefs
.commit_cleanup(context
)
875 if cleanup_mode
in ('verbatim', 'scissors', 'whitespace'):
877 comment_char
= prefs
.comment_char(context
)
879 line
for line
in content
.splitlines() if not line
.startswith(comment_char
)
883 def prepare_commit_message_hook(context
):
884 """Run the cola.preparecommitmessagehook to prepare the commit message"""
886 default_hook
= config
.hooks_path('cola-prepare-commit-msg')
887 return config
.get('cola.preparecommitmessagehook', default
=default_hook
)
890 def cherry_pick(context
, revs
):
891 """Cherry-picks each revision into the current branch.
893 Returns (0, out, err) where stdout and stderr across all "git cherry-pick"
894 invocations are combined into single values when all cherry-picks succeed.
896 Returns a combined (status, out, err) of the first failing "git cherry-pick"
897 in the event of a non-zero exit status.
905 status
, out
, err
= context
.git
.cherry_pick(rev
)
908 'Hint: The "Actions > Abort Cherry-Pick" menu action can be used to '
909 'cancel the current cherry-pick.'
911 output
= f
'# git cherry-pick {rev}\n# {details}\n\n{out}'
912 return (status
, output
, err
)
915 return (0, '\n'.join(outs
), '\n'.join(errs
))
918 def abort_apply_patch(context
):
919 """Abort a "git am" session."""
922 status
, out
, err
= git
.am(abort
=True)
923 return status
, out
, err
926 def abort_cherry_pick(context
):
927 """Abort a cherry-pick."""
930 status
, out
, err
= git
.cherry_pick(abort
=True)
931 return status
, out
, err
934 def abort_merge(context
):
938 status
, out
, err
= git
.merge(abort
=True)
939 return status
, out
, err
942 def strip_remote(remotes
, remote_branch
):
943 """Get branch names with the "<remote>/" prefix removed"""
944 for remote
in remotes
:
945 prefix
= remote
+ '/'
946 if remote_branch
.startswith(prefix
):
947 return remote_branch
[len(prefix
) :]
948 return remote_branch
.split('/', 1)[-1]
951 def parse_refs(context
, argv
):
952 """Parse command-line arguments into object IDs"""
954 status
, out
, _
= git
.rev_parse(_readonly
=True, *argv
)
956 oids
= [oid
for oid
in out
.splitlines() if oid
]
962 def prev_commitmsg(context
, *args
):
963 """Queries git for the latest commit message."""
966 '-1', no_color
=True, pretty
='format:%s%n%n%b', _readonly
=True, *args
970 def rev_parse(context
, name
):
971 """Call git rev-parse and return the output"""
973 status
, out
, _
= git
.rev_parse(name
, _readonly
=True)
981 def write_blob(context
, oid
, filename
):
982 """Write a blob to a temporary file and return the path
984 Modern versions of Git allow invoking filters. Older versions
985 get the object content as-is.
988 if version
.check_git(context
, 'cat-file-filters-path'):
989 return cat_file_to_path(context
, filename
, oid
)
990 return cat_file_blob(context
, filename
, oid
)
993 def cat_file_blob(context
, filename
, oid
):
994 """Write a blob from git to the specified filename"""
995 return cat_file(context
, filename
, 'blob', oid
)
998 def cat_file_to_path(context
, filename
, oid
):
999 """Extract a file from an commit ref and a write it to the specified filename"""
1000 return cat_file(context
, filename
, oid
, path
=filename
, filters
=True)
1003 def cat_file(context
, filename
, *args
, **kwargs
):
1004 """Redirect git cat-file output to a path"""
1007 # Use the original filename in the suffix so that the generated filename
1008 # has the correct extension, and so that it resembles the original name.
1009 basename
= os
.path
.basename(filename
)
1010 suffix
= '-' + basename
# ensures the correct filename extension
1011 path
= utils
.tmp_filename('blob', suffix
=suffix
)
1012 with
open(path
, 'wb') as tmp_file
:
1013 status
, out
, err
= git
.cat_file(
1014 _raw
=True, _readonly
=True, _stdout
=tmp_file
, *args
, **kwargs
1016 Interaction
.command(N_('Error'), 'git cat-file', status
, out
, err
)
1024 def write_blob_path(context
, head
, oid
, filename
):
1025 """Use write_blob() when modern git is available"""
1026 if version
.check_git(context
, 'cat-file-filters-path'):
1027 return write_blob(context
, oid
, filename
)
1028 return cat_file_blob(context
, filename
, head
+ ':' + filename
)
1031 def annex_path(context
, head
, filename
):
1032 """Return the git-annex path for a filename at the specified commit"""
1037 # unfortunately there's no way to filter this down to a single path
1038 # so we just have to scan all reported paths
1039 status
, out
, _
= git
.annex('findref', '--json', head
, _readonly
=True)
1041 for line
in out
.splitlines():
1042 info
= json
.loads(line
)
1044 annex_file
= info
['file']
1045 except (ValueError, KeyError):
1047 # we only care about this file so we can skip the rest
1048 if annex_file
== filename
:
1051 key
= annex_info
.get('key', '')
1053 status
, out
, _
= git
.annex('contentlocation', key
, _readonly
=True)
1054 if status
== 0 and os
.path
.exists(out
):
1060 def is_binary(context
, filename
):
1061 """A heuristic to determine whether `filename` contains (non-text) binary content"""
1062 cfg_is_binary
= context
.cfg
.is_binary(filename
)
1063 if cfg_is_binary
is not None:
1064 return cfg_is_binary
1065 # This is the same heuristic as xdiff-interface.c:buffer_is_binary().
1068 result
= core
.read(filename
, size
=size
, encoding
='bytes')
1072 return b
'\0' in result
1075 def is_valid_ref(context
, ref
):
1076 """Is the provided Git ref a valid refname?"""
1077 status
, _
, _
= context
.git
.rev_parse(ref
, quiet
=True, verify
=True, _readonly
=True)