Last active
September 11, 2025 21:51
-
-
Save mikeslattery/48e196a3623608e021e61f6864a3ee74 to your computer and use it in GitHub Desktop.
Git Directory Synchronizer
This file contains hidden or 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
| #!/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 "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Can you explain a little bit more about how to use this?
Especially in aider?
Thanks!