-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathwtadd.sh
More file actions
executable file
·249 lines (217 loc) · 8.43 KB
/
wtadd.sh
File metadata and controls
executable file
·249 lines (217 loc) · 8.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
#!/usr/bin/env bash
# Based on: https://github.com/llimllib/personal_code/blob/master/homedir/.local/bin/worktree
# Adjusted to work with bare repos
RED="\033[0;31m"
GREEN="\033[0;32m"
YELLOW="\033[0;33m"
CLEAR="\033[0m"
VERBOSE=
BRANCH_NAME=
COMMIT_ISH=
function usage {
cat <<EOF
Usage: wtadd [-vh] [-b BRANCH_NAME] [-c COMMIT-ISH] WORKTREE_NAME
Create a git worktree named WORKTREE_NAME.
Will copy over any .env, .envrc, .tool-versions, or mise.toml files to the
new worktree as well as node_modules.
FLAGS:
-h, --help Print this help
-v, --verbose Verbose mode
-b, --branch BRANCH_NAME Specify branch name for the new worktree
(defaults to WORKTREE_NAME if not provided)
-c, --commit COMMIT-ISH Create worktree from specific commit, tag, or branch
(can be SHA, tag name, or branch reference)
EXAMPLES:
wtadd feature-work # Creates worktree and branch both named "feature-work"
wtadd -b my-feature feature-work # Creates worktree "feature-work" with branch "my-feature"
wtadd -c main experiment # Creates worktree "experiment" from main branch
wtadd -b hotfix -c abc123def urgent # Creates worktree "urgent" with branch "hotfix" from commit SHA
EOF
kill -INT $$
}
function die {
printf '%b%s%b\n' "$RED" "$1" "$CLEAR"
# exit the script, but if it was sourced, don't kill the shell
kill -INT $$
}
function warn {
printf '%b%s%b\n' "$YELLOW" "$1" "$CLEAR"
}
# If at all possible, use copy-on-write to copy files. This is especially
# important to allow us to copy node_modules directories efficiently
#
# On mac or bsd: try to use -c
# see:
# https://spin.atomicobject.com/2021/02/23/git-worktrees-untracked-files/
#
# On gnu: use --reflink
#
# Use /bin/cp directly to avoid any of the user's aliases - this script is
# often eval'ed
#
# I tried to figure out how to actually determine the filesystem support for
# copy-on-write, but did not find any good references, so I'm falling back on
# "try and see if it fails"
function cp_cow {
if ! /bin/cp -Rc "$1" "$2" 2>/dev/null; then
if ! /bin/cp -R --reflink "$1" "$2" 2>/dev/null; then
if ! /bin/cp -R "$1" "$2" 2>/dev/null; then
warn "Unable to copy file $1 to $2 - folder may not exist"
fi
fi
fi
}
# Detect which find variant is available by testing capabilities
function detect_find_variant {
# Test for BSD find (supports -E flag)
if find -E /dev/null -maxdepth 0 2>/dev/null; then
echo "bsd"
# Test for GNU find (supports --regextype)
elif find /dev/null -maxdepth 0 -regextype posix-extended 2>/dev/null; then
echo "gnu"
else
echo "basic"
fi
}
# Create a worktree from a given branchname, and copy some untracked files
function _worktree {
if [ -z "$1" ]; then
usage
fi
if [ -n "$VERBOSE" ]; then
set -x
fi
worktree_name="$1"
# If no branch name specified via -b flag, use worktree name as branch name
if [ -z "$BRANCH_NAME" ]; then
branchname="$worktree_name"
else
branchname="$BRANCH_NAME"
fi
# Replace slashes with underscores. If there's no slash, dirname will equal
# worktree_name. So "feature/something-other" becomes "feature_something-other", but
# "quick-fix" stays unchanged
# https://www.tldp.org/LDP/abs/html/parameter-substitution.html
dirname=${worktree_name//\//_}
is_worktree=$(git rev-parse --is-inside-work-tree)
if $is_worktree; then
parent_dir=".."
else
parent_dir="."
fi
# Validate commit-ish if provided
if [ -n "$COMMIT_ISH" ]; then
if ! git rev-parse --verify "$COMMIT_ISH^{commit}" >/dev/null 2>&1; then
die "Invalid commit-ish: $COMMIT_ISH"
fi
fi
# Handle worktree creation based on whether commit-ish is provided
if [ -n "$COMMIT_ISH" ]; then
# When commit-ish is provided, always create a new branch from that commit-ish
if ! git worktree add -b "$branchname" "$parent_dir/$dirname" "$COMMIT_ISH"; then
die "failed to create git worktree $branchname from $COMMIT_ISH"
fi
else
# Original logic: check if branch exists locally/remotely, or create new
# if the branch exists locally:
if git for-each-ref --format='%(refname:lstrip=2)' refs/heads | grep -E "^$branchname$" > /dev/null 2>&1; then
if ! git worktree add "$parent_dir/$dirname" "$branchname"; then
die "failed to create git worktree $branchname"
fi
# if the branch exists on a remote:
elif git for-each-ref --format='%(refname:lstrip=3)' refs/remotes/origin | grep -E "^$branchname$" > /dev/null 2>&1; then
if ! git worktree add "$parent_dir/$dirname" "$branchname"; then
die "failed to create git worktree $branchname"
fi
else
# otherwise, create a new branch
if ! git worktree add -b "$branchname" "$parent_dir/$dirname"; then
die "failed to create git worktree $branchname"
fi
fi
fi
# Find untracked files that we want to copy to the new worktree
# packages in node_modules packages can have sub-node-modules packages, and
# we don't want to copy them; only copy the root node_modules directory
if [ -d "node_modules" ]; then
cp_cow node_modules "$parent_dir/$dirname"/node_modules
fi
# this will fail for any files with \n in their names. don't do that.
IFS=$'\n'
# (XXX: should I add some mechanism for users to spcify this list? perhaps
# ~/.config/worktree/untracked or something?)
#
# this is the best of a bunch of bad options for reading the files into an
# array. We're often executing in bash or zsh, so we're going to let them
# use their file splitting rules, with an explicit IFS. We can't use find's
# exec because we want to use cp_cow to copy files copy-on-write when
# possible.
#
# Skip any of these files if they're found within node_modules.
#
# Putting the `-not -path` argument first is a great deal faster than the
# other way around
#
# shellcheck disable=SC2207
find_variant=$(detect_find_variant)
if $is_worktree; then
copy_source="."
else
copy_source=./$(git rev-parse --abbrev-ref HEAD)
fi
case "$find_variant" in
"bsd")
files_to_copy=( $(find -E "$copy_source" -not -path '*node_modules*' -and \
-iregex '.*\/\.(envrc|env|env.local|tool-versions|mise.toml)' ) )
;;
"gnu")
files_to_copy=( $(find "$copy_source" -not -path '*node_modules*' -and \
-regextype posix-extended -iregex '.*\/\.(envrc|env|env.local|tool-versions|mise.toml)' ) )
;;
"basic")
# Fallback to basic find without extended regex - use simple name matching
files_to_copy=( $(find "$copy_source" -not -path '*node_modules*' \
\( -name '.envrc' -o -name '.env' -o -name '.env.local' -o -name '.tool-versions' -o -name 'mise.toml' \) ) )
;;
esac
for f in "${files_to_copy[@]}"; do
target_path="${f#"$copy_source"/}"
cp_cow "$f" "$parent_dir/$dirname/$target_path"
done
# return the shell to normal splitting mode
unset IFS
# pull the most recent version of the remote
# ensure any inherited bare-repo env (GIT_DIR/GIT_WORK_TREE) doesn't leak into this call
# silence stdout/stderr from git; only show our warning on failure
if ! env -u GIT_DIR -u GIT_WORK_TREE git -C "$parent_dir/$dirname" pull >/dev/null 2>&1; then
warn "Unable to run git pull, there may not be an upstream"
fi
# if there was an envrc file, tell direnv -- if it's installed -- that it's ok to run it
if [ type -t direnv 2>/dev/null && -f "$parent_dir/$dirname/.envrc" ]; then
direnv allow "$parent_dir/$dirname"
fi
printf "%bcreated worktree %s%b\n" "$GREEN" "$parent_dir/$dirname" "$CLEAR"
}
while true; do
case $1 in
help | -h | --help)
usage
;;
-v | --verbose)
VERBOSE=true
shift
;;
-b | --branch)
BRANCH_NAME="$2"
shift 2
;;
-c | --commit)
COMMIT_ISH="$2"
shift 2
;;
*)
break
;;
esac
done
_worktree "$@"