You can write a custom merge driver.
Suppose we have a merge driver /usr/bin/latest.py
. Remember to make it executable if not on Windows.
In gitconfig:
[merge."latest"]
name = "choose the latest timestamp"
driver = /usr/bin/latest.py %O %A %B %L %P
In .gitattributes:
package-lock.json merge=latest
Here is an crude implementation of latest.py
in Python 3 (3.11.2 in my machine). If the shebang #!/usr/bin/env python
does not work, try #!/usr/bin/env python3
. You can write the merge driver in any programming language you like. It's a bit crude, as it's not really aware of JSON, and relies on the fact that ISO date/times sort alphabetically in this scenario.
It first creates two temporary files for the two branches being merged, and replace the syncAt
line with the latest timestamp in both. After that it proceeds to call git merge-file -p
passing these instead of the original. So for that particular line, git merge-file
will see the same change, which will not conflict, but anything else will fallback to default git merge behaviour, making sure other conflicts will fail, requiring manual resolution.
#!/usr/bin/env python3
import sys
import os
import subprocess
import tempfile
import logging
import traceback
ancestor = sys.argv[1]
current = sys.argv[2]
other = sys.argv[3]
KEYWORD = '"syncAt":'
def merge_file(current, ancestor, other):
cmd = 'git merge-file -p "%s" "%s" "%s"' % (current, ancestor, other)
status, output = subprocess.getstatusoutput(cmd)
return status, output
def get_syncat_line(p):
syncat = ''
with open(p) as f:
for line in f:
if line.strip().startswith(KEYWORD):
syncat = line
break
return syncat
def replace_syncat_line(p, replacement, output):
with open(p) as f:
for line in f:
if line.strip().startswith(KEYWORD):
output.write(replacement)
output.write("\n")
else:
output.write(line)
output.write("\n")
def write_output_exit(output, status):
with open(current, 'w') as f:
f.write(output)
sys.exit(status)
current_syncat = get_syncat_line(current)
other_syncat = get_syncat_line(other)
ancestor_syncat = get_syncat_line(ancestor)
def create_tmp_file():
return tempfile.NamedTemporaryFile('w', delete_on_close=False)
if (current_syncat.strip() == other_syncat.strip()):
status, output = merge_file(current, ancestor, other)
write_output_exit(output, status)
else:
try:
if current_syncat.strip() > other_syncat.strip():
latest_syncat = current_syncat.rstrip()
else:
latest_syncat = other_syncat.rstrip()
with create_tmp_file() as tmp_current, \
create_tmp_file() as tmp_other:
replace_syncat_line(current, latest_syncat, tmp_current)
replace_syncat_line(other, latest_syncat, tmp_other)
tmp_current.close()
tmp_other.close()
status, output = merge_file(
tmp_current.name, ancestor, tmp_other.name)
write_output_exit(output, status)
except Exception as e:
logging.error(traceback.format_exc())
status, output = merge_file(current, ancestor, other)
write_output_exit(output, 1)