gitbash 1.9.1 → 2.0.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.
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env bash
2
- # shellcheck disable=SC2155
3
2
 
4
3
  # Source common utilities
5
- SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
4
+ SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
5
  # shellcheck source=_utils.sh
7
6
  source "$SOURCE_DIR/_utils.sh"
8
7
 
@@ -62,7 +61,7 @@ Examples:
62
61
  ✖ Abort
63
62
 
64
63
  Applying stash@{0} ...
65
- Drop stash@{0} now? (y/N): y
64
+ Drop stash@{0} now? (Y/n): y
66
65
 
67
66
  ---
68
67
 
@@ -91,8 +90,7 @@ Actions:
91
90
  🗑️ Clean up - Delete stashes without applying (supports multi-select)
92
91
 
93
92
  Requirements:
94
- - fzf (fuzzy finder) - will prompt to install if not found
95
- - stash, unstash and cleanstash functions must be available
93
+ - fzf (fuzzy finder)
96
94
 
97
95
  See also:
98
96
  stash -h Show help for stash command
@@ -130,20 +128,19 @@ EOF
130
128
  # 3. Run fzf menu
131
129
  # -----------------------------
132
130
  local selection
133
- selection=$(fzf --prompt="Stash action > " \
131
+ selection=$(run_fzf --prompt="Stash action > " \
134
132
  -i \
135
133
  --reverse \
136
134
  --border \
137
135
  --header="What would you like to do?" \
138
136
  --no-multi \
139
- --bind=enter:accept \
140
137
  <<< "$choices"
141
- ) </dev/tty || true
138
+ ) || true
142
139
 
143
140
  # ESC or Ctrl-C
144
141
  if [[ -z "$selection" ]]; then
145
142
  echo "Aborted."
146
- return 1
143
+ return 0
147
144
  fi
148
145
 
149
146
  # -----------------------------
@@ -151,17 +148,17 @@ EOF
151
148
  # -----------------------------
152
149
  case "$selection" in
153
150
  "$stash_option")
154
- gitbash stash
151
+ gb_run stash
155
152
  ;;
156
153
  "$unstash_option")
157
- gitbash unstash
154
+ gb_run unstash
158
155
  ;;
159
156
  "$cleanup_option")
160
- gitbash cleanstash
157
+ gb_run cleanstash
161
158
  ;;
162
159
  "$abort_label")
163
160
  echo "Aborted."
164
- return 1
161
+ return 0
165
162
  ;;
166
163
  *)
167
164
  echo "Unknown selection."
@@ -1,16 +1,45 @@
1
1
  #!/usr/bin/env bash
2
- # shellcheck disable=SC2155
3
2
 
4
3
  # Source common utilities
5
- SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
4
+ SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
5
  # shellcheck source=_utils.sh
7
6
  source "$SOURCE_DIR/_utils.sh"
8
7
 
8
+ # Print one line per changed file: "<display>\t<XY>\t<path>\t<original path>"
9
+ # The path fields are exact (spaces, quotes and renames are handled).
10
+ _status_list() {
11
+ local entry xy path orig label display
12
+ while IFS= read -r -d '' entry; do
13
+ xy="${entry:0:2}"
14
+ path="${entry:3}"
15
+ orig=""
16
+ if [[ "${xy:0:1}" == [RC] || "${xy:1:1}" == [RC] ]]; then
17
+ IFS= read -r -d '' orig
18
+ fi
19
+ case "$xy" in
20
+ "??") label="UNTRACKED" ;;
21
+ DD|AU|UD|UA|DU|AA|UU) label="CONFLICT" ;;
22
+ " "?) label="UNSTAGED" ;;
23
+ ?" ") label="STAGED" ;;
24
+ *) label="PARTIAL" ;;
25
+ esac
26
+ display="$path"
27
+ [[ -n "$orig" ]] && display="$orig -> $path"
28
+ printf '%-12s %s %s\t%s\t%s\t%s\n' "[$label]" "$xy" "$display" "$xy" "$path" "$orig"
29
+ done < <(git status --porcelain=v1 -z 2>/dev/null)
30
+ }
31
+
32
+ # Unstage a file (works before the first commit, too)
33
+ _status_unstage() {
34
+ if git rev-parse --verify --quiet HEAD >/dev/null; then
35
+ git restore --staged -- "$@"
36
+ else
37
+ git rm --cached --quiet -r -- "$@"
38
+ fi
39
+ }
40
+
9
41
  # Show git status with fzf file selector and diff preview
