Skip to content

Commit e1f820c

Browse files
committed
init
0 parents  commit e1f820c

File tree

4 files changed

+496
-0
lines changed

4 files changed

+496
-0
lines changed

.github/workflows/tests.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: tests
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Check out repo
12+
uses: actions/checkout@v4
13+
14+
- name: Run tests
15+
run: ./tests/run.sh

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# gtree
2+
3+
An simple convention based, auto-complete enabled helper to deal with git worktrees.
4+
5+
## Usage
6+
7+
```sh
8+
gtree add <branch> # create/add a worktree in $GTREE_DIR
9+
gtree rm <branch> # remove the worktree for <branch>
10+
gtree cd [branch] # print/cd to worktree (or main repo if omitted)
11+
gtree packup [-f] # move from worktree back to main repo on the branch, remove worktree directory and checkout worktree branch
12+
gtree ls # list worktrees under $GTREE_DIR
13+
```
14+
15+
## Workflow
16+
1. `gtree add my-branch`: will use (or create if not yet exist) the branch for a new worktree in a default directory, and cd there
17+
2. do your work, commit what you like
18+
3. `gtree packup`: delete worktree directory, change back to repo location and check out the branch there
19+
20+
If you need to briefly need to jump to your main repo, just use `gtree cd` to get there, and jump back to the worktree with `gtree cd <branch>` (supports auto-complete)
21+
22+
## Install
23+
24+
Put `gtree` on your `PATH`, then enable the shell function:
25+
26+
```sh
27+
eval "$(gtree init)"
28+
```
29+
30+
31+
## Tests
32+
33+
```sh
34+
./tests/run.sh
35+
```

