gitbash 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/bin/gitbash +124 -0
- package/commands/branch.sh +184 -0
- package/commands/cleanstash.sh +187 -0
- package/commands/cleanup.sh +452 -0
- package/commands/commit.sh +169 -0
- package/commands/commits.sh +211 -0
- package/commands/create.sh +205 -0
- package/commands/pr.sh +130 -0
- package/commands/stale.sh +403 -0
- package/commands/stale.zsh +403 -0
- package/commands/stash.sh +93 -0
- package/commands/stashes.sh +183 -0
- package/commands/status.sh +323 -0
- package/commands/switch.sh +174 -0
- package/commands/unstash.sh +174 -0
- package/commands/update.sh +191 -0
- package/package.json +42 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# Show git status with fzf file selector and diff preview
|
|
2
|
+
status() {
|
|
3
|
+
# -----------------------------
|
|
4
|
+
# 0. Check for help flag
|
|
5
|
+
# -----------------------------
|
|
6
|
+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
7
|
+
cat << 'EOF'
|
|
8
|
+
Usage: status [OPTIONS]
|
|
9
|
+
|
|
10
|
+
Interactive git status viewer with fzf.
|
|
11
|
+
Browse uncommitted changes and view diffs in preview pane.
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
-h, --help Show this help message
|
|
15
|
+
|
|
16
|
+
Features:
|
|
17
|
+
- Lists all modified, staged, and untracked files
|
|
18
|
+
- Preview shows git diff for each file
|
|
19
|
+
- Color-coded file status indicators
|
|
20
|
+
- Supports both staged and unstaged changes
|
|
21
|
+
- Revert changes to discard modifications
|
|
22
|
+
|
|
23
|
+
File Status Indicators:
|
|
24
|
+
M Modified (unstaged)
|
|
25
|
+
A Added (staged)
|
|
26
|
+
D Deleted
|
|
27
|
+
R Renamed
|
|
28
|
+
?? Untracked
|
|
29
|
+
|
|
30
|
+
Navigation:
|
|
31
|
+
↑/↓ or j/k Navigate through files
|
|
32
|
+
TAB Select/deselect file (multi-select)
|
|
33
|
+
Enter Toggle staging for selected file(s)
|
|
34
|
+
Ctrl-R Revert changes (discard modifications)
|
|
35
|
+
ESC/Ctrl-C Exit
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
$ status
|
|
39
|
+
Git Status >
|
|
40
|
+
> M src/app.js
|
|
41
|
+
A src/new-feature.js
|
|
42
|
+
?? temp.txt
|
|
43
|
+
M README.md
|
|
44
|
+
|
|
45
|
+
# Preview shows diff of selected file
|
|
46
|
+
# Navigate with arrow keys or j/k
|
|
47
|
+
|
|
48
|
+
Requirements:
|
|
49
|
+
- Must be in a git repository
|
|
50
|
+
- fzf (fuzzy finder) - will prompt to install if not found
|
|
51
|
+
|
|
52
|
+
Notes:
|
|
53
|
+
- Staged changes show diff with --cached
|
|
54
|
+
- Unstaged changes show working tree diff
|
|
55
|
+
- Untracked files show file contents
|
|
56
|
+
- Empty status means working tree is clean
|
|
57
|
+
|
|
58
|
+
EOF
|
|
59
|
+
return 0
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# -----------------------------
|
|
63
|
+
# 1. Check prerequisites
|
|
64
|
+
# -----------------------------
|
|
65
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
66
|
+
echo "Not inside a git repository."
|
|
67
|
+
return 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
if ! command -v fzf >/dev/null 2>&1; then
|
|
71
|
+
echo "fzf is not installed."
|
|
72
|
+
|
|
73
|
+
# Ask whether to install with Homebrew
|
|
74
|
+
read -rp "Install fzf with Homebrew? (y/N): " ans
|
|
75
|
+
case "$ans" in
|
|
76
|
+
[yY][eE][sS]|[yY])
|
|
77
|
+
echo "Installing fzf with brew..."
|
|
78
|
+
if ! command -v brew >/dev/null 2>&1; then
|
|
79
|
+
echo "Homebrew is not installed. Aborting."
|
|
80
|
+
return 1
|
|
81
|
+
fi
|
|
82
|
+
brew install fzf || { echo "fzf install failed. Aborting."; return 1; }
|
|
83
|
+
# Optional: install shell integrations automatically
|
|
84
|
+
if [[ -f "$(brew --prefix)/opt/fzf/install" ]]; then
|
|
85
|
+
echo "Running fzf install script..."
|
|
86
|
+
yes | "$(brew --prefix)/opt/fzf/install"
|
|
87
|
+
fi
|
|
88
|
+
;;
|
|
89
|
+
*)
|
|
90
|
+
echo "Aborted (skipped fzf installation)."
|
|
91
|
+
return 1
|
|
92
|
+
;;
|
|
93
|
+
esac
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# -----------------------------
|
|
97
|
+
# 2. Get git status and format with staging info
|
|
98
|
+
# -----------------------------
|
|
99
|
+
local status_output
|
|
100
|
+
status_output=$(git status --short | awk '{
|
|
101
|
+
index_status = substr($0, 1, 1);
|
|
102
|
+
work_status = substr($0, 2, 1);
|
|
103
|
+
file = substr($0, 4);
|
|
104
|
+
|
|
105
|
+
# Determine staging status
|
|
106
|
+
if (index_status ~ /[MADRC]/) {
|
|
107
|
+
staged = "[STAGED]";
|
|
108
|
+
} else if (work_status == "?" || index_status == "?") {
|
|
109
|
+
staged = "[UNTRACKED]";
|
|
110
|
+
} else {
|
|
111
|
+
staged = "[UNSTAGED]";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
printf "%-12s %s %s\n", staged, substr($0, 1, 2), file;
|
|
115
|
+
}')
|
|
116
|
+
|
|
117
|
+
if [[ -z "$status_output" ]]; then
|
|
118
|
+
echo "Working tree clean - no changes to display."
|
|
119
|
+
return 0
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# -----------------------------
|
|
123
|
+
# 3. Run fzf with diff preview in a loop
|
|
124
|
+
# -----------------------------
|
|
125
|
+
while true; do
|
|
126
|
+
local selected_output
|
|
127
|
+
selected_output=$(
|
|
128
|
+
echo "$status_output" | fzf \
|
|
129
|
+
--height=100% \
|
|
130
|
+
-i \
|
|
131
|
+
--reverse \
|
|
132
|
+
--border \
|
|
133
|
+
--prompt="Git Status > " \
|
|
134
|
+
--header="[Enter] toggle stage | [Ctrl-r] revert | [TAB] multi-select | [ESC] exit" \
|
|
135
|
+
--multi \
|
|
136
|
+
--ansi \
|
|
137
|
+
--expect=ctrl-r \
|
|
138
|
+
--preview='
|
|
139
|
+
file=$(echo {} | awk "{print \$NF}");
|
|
140
|
+
file_status=$(echo {} | awk "{print \$2}");
|
|
141
|
+
|
|
142
|
+
# Show diff based on file status
|
|
143
|
+
if [[ "$file_status" == "??" ]]; then
|
|
144
|
+
# Untracked file - show contents
|
|
145
|
+
echo "=== UNTRACKED FILE ===";
|
|
146
|
+
echo;
|
|
147
|
+
bat --color=always --style=numbers "$file" 2>/dev/null || cat "$file" 2>/dev/null || echo "Cannot preview file";
|
|
148
|
+
elif [[ "$file_status" =~ ^[MAC] ]]; then
|
|
149
|
+
# Staged file - show cached diff
|
|
150
|
+
echo "=== STAGED CHANGES ===";
|
|
151
|
+
echo;
|
|
152
|
+
git diff --cached --color=always "$file" 2>/dev/null | delta 2>/dev/null || git diff --cached --color=always "$file" 2>/dev/null;
|
|
153
|
+
if git diff --color=always "$file" 2>/dev/null | grep -q .; then
|
|
154
|
+
echo;
|
|
155
|
+
echo "=== UNSTAGED CHANGES ===";
|
|
156
|
+
echo;
|
|
157
|
+
git diff --color=always "$file" 2>/dev/null | delta 2>/dev/null || git diff --color=always "$file" 2>/dev/null;
|
|
158
|
+
fi
|
|
159
|
+
else
|
|
160
|
+
# Unstaged changes
|
|
161
|
+
echo "=== UNSTAGED CHANGES ===";
|
|
162
|
+
echo;
|
|
163
|
+
git diff --color=always "$file" 2>/dev/null | delta 2>/dev/null || git diff --color=always "$file" 2>/dev/null;
|
|
164
|
+
fi
|
|
165
|
+
' \
|
|
166
|
+
--preview-window=right:60%
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Parse the key pressed and the selected files
|
|
170
|
+
local key_pressed
|
|
171
|
+
key_pressed=$(echo "$selected_output" | head -1)
|
|
172
|
+
|
|
173
|
+
# Get all selected files (skip first line which is the key)
|
|
174
|
+
local selected_files=()
|
|
175
|
+
while IFS= read -r line; do
|
|
176
|
+
[[ -n "$line" ]] && selected_files+=("$line")
|
|
177
|
+
done <<< "$(echo "$selected_output" | tail -n +2)"
|
|
178
|
+
|
|
179
|
+
# Exit if no selection (ESC or Ctrl-C)
|
|
180
|
+
if [[ ${#selected_files[@]} -eq 0 ]]; then
|
|
181
|
+
echo "Exited status viewer."
|
|
182
|
+
break
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
# -----------------------------
|
|
186
|
+
# 4. Handle action based on key pressed
|
|
187
|
+
# -----------------------------
|
|
188
|
+
if [[ "$key_pressed" == "ctrl-r" ]]; then
|
|
189
|
+
# Revert changes for all selected files
|
|
190
|
+
local files_to_revert=()
|
|
191
|
+
local untracked_to_delete=()
|
|
192
|
+
|
|
193
|
+
for selected_file in "${selected_files[@]}"; do
|
|
194
|
+
local staging_status=$(echo "$selected_file" | awk '{print $1}')
|
|
195
|
+
local filename=$(echo "$selected_file" | awk '{print $NF}')
|
|
196
|
+
|
|
197
|
+
if [[ "$staging_status" == "[UNTRACKED]" ]]; then
|
|
198
|
+
untracked_to_delete+=("$filename")
|
|
199
|
+
else
|
|
200
|
+
files_to_revert+=("$filename")
|
|
201
|
+
fi
|
|
202
|
+
done
|
|
203
|
+
|
|
204
|
+
# Handle untracked files
|
|
205
|
+
if [[ ${#untracked_to_delete[@]} -gt 0 ]]; then
|
|
206
|
+
echo "Untracked files to delete:"
|
|
207
|
+
for f in "${untracked_to_delete[@]}"; do
|
|
208
|
+
echo " - $f"
|
|
209
|
+
done
|
|
210
|
+
read -rp "Delete these ${#untracked_to_delete[@]} untracked file(s)? (y/N): " confirm
|
|
211
|
+
case "$confirm" in
|
|
212
|
+
[yY][eE][sS]|[yY])
|
|
213
|
+
for f in "${untracked_to_delete[@]}"; do
|
|
214
|
+
rm "$f"
|
|
215
|
+
echo "✓ Deleted: $f"
|
|
216
|
+
done
|
|
217
|
+
;;
|
|
218
|
+
*)
|
|
219
|
+
echo "Skipped deletion."
|
|
220
|
+
;;
|
|
221
|
+
esac
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
# Handle tracked files
|
|
225
|
+
if [[ ${#files_to_revert[@]} -gt 0 ]]; then
|
|
226
|
+
echo "Files to revert:"
|
|
227
|
+
for f in "${files_to_revert[@]}"; do
|
|
228
|
+
echo " - $f"
|
|
229
|
+
done
|
|
230
|
+
read -rp "Revert changes in these ${#files_to_revert[@]} file(s)? This cannot be undone. (y/N): " confirm
|
|
231
|
+
case "$confirm" in
|
|
232
|
+
[yY][eE][sS]|[yY])
|
|
233
|
+
for f in "${files_to_revert[@]}"; do
|
|
234
|
+
git reset HEAD "$f" 2>/dev/null
|
|
235
|
+
git checkout -- "$f"
|
|
236
|
+
echo "✓ Reverted: $f"
|
|
237
|
+
done
|
|
238
|
+
;;
|
|
239
|
+
*)
|
|
240
|
+
echo "Skipped revert."
|
|
241
|
+
;;
|
|
242
|
+
esac
|
|
243
|
+
fi
|
|
244
|
+
else
|
|
245
|
+
# Toggle staging for all selected files
|
|
246
|
+
local did_stage=false
|
|
247
|
+
for selected_file in "${selected_files[@]}"; do
|
|
248
|
+
local staging_status=$(echo "$selected_file" | awk '{print $1}')
|
|
249
|
+
local filename=$(echo "$selected_file" | awk '{print $NF}')
|
|
250
|
+
|
|
251
|
+
if [[ "$staging_status" == "[STAGED]" ]]; then
|
|
252
|
+
echo "Unstaging: $filename"
|
|
253
|
+
git reset HEAD "$filename"
|
|
254
|
+
elif [[ "$staging_status" == "[UNSTAGED]" ]]; then
|
|
255
|
+
echo "Staging: $filename"
|
|
256
|
+
git add "$filename"
|
|
257
|
+
did_stage=true
|
|
258
|
+
elif [[ "$staging_status" == "[UNTRACKED]" ]]; then
|
|
259
|
+
echo "Adding: $filename"
|
|
260
|
+
git add "$filename"
|
|
261
|
+
did_stage=true
|
|
262
|
+
fi
|
|
263
|
+
done
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
# Refresh status output
|
|
267
|
+
status_output=$(git status --short | awk '{
|
|
268
|
+
index_status = substr($0, 1, 1);
|
|
269
|
+
work_status = substr($0, 2, 1);
|
|
270
|
+
file = substr($0, 4);
|
|
271
|
+
|
|
272
|
+
# Determine staging status
|
|
273
|
+
if (index_status ~ /[MADRC]/) {
|
|
274
|
+
staged = "[STAGED]";
|
|
275
|
+
} else if (work_status == "?" || index_status == "?") {
|
|
276
|
+
staged = "[UNTRACKED]";
|
|
277
|
+
} else {
|
|
278
|
+
staged = "[UNSTAGED]";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
printf "%-12s %s %s\n", staged, substr($0, 1, 2), file;
|
|
282
|
+
}')
|
|
283
|
+
|
|
284
|
+
# Check if working tree is now clean
|
|
285
|
+
if [[ -z "$status_output" ]]; then
|
|
286
|
+
echo "✓ Working tree is now clean - all changes staged or reverted."
|
|
287
|
+
break
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
# Only ask for commit if we just staged something (not unstaged)
|
|
291
|
+
if [[ "$did_stage" == true ]] && echo "$status_output" | grep -q '\[STAGED\]'; then
|
|
292
|
+
echo
|
|
293
|
+
echo "Staged files:"
|
|
294
|
+
|
|
295
|
+
# Get list of staged files and display in tree-like structure
|
|
296
|
+
git diff --cached --name-only | while IFS= read -r file; do
|
|
297
|
+
# Get the directory and filename
|
|
298
|
+
dir=$(dirname "$file")
|
|
299
|
+
base=$(basename "$file")
|
|
300
|
+
|
|
301
|
+
# Simple tree-like output with indentation
|
|
302
|
+
if [[ "$dir" == "." ]]; then
|
|
303
|
+
echo " └─ $base"
|
|
304
|
+
else
|
|
305
|
+
echo " └─ $file"
|
|
306
|
+
fi
|
|
307
|
+
done
|
|
308
|
+
|
|
309
|
+
echo
|
|
310
|
+
read -rp "Would you like to commit and push these changes? (Y/n): " ans
|
|
311
|
+
case "$ans" in
|
|
312
|
+
[nN][oO]|[nN])
|
|
313
|
+
echo "Continuing..."
|
|
314
|
+
;;
|
|
315
|
+
*)
|
|
316
|
+
# Use the commit function with staged and push flags
|
|
317
|
+
commit -s -p
|
|
318
|
+
break
|
|
319
|
+
;;
|
|
320
|
+
esac
|
|
321
|
+
fi
|
|
322
|
+
done
|
|
323
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Select and switch to a branch using fzf
|
|
2
|
+
switch() {
|
|
3
|
+
# -----------------------------
|
|
4
|
+
# 0. Check for help flag
|
|
5
|
+
# -----------------------------
|
|
6
|
+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
7
|
+
cat << 'EOF'
|
|
8
|
+
Usage: switch [FILTER...]
|
|
9
|
+
|
|
10
|
+
Select a git branch using fzf and switch to it.
|
|
11
|
+
|
|
12
|
+
Arguments:
|
|
13
|
+
FILTER... Optional search filter words to pre-fill fzf (joined with spaces)
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
-h, --help Show this help message
|
|
17
|
+
|
|
18
|
+
Behavior:
|
|
19
|
+
- Lists both local and remote branches
|
|
20
|
+
- Remote branches are hidden if a local branch with the same name exists
|
|
21
|
+
- Uses fzf for interactive selection
|
|
22
|
+
- Automatically switches to the selected branch
|
|
23
|
+
- For remote branches, creates a local tracking branch if needed
|
|
24
|
+
- Current branch is highlighted with an asterisk (*)
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
$ switch
|
|
28
|
+
# Opens fzf with list of branches, select one to switch
|
|
29
|
+
|
|
30
|
+
$ switch captcha
|
|
31
|
+
# Opens fzf with 'captcha' pre-filled as filter
|
|
32
|
+
|
|
33
|
+
$ switch LOVE-123
|
|
34
|
+
# Opens fzf filtering for branches containing 'LOVE-123'
|
|
35
|
+
|
|
36
|
+
$ switch update figma guidelines
|
|
37
|
+
# Opens fzf with 'update figma guidelines' pre-filled as filter
|
|
38
|
+
|
|
39
|
+
Requirements:
|
|
40
|
+
- Must be in a git repository
|
|
41
|
+
- fzf must be installed
|
|
42
|
+
|
|
43
|
+
EOF
|
|
44
|
+
return 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# -----------------------------
|
|
48
|
+
# 1. Check prerequisites
|
|
49
|
+
# -----------------------------
|
|
50
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
51
|
+
echo "Not inside a git repository."
|
|
52
|
+
return 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
if ! command -v fzf >/dev/null 2>&1; then
|
|
56
|
+
echo "fzf is not installed."
|
|
57
|
+
|
|
58
|
+
# Ask whether to install with Homebrew
|
|
59
|
+
read -rp "Install fzf with Homebrew? (y/N): " ans
|
|
60
|
+
case "$ans" in
|
|
61
|
+
[yY][eE][sS]|[yY])
|
|
62
|
+
echo "Installing fzf with brew..."
|
|
63
|
+
if ! command -v brew >/dev/null 2>&1; then
|
|
64
|
+
echo "Homebrew is not installed. Aborting."
|
|
65
|
+
return 1
|
|
66
|
+
fi
|
|
67
|
+
brew install fzf || { echo "fzf install failed. Aborting."; return 1; }
|
|
68
|
+
# Optional: install shell integrations automatically
|
|
69
|
+
if [[ -f "$(brew --prefix)/opt/fzf/install" ]]; then
|
|
70
|
+
echo "Running fzf install script..."
|
|
71
|
+
yes | "$(brew --prefix)/opt/fzf/install"
|
|
72
|
+
fi
|
|
73
|
+
;;
|
|
74
|
+
*)
|
|
75
|
+
echo "Aborted (skipped fzf installation)."
|
|
76
|
+
return 1
|
|
77
|
+
;;
|
|
78
|
+
esac
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# -----------------------------
|
|
82
|
+
# 2. Get current branch and filter
|
|
83
|
+
# -----------------------------
|
|
84
|
+
local current_branch
|
|
85
|
+
current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
86
|
+
|
|
87
|
+
local filter="$*"
|
|
88
|
+
|
|
89
|
+
# -----------------------------
|
|
90
|
+
# 3. List branches and select with fzf
|
|
91
|
+
# -----------------------------
|
|
92
|
+
# Get local branches
|
|
93
|
+
local local_branches
|
|
94
|
+
local_branches=$(git branch --format='%(refname:short)')
|
|
95
|
+
|
|
96
|
+
# Get remote branches, filter out those that already exist locally
|
|
97
|
+
local remote_branches
|
|
98
|
+
remote_branches=$(git branch -r --format='%(refname:short)' | grep -v 'HEAD' | grep -v '^origin$' | while read -r remote; do
|
|
99
|
+
local_name="${remote#*/}" # Remove origin/ prefix
|
|
100
|
+
# Only include if no local branch with this name exists
|
|
101
|
+
if ! echo "$local_branches" | grep -qx "$local_name"; then
|
|
102
|
+
echo "$remote"
|
|
103
|
+
fi
|
|
104
|
+
done)
|
|
105
|
+
|
|
106
|
+
local selected_branch
|
|
107
|
+
selected_branch=$(
|
|
108
|
+
{
|
|
109
|
+
# List local branches
|
|
110
|
+
echo "$local_branches" | sed 's/^/local: /'
|
|
111
|
+
# Spacer (only if there are remote branches to show)
|
|
112
|
+
if [[ -n "$remote_branches" ]]; then
|
|
113
|
+
echo "─────────────────────────────"
|
|
114
|
+
# List filtered remote branches
|
|
115
|
+
echo "$remote_branches" | sed 's/^/remote: /'
|
|
116
|
+
fi
|
|
117
|
+
} | fzf \
|
|
118
|
+
--height=40% \
|
|
119
|
+
--reverse \
|
|
120
|
+
--border \
|
|
121
|
+
--prompt="Select branch: " \
|
|
122
|
+
--query="$filter" \
|
|
123
|
+
-i \
|
|
124
|
+
--preview="branch=\$(echo {} | sed 's/^[^:]*: //'); if [[ \"\$branch\" == *───* ]]; then echo 'Spacer - not selectable'; else git log --color=always -n 1 --format='%C(bold cyan)Author:%C(reset) %an%n%C(bold cyan)Date:%C(reset) %ar (%ad)%n%C(bold cyan)Message:%C(reset) %s%n' --date=format:'%Y-%m-%d %H:%M' \"\$branch\" 2>/dev/null && echo && git log --oneline --color=always -n 10 \"\$branch\" 2>/dev/null; fi" \
|
|
125
|
+
--preview-window=right:50% \
|
|
126
|
+
--header="Current: $current_branch"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# -----------------------------
|
|
130
|
+
# 4. Handle selection
|
|
131
|
+
# -----------------------------
|
|
132
|
+
if [[ -z "$selected_branch" ]]; then
|
|
133
|
+
echo "No branch selected."
|
|
134
|
+
return 0
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# Ignore spacer selection
|
|
138
|
+
if [[ "$selected_branch" == *───* ]]; then
|
|
139
|
+
echo "Invalid selection."
|
|
140
|
+
return 0
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# Extract branch type and name
|
|
144
|
+
local branch_type="${selected_branch%%:*}"
|
|
145
|
+
local branch_name="${selected_branch#*: }"
|
|
146
|
+
|
|
147
|
+
# -----------------------------
|
|
148
|
+
# 5. Switch to branch
|
|
149
|
+
# -----------------------------
|
|
150
|
+
if [[ "$branch_type" == "local" ]]; then
|
|
151
|
+
# Switch to local branch
|
|
152
|
+
echo "Switching to local branch: $branch_name"
|
|
153
|
+
git checkout "$branch_name"
|
|
154
|
+
else
|
|
155
|
+
# Handle remote branch
|
|
156
|
+
local local_branch="${branch_name#*/}" # Remove origin/ prefix
|
|
157
|
+
|
|
158
|
+
# Check if local branch already exists
|
|
159
|
+
if git show-ref --verify --quiet "refs/heads/$local_branch"; then
|
|
160
|
+
echo "Switching to existing local branch: $local_branch"
|
|
161
|
+
git checkout "$local_branch"
|
|
162
|
+
else
|
|
163
|
+
echo "Creating local tracking branch: $local_branch (tracking $branch_name)"
|
|
164
|
+
git checkout -b "$local_branch" --track "$branch_name"
|
|
165
|
+
fi
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
if [[ $? -eq 0 ]]; then
|
|
169
|
+
echo "✓ Successfully switched to branch: $(git rev-parse --abbrev-ref HEAD)"
|
|
170
|
+
else
|
|
171
|
+
echo "✗ Failed to switch branch."
|
|
172
|
+
return 1
|
|
173
|
+
fi
|
|
174
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
unstash() {
|
|
2
|
+
# -----------------------------
|
|
3
|
+
# 0. Check for help flag
|
|
4
|
+
# -----------------------------
|
|
5
|
+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
6
|
+
cat << 'EOF'
|
|
7
|
+
Usage: unstash [OPTIONS]
|
|
8
|
+
|
|
9
|
+
Apply a git stash to your working directory with optional removal.
|
|
10
|
+
Interactive fuzzy finder (fzf) with preview support.
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
-h, --help Show this help message
|
|
14
|
+
|
|
15
|
+
Features:
|
|
16
|
+
- Single-select stash picker
|
|
17
|
+
- Preview stash contents before applying
|
|
18
|
+
- Apply stash to working directory
|
|
19
|
+
- Optional confirmation to drop stash after applying
|
|
20
|
+
|
|
21
|
+
Navigation:
|
|
22
|
+
↑/↓ or j/k Navigate through stashes
|
|
23
|
+
Enter Select and apply stash
|
|
24
|
+
ESC/Ctrl-C Abort
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
$ unstash
|
|
28
|
+
Available stashes >
|
|
29
|
+
> stash@{0}: WIP on main: 1a2b3c4 commit message
|
|
30
|
+
stash@{1}: WIP on feature: 5d6e7f8 another commit
|
|
31
|
+
stash@{2}: On dev: 9a8b7c6 old work
|
|
32
|
+
✖ Abort
|
|
33
|
+
|
|
34
|
+
# Navigate to desired stash and press Enter
|
|
35
|
+
|
|
36
|
+
Applying stash@{1} ...
|
|
37
|
+
On branch main
|
|
38
|
+
Changes not staged for commit:
|
|
39
|
+
modified: src/app.js
|
|
40
|
+
|
|
41
|
+
Drop stash@{1} now? (y/N): y
|
|
42
|
+
Dropping stash@{1} ...
|
|
43
|
+
Dropped stash@{1} (5d6e7f8abcd...)
|
|
44
|
+
|
|
45
|
+
Workflow:
|
|
46
|
+
1. Select a stash from the list
|
|
47
|
+
2. Stash is applied to your working directory
|
|
48
|
+
3. Choose whether to drop (delete) the stash or keep it
|
|
49
|
+
|
|
50
|
+
Requirements:
|
|
51
|
+
- fzf (fuzzy finder) - will prompt to install if not found
|
|
52
|
+
- git stash list with at least one stash
|
|
53
|
+
|
|
54
|
+
Notes:
|
|
55
|
+
- Uses 'git stash apply' to preserve the stash initially
|
|
56
|
+
- Stash is only dropped if you confirm after successful apply
|
|
57
|
+
- Preview shows the full diff of the selected stash
|
|
58
|
+
- If apply fails, the stash is automatically kept
|
|
59
|
+
|
|
60
|
+
EOF
|
|
61
|
+
return 0
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# -----------------------------
|
|
65
|
+
# 1. Check for fzf
|
|
66
|
+
# -----------------------------
|
|
67
|
+
if ! command -v fzf >/dev/null 2>&1; then
|
|
68
|
+
echo "fzf is not installed."
|
|
69
|
+
|
|
70
|
+
# Ask whether to install with Homebrew
|
|
71
|
+
read -rp "Install fzf with Homebrew? (y/N): " ans
|
|
72
|
+
case "$ans" in
|
|
73
|
+
[yY][eE][sS]|[yY])
|
|
74
|
+
echo "Installing fzf with brew..."
|
|
75
|
+
if ! command -v brew >/dev/null 2>&1; then
|
|
76
|
+
echo "Homebrew is not installed. Aborting."
|
|
77
|
+
return 1
|
|
78
|
+
fi
|
|
79
|
+
brew install fzf || { echo "fzf install failed. Aborting."; return 1; }
|
|
80
|
+
# Optional: install shell integrations automatically
|
|
81
|
+
if [[ -f "$(brew --prefix)/opt/fzf/install" ]]; then
|
|
82
|
+
echo "Running fzf install script..."
|
|
83
|
+
yes | "$(brew --prefix)/opt/fzf/install"
|
|
84
|
+
fi
|
|
85
|
+
;;
|
|
86
|
+
*)
|
|
87
|
+
echo "Aborted (skipped fzf installation)."
|
|
88
|
+
return 1
|
|
89
|
+
;;
|
|
90
|
+
esac
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# -----------------------------
|
|
94
|
+
# 2. Build menu entries
|
|
95
|
+
# -----------------------------
|
|
96
|
+
local abort_label="✖ Abort"
|
|
97
|
+
|
|
98
|
+
local choices
|
|
99
|
+
choices=$(
|
|
100
|
+
{
|
|
101
|
+
git stash list
|
|
102
|
+
echo "$abort_label"
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# -----------------------------
|
|
107
|
+
# 3. Run fzf full-screen picker with preview
|
|
108
|
+
# -----------------------------
|
|
109
|
+
local selection
|
|
110
|
+
selection=$(printf "%s\n" "$choices" \
|
|
111
|
+
| fzf --prompt="Available stashes > " \
|
|
112
|
+
-i \
|
|
113
|
+
--reverse \
|
|
114
|
+
--border \
|
|
115
|
+
--header="Select a stash to apply" \
|
|
116
|
+
--no-multi \
|
|
117
|
+
--bind=enter:accept \
|
|
118
|
+
--preview='
|
|
119
|
+
sel=$(echo {} | grep -o "stash@{[0-9]\+}" || true);
|
|
120
|
+
if [[ -n "$sel" ]]; then
|
|
121
|
+
git stash show -p "$sel";
|
|
122
|
+
else
|
|
123
|
+
echo "No preview";
|
|
124
|
+
fi
|
|
125
|
+
'
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# ESC or Ctrl-C
|
|
129
|
+
if [[ -z "$selection" ]]; then
|
|
130
|
+
echo "Aborted."
|
|
131
|
+
return 1
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# Abort option
|
|
135
|
+
if [[ "$selection" == "$abort_label" ]]; then
|
|
136
|
+
echo "Aborted."
|
|
137
|
+
return 1
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# -----------------------------
|
|
141
|
+
# 4. Extract stash ID safely
|
|
142
|
+
# -----------------------------
|
|
143
|
+
local stash_id
|
|
144
|
+
stash_id=$(echo "$selection" | grep -o 'stash@{[0-9]\+}')
|
|
145
|
+
|
|
146
|
+
if [[ -z "$stash_id" ]]; then
|
|
147
|
+
echo "Error: Could not determine stash ID."
|
|
148
|
+
return 1
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# -----------------------------
|
|
152
|
+
# 5. Apply the stash
|
|
153
|
+
# -----------------------------
|
|
154
|
+
echo "Applying $stash_id ..."
|
|
155
|
+
if ! git stash apply "$stash_id"; then
|
|
156
|
+
echo "Apply failed, kept stash."
|
|
157
|
+
return 1
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# -----------------------------
|
|
161
|
+
# 6. Confirm before dropping
|
|
162
|
+
# -----------------------------
|
|
163
|
+
echo
|
|
164
|
+
read -rp "Drop $stash_id now? (y/N): " drop_ans
|
|
165
|
+
case "$drop_ans" in
|
|
166
|
+
[yY][eE][sS]|[yY])
|
|
167
|
+
echo "Dropping $stash_id ..."
|
|
168
|
+
git stash drop "$stash_id"
|
|
169
|
+
;;
|
|
170
|
+
*)
|
|
171
|
+
echo "Stash NOT dropped."
|
|
172
|
+
;;
|
|
173
|
+
esac
|
|
174
|
+
}
|