10
42
  status() {
11
- # -----------------------------
12
- # 0. Check for help/version flag
13
- # -----------------------------
14
43
  if [[ "${1:-}" == "-v" || "${1:-}" == "--version" ]]; then
15
44
  echo "gitbash ${FUNCNAME[0]} v$VERSION"
16
45
  return 0
@@ -19,296 +48,214 @@ status() {
19
48
  cat << 'EOF'
20
49
  Usage: status [OPTIONS]
21
50
 
22
- Interactive git status viewer with fzf.
23
- Browse uncommitted changes and view diffs in preview pane.
51
+ Interactive git status: stage, unstage, discard and commit with diff previews.
24
52
 
25
53
  Options:
26
54
  -h, --help Show this help message
27
55
 
28
- Features:
29
- - Lists all modified, staged, and untracked files
30
- - Preview shows git diff for each file
31
- - Color-coded file status indicators
32
- - Supports both staged and unstaged changes
33
- - Revert changes to discard modifications
56
+ Labels:
57
+ [STAGED] All changes of the file are staged
58
+ [PARTIAL] Some changes staged, some not
59
+ [UNSTAGED] Changes not staged yet
60
+ [UNTRACKED] New file or directory
61
+ [CONFLICT] Merge conflict - resolve it in your editor
34
62
 
35
- File Status Indicators:
36
- M Modified (unstaged)
37
- A Added (staged)
38
- D Deleted
39
- R Renamed
40
- ?? Untracked
41
-
42
- Navigation:
43
- ↑/↓ or j/k Navigate through files
63
+ Keys:
44
64
  TAB Select/deselect file (multi-select)
45
- Enter Toggle staging for selected file(s)
46
- Ctrl-R Revert changes (discard modifications)
65
+ Enter Stage the selected files (unstage them if fully staged)
66
+ Ctrl-R Discard changes of the selected files (asks first)
67
+ Ctrl-O Commit the staged changes (then asks whether to push)
47
68
  ESC/Ctrl-C Exit
48
69
 
49
- Examples:
50
- $ status
51
- Git Status >
52
- > M src/app.js
53
- A src/new-feature.js
54
- ?? temp.txt
55
- M README.md
56
-
57
- # Preview shows diff of selected file
58
- # Navigate with arrow keys or j/k
59
-
60
70
  Requirements:
61
71
  - Must be in a git repository
62
- - fzf (fuzzy finder) - will prompt to install if not found
63
-
64
- Notes:
65
- - Staged changes show diff with --cached
66
- - Unstaged changes show working tree diff
67
- - Untracked files show file contents
68
- - Empty status means working tree is clean
72
+ - fzf
69
73
 
70
74
  EOF
71
75
  return 0
72
76
  fi
73
77
 
74
- # -----------------------------
75
- # 1. Check prerequisites
76
- # -----------------------------
77
78
  require_git_repo || return 1
78
79
  require_fzf || return 1
79
80
 
80
- # -----------------------------
81
- # 2. Get git status and format with staging info
82
- # -----------------------------
83
- local status_output
84
- status_output=$(git status --short | awk '{
85
- index_status = substr($0, 1, 1);
86
- work_status = substr($0, 2, 1);
87
- file = substr($0, 4);
88
-
89
- # Determine staging status
90
- if (index_status ~ /[MADRC]/) {
91
- staged = "[STAGED]";
92
- } else if (work_status == "?" || index_status == "?") {
93
- staged = "[UNTRACKED]";
94
- } else {
95
- staged = "[UNSTAGED]";
96
- }
97
-
98
- printf "%-12s %s %s\n", staged, substr($0, 1, 2), file;
99
- }')
81
+ # Commands on file paths must run from the repository root
82
+ local repo_root
83
+ repo_root=$(git rev-parse --show-toplevel) || return 1
84
+ cd "$repo_root" || return 1
100
85
 
101
- if [[ -z "$status_output" ]]; then
102
- echo "Working tree clean - no changes to display."
103
- return 0
104
- fi
86
+ local pager bat_cmd preview
87
+ pager=$(gb_diff_pager)
88
+ bat_cmd=$(gb_bat_cmd)
89
+ preview='
90
+ xy=$(printf "%s" {} | cut -f2); file=$(printf "%s" {} | cut -f3)
91
+ if [[ "$xy" == "??" ]]; then
92
+ echo "=== UNTRACKED ==="; echo
93
+ if [[ -d "$file" ]]; then ls -la "$file"; else __BAT__ "$file" 2>/dev/null || echo "Cannot preview file"; fi
94
+ else
95
+ staged=$(git diff --cached --color=always -- "$file" 2>/dev/null)
96
+ unstaged=$(git diff --color=always -- "$file" 2>/dev/null)
97
+ if [[ -n "$staged" ]]; then echo "=== STAGED CHANGES ==="; echo; printf "%s\n" "$staged" | __PAGER__; fi
98
+ if [[ -n "$unstaged" ]]; then
99
+ [[ -n "$staged" ]] && echo
100
+ echo "=== UNSTAGED CHANGES ==="; echo; printf "%s\n" "$unstaged" | __PAGER__
101
+ fi
102
+ fi'
103
+ preview="${preview//__PAGER__/$pager}"
104
+ preview="${preview//__BAT__/$bat_cmd}"
105
105
 
106
- # -----------------------------
107
- # 3. Run fzf with diff preview in a loop
108
- # -----------------------------
106
+ local status_list
109
107
  while true; do
110
- local selected_output
111
- selected_output=$(
112
- fzf \
108
+ status_list=$(_status_list)
109
+ if [[ -z "$status_list" ]]; then
110
+ print_success "Working tree clean."
111
+ return 0
112
+ fi
113
+
114
+ local output
115
+ output=$(run_fzf \
113
116
  --height=100% \
114
117
  -i \
115
118
  --reverse \
116
119
  --border \
117
120
  --prompt="Git Status > " \
118
- --header="[Enter] toggle stage | [Ctrl-r] revert | [TAB] multi-select | [ESC] exit" \
121
+ --header="[Enter] stage/unstage | [Ctrl-R] discard | [Ctrl-O] commit staged | [TAB] multi-select | [ESC] exit" \
119
122
  --multi \
120
- --ansi \
121
- --expect=ctrl-r \
122
- --preview='
123
- file=$(echo {} | awk "{print \$NF}");
124
- file_status=$(echo {} | awk "{print \$2}");
125
-
126
- # Show diff based on file status
127
- if [[ "$file_status" == "??" ]]; then
128
- # Untracked file - show contents
129
- echo "=== UNTRACKED FILE ===";
130
- echo;
131
- bat --color=always --style=numbers --theme=\"GitHub\" "$file" 2>/dev/null || cat "$file" 2>/dev/null || echo "Cannot preview file";
132
- elif [[ "$file_status" =~ ^[MAC] ]]; then
133
- # Staged file - show cached diff
134
- staged_diff=$(git diff --cached --color=always "$file" 2>/dev/null);
135
- unstaged_diff=$(git diff --color=always "$file" 2>/dev/null);
136
-
137
- # Only show staged section if there are staged changes
138
- if [[ -n "$staged_diff" ]]; then
139
- echo "=== STAGED CHANGES ===";
140
- echo;
141
- echo "$staged_diff" | delta --light 2>/dev/null || echo "$staged_diff";
142
- fi
143
-
144
- # Only show unstaged section if there are unstaged changes
145
- if [[ -n "$unstaged_diff" ]]; then
146
- [[ -n "$staged_diff" ]] && echo;
147
- echo "=== UNSTAGED CHANGES ===";
148
- echo;
149
- echo "$unstaged_diff" | delta --light 2>/dev/null || echo "$unstaged_diff";
150
- fi
151
- else
152
- # Unstaged changes
153
- unstaged_diff=$(git diff --color=always "$file" 2>/dev/null);
154
- if [[ -n "$unstaged_diff" ]]; then
155
- echo "=== UNSTAGED CHANGES ===";
156
- echo;
157
- echo "$unstaged_diff" | delta --light 2>/dev/null || echo "$unstaged_diff";
158
- fi
159
- fi
160
- ' \
123
+ --delimiter=$'\t' \
124
+ --with-nth=1 \
125
+ --expect=ctrl-r,ctrl-o \
126
+ --preview="$preview" \
161
127
  --preview-window=right:60% \
162
- <<< "$status_output"
163
- ) </dev/tty || true
128
+ <<< "$status_list"
129
+ ) || true
164
130
 
165
- # Parse the key pressed and the selected files
166
131
  local key_pressed
167
- key_pressed=$(echo "$selected_output" | head -1)
168
-
169
- # Get all selected files (skip first line which is the key)
170
- local selected_files=()
132
+ key_pressed=$(printf '%s\n' "$output" | head -n 1)
133
+ local selected=() line
171
134
  while IFS= read -r line; do
172
- [[ -n "$line" ]] && selected_files+=("$line")
173
- done <<< "$(echo "$selected_output" | tail -n +2)"
135
+ [[ -n "$line" ]] && selected+=("$line")
136
+ done < <(printf '%s\n' "$output" | tail -n +2)
174
137
 
175
- # Exit if no selection (ESC or Ctrl-C)
176
- if [[ ${#selected_files[@]} -eq 0 ]]; then
177
- echo "Exited status viewer."
178
- break
138
+ if [[ -z "$output" ]]; then
139
+ return 0
179
140
  fi
180
141
 
181
- # -----------------------------
182
- # 4. Handle action based on key pressed
183
- # -----------------------------
184
- if [[ "$key_pressed" == "ctrl-r" ]]; then
185
- # Revert changes for all selected files
186
- local files_to_revert=()
187
- local untracked_to_delete=()
188
-
189
- for selected_file in "${selected_files[@]}"; do
190
- local staging_status=$(echo "$selected_file" | awk '{print $1}')
191
- local filename=$(echo "$selected_file" | awk '{print $NF}')
192
-
193
- if [[ "$staging_status" == "[UNTRACKED]" ]]; then
194
- untracked_to_delete+=("$filename")
142
+ case "$key_pressed" in
143
+ ctrl-o)
144
+ if git diff --cached --quiet 2>/dev/null; then
145
+ print_warning "Nothing staged yet. Stage files with Enter first."
146
+ continue
147
+ fi
148
+ if gb_run commit --staged; then
149
+ if gb_confirm "Push to the remote?" n; then
150
+ gb_sync_and_push
151
+ fi
152
+ fi
153
+ continue
154
+ ;;
155
+ ctrl-r)
156
+ [[ ${#selected[@]} -eq 0 ]] && continue
157
+ _status_discard "${selected[@]}"
158
+ continue
159
+ ;;
160
+ esac
161
+
162
+ [[ ${#selected[@]} -eq 0 ]] && return 0
163
+
164
+ # Enter: stage, or unstage fully staged files
165
+ local xy path orig
166
+ for line in "${selected[@]}"; do
167
+ xy=$(printf '%s' "$line" | cut -f2)
168
+ path=$(printf '%s' "$line" | cut -f3)
169
+ orig=$(printf '%s' "$line" | cut -f4)
170
+ case "$xy" in
171
+ DD|AU|UD|UA|DU|AA|UU)
172
+ print_warning "$path has a merge conflict - resolve it in your editor, then stage it."
173
+ ;;
174
+ ?" ")
175
+ echo "Unstaging: $path"
176
+ if [[ -n "$orig" ]]; then
177
+ _status_unstage "$path" "$orig" || print_error "Failed to unstage $path"
178
+ else
179
+ _status_unstage "$path" || print_error "Failed to unstage $path"
180
+ fi
181
+ ;;
182
+ *)
183
+ echo "Staging: $path"
184
+ git add -A -- "$path" || print_error "Failed to stage $path"
185
+ ;;
186
+ esac
187
+ done
188
+ done
189
+ }
190
+
191
+ # Discard changes of the selected status lines (asks first)
192
+ _status_discard() {
193
+ local line xy path orig
194
+ local untracked=() added=() tracked=() tracked_orig=()
195
+ for line in "$@"; do
196
+ xy=$(printf '%s' "$line" | cut -f2)
197
+ path=$(printf '%s' "$line" | cut -f3)
198
+ orig=$(printf '%s' "$line" | cut -f4)
199
+ case "$xy" in
200
+ "??") untracked+=("$path") ;;
201
+ A?) added+=("$path") ;;
202
+ *) tracked+=("$path"); [[ -n "$orig" ]] && tracked_orig+=("$orig") ;;
203
+ esac
204
+ done
205
+
206
+ if [[ ${#untracked[@]} -gt 0 ]]; then
207
+ echo "Untracked files/directories to delete:"
208
+ printf ' - %s\n' "${untracked[@]}"
209
+ if gb_confirm --strict "Delete these ${#untracked[@]} item(s)? This cannot be undone." n; then
210
+ for path in "${untracked[@]}"; do
211
+ if rm -rf -- "$path"; then
212
+ print_success "Deleted: $path"
195
213
  else
196
- files_to_revert+=("$filename")
214
+ print_error "Failed to delete: $path"
197
215
  fi
198
216
  done
199
-
200
- # Handle untracked files
201
- if [[ ${#untracked_to_delete[@]} -gt 0 ]]; then
202
- echo "Untracked files to delete:"
203
- for f in "${untracked_to_delete[@]}"; do
204
- echo " - $f"
205
- done
206
- prompt_read "Delete these ${#untracked_to_delete[@]} untracked file(s)? (y/N): " confirm
207
- case "$confirm" in
208
- [yY][eE][sS]|[yY])
209
- for f in "${untracked_to_delete[@]}"; do
210
- rm "$f"
211
- echo "✓ Deleted: $f"
212
- done
213
- ;;
214
- *)
215
- echo "Skipped deletion."
216
- ;;
217
- esac
218
- fi
219
-
220
- # Handle tracked files
221
- if [[ ${#files_to_revert[@]} -gt 0 ]]; then
222
- echo "Files to revert:"
223
- for f in "${files_to_revert[@]}"; do
224
- echo " - $f"
225
- done
226
- prompt_read "Revert changes in these ${#files_to_revert[@]} file(s)? This cannot be undone. (y/N): " confirm
227
- case "$confirm" in
228
- [yY][eE][sS]|[yY])
229
- for f in "${files_to_revert[@]}"; do
230
- git reset HEAD "$f" 2>/dev/null
231
- git checkout -- "$f"
232
- echo "✓ Reverted: $f"
233
- done
234
- ;;
235
- *)
236
- echo "Skipped revert."
237
- ;;
238
- esac
239
- fi
240
- else
241
- # Toggle staging for all selected files
242
- local did_stage=false
243
- for selected_file in "${selected_files[@]}"; do
244
- local staging_status=$(echo "$selected_file" | awk '{print $1}')
245
- local filename=$(echo "$selected_file" | awk '{print $NF}')
217
+ fi
218
+ fi
246
219
 
247
- if [[ "$staging_status" == "[STAGED]" ]]; then
248
- echo "Unstaging: $filename"
249
- git reset HEAD "$filename"
250
- elif [[ "$staging_status" == "[UNSTAGED]" ]]; then
251
- echo "Staging: $filename"
252
- git add "$filename"
253
- did_stage=true
254
- elif [[ "$staging_status" == "[UNTRACKED]" ]]; then
255
- echo "Adding: $filename"
256
- git add "$filename"
257
- did_stage=true
220
+ if [[ ${#added[@]} -gt 0 ]]; then
221
+ echo "Newly added files to unstage:"
222
+ printf ' - %s\n' "${added[@]}"
223
+ if gb_confirm --strict "Unstage these ${#added[@]} file(s)?" n; then
224
+ local delete_too=false
225
+ gb_confirm --strict "Also delete them from disk?" n && delete_too=true
226
+ for path in "${added[@]}"; do
227
+ if ! git rm --cached --quiet -r -- "$path"; then
228
+ print_error "Failed to unstage: $path"
229
+ continue
230
+ fi
231
+ if [[ "$delete_too" == true ]]; then
232
+ rm -rf -- "$path" && print_success "Deleted: $path"
233
+ else
234
+ print_success "Unstaged (file kept): $path"
258
235
  fi
259
236
  done
260
237
  fi
238
+ fi
261
239
 
262
- # Refresh status output
263
- status_output=$(git status --short | awk '{
264
- index_status = substr($0, 1, 1);
265
- work_status = substr($0, 2, 1);
266
- file = substr($0, 4);
267
-
268
- # Determine staging status
269
- if (index_status ~ /[MADRC]/) {
270
- staged = "[STAGED]";
271
- } else if (work_status == "?" || index_status == "?") {
272
- staged = "[UNTRACKED]";
273
- } else {
274
- staged = "[UNSTAGED]";
275
- }
276
-
277
- printf "%-12s %s %s\n", staged, substr($0, 1, 2), file;
278
- }')
279
-
280
- # Check if working tree is now clean
281
- if [[ -z "$status_output" ]]; then
282
- echo "✓ Working tree is now clean - all changes staged or reverted."
283
- break
240
+ if [[ ${#tracked[@]} -gt 0 ]]; then
241
+ if ! git rev-parse --verify --quiet HEAD >/dev/null; then
242
+ print_warning "No commits yet - nothing to restore tracked files from."
243
+ return 0
284
244
  fi
285
-
286
- # Only ask for commit if we just staged something (not unstaged)
287
- if [[ "$did_stage" == true ]] && echo "$status_output" | grep -q '\[STAGED\]'; then
288
- echo
289
- echo "Staged files:"
290
-
291
- # Get list of staged files and display in tree-like structure
292
- git diff --cached --name-only | while IFS= read -r file; do
293
- # Get the directory and filename
294
- dir=$(dirname "$file")
295
- base=$(basename "$file")
296
-
297
- # Simple tree-like output with indentation
298
- if [[ "$dir" == "." ]]; then
299
- echo " └─ $base"
245
+ echo "Files to restore to the last commit:"
246
+ printf ' - %s\n' "${tracked[@]}"
247
+ if gb_confirm --strict "Discard all changes in these ${#tracked[@]} file(s)? This cannot be undone." n; then
248
+ for path in "${tracked[@]}"; do
249
+ if git restore --source=HEAD --staged --worktree -- "$path" 2>/dev/null ||
250
+ git restore --staged -- "$path" 2>/dev/null; then
251
+ print_success "Restored: $path"
300
252
  else
301
- echo " └─ $file"
253
+ print_error "Failed to restore: $path"
302
254
  fi
303
255
  done
304
-
305
- echo
306
- echo "Ready to commit and push staged changes."
307
- echo "Press Ctrl+C to abort."
308
- echo
309
- # Directly prompt for commit message (user can Ctrl+C to abort)
310
- gitbash commit -s -p
311
- break
256
+ if [[ ${#tracked_orig[@]} -gt 0 ]]; then
257
+ git restore --source=HEAD --staged --worktree -- "${tracked_orig[@]}" 2>/dev/null
258
+ fi
312
259
  fi
313
- done
260
+ fi
314
261
  }