1"""Git commands and queries for Git""" 2from __future__ import division, absolute_import, unicode_literals 3import json 4import os 5import re 6from io import StringIO 7 8from . import core 9from . import utils 10from . import version 11from .git import STDOUT 12from .git import EMPTY_TREE_OID 13from .git import OID_LENGTH 14from .i18n import N_ 15from .interaction import Interaction 16 17 18class InvalidRepositoryError(Exception): 19 pass 20 21 22def 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) 27 ) 28 29 30def apply_diff(context, filename): 31 git = context.git 32 return git.apply(filename, index=True, cached=True) 33 34 35def apply_diff_to_worktree(context, filename): 36 git = context.git 37 return git.apply(filename) 38 39 40def get_branch(context, branch): 41 if branch is None: 42 branch = current_branch(context) 43 return branch 44 45 46def 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) 51 52 53def 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 60 61 62def 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) 69 70 71def 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) 78 79 80def listdir(context, dirname, ref='HEAD'): 81 """Get the contents of a directory according to Git 82 83 Query Git for the content of a directory, taking ignored 84 files into account. 85 86 """ 87 dirs = [] 88 files = [] 89 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]) 98 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) 106 107 dirs.sort() 108 files.sort() 109 110 return (dirs, files) 111 112 113def diff(context, args): 114 """Return a list of filenames for the given diff arguments 115 116 :param args: list of arguments to pass to "git diff --name-only" 117 118 """ 119 git = context.git 120 out = git.diff(name_only=True, z=True, *args)[STDOUT] 121 return _parse_diff_filenames(out) 122 123 124def _parse_diff_filenames(out): 125 if out: 126 return out[:-1].split('\0') 127 return [] 128 129 130def 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 [] 137 138 139def 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]) 146 147 148class _current_branch(object): 149 """Cache for current_branch()""" 150 151 key = None 152 value = None 153 154 155def reset(): 156 _current_branch.key = None 157 158 159def 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 170 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) 175 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 184 185 186def _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 :] 204 205 return default 206 207 208def branch_list(context, remote=False): 209 """ 210 Return a list of local or remote branches 211 212 This explicitly removes HEAD from the list of remote branches. 213 214 """ 215 if remote: 216 return for_each_ref_basename(context, 'refs/remotes') 217 return for_each_ref_basename(context, 'refs/heads') 218 219 220def _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 226 227 228def 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] 237 238 239def _triple(x, y): 240 return (x, len(x) + 1, y) 241 242 243def 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), 254 ) 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 266 267 268def 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 285 286 287def 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) 297 298 299def 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 307 ] 308 if out: 309 return out[:-1].split('\0') 310 return [] 311 312 313def tag_list(context): 314 """Return a list of tags.""" 315 result = for_each_ref_basename(context, 'refs/tags') 316 result.reverse() 317 return result 318 319 320def 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] 329 330 331def commit_diff(context, oid): 332 git = context.git 333 return log(git, '-1', oid, '--') + '\n\n' + oid_diff(context, oid) 334 335 336_diff_overrides = {} 337 338 339def 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 344 345 346def 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, 358 } 359 opts.update(_diff_overrides) 360 return opts 361 362 363def _add_filename(args, filename): 364 if filename: 365 args.extend(['--', filename]) 366 367 368def 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 386 387 388def 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) 394 395 396def diff_helper( 397 context, 398 commit=None, 399 ref=None, 400 endref=None, 401 filename=None, 402 cached=True, 403 deleted=False, 404 head=None, 405 amending=False, 406 with_diff_header=False, 407 suppress_header=True, 408 reverse=False, 409): 410 "Invokes git diff on a filepath." 411 git = context.git 412 cfg = context.cfg 413 if commit: 414 ref, endref = commit + '^', commit 415 argv = [] 416 if ref and endref: 417 argv.append('%s..%s' % (ref, endref)) 418 elif ref: 419 for r in utils.shell_split(ref.strip()): 420 argv.append(r) 421 elif head and amending and cached: 422 argv.append(head) 423 424 encoding = None 425 if filename: 426 argv.append('--') 427 if isinstance(filename, (list, tuple)): 428 argv.extend(filename) 429 else: 430 argv.append(filename) 431 encoding = cfg.file_encoding(filename) 432 433 status, out, _ = git.diff( 434 R=reverse, 435 M=True, 436 cached=cached, 437 _encoding=encoding, 438 *argv, 439 **common_diff_opts(context) 440 ) 441 if status != 0: 442 # git init 443 if with_diff_header: 444 return ('', '') 445 return '' 446 447 result = extract_diff_header(deleted, with_diff_header, suppress_header, out) 448 return core.UStr(result, out.encoding) 449 450 451def extract_diff_header(deleted, with_diff_header, suppress_header, diffoutput): 452 """Split a diff into a header section and payload section""" 453 454 if diffoutput.startswith('Submodule'): 455 if with_diff_header: 456 return ('', diffoutput) 457 return diffoutput 458 459 start = False 460 del_tag = 'deleted file mode ' 461 462 output = StringIO() 463 headers = StringIO() 464 465 for line in diffoutput.split('\n'): 466 if not start and line[:2] == '@@' and '@@' in line[2:]: 467 start = True 468 if start or (deleted and del_tag in line): 469 output.write(line + '\n') 470 else: 471 if with_diff_header: 472 headers.write(line + '\n') 473 elif not suppress_header: 474 output.write(line + '\n') 475 476 output_text = output.getvalue() 477 output.close() 478 479 headers_text = headers.getvalue() 480 headers.close() 481 482 if with_diff_header: 483 return (headers_text, output_text) 484 return output_text 485 486 487def format_patchsets(context, to_export, revs, output='patches'): 488 """ 489 Group contiguous revision selection into patchsets 490 491 Exists to handle multi-selection. 492 Multiple disparate ranges in the revision selection 493 are grouped into continuous lists. 494 495 """ 496 497 outs = [] 498 errs = [] 499 500 cur_rev = to_export[0] 501 cur_rev_idx = revs.index(cur_rev) 502 503 patches_to_export = [[cur_rev]] 504 patchset_idx = 0 505 506 # Group the patches into continuous sets 507 for rev in to_export[1:]: 508 # Limit the search to the current neighborhood for efficiency 509 try: 510 rev_idx = revs[cur_rev_idx:].index(rev) 511 rev_idx += cur_rev_idx 512 except ValueError: 513 rev_idx = revs.index(rev) 514 515 if rev_idx == cur_rev_idx + 1: 516 patches_to_export[patchset_idx].append(rev) 517 cur_rev_idx += 1 518 else: 519 patches_to_export.append([rev]) 520 cur_rev_idx = rev_idx 521 patchset_idx += 1 522 523 # Export each patchsets 524 status = 0 525 for patchset in patches_to_export: 526 stat, out, err = export_patchset( 527 context, 528 patchset[0], 529 patchset[-1], 530 output=output, 531 n=len(patchset) > 1, 532 thread=True, 533 patch_with_stat=True, 534 ) 535 outs.append(out) 536 if err: 537 errs.append(err) 538 status = max(stat, status) 539 return (status, '\n'.join(outs), '\n'.join(errs)) 540 541 542def export_patchset(context, start, end, output='patches', **kwargs): 543 """Export patches from start^ to end.""" 544 git = context.git 545 return git.format_patch('-o', output, start + '^..' + end, **kwargs) 546 547 548def reset_paths(context, head, items): 549 """Run "git reset" while preventing argument overflow""" 550 items = list(set(items)) 551 fn = context.git.reset 552 status, out, err = utils.slice_fn(items, lambda paths: fn(head, '--', *paths)) 553 return (status, out, err) 554 555 556def unstage_paths(context, args, head='HEAD'): 557 """Unstage paths while accounting for git init""" 558 status, out, err = reset_paths(context, head, args) 559 if status == 128: 560 # handle git init: we have to use 'git rm --cached' 561 # detect this condition by checking if the file is still staged 562 return untrack_paths(context, args) 563 return (status, out, err) 564 565 566def untrack_paths(context, args): 567 if not args: 568 return (-1, N_('Nothing to do'), '') 569 git = context.git 570 return git.update_index('--', force_remove=True, *set(args)) 571 572 573def worktree_state( 574 context, head='HEAD', update_index=False, display_untracked=True, paths=None 575): 576 """Return a dict of files in various states of being 577 578 :rtype: dict, keys are staged, unstaged, untracked, unmerged, 579 changed_upstream, and submodule. 580 581 """ 582 git = context.git 583 if update_index: 584 git.update_index(refresh=True) 585 586 staged, unmerged, staged_deleted, staged_submods = diff_index( 587 context, head, paths=paths 588 ) 589 modified, unstaged_deleted, modified_submods = diff_worktree(context, paths) 590 if display_untracked: 591 untracked = untracked_files(context, paths=paths) 592 else: 593 untracked = [] 594 595 # Remove unmerged paths from the modified list 596 if unmerged: 597 unmerged_set = set(unmerged) 598 modified = [path for path in modified if path not in unmerged_set] 599 600 # Look for upstream modified files if this is a tracking branch 601 upstream_changed = diff_upstream(context, head) 602 603 # Keep stuff sorted 604 staged.sort() 605 modified.sort() 606 unmerged.sort() 607 untracked.sort() 608 upstream_changed.sort() 609 610 return { 611 'staged': staged, 612 'modified': modified, 613 'unmerged': unmerged, 614 'untracked': untracked, 615 'upstream_changed': upstream_changed, 616 'staged_deleted': staged_deleted, 617 'unstaged_deleted': unstaged_deleted, 618 'submodules': staged_submods | modified_submods, 619 } 620 621 622def _parse_raw_diff(out): 623 while out: 624 info, path, out = out.split('\0', 2) 625 status = info[-1] 626 is_submodule = '160000' in info[1:14] 627 yield (path, status, is_submodule) 628 629 630def diff_index(context, head, cached=True, paths=None): 631 git = context.git 632 staged = [] 633 unmerged = [] 634 deleted = set() 635 submodules = set() 636 637 if paths is None: 638 paths = [] 639 args = [head, '--'] + paths 640 status, out, _ = git.diff_index(cached=cached, z=True, *args) 641 if status != 0: 642 # handle git init 643 args[0] = EMPTY_TREE_OID 644 status, out, _ = git.diff_index(cached=cached, z=True, *args) 645 646 for path, status, is_submodule in _parse_raw_diff(out): 647 if is_submodule: 648 submodules.add(path) 649 if status in 'DAMT': 650 staged.append(path) 651 if status == 'D': 652 deleted.add(path) 653 elif status == 'U': 654 unmerged.append(path) 655 656 return staged, unmerged, deleted, submodules 657 658 659def diff_worktree(context, paths=None): 660 git = context.git 661 modified = [] 662 deleted = set() 663 submodules = set() 664 665 if paths is None: 666 paths = [] 667 args = ['--'] + paths 668 status, out, _ = git.diff_files(z=True, *args) 669 for path, status, is_submodule in _parse_raw_diff(out): 670 if is_submodule: 671 submodules.add(path) 672 if status in 'DAMT': 673 modified.append(path) 674 if status == 'D': 675 deleted.add(path) 676 677 return modified, deleted, submodules 678 679 680def diff_upstream(context, head): 681 """Given `ref`, return $(git merge-base ref HEAD)..ref.""" 682 tracked = tracked_branch(context) 683 if not tracked: 684 return [] 685 base = merge_base(context, head, tracked) 686 return diff_filenames(context, base, tracked) 687 688 689def list_submodule(context): 690 """Return submodules in the format(state, sha1, path, describe)""" 691 git = context.git 692 status, data, _ = git.submodule('status') 693 ret = [] 694 if status == 0 and data: 695 data = data.splitlines() 696 # see git submodule status 697 # TODO better separation 698 for line in data: 699 state = line[0].strip() 700 sha1 = line[1 : OID_LENGTH + 1] 701 left_bracket = line.find('(', OID_LENGTH + 3) 702 if left_bracket == -1: 703 left_bracket = len(line) + 1 704 path = line[OID_LENGTH + 2 : left_bracket - 1] 705 describe = line[left_bracket + 1 : -1] 706 ret.append((state, sha1, path, describe)) 707 return ret 708 709 710def merge_base(context, head, ref): 711 """Return the merge-base of head and ref""" 712 git = context.git 713 return git.merge_base(head, ref, _readonly=True)[STDOUT] 714 715 716def merge_base_parent(context, branch): 717 tracked = tracked_branch(context, branch=branch) 718 if tracked: 719 return tracked 720 return 'HEAD' 721 722 723# TODO Unused? 724def parse_ls_tree(context, rev): 725 """Return a list of (mode, type, oid, path) tuples.""" 726 output = [] 727 git = context.git 728 lines = git.ls_tree(rev, r=True, _readonly=True)[STDOUT].splitlines() 729 regex = re.compile(r'^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$') 730 for line in lines: 731 match = regex.match(line) 732 if match: 733 mode = match.group(1) 734 objtype = match.group(2) 735 oid = match.group(3) 736 filename = match.group(4) 737 output.append( 738 ( 739 mode, 740 objtype, 741 oid, 742 filename, 743 ) 744 ) 745 return output 746 747 748# TODO unused? 749def ls_tree(context, path, ref='HEAD'): 750 """Return a parsed git ls-tree result for a single directory""" 751 git = context.git 752 result = [] 753 status, out, _ = git.ls_tree(ref, '--', path, z=True, full_tree=True) 754 if status == 0 and out: 755 path_offset = 6 + 1 + 4 + 1 + OID_LENGTH + 1 756 for line in out[:-1].split('\0'): 757 # 1 1 1 758 # .....6 ...4 ......................................40 759 # 040000 tree c127cde9a0c644a3a8fef449a244f47d5272dfa6 relative 760 # 100644 blob 139e42bf4acaa4927ec9be1ec55a252b97d3f1e2 relative/path 761 # 0..... 7... 12...................................... 53 762 # path_offset = 6 + 1 + 4 + 1 + OID_LENGTH(40) + 1 763 objtype = line[7:11] 764 relpath = line[path_offset:] 765 result.append((objtype, relpath)) 766 767 return result 768 769 770# A regex for matching the output of git(log|rev-list) --pretty=oneline 771REV_LIST_REGEX = re.compile(r'^([0-9a-f]{40}) (.*)$') 772 773 774def parse_rev_list(raw_revs): 775 """Parse `git log --pretty=online` output into (oid, summary) pairs.""" 776 revs = [] 777 for line in raw_revs.splitlines(): 778 match = REV_LIST_REGEX.match(line) 779 if match: 780 rev_id = match.group(1) 781 summary = match.group(2) 782 revs.append( 783 ( 784 rev_id, 785 summary, 786 ) 787 ) 788 return revs 789 790 791# pylint: disable=redefined-builtin 792def log_helper(context, all=False, extra_args=None): 793 """Return parallel arrays containing oids and summaries.""" 794 revs = [] 795 summaries = [] 796 args = [] 797 if extra_args: 798 args = extra_args 799 git = context.git 800 output = log(git, pretty='oneline', all=all, *args) 801 for line in output.splitlines(): 802 match = REV_LIST_REGEX.match(line) 803 if match: 804 revs.append(match.group(1)) 805 summaries.append(match.group(2)) 806 return (revs, summaries) 807 808 809def rev_list_range(context, start, end): 810 """Return (oid, summary) pairs between start and end.""" 811 git = context.git 812 revrange = '%s..%s' % (start, end) 813 out = git.rev_list(revrange, pretty='oneline')[STDOUT] 814 return parse_rev_list(out) 815 816 817def commit_message_path(context): 818 """Return the path to .git/GIT_COLA_MSG""" 819 git = context.git 820 path = git.git_path('GIT_COLA_MSG') 821 if core.exists(path): 822 return path 823 return None 824 825 826def merge_message_path(context): 827 """Return the path to .git/MERGE_MSG or .git/SQUASH_MSG.""" 828 git = context.git 829 for basename in ('MERGE_MSG', 'SQUASH_MSG'): 830 path = git.git_path(basename) 831 if core.exists(path): 832 return path 833 return None 834 835 836def prepare_commit_message_hook(context): 837 """Run the cola.preparecommitmessagehook to prepare the commit message""" 838 config = context.cfg 839 default_hook = config.hooks_path('cola-prepare-commit-msg') 840 return config.get('cola.preparecommitmessagehook', default=default_hook) 841 842 843def abort_merge(context): 844 """Abort a merge by reading the tree at HEAD.""" 845 # Reset the worktree 846 git = context.git 847 status, out, err = git.read_tree('HEAD', reset=True, u=True, v=True) 848 # remove MERGE_HEAD 849 merge_head = git.git_path('MERGE_HEAD') 850 if core.exists(merge_head): 851 core.unlink(merge_head) 852 # remove MERGE_MESSAGE, etc. 853 merge_msg_path = merge_message_path(context) 854 while merge_msg_path: 855 core.unlink(merge_msg_path) 856 merge_msg_path = merge_message_path(context) 857 return status, out, err 858 859 860def strip_remote(remotes, remote_branch): 861 for remote in remotes: 862 prefix = remote + '/' 863 if remote_branch.startswith(prefix): 864 return remote_branch[len(prefix) :] 865 return remote_branch.split('/', 1)[-1] 866 867 868def parse_refs(context, argv): 869 """Parse command-line arguments into object IDs""" 870 git = context.git 871 status, out, _ = git.rev_parse(*argv) 872 if status == 0: 873 oids = [oid for oid in out.splitlines() if oid] 874 else: 875 oids = argv 876 return oids 877 878 879def prev_commitmsg(context, *args): 880 """Queries git for the latest commit message.""" 881 git = context.git 882 return git.log('-1', no_color=True, pretty='format:%s%n%n%b', *args)[STDOUT] 883 884 885def rev_parse(context, name): 886 """Call git rev-parse and return the output""" 887 git = context.git 888 status, out, _ = git.rev_parse(name) 889 if status == 0: 890 result = out.strip() 891 else: 892 result = name 893 return result 894 895 896def write_blob(context, oid, filename): 897 """Write a blob to a temporary file and return the path 898 899 Modern versions of Git allow invoking filters. Older versions 900 get the object content as-is. 901 902 """ 903 if version.check_git(context, 'cat-file-filters-path'): 904 return cat_file_to_path(context, filename, oid) 905 return cat_file_blob(context, filename, oid) 906 907 908def cat_file_blob(context, filename, oid): 909 return cat_file(context, filename, 'blob', oid) 910 911 912def cat_file_to_path(context, filename, oid): 913 return cat_file(context, filename, oid, path=filename, filters=True) 914 915 916def cat_file(context, filename, *args, **kwargs): 917 """Redirect git cat-file output to a path""" 918 result = None 919 git = context.git 920 # Use the original filename in the suffix so that the generated filename 921 # has the correct extension, and so that it resembles the original name. 922 basename = os.path.basename(filename) 923 suffix = '-' + basename # ensures the correct filename extension 924 path = utils.tmp_filename('blob', suffix=suffix) 925 with open(path, 'wb') as fp: 926 status, out, err = git.cat_file( 927 _raw=True, _readonly=True, _stdout=fp, *args, **kwargs 928 ) 929 Interaction.command(N_('Error'), 'git cat-file', status, out, err) 930 if status == 0: 931 result = path 932 if not result: 933 core.unlink(path) 934 return result 935 936 937def write_blob_path(context, head, oid, filename): 938 """Use write_blob() when modern git is available""" 939 if version.check_git(context, 'cat-file-filters-path'): 940 return write_blob(context, oid, filename) 941 return cat_file_blob(context, filename, head + ':' + filename) 942 943 944def annex_path(context, head, filename): 945 """Return the git-annex path for a filename at the specified commit""" 946 git = context.git 947 path = None 948 annex_info = {} 949 950 # unfortunately there's no way to filter this down to a single path 951 # so we just have to scan all reported paths 952 status, out, _ = git.annex('findref', '--json', head) 953 if status == 0: 954 for line in out.splitlines(): 955 info = json.loads(line) 956 try: 957 annex_file = info['file'] 958 except (ValueError, KeyError): 959 continue 960 # we only care about this file so we can skip the rest 961 if annex_file == filename: 962 annex_info = info 963 break 964 key = annex_info.get('key', '') 965 if key: 966 status, out, _ = git.annex('contentlocation', key) 967 if status == 0 and os.path.exists(out): 968 path = out 969 970 return path 971