gtree

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
GTREE_DIR_DEFAULT="${HOME}/.gtree/trees"
5+
GTREE_DIR="${GTREE_DIR:-$GTREE_DIR_DEFAULT}"
6+
7+
usage() {
8+
cat <<'USAGE'
9+
Usage: gtree <command> [branch]
10+
11+
Commands:
12+
add <branch> Create/add a worktree in $GTREE_DIR
13+
rm <branch> Remove the worktree for <branch>
14+
cd [branch] Print the worktree path (or the main repo when omitted)
15+
packup [-f] If clean, remove the current worktree and checkout its branch in main
16+
ls List worktrees under $GTREE_DIR
17+
init Print a shell function to enable gtree cd
18+
USAGE
19+
}
20+
21+
die() {
22+
echo "gtree: $*" >&2
23+
exit 1
24+
}
25+
26+
require_git_repo() {
27+
git rev-parse --show-toplevel >/dev/null 2>&1 || die "not inside a git repository"
28+
}
29+
30+
ensure_dir() {
31+
mkdir -p "$GTREE_DIR"
32+
}
33+
34+
branch_exists() {
35+
git show-ref --verify --quiet "refs/heads/$1"
36+
}
37+
38+
worktree_path_for_branch() {
39+
local branch="$1"
40+
local path=""
41+
local wt=""
42+
local br=""
43+
local in_block=0
44+
45+
while IFS= read -r line; do
46+
case "$line" in
47+
worktree\ *)
48+
wt="${line#worktree }"
49+
br=""
50+
in_block=1
51+
;;
52+
branch\ *)
53+
br="${line#branch refs/heads/}"
54+
;;
55+
"")
56+
if [[ $in_block -eq 1 && "$br" == "$branch" ]]; then
57+
path="$wt"
58+
break
59+
fi
60+
in_block=0
61+
;;
62+
esac
63+
done < <(git worktree list --porcelain)
64+
65+
if [[ -n "$path" ]]; then
66+
echo "$path"
67+
return 0
68+
fi
69+
70+
return 1
71+
}
72+
73+
primary_worktree_path() {
74+
local line=""
75+
while IFS= read -r line; do
76+
case "$line" in
77+
worktree\ *)
78+
echo "${line#worktree }"
79+
return 0
80+
;;
81+
esac
82+
done < <(git worktree list --porcelain)
83+
return 1
84+
}
85+
86+
require_clean_worktree() {
87+
if [[ -n "$(git status --porcelain)" ]]; then
88+
die "working tree has uncommitted changes (use -f to discard unstaged/untracked)"
89+
fi
90+
}
91+
92+
cmd="${1:-}"
93+
case "$cmd" in
94+
add)
95+
branch="${2:-}"
96+
[[ -n "$branch" ]] || die "missing branch name"
97+
require_git_repo
98+
ensure_dir
99+
path="${GTREE_DIR%/}/$branch"
100+
if [[ -e "$path" ]]; then
101+
die "path already exists: $path"
102+
fi
103+
if branch_exists "$branch"; then
104+
git worktree add "$path" "$branch"
105+
else
106+
git worktree add -b "$branch" "$path"
107+
fi
108+
;;
109+
rm)
110+
branch="${2:-}"
111+
[[ -n "$branch" ]] || die "missing branch name"
112+
require_git_repo
113+
if path="$(worktree_path_for_branch "$branch")"; then
114+
git worktree remove "$path"
115+
else
116+
path="${GTREE_DIR%/}/$branch"
117+
[[ -d "$path" ]] || die "no worktree for branch: $branch"
118+
git worktree remove "$path"
119+
fi
120+
;;
121+
cd)
122+
branch="${2:-}"
123+
require_git_repo
124+
if [[ -z "$branch" ]]; then
125+
path="$(primary_worktree_path)" || die "could not resolve main worktree"
126+
echo "$path"
127+
else
128+
path="${GTREE_DIR%/}/$branch"
129+
[[ -d "$path" ]] || die "no worktree directory: $path"
130+
echo "$path"
131+
fi
132+
;;
133+
packup)
134+
require_git_repo
135+
force="${2:-}"
136+
if [[ -n "$force" && "$force" != "-f" ]]; then
137+
die "usage: gtree packup [-f]"
138+
fi
139+
current_path="$(git rev-parse --show-toplevel)"
140+
primary_path="$(primary_worktree_path)" || die "could not resolve main worktree"
141+
if [[ "$current_path" == "$primary_path" ]]; then
142+
die "already in main worktree"
143+
fi
144+
if [[ "$force" == "-f" ]]; then
145+
git checkout -- .
146+
git clean -fd
147+
fi
148+
require_clean_worktree
149+
branch="$(git symbolic-ref --quiet --short HEAD)" || die "detached HEAD"
150+
if git -C "$primary_path" status --porcelain | grep -Eq '^.[^ ]'; then
151+
echo "changes detected. check out worktree with git checkout $branch"
152+
exit 1
153+
fi
154+
git worktree remove "$current_path"
155+
git -C "$primary_path" checkout "$branch"
156+
echo "$primary_path"
157+
;;
158+
ls)
159+
require_git_repo
160+
ensure_dir
161+
prefix="${GTREE_DIR%/}/"
162+
wt=""
163+
br=""
164+
in_block=0
165+
while IFS= read -r line; do
166+
case "$line" in
167+
worktree\ *)
168+
wt="${line#worktree }"
169+
br=""
170+
in_block=1
171+
;;
172+
branch\ *)
173+
br="${line#branch refs/heads/}"
174+
;;
175+
"")
176+
if [[ $in_block -eq 1 && "$wt" == "$prefix"* ]]; then
177+
if [[ -n "$br" ]]; then
178+
echo "$br"
179+
else
180+
echo "${wt#"$prefix"} (detached)"
181+
fi
182+
fi
183+
in_block=0
184+
;;
185+
esac
186+
done < <(git worktree list --porcelain)
187+
;;
188+
init)
189+
cat <<'SH'
190+
gtree() {
191+
local __gtree_bin;
192+
if [[ -n "${ZSH_VERSION:-}" ]]; then
193+
__gtree_bin="$(whence -p gtree 2>/dev/null || true)";
194+
else
195+
__gtree_bin="$(type -P gtree 2>/dev/null || true)";
196+
fi;
197+
if [[ -z "$__gtree_bin" ]]; then
198+
echo "gtree: could not resolve binary path" >&2;
199+
return 1;
200+
fi;
201+
if [[ "${1:-}" == "cd" || "${1:-}" == "packup" ]]; then
202+
local dir;
203+
dir="$("$__gtree_bin" "$@")" || return;
204+
builtin cd "$dir";
205+
elif [[ "${1:-}" == "add" ]]; then
206+
local dir;
207+
"$__gtree_bin" "$@" || return;
208+
dir="$("$__gtree_bin" cd "${2:-}")" || return;
209+
builtin cd "$dir";
210+
else
211+
"$__gtree_bin" "$@";
212+
fi;
213+
};
214+
215+
_gtree_complete() {
216+
local __gtree_bin;
217+
if [[ -n "${ZSH_VERSION:-}" ]]; then
218+
__gtree_bin="$(whence -p gtree 2>/dev/null || true)";
219+
else
220+
__gtree_bin="$(command -v gtree 2>/dev/null || true)";
221+
fi;
222+
[[ -n "$__gtree_bin" ]] || return 0;
223+
224+
if [[ -n "${ZSH_VERSION:-}" ]]; then
225+
local -a cmds;
226+
cmds=(add rm cd packup ls init);
227+
if (( CURRENT == 2 )); then
228+
compadd -- "${cmds[@]}";
229+
return;
230+
fi;
231+
if (( CURRENT == 3 )) && [[ "${words[2]}" == "cd" || "${words[2]}" == "rm" ]]; then
232+
compadd -- "$("$__gtree_bin" ls)";
233+
fi;
234+
else
235+
local cur prev;
236+
cur="${COMP_WORDS[COMP_CWORD]}";
237+
prev="${COMP_WORDS[COMP_CWORD-1]}";
238+
if [[ $COMP_CWORD -eq 1 ]]; then
239+
COMPREPLY=( $(compgen -W "add rm cd packup ls init" -- "$cur") );
240+
return;
241+
fi;
242+
if [[ $COMP_CWORD -eq 2 && ( "$prev" == "cd" || "$prev" == "rm" ) ]]; then
243+
COMPREPLY=( $(compgen -W "$("$__gtree_bin" ls)" -- "$cur") );
244+
fi;
245+
fi;
246+
};
247+
248+
if [[ -n "${ZSH_VERSION:-}" ]]; then
249+
compdef _gtree_complete gtree;
250+
elif [[ -n "${BASH_VERSION:-}" ]]; then
251+
complete -F _gtree_complete gtree;
252+
fi;
253+
SH
254+
;;
255+
""|-h|--help|help)
256+
usage
257+
;;
258+
*)
259+
usage >&2
260+
exit 1
261+
;;
262+
esac

0 commit comments

Comments
 (0)