Skip to content

Instantly share code, notes, and snippets.

@mikeslattery
Last active September 11, 2025 21:51
Show Gist options
  • Save mikeslattery/48e196a3623608e021e61f6864a3ee74 to your computer and use it in GitHub Desktop.
Save mikeslattery/48e196a3623608e021e61f6864a3ee74 to your computer and use it in GitHub Desktop.
Git Directory Synchronizer
#!/bin/bash
help() { cat<<HELP
git-syncr - git worktree synchronizer
Usage: git syncr <command>
COMMANDS:
create <path> [<branch>]
Create new worktree at <path> forked from current directory.
Set branch name same as basename of path, if not provided.
Stores information about the parent branch in config.
link <tree-path> <local-path>
Share local project path with the sub-tree projects.
Must be run in parent directory.
Creates a mount between the projects.
Example: git syncr ../tree1 node_modules
new-window <path>
Create tmux window for a child tree at path.
Places at immediate right to current window.
sync [<path>]
Synchronizes repo in path bi-directionally with its parent.
Uses current directory, if none given.
This is the only command that can be run in sub-tree directory.
csync <message> [--edit]
Commit and sync.
Same as: git commit -a -m <message> && git syncr sync
aisync
Commit and sync with message generated by AI.
killwindow [<path>]
Close tmux window
remove <path>
Removes the worktree at <path> and the corresponding branch.
unlink <tree-path> <local-path>
Undoes link
info [<path>] Various information
[help] This help.
example Display example workflow and configuration.
HELP
}
example() { cat<<EXAMPLE
SUGGESTED ALIASES:
git config --global alias.sync 'syncr csync'
alias gy='git syncr csync'
POSSIBLE WORKFLOW:
# Setup
cd project
git syncr create ../p2
git syncr create ../p3
git syncr link ../p3 node_modules
git syncr new-window ../p3
# Work Loop.
git add -u
git commit
gy
#or: git syncr sync
# Clean up
cd ../project
git syncr sync ../p2
git syncr remove ../p2
git syncr sync ../p3
git syncr unlink ../p3 node_modules
git syncr remove ../p3
EXAMPLE
}
set -euo pipefail
pwd="$(pwd)"
thisdir="$(cd "$(dirname "$0")" || exit; pwd)"
this="${thisdir}/$(basename "$0")"
_die() {
echo "$* [${BASH_LINENO[0]}]" >&2
exit 1
}
_checkchild() {
path="$1"
branch="$(git -C "$path" rev-parse --abbrev-ref HEAD)"
git -C "$path" config --get "branch.${branch}.parent-branch" > /dev/null || \
_die "Not a child sync directory. [${BASH_LINENO[1]}]"
}
# Create new worktree
create() {
path="${1}"
path="$(readlink -f "$path")"
branch="${2:-$(basename "$path")}"
parent_path="$pwd"
parent_branch="$(git rev-parse --abbrev-ref HEAD)"
# Create worktree
git worktree add "$path" -b "$branch"
git config "branch.${branch}.parent-branch" "$parent_branch"
git config "branch.${branch}.parent-path" "$parent_path"
git config "branch.${branch}.path" "$path"
_checkchild "$path"
}
link() {
tree_path="${1}"
sub_path="${2}"
full_sub_path="$(readlink -f "$sub_path")"
full_tree_path="$(readlink -f "${tree_path}/${sub_path}")"
set -x
mkdir -p "$full_tree_path"
sudo mount --bind "$full_sub_path" "$full_tree_path"
}
unlink() {
tree_path="${1}"
sub_path="${2}"
full_sub_path="$(readlink -f "$sub_path")"
full_tree_path="$(readlink -f "${tree_path}/${sub_path}")"
set -x
sudo umount "$full_tree_path"
}
# Remove worktree
remove() {
path="${1}"
branch="$(git -C "$path" rev-parse --abbrev-ref HEAD)"
parent_branch="$(git -C "$path" config --get "branch.${branch}.parent-branch")"
parent_path="$( git -C "$path" config --get "branch.${branch}.parent-path")"
_checkchild "$path"
# Remove tree
git -C "$parent_path" worktree remove "$path"
git -C "$parent_path" branch -D "$branch"
}
info() {
path="${1:-$pwd}"
branch="$(git -C "$path" rev-parse --abbrev-ref HEAD)"
parent_branch="$(git -C "$path" config --get "branch.${branch}.parent-branch")"
( set -x; git worktree list; )
echo ''
( set -x; git --no-pager config --get-regexp "^branch\\.${branch}\\."; ) \
| sed 's/ /\t/;' \
|| true
echo ''
( set -x; git rev-parse --git-common-dir; )
echo ''
( set -x; git --no-pager log "$parent_branch"~..HEAD --oneline; )
}
csync() {
git commit -am "$@"
sync
}
aisync() {
prompt="Summarize the above git diff, for use as a git commit message."
path="${pwd}"
branch="$(git -C "$path" rev-parse --abbrev-ref HEAD)"
parent_branch="$(git "$path" config --get "branch.${branch}.parent-branch")"
nl=$'\n'
nl2=$'\n\n'
xxx='```'
full_prompt='```'"diff
$(git diff "$parent_branch"..HEAD)
"'```'"
---
${prompt}
"
message="$(
openai api chat.completions.create \
-m gpt-3.5-turbo -t 0.7 \
-g user "$full_prompt"
)"
git commit -a -m "$message" --edit "$@"
sync
}
sync() {
path="${1:-$pwd}"
path="$(readlink -f "$path")"
branch="$(git -C "$path" rev-parse --abbrev-ref HEAD)"
parent_branch="$(git -C "$path" config --get "branch.${branch}.parent-branch")"
parent_path="$( git -C "$path" config --get "branch.${branch}.parent-path")"
_checkchild "$path"
# Get latest changes from parent
git -C "$path" rebase "${parent_branch}"
# Push changes to parent
git -C "$parent_path" merge "${branch}" --ff
}
new-window() {
path="${1}"; shift
branch="$(git -C "$path" rev-parse --abbrev-ref HEAD)"
# currentwindow="$(tmux display-message -p '#I')"
# nextwindow="$(( currentwindow + 1 ))"
_checkchild "$path"
# # if next window index is not free, make room
# if tmux list-windows -F '#I' | grep -sq "^${nextwindow}\$"; then
# # Shift all other windows right
# #shellcheck disable=SC2016
# tmux list-windows -F '#I' | sed -E "/^${currentwindow}\$/,$ p" | sed 1d | \
# tac | xargs -rn1 bash -c 'tmux move-window -s $1 -t $(( $1 + 1 ))' --
# fi
# tmux new-window -c "$path" -n "$branch" -t "$nextwindow"
pane="$(tmux new-window -c "$path" -n "$branch" -P -F '#D')"
# tmux send-keys 'tmux +State' Enter
# tmux split-window -h -c "$path"
git config "branch.${branch}.tmux-pane-id" "$pane"
}
killwindow() {
path="${1:-$(pwd)}"; shift
branch="$(git -C "$path" rev-parse --abbrev-ref HEAD)"
window="$(git -C "$path" config --get "branch.${branch}.tmux-pane-id")"
_checkchild "$path"
tmux kill-window -t "$window"
}
main() {
if [[ "$#" == "0" ]]; then
help
exit 0
fi
"$@"
}
# Testing
test() {
# git() {
# echo '' >&2
# echo "$ git $* [${BASH_LINENO[0]}]" >&2
# command git "$@"
# }
set -eu
shellcheck "$this"
die() {
_die "$@"
}
# prepare
main=/tmp/tree
sub="${main}/trees/abc"
sub2="${main}/trees/def"
rm "${main:?}/" -rf
mkdir -p "${main}/trees"
# TODO: remove
pwd="$main"
# create initial parent project
cd "${main}"
git init -b master -q
rm .git/hooks -rf
echo trees > .gitignore
echo first > edit
git add .gitignore edit
git commit -q -m Initial
# create subtrees
"$0" create ./trees/abc
"$0" create ./trees/def
# echo -e "\nCHECKPOINT"
# cat .git/config
# exit 0
echo '----- TEST START'
# edit file in sub, see in main
# given
cd "$sub"
! grep -sq second "${sub}/edit" || die "Already editted"
! grep -sq second "${main}/edit" || die "Already editted"
! grep -sq second "${sub2}/edit" || die "Already editted"
# when
echo second >> edit
grep -sq second "${sub}/edit" || die "Not editted"
! grep -sq second "${main}/edit" || die "Already editted"
git add -u
git commit -q -m editted
"$0" sync "$sub"
# then
grep -sq second "${sub}/edit" || die "Not editted"
grep -sq second "${main}/edit" || die "Not editted: ${main}/edit"
! grep -sq second "${sub2}/edit" || die "Already editted"
# edit file in main, see in sub
# given
cd "$main"
# when
echo third >> edit
git add -u
git commit -q -m editted
"$0" sync "$sub"
# then
grep -sq third "${main}/edit" || die "Not editted"
grep -sq third "${sub}/edit" || die "Not editted"
echo '---CLEANUP'
"$0" remove ./trees/abc
"$0" remove ./trees/def
# confirm cleanup
[[ "$(git branch | wc -l)" == "1" ]] || die "branches exist"
rmdir ./trees/
cd /tmp
rm -rf "$main"
echo 'SUCCESS'
}
main "$@"
@heijligers
Copy link

Can you explain a little bit more about how to use this?
Especially in aider?
Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment