-
-
Save 166MMX/4f67ef807938078ee4475192a113578c to your computer and use it in GitHub Desktop.
git script to manually associate files in a conflicting merge when rename detection fails
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
import dataclasses | |
import locale | |
import subprocess | |
import sys | |
""" | |
Purpose: Manually associate missed renames in merge conflicts during Git merges. | |
Usage: git merge-associate <our-target> <base> <theirs> | |
Example: After a failed rename detection A/a -> B/b which results | |
in a CONFLICT (delete/modify) for A/a and corresponding "deleted by us" | |
messages in git status, use the following command to manually establish the link: | |
git merge-associate B/b :1:A/a :3:A/a | |
or using a shell with brace expansion: | |
git merge-associate B/b :{1,3}:A/a | |
This script registers the following associations: | |
- :1:A/a as :1:B/b | |
- HEAD:B/b as :2:B/b | |
- :3:A/a as :3:B/b | |
and replaces B/b with a merge-conflict version using "git checkout -m -- B/b". | |
After manual resolution of B/b and "git add B/b", A/a can be resolved by "git rm A/a". | |
""" | |
encoding = locale.getpreferredencoding() | |
def run_git_command(args: list[str], **kwargs) -> str: | |
""" Run a Git command and return its output. """ | |
git_args = ['git'] + args | |
# print(f'Running {" ".join(git_args)}') | |
try: | |
return subprocess.check_output(git_args, **kwargs).decode(encoding) | |
except subprocess.CalledProcessError as e: | |
sys.exit(f'Git command failed: {" ".join(git_args)}\n{e}') | |
def get_tree(spec: str) -> tuple[str, str]: | |
""" Determine the tree and basename from a given spec. """ | |
if not spec: | |
return 'EMPTY', spec | |
elif spec[0] == ':': # ^:(\d): | |
if spec[1:2].isdigit() and spec[2] == ':': | |
return f'INDEX:{spec[1:2]}', spec[3:] | |
return 'INDEX:0', spec[1:] | |
elif ':' in spec: # ^([^:]+): | |
tree, basename = spec.split(':', 1) | |
return tree, basename | |
else: | |
return 'HEAD', spec | |
def get_mode_and_sha(spec: str) -> tuple[str, str]: | |
""" Retrieve mode and SHA for a given spec. """ | |
tree, basename = get_tree(spec) | |
if tree == 'EMPTY': | |
return '100644', 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391' | |
if tree.startswith('INDEX:'): | |
req_stage = tree[6:] | |
output = run_git_command(['ls-files', '--stage', '-z', '--', basename]).strip() | |
lines = output.split('\0') | |
for line in lines: | |
if line: | |
mode, sha, stage, _ = line.split() | |
if stage == req_stage: | |
return mode, sha | |
else: | |
output = run_git_command(['ls-tree', '-z', tree, basename]).strip() | |
if output: | |
mode, _, sha, _ = output.split() | |
return mode, sha | |
raise ValueError(f'Could not determine mode or SHA1 for {spec}') | |
@dataclasses.dataclass(frozen=True) | |
class Spec: | |
""" Data class representing a Git spec. """ | |
tree: str | |
mode: str | |
sha: str | |
basename: str | |
@classmethod | |
def from_spec(cls, spec: str) -> 'Spec': | |
""" Create a Spec instance from a spec string. """ | |
tree, basename = get_tree(spec) | |
mode, sha = get_mode_and_sha(spec) | |
return cls(tree, mode, sha, basename) | |
def main(target: str, base: str, theirs: str) -> None: | |
""" Main function to process git merge association. """ | |
target = Spec.from_spec(target) | |
base = Spec.from_spec(base) | |
theirs = Spec.from_spec(theirs) | |
index_info = f'000000 0000000000000000000000000000000000000000 0\t{target.basename}\n' | |
index_info += f'000000 0000000000000000000000000000000000000000 0\t{base.basename}\n' | |
if base.basename != theirs.basename: | |
index_info += f'000000 0000000000000000000000000000000000000000 0\t{theirs.basename}\n' | |
index_info += f'{base.mode} {base.sha} 1\t{target.basename}\n' | |
index_info += f'{target.mode} {target.sha} 2\t{target.basename}\n' | |
index_info += f'{theirs.mode} {theirs.sha} 3\t{target.basename}\n' | |
# print(f"Writing index info:\n{index_info}") | |
run_git_command(['update-index', '--index-info'], input=index_info.encode(encoding)) | |
run_git_command(['checkout', '--merge', '--', target.basename]) | |
if __name__ == "__main__": | |
if len(sys.argv) != 4: | |
print("Usage: git merge-associate <our-target> <base> <theirs>", file=sys.stderr) | |
sys.exit(1) | |
main(*sys.argv[1:]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment