Skip to content

Instantly share code, notes, and snippets.

@166MMX
Forked from tvogel/git-merge-associate
Last active January 2, 2024 15:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 166MMX/4f67ef807938078ee4475192a113578c to your computer and use it in GitHub Desktop.
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
#!/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