Skip to content

Instantly share code, notes, and snippets.

@mibmo
Last active November 5, 2024 04:55
Show Gist options
  • Save mibmo/986b3996ca801abcc03b4200a88ea6f6 to your computer and use it in GitHub Desktop.
Save mibmo/986b3996ca801abcc03b4200a88ea6f6 to your computer and use it in GitHub Desktop.
Automatically fix Syncthing conflicts via 3-way merges (requires trashbin versioning or stronger, as well as fd, ripgrep, and git)

Replace shebang with following if Nix is installed to automatically pull in dependencies.

#!/usr/bin/env nix-shell
#! nix-shell -i bash -p git fd ripgrep
#!/usr/bin/env bash
# syncthing share. some parent directory
targetShare="$PWD"
while true; do
if [ -d "$targetShare/.stfolder" ]; then break; fi
if [ "$targetShare" == "/" ]; then
echo "Could not find parent Syncthing share"
exit 1;
fi
targetShare=$(realpath "$targetShare/..")
done
# search scope
fdArgs="--no-ignore"
# regex to detect sync conflicts
conflictRegex='\.sync-conflict-[0-9]+-[0-9]{6}-[A-Z0-9]{7}(\..+)?$'
# regex to detect if file is in progress of being merged
mergeRegex='^<<<<<<< .+$\n(.*\n)*^=======$\n(.*\n)*^>>>>>>> .+$\n'
sharePath="${PWD#"$targetShare/"}"
conflicts="$(fd "$conflictRegex" . --type file $fdArgs)"
if [ "$conflicts" == "" ]; then echo "No conflicts, exiting."; exit 0; fi
count=$(echo "%s" "$conflicts" | wc -l)
countLen=$(printf "%d" "$count" | wc -c)
echo "Fixing $count conflict(s) via 3-way merge:";
echo "$conflicts" | while read -r path; do
i=$((${i:-0} + 1))
dir=$(dirname "$path")
name=$(basename "$path")
sanitizedName="$(echo "$name" | sed -r "s/$conflictRegex//")"
extension="$(echo "$name" | rev | cut -d. -f1 | rev)" # might be empty
# find paths for 3-way merge
base="$dir/$sanitizedName"
ancestors="$(fd \
"$(echo "$sanitizedName" | sed -r "s/\.$extension\$//")" \
"$targetShare/.stversions/$sharePath/$dir" \
--type file $fdArgs 2>/dev/null)"
other="$path"
printf "[$(printf "%0${countLen}d" "$i")/$count] Merging '%s': " "$(echo "$base" | sed -r 's/^\.\///')"
# pick "latest" ancestor
ancestor="$(echo "$ancestors" | sort -hr | head -1)"
# if no common ancestor, try empty file
ancestor="${ancestor:-/dev/null}"
# check if base even exists
if [ ! -f "$base" ]; then echo "conflict file references non-existant base, skipping."; continue; fi
if rg --quiet --multiline "$mergeRegex" "$base"; then echo "file has pending manual merge, skipping."; continue; fi
# perform actual merge
git merge-file "$base" "$ancestor" "$other" 2>/dev/null
# for documentation on exit codes, see: `man git-merge-file`
mergeStatus="$?"
if [ "$mergeStatus" -eq 255 ]; then echo "can't merge binary files, skipping."; continue; fi
if [ "$mergeStatus" -gt 127 ]; then echo "could not perform merge due to unknown error ($mergeStatus)."; continue; fi
if [ "$mergeStatus" -eq 0 ]; then echo "merged cleanly."; rm "$other"; continue; fi
if [ "$mergeStatus" -lt 0 ]; then echo "could not perform merge due to error ($mergeStatus)."; continue; fi
if [ "$mergeStatus" -gt 0 ]; then echo "needs manual intervention for $mergeStatus merge(s)."; rm "$other"; continue; fi
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment