gitbasher 5.2.0 → 5.3.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/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  All notable changes to gitbasher are generated automatically by [semantic-release](https://github.com/semantic-release/semantic-release) from [Conventional Commits](https://www.conventionalcommits.org/) on `main`. The project follows [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## [5.3.0](https://github.com/maxbolgarin/gitbasher/compare/v5.2.1...v5.3.0) (2026-09-30)
6
+
7
+ ### 🚀 Features
8
+
9
+ * **config:** add `gitb cfg fast-split` to toggle split in fast modes ([c56e173](https://github.com/maxbolgarin/gitbasher/commit/c56e1735efeaea8c71f83e5d983cd24ff26fcd2b))
10
+ * **commit:** ask before splitting, add undo split and fast-split setting ([1de1fcd](https://github.com/maxbolgarin/gitbasher/commit/1de1fcd523b5080da0c8248347558a421c8d72bb))
11
+ * **commit:** show undo-split hint in the split type menu ([520e66a](https://github.com/maxbolgarin/gitbasher/commit/520e66aec25b43b7fb83b27f863cc6af4a0ab0e2))
12
+
13
+ ### 🐛 Bug Fixes
14
+
15
+ * **branch:** pass HEAD to --merged when deleting merged branches ([2cdaf58](https://github.com/maxbolgarin/gitbasher/commit/2cdaf58c3a92e9f26eba47bd180b582cb9676d04))
16
+
17
+ ## [5.2.1](https://github.com/maxbolgarin/gitbasher/compare/v5.2.0...v5.2.1) (2026-07-11)
18
+
19
+ ### 🐛 Bug Fixes
20
+
21
+ * **commit:** gate AI split by scope count and stop discarding near-complete groupings ([90becec](https://github.com/maxbolgarin/gitbasher/commit/90becec0f49a7a3e57e57e11a71b6909884ab337))
22
+
5
23
  ## [5.2.0](https://github.com/maxbolgarin/gitbasher/compare/v5.1.0...v5.2.0) (2026-07-07)
6
24
 
7
25
  ### 🚀 Features
package/README.md CHANGED
@@ -360,7 +360,7 @@ gitb commit <combined> # compact form: ff, aifp, fastsp, ...
360
360
  | `staged` | `st` | Use already-staged files (skip the add step) |
361
361
  | `push` | `p` `pu` | Push after the commit succeeds |
362
362
  | `scope` | `s` | Force a scope: `type(scope): message` (useful with fast mode) |
363
- | `no-split` | `nsp` `nsl` | Disable automatic split detection for this commit |
363
+ | `no-split` | `nos` `nsp` `nsl` | Don't offer to split this commit (fast modes split by default) |
364
364
  | `ai` | `i` `llm` | Generate the commit message with AI |
365
365
  | `msg` | `m` | Open `$EDITOR` for a multiline message body |
366
366
  | `ticket` | `t` `j` `jira` | Append ticket info to the header |
@@ -383,6 +383,8 @@ gitb commit <combined> # compact form: ff, aifp, fastsp, ...
383
383
  - Word order doesn't matter: `ai fast push` == `push fast ai` == `aifp`.
384
384
  - Modifiers stack on actions: `ai+fixup`, `fast+amend`, `split+push`, `ai+staged`, …
385
385
  - `fast` and `staged` are mutually exclusive (one stages all, the other uses what's staged).
386
+ - Splitting: a regular commit **asks first** whether to try a split (before any grouping or AI work), then asks again to confirm the resulting groups. Fast modes (`fast`, `ff`, …) split without asking unless you turn it off with `gitb cfg fast-split`.
387
+ - Press `u` at any split prompt to **undo the split**: commits already made by the split are dropped, staging is restored, and you continue with a single commit. `ff` never stops, so use the setting or `no-split` there.
386
388
  - `revert` and `ff` only accept `push` (as `revp` / `ffp`); to rewrite the last commit message use [`gitb edit`](#gitb-edit).
387
389
 
388
390
  ### `gitb edit`
@@ -804,6 +806,7 @@ gitb origin remove # delete the remote
804
806
  | `history` | `hist` | How many recent commits to include in AI prompts |
805
807
  | `diff` | `payload` | AI diff payload size (line and character caps) |
806
808
  | `push-size` | `ps` `pushsize` | Warn before pushing more than N MB (0 disables) |
809
+ | `fast-split` | `fs` `fastsplit` | Split automatically in fast commit modes (on/off) |
807
810
  | `completion` | `comp` | Install / uninstall bash & zsh tab completion |
808
811
  | `delete` | `unset` `del` | Remove global config |
809
812
 
@@ -890,7 +893,8 @@ Overview-first diffs built for the gitbasher workflow — no flag memorization.
890
893
  | `gitbasher.ai-commit-history-limit` | `gitb cfg history` | Recent commits included in AI prompts (default `10`) |
891
894
  | `gitbasher.push-warn-size` | `gitb cfg push-size` | Warn before pushing more than N MB (default `50`, `0` disables) |
892
895
  | `gitbasher.worktreebase` | `git config gitbasher.worktreebase <dir>` | Base directory for new worktrees (default `.worktree` under the repo root) |
893
- | `gitbasher.commit-auto-split` | `git config gitbasher.commit-auto-split <ask\|always\|never>` | Offer to split a commit per scope (default `ask`) |
896
+ | `gitbasher.commit-auto-split` | `git config gitbasher.commit-auto-split <ask\|always\|never>` | Offer to split a commit per scope (default `ask`: ask before splitting, then confirm the groups) |
897
+ | `gitbasher.commit-fast-split` | `gitb cfg fast-split` | Fast modes split automatically (default `true`); `false` makes `fast` ask and `ff` commit as one |
894
898
  | `gitbasher.commit-max-split-groups` | `git config gitbasher.commit-max-split-groups <2..20>` | Cap on split commits per run (default `7`) |
895
899
  | `gitbasher.commit-split-order` | `git config gitbasher.commit-split-order <auto\|alpha>` | Order split commits by dependency (`auto`, default) or alphabetically (`alpha`) |
896
900
  | `gitbasher.commit-ai-grouping` | `git config gitbasher.commit-ai-grouping <never\|auto\|always>` | AI feature grouping for commit splits (default `auto`) |
package/dist/gitb CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  set -o pipefail
3
- GITBASHER_VERSION="5.2.0"
3
+ GITBASHER_VERSION="5.3.0"
4
4
  if [ "$1" == "init" ] || [ "$1" == "i" ]; then
5
5
  git init
6
6
  fi
@@ -948,6 +948,11 @@ function print_configuration {
948
948
  else
949
949
  echo -e "\tPush warn:\t${GREEN}${push_warn} MB${ENDCOLOR} $(config_source_tag gitbasher.push-warn-size)"
950
950
  fi
951
+ if [ "$(get_fast_split)" = "true" ]; then
952
+ echo -e "\tFast split:\t${GREEN}auto${ENDCOLOR} $(config_source_tag gitbasher.commit-fast-split)"
953
+ else
954
+ echo -e "\tFast split:\t${YELLOW}off${ENDCOLOR} $(config_source_tag gitbasher.commit-fast-split)"
955
+ fi
951
956
  echo
952
957
  echo -e "${GRAY}Source:${ENDCOLOR} ${BLUE}(project)${ENDCOLOR}${GRAY} this repo,${ENDCOLOR} ${PURPLE}(global)${ENDCOLOR}${GRAY} ~/.gitconfig,${ENDCOLOR} ${GRAY}(default) built-in fallback${ENDCOLOR}"
953
958
  }
@@ -1204,6 +1209,14 @@ function get_push_warn_size {
1204
1209
  function set_push_warn_size {
1205
1210
  set_config_value gitbasher.push-warn-size "$1"
1206
1211
  }
1212
+ function get_fast_split {
1213
+ local v
1214
+ v=$(get_config_value gitbasher.commit-fast-split "true")
1215
+ if [ "$v" = "false" ]; then echo "false"; else echo "true"; fi
1216
+ }
1217
+ function set_fast_split {
1218
+ set_config_value gitbasher.commit-fast-split "$1"
1219
+ }
1207
1220
  function human_size {
1208
1221
  local bytes="${1:-0}"
1209
1222
  case "$bytes" in ''|*[!0-9]*) bytes=0;; esac
@@ -3691,6 +3704,40 @@ function configure_push_warn_size {
3691
3704
  yes_no_choice "\nSet push size warning globally" "true"
3692
3705
  git config --global gitbasher.push-warn-size "$limit_input"
3693
3706
  }
3707
+ function configure_fast_split {
3708
+ echo -e "${YELLOW}Configure Fast Mode Split${ENDCOLOR}"
3709
+ echo
3710
+ if [ "$(get_fast_split)" = "true" ]; then
3711
+ echo -e "Current: ${GREEN}auto${ENDCOLOR} (fast modes split without asking)"
3712
+ else
3713
+ echo -e "Current: ${YELLOW}off${ENDCOLOR} (fast asks first, ff commits everything as one)"
3714
+ fi
3715
+ echo
3716
+ echo -e "When staged changes span several scopes, fast commit modes (${BLUE}fast${ENDCOLOR}, ${BLUE}ff${ENDCOLOR}, ...)"
3717
+ echo -e "split them into atomic commits without asking. Turn it off to have ${BLUE}fast${ENDCOLOR}"
3718
+ echo -e "ask first (like a regular commit) and ${BLUE}ff${ENDCOLOR} make a single commit."
3719
+ echo
3720
+ echo -e "1. ${GREEN}auto${ENDCOLOR}:\tsplit automatically in fast modes"
3721
+ echo -e "2. ${YELLOW}off${ENDCOLOR}:\tdon't split by default in fast modes"
3722
+ echo -e "0. Exit without changes"
3723
+ local choice value label
3724
+ while true; do
3725
+ read_key choice || exit
3726
+ case "$choice" in
3727
+ 1) value="true"; label="auto"; break ;;
3728
+ 2) value="false"; label="off"; break ;;
3729
+ 0) exit ;;
3730
+ esac
3731
+ done
3732
+ echo
3733
+ set_fast_split "$value" >/dev/null
3734
+ echo -e "${GREEN}✓ Set fast mode split to ${label} for '${project_name}'${ENDCOLOR}"
3735
+ echo
3736
+ [ "$GITBASHER_NO_REPO" = "true" ] && exit
3737
+ echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (Y/n)?"
3738
+ yes_no_choice "\nSet fast mode split globally" "true"
3739
+ set_config_value gitbasher.commit-fast-split "$value" "true" >/dev/null
3740
+ }
3694
3741
  function configure_ai_diff {
3695
3742
  echo -e "${YELLOW}Configure AI Diff Payload Size${ENDCOLOR}"
3696
3743
  echo
@@ -3852,6 +3899,8 @@ function delete_global {
3852
3899
  _delete_global_add "gitbasher.ai-commit-history-limit" "AI commit history limit" "$global_ai_history" "$GREEN"
3853
3900
  local global_push_warn=$(git config --global --get gitbasher.push-warn-size)
3854
3901
  _delete_global_add "gitbasher.push-warn-size" "Push size warning" "${global_push_warn:+${global_push_warn} MB}" "$GREEN"
3902
+ local global_fast_split=$(git config --global --get gitbasher.commit-fast-split)
3903
+ _delete_global_add "gitbasher.commit-fast-split" "Fast mode split" "$global_fast_split" "$GREEN"
3855
3904
  local global_ai_diff_lines=$(git config --global --get gitbasher.ai-diff-limit)
3856
3905
  local global_ai_diff_chars=$(git config --global --get gitbasher.ai-diff-max-chars)
3857
3906
  if [ -n "$global_ai_diff_lines" ] || [ -n "$global_ai_diff_chars" ]; then
@@ -3949,6 +3998,7 @@ function config_script {
3949
3998
  history|hist) set_ai_history_cfg="true";;
3950
3999
  diff|payload) set_ai_diff_cfg="true";;
3951
4000
  push-size|pushsize|ps) set_push_warn_cfg="true";;
4001
+ fast-split|fastsplit|fs) set_fast_split_cfg="true";;
3952
4002
  delete|unset|del) delete_cfg="true";;
3953
4003
  user|name|email|u) set_user_cfg="true";;
3954
4004
  auto|completion|comp) auto_cfg="true";;
@@ -3982,6 +4032,8 @@ function config_script {
3982
4032
  header="$header AI DIFF PAYLOAD"
3983
4033
  elif [ -n "${set_push_warn_cfg}" ]; then
3984
4034
  header="$header PUSH SIZE WARNING"
4035
+ elif [ -n "${set_fast_split_cfg}" ]; then
4036
+ header="$header FAST MODE SPLIT"
3985
4037
  elif [ -n "${delete_cfg}" ]; then
3986
4038
  header="$header UNSET GLOBAL CONFIG"
3987
4039
  elif [ -n "${set_user_cfg}" ]; then
@@ -4055,6 +4107,10 @@ function config_script {
4055
4107
  configure_push_warn_size
4056
4108
  exit
4057
4109
  fi
4110
+ if [ "$set_fast_split_cfg" == "true" ]; then
4111
+ configure_fast_split
4112
+ exit
4113
+ fi
4058
4114
  if [ "$delete_cfg" == "true" ]; then
4059
4115
  delete_global
4060
4116
  exit
@@ -4083,6 +4139,7 @@ function config_script {
4083
4139
  print_help_row $PAD "history" "hist" "Set how many recent commits to include in AI prompts"
4084
4140
  print_help_row $PAD "diff" "payload" "Set the diff payload size (lines and char cap) sent to AI"
4085
4141
  print_help_row $PAD "push-size" "ps, pushsize" "Warn before pushing more than N MB (0 disables)"
4142
+ print_help_row $PAD "fast-split" "fs, fastsplit" "Split automatically in fast commit modes (on/off)"
4086
4143
  print_help_row $PAD "auto" "completion, comp" "Install/remove tab completion for bash, zsh, or fish"
4087
4144
  print_help_row $PAD "delete" "unset, del" "Unset a global gitbasher configuration value"
4088
4145
  print_help_row $PAD "help" "h" "Show this help"
@@ -6972,6 +7029,18 @@ function get_max_split_groups {
6972
7029
  [ "$v" -gt 20 ] && v=20
6973
7030
  printf '%s' "$v"
6974
7031
  }
7032
+ function should_attempt_ai_grouping {
7033
+ local scope_count="$1" mode="$2"
7034
+ case "$mode" in
7035
+ always) return 0 ;;
7036
+ never) return 1 ;;
7037
+ esac
7038
+ case "$scope_count" in
7039
+ ''|*[!0-9]*) return 1 ;;
7040
+ esac
7041
+ [ "$scope_count" -ge 3 ] && return 0
7042
+ return 1
7043
+ }
6975
7044
  #
6976
7045
  #
6977
7046
  #
@@ -7146,6 +7215,7 @@ Read <diff> to decide which files implement the same feature. Output TSV (scope<
7146
7215
  local ai_response
7147
7216
  ai_response=$(call_ai_api "$system_prompt" "$user_prompt" "$AI_MAX_TOKENS_FULL" "" "" "low" 2>/dev/null)
7148
7217
  if [ $? -ne 0 ] || [ -z "$ai_response" ]; then
7218
+ ai_grouping_fail_reason="API error"
7149
7219
  return 1
7150
7220
  fi
7151
7221
  ai_response=$(printf '%s' "$ai_response" | LC_ALL=C sed -e 's/^```[a-zA-Z]*$//' -e 's/^```$//')
@@ -7153,6 +7223,14 @@ Read <diff> to decide which files implement the same feature. Output TSV (scope<
7153
7223
  while IFS= read -r f; do
7154
7224
  [ -n "$f" ] && gset_add staged_set "$f"
7155
7225
  done <<< "$staged_files"
7226
+ gmap_clear folder_scope_of
7227
+ local _fk _ff
7228
+ for _fk in "${split_group_keys[@]}"; do
7229
+ while IFS= read -r _ff; do
7230
+ [ -n "$_ff" ] && gmap_set folder_scope_of "$_ff" "$_fk"
7231
+ done <<< "$(gmap_get split_groups "$_fk")"
7232
+ done
7233
+ gmap_clear assigned_set
7156
7234
  gmap_clear new_groups
7157
7235
  local -a new_keys=()
7158
7236
  local line scope file tab_count
@@ -7174,6 +7252,8 @@ Read <diff> to decide which files implement the same feature. Output TSV (scope<
7174
7252
  fi
7175
7253
  scope=$(printf '%s' "$scope" | LC_ALL=C tr '[:upper:]' '[:lower:]')
7176
7254
  gset_has staged_set "$file" || continue
7255
+ gset_has assigned_set "$file" && continue
7256
+ gset_add assigned_set "$file"
7177
7257
  if ! gmap_has new_groups "$scope"; then
7178
7258
  new_keys+=("$scope")
7179
7259
  gmap_set new_groups "$scope" "$file"
@@ -7181,15 +7261,44 @@ Read <diff> to decide which files implement the same feature. Output TSV (scope<
7181
7261
  gmap_set new_groups "$scope" "$(gmap_get new_groups "$scope")"$'\n'"$file"
7182
7262
  fi
7183
7263
  done <<< "$ai_response"
7184
- local total_assigned=0 total_staged
7264
+ local total_assigned=0 total_staged unmatched
7185
7265
  local key
7186
7266
  for key in "${new_keys[@]}"; do
7187
7267
  total_assigned=$((total_assigned + $(printf '%s\n' "$(gmap_get new_groups "$key")" | grep -c .)))
7188
7268
  done
7189
7269
  total_staged=$(printf '%s\n' "$staged_files" | grep -c .)
7190
- if [ "$total_assigned" -ne "$total_staged" ] || [ ${#new_keys[@]} -lt 1 ]; then
7270
+ if [ ${#new_keys[@]} -lt 1 ] || [ "$total_assigned" -eq 0 ]; then
7271
+ ai_grouping_fail_reason="no usable grouping returned"
7191
7272
  return 1
7192
7273
  fi
7274
+ unmatched=$((total_staged - total_assigned))
7275
+ [ "$unmatched" -lt 0 ] && unmatched=0 # dedup should prevent this; be safe
7276
+ if [ "$unmatched" -gt 0 ] && [ $((unmatched * 5)) -gt "$total_staged" ]; then
7277
+ ai_grouping_fail_reason="${unmatched} of ${total_staged} files unassigned"
7278
+ return 1
7279
+ fi
7280
+ if [ "$unmatched" -gt 0 ]; then
7281
+ local single_group=""
7282
+ [ ${#new_keys[@]} -eq 1 ] && single_group="${new_keys[0]}"
7283
+ local sf leftover_scope
7284
+ while IFS= read -r sf; do
7285
+ [ -z "$sf" ] && continue
7286
+ gset_has assigned_set "$sf" && continue
7287
+ if [ -n "$single_group" ]; then
7288
+ leftover_scope="$single_group"
7289
+ else
7290
+ leftover_scope=$(gmap_get folder_scope_of "$sf")
7291
+ [ -z "$leftover_scope" ] && leftover_scope="misc"
7292
+ fi
7293
+ if ! gmap_has new_groups "$leftover_scope"; then
7294
+ new_keys+=("$leftover_scope")
7295
+ gmap_set new_groups "$leftover_scope" "$sf"
7296
+ else
7297
+ gmap_set new_groups "$leftover_scope" "$(gmap_get new_groups "$leftover_scope")"$'\n'"$sf"
7298
+ fi
7299
+ gset_add assigned_set "$sf"
7300
+ done <<< "$staged_files"
7301
+ fi
7193
7302
  gmap_clear split_groups
7194
7303
  split_group_keys=()
7195
7304
  for key in "${new_keys[@]}"; do
@@ -7243,11 +7352,35 @@ function _restore_split_snapshot {
7243
7352
  done < "$snapshot"
7244
7353
  rm -f "$snapshot"
7245
7354
  }
7355
+ function _undo_commit_split {
7356
+ local start_head="$1"
7357
+ local orig_tree="$2"
7358
+ local snapshot="$3"
7359
+ local made="$4"
7360
+ if [ "${made:-0}" -gt 0 ]; then
7361
+ if [ -n "$start_head" ]; then
7362
+ git reset -q --soft "$start_head" >/dev/null 2>&1
7363
+ else
7364
+ git update-ref -d HEAD >/dev/null 2>&1
7365
+ fi
7366
+ fi
7367
+ if [ -n "$orig_tree" ] && git read-tree "$orig_tree" >/dev/null 2>&1; then
7368
+ rm -f "$snapshot"
7369
+ else
7370
+ _restore_split_snapshot "$snapshot"
7371
+ fi
7372
+ }
7246
7373
  function print_split_type_menu {
7247
7374
  local scope="$1"
7248
7375
  local ai_available="$2"
7376
+ local made="${3:-0}"
7249
7377
  echo -e "${YELLOW}What type of changes for ${BLUE}${BOLD}${scope}${ENDCOLOR}${YELLOW}?${ENDCOLOR}"
7250
7378
  echo -e "Final message will be ${YELLOW}<type>${ENDCOLOR}(${BLUE}<scope>${ENDCOLOR}): ${BLUE}<summary>${ENDCOLOR}"
7379
+ if [ "$made" -gt 0 ]; then
7380
+ echo -e "${GRAY}Changes were split into groups — press ${ENDCOLOR}${BOLD}u${NORMAL}${GRAY} to undo the split (drops the ${made} split commit(s) made so far) and commit everything at once.${ENDCOLOR}"
7381
+ else
7382
+ echo -e "${GRAY}Changes were split into groups — press ${ENDCOLOR}${BOLD}u${NORMAL}${GRAY} to undo the split and commit everything at once.${ENDCOLOR}"
7383
+ fi
7251
7384
  echo -e "1. ${BOLD}feat${ENDCOLOR}:\tnew feature, logic change or performance improvement"
7252
7385
  echo -e "2. ${BOLD}fix${ENDCOLOR}:\t\tsmall changes, eg. bug fix"
7253
7386
  echo -e "3. ${BOLD}refactor${ENDCOLOR}:\tcode change that neither fixes a bug nor adds a feature, style changes"
@@ -7261,6 +7394,7 @@ function print_split_type_menu {
7261
7394
  echo -e "g. ${GREEN}${BOLD}ai${ENDCOLOR}:\t\tgenerate commit message using AI"
7262
7395
  fi
7263
7396
  echo -e "s. ${YELLOW}${BOLD}skip${ENDCOLOR}:\tSkip this group and leave its files unstaged"
7397
+ echo -e "u. ${CYAN}${BOLD}undo${ENDCOLOR}:\tUndo split and commit all changes as a single commit"
7264
7398
  echo -e "0. ${RED}${BOLD}abort${ENDCOLOR}:\tAbort split and restore original staging"
7265
7399
  echo
7266
7400
  }
@@ -7334,7 +7468,7 @@ function run_split_ai_for_scope {
7334
7468
  echo
7335
7469
  echo -e "${GREEN}AI suggestion:${ENDCOLOR} ${BOLD}$ai_msg${ENDCOLOR}"
7336
7470
  echo
7337
- read_key choice "Use it? (y/e to edit/r to regenerate/s to skip group/0 to abort) " || choice="0"
7471
+ read_key choice "Use it? (y/e to edit/r to regenerate/s to skip group/u to undo split/0 to abort) " || choice="0"
7338
7472
  echo
7339
7473
  normalize_key "$choice"
7340
7474
  while [ "$normalized_key" = "r" ]; do
@@ -7354,7 +7488,7 @@ ${ai_msg}"
7354
7488
  echo
7355
7489
  echo -e "${GREEN}AI suggestion:${ENDCOLOR} ${BOLD}$ai_msg${ENDCOLOR}"
7356
7490
  echo
7357
- read_key choice "Use it? (y/e to edit/r to regenerate/s to skip group/0 to abort) " || choice="0"
7491
+ read_key choice "Use it? (y/e to edit/r to regenerate/s to skip group/u to undo split/0 to abort) " || choice="0"
7358
7492
  echo
7359
7493
  normalize_key "$choice"
7360
7494
  done
@@ -7371,6 +7505,8 @@ ${ai_msg}"
7371
7505
  [ -n "$f" ] && git restore --staged -- ":(top,literal)$f" >/dev/null 2>&1
7372
7506
  done <<< "$_files_str"
7373
7507
  return 2
7508
+ elif [ "$normalized_key" = "u" ]; then
7509
+ return 4
7374
7510
  elif [ "$choice" = "0" ]; then
7375
7511
  return 3
7376
7512
  fi
@@ -7384,6 +7520,9 @@ function perform_commit_split {
7384
7520
  return 1
7385
7521
  fi
7386
7522
  _capture_split_statuses
7523
+ local start_head orig_tree
7524
+ start_head=$(git rev-parse -q --verify HEAD 2>/dev/null)
7525
+ orig_tree=$(git write-tree 2>/dev/null)
7387
7526
  local snapshot_file
7388
7527
  snapshot_file=$(mktemp "${TMPDIR:-/tmp}/gitb-split-snapshot.XXXXXX")
7389
7528
  chmod 600 "$snapshot_file" 2>/dev/null || true
@@ -7406,6 +7545,10 @@ function perform_commit_split {
7406
7545
  fi
7407
7546
  local scope files_str msg ai_msg choice scope_for_msg prefix manual_input
7408
7547
  local -a files_array
7548
+ if [ -z "$auto_accept" ] && [ -n "$llm" ] && [ "$ai_ok" = "true" ]; then
7549
+ echo
7550
+ echo -e "${GRAY}Tip: press ${ENDCOLOR}${BOLD}u${NORMAL}${GRAY} at a split prompt to undo the split and make a single commit instead.${ENDCOLOR}"
7551
+ fi
7409
7552
  for scope in "${split_group_keys[@]}"; do
7410
7553
  idx=$((idx + 1))
7411
7554
  echo
@@ -7454,6 +7597,13 @@ function perform_commit_split {
7454
7597
  run_split_ai_for_scope "$scope" "$scope_for_msg" "$files_str"
7455
7598
  case $? in
7456
7599
  2) continue ;;
7600
+ 4)
7601
+ _undo_commit_split "$start_head" "$orig_tree" "$snapshot_file" "$commit_count"
7602
+ trap - INT TERM
7603
+ echo
7604
+ echo -e "${YELLOW}Split undone, continuing with a single commit.${ENDCOLOR}"
7605
+ return 3
7606
+ ;;
7457
7607
  3)
7458
7608
  _restore_split_snapshot "$snapshot_file"
7459
7609
  trap - INT TERM
@@ -7474,11 +7624,11 @@ function perform_commit_split {
7474
7624
  local commit_type=""
7475
7625
  local is_empty_msg=""
7476
7626
  echo
7477
- print_split_type_menu "$scope" "$ai_ok"
7627
+ print_split_type_menu "$scope" "$ai_ok" "$commit_count"
7478
7628
  local tchoice
7479
7629
  while true; do
7480
7630
  read_key tchoice || tchoice="0"
7481
- if ! sanitize_choice_input "$tchoice" "^[0-9sg]$"; then
7631
+ if ! sanitize_choice_input "$tchoice" "^[0-9sgu]$"; then
7482
7632
  continue
7483
7633
  fi
7484
7634
  tchoice="$sanitized_choice"
@@ -7500,6 +7650,13 @@ function perform_commit_split {
7500
7650
  case $? in
7501
7651
  0) break ;;
7502
7652
  2) continue 2 ;;
7653
+ 4)
7654
+ _undo_commit_split "$start_head" "$orig_tree" "$snapshot_file" "$commit_count"
7655
+ trap - INT TERM
7656
+ echo
7657
+ echo -e "${YELLOW}Split undone, continuing with a single commit.${ENDCOLOR}"
7658
+ return 3
7659
+ ;;
7503
7660
  3)
7504
7661
  _restore_split_snapshot "$snapshot_file"
7505
7662
  trap - INT TERM
@@ -7509,10 +7666,17 @@ function perform_commit_split {
7509
7666
  ;;
7510
7667
  *)
7511
7668
  echo
7512
- print_split_type_menu "$scope" "$ai_ok"
7669
+ print_split_type_menu "$scope" "$ai_ok" "$commit_count"
7513
7670
  ;;
7514
7671
  esac
7515
7672
  ;;
7673
+ u)
7674
+ _undo_commit_split "$start_head" "$orig_tree" "$snapshot_file" "$commit_count"
7675
+ trap - INT TERM
7676
+ echo
7677
+ echo -e "${YELLOW}Split undone, continuing with a single commit.${ENDCOLOR}"
7678
+ return 3
7679
+ ;;
7516
7680
  s)
7517
7681
  echo -e "${YELLOW}Skipping scope '${scope}' (files left unstaged)${ENDCOLOR}"
7518
7682
  while IFS= read -r f; do
@@ -7606,18 +7770,35 @@ function try_offer_commit_split {
7606
7770
  local ai_grouping
7607
7771
  ai_grouping=$(get_config_value gitbasher.commit-ai-grouping "auto")
7608
7772
  local should_use_ai="false"
7609
- case "$ai_grouping" in
7610
- never) should_use_ai="false" ;;
7611
- *) should_use_ai="true" ;; # auto + always: attempt AI
7612
- esac
7613
- if [ -z "$llm" ]; then
7614
- should_use_ai="false"
7773
+ if [ -n "$llm" ] && should_attempt_ai_grouping "${#split_group_keys[@]}" "$ai_grouping"; then
7774
+ should_use_ai="true"
7775
+ fi
7776
+ if [ ${#split_group_keys[@]} -lt 2 ] && [ "$should_use_ai" != "true" ]; then
7777
+ if [ -n "$force_split" ]; then
7778
+ echo -e "${YELLOW}Cannot split: all changes look like a single feature.${ENDCOLOR}"
7779
+ fi
7780
+ return 1
7781
+ fi
7782
+ local choice
7783
+ if [ -z "$force_split" ] && [ -z "$auto_yes" ] && [ "$auto_split" != "always" ]; then
7784
+ echo
7785
+ if [ ${#split_group_keys[@]} -ge 2 ]; then
7786
+ echo -e "${YELLOW}Staged changes span ${#split_group_keys[@]} scopes:${ENDCOLOR} ${BLUE}${split_group_keys[*]}${ENDCOLOR}"
7787
+ fi
7788
+ read_key choice "Try to split them into atomic commits? (y/N) " || choice=""
7789
+ echo
7790
+ if [ -z "$choice" ] || ! is_yes "$choice"; then
7791
+ return 1
7792
+ fi
7615
7793
  fi
7616
7794
  if [ "$should_use_ai" = "true" ] && check_ai_available 2>/dev/null; then
7617
7795
  echo
7618
7796
  echo -e "${YELLOW}Grouping changes by feature with AI...${ENDCOLOR}"
7797
+ local ai_grouping_fail_reason=""
7619
7798
  if ! group_files_by_feature_with_ai; then
7620
- echo -e "${YELLOW}AI feature grouping failed, falling back to folder-based grouping.${ENDCOLOR}"
7799
+ local reason_suffix=""
7800
+ [ -n "$ai_grouping_fail_reason" ] && reason_suffix=" (${ai_grouping_fail_reason})"
7801
+ echo -e "${YELLOW}AI feature grouping failed${reason_suffix}, using folder-based grouping.${ENDCOLOR}"
7621
7802
  fi
7622
7803
  consolidate_split_groups "$(get_max_split_groups)"
7623
7804
  fi
@@ -7631,7 +7812,6 @@ function try_offer_commit_split {
7631
7812
  echo
7632
7813
  print_split_groups_preview
7633
7814
  echo
7634
- local choice
7635
7815
  if [ -n "$force_split" ] || [ -n "$auto_yes" ] || [ "$auto_split" = "always" ]; then
7636
7816
  choice="y"
7637
7817
  else
@@ -7641,8 +7821,13 @@ function try_offer_commit_split {
7641
7821
  if [ -z "$choice" ] || ! is_yes "$choice"; then
7642
7822
  return 1
7643
7823
  fi
7644
- perform_commit_split || return 2
7645
- return 0
7824
+ local split_rc=0
7825
+ perform_commit_split || split_rc=$?
7826
+ case "$split_rc" in
7827
+ 0) return 0 ;;
7828
+ 3) return 3 ;;
7829
+ *) return 2 ;;
7830
+ esac
7646
7831
  }
7647
7832
  function handle_ai_commit_generation {
7648
7833
  local step="$1"
@@ -8071,7 +8256,7 @@ function commit_script {
8071
8256
  print_help_row $FPAD "staged" "st" "Use already-staged files (skip the add step)"
8072
8257
  print_help_row $FPAD "push" "p, pu" "Push after the commit succeeds"
8073
8258
  print_help_row $FPAD "scope" "s" "Force a scope: 'type(scope): message' (useful with fast mode)"
8074
- print_help_row $FPAD "no-split" "nos, nsp, nsl" "Disable automatic split detection for this commit"
8259
+ print_help_row $FPAD "no-split" "nos, nsp, nsl" "Don't offer to split this commit (fast modes split by default)"
8075
8260
  print_help_row $FPAD "ai" "i, llm" "Generate the commit message with AI"
8076
8261
  print_help_row $FPAD "msg" "m" "Open \$EDITOR for a multiline message body"
8077
8262
  print_help_row $FPAD "ticket" "t, j, jira" "Append ticket info to the header"
@@ -8090,6 +8275,8 @@ function commit_script {
8090
8275
  echo -e " ${BLUE}•${ENDCOLOR} Word order doesn't matter: ${GREEN}ai fast push${ENDCOLOR} == ${GREEN}push fast ai${ENDCOLOR} == ${GREEN}aifp${ENDCOLOR}"
8091
8276
  echo -e " ${BLUE}•${ENDCOLOR} Modifiers stack on actions: ${GREEN}ai+fixup${ENDCOLOR}, ${GREEN}fast+amend${ENDCOLOR}, ${GREEN}split+push${ENDCOLOR}, ${GREEN}ai+staged${ENDCOLOR}, ..."
8092
8277
  echo -e " ${BLUE}•${ENDCOLOR} ${BOLD}fast${NORMAL} and ${BOLD}staged${NORMAL} are mutually exclusive (one stages all, the other uses what's staged)"
8278
+ echo -e " ${BLUE}•${ENDCOLOR} Regular commits ask before splitting; fast modes split by default (toggle: ${GREEN}gitb cfg fast-split${ENDCOLOR})"
8279
+ echo -e " ${BLUE}•${ENDCOLOR} Press ${BOLD}u${NORMAL} at a split prompt to undo the split and commit everything as one"
8093
8280
  echo -e " ${BLUE}•${ENDCOLOR} ${BOLD}revert${NORMAL} and ${BOLD}ff${NORMAL} only accept ${BOLD}push${NORMAL} (as ${BOLD}revp${NORMAL}/${BOLD}ffp${NORMAL}); to rewrite the last message use ${GREEN}gitb edit${ENDCOLOR}"
8094
8281
  git config --unset gitbasher.cached-git-add 2>/dev/null || true
8095
8282
  exit 0
@@ -8244,14 +8431,19 @@ function commit_script {
8244
8431
  else
8245
8432
  staged_files_list="$(sed 's/^/\t/' <<< "$(git -c core.quotePath=false diff --no-renames --name-only --cached)")"
8246
8433
  fi
8247
- if [ -z "${amend}" ] && [ -z "${fixup}" ] && [ -z "${msg}" ] && [ -z "${ticket}" ] && [ -z "${no_split}" ]; then
8248
- _split_auto_yes=""
8249
- if [ -n "$fast" ] || [ -n "$auto_accept" ]; then
8434
+ _split_skip="${no_split}"
8435
+ _split_auto_yes=""
8436
+ if [ -z "$split" ] && { [ -n "$fast" ] || [ -n "$auto_accept" ]; }; then
8437
+ if [ "$(get_fast_split)" = "false" ]; then
8438
+ [ -n "$auto_accept" ] && _split_skip="true"
8439
+ else
8250
8440
  _split_auto_yes="true"
8251
8441
  fi
8442
+ fi
8443
+ if [ -z "${amend}" ] && [ -z "${fixup}" ] && [ -z "${msg}" ] && [ -z "${ticket}" ] && [ -z "${_split_skip}" ]; then
8252
8444
  try_offer_commit_split "$split" "$_split_auto_yes"
8253
8445
  _split_rc=$?
8254
- if [ -n "$split" ]; then
8446
+ if [ -n "$split" ] && [ "$_split_rc" -ne 3 ]; then
8255
8447
  if [ "$_split_rc" -ge 2 ]; then
8256
8448
  exit 1
8257
8449
  fi
@@ -9123,7 +9315,7 @@ function branch_script {
9123
9315
  "$main_branch"|"$current_branch"|master|main|develop) continue ;;
9124
9316
  esac
9125
9317
  merged_to_delete+=("$_mb")
9126
- done < <(git branch --merged --format='%(refname:short)')
9318
+ done < <(git branch --merged HEAD --format='%(refname:short)')
9127
9319
  for branch_to_delete in "${merged_to_delete[@]}"; do
9128
9320
  delete_output=$(git branch -d "$branch_to_delete" 2>&1)
9129
9321
  delete_code=$?
@@ -15428,7 +15620,7 @@ _gitb_sub_sync="push p merge m mergep mp pm dry d dr help h"
15428
15620
  _gitb_sub_wip="up u down d help h"
15429
15621
  _gitb_sub_branch="list l remote r re main def m new n c newd nd cd delete del d prev p - recent rc gone g tag t help h"
15430
15622
  _gitb_sub_tag="annotated a an commit c co cm all al push ps ph p push-all pa delete del d delete-all da list log l remote re r fetch help h"
15431
- _gitb_sub_config="default def d b main separator sep editor ed e ticket jira ti t scopes scope sc s ai llm key provider prov model m proxy prx p history hist diff payload push-size pushsize ps delete unset del user name email u auto completion comp help h"
15623
+ _gitb_sub_config="default def d b main separator sep editor ed e ticket jira ti t scopes scope sc s ai llm key provider prov model m proxy prx p history hist diff payload push-size pushsize ps fast-split fastsplit fs delete unset del user name email u auto completion comp help h"
15432
15624
  _gitb_sub_undo="commit c amend a merge m rebase r stash s help h"
15433
15625
  _gitb_sub_reset="soft s undo u interactive i ref r help h"
15434
15626
  _gitb_sub_stash="select sel all list l pop p show s drop d apply a help h"
@@ -15777,7 +15969,7 @@ _gitb() {
15777
15969
  _values 'tag mode' 'annotated[create annotated tag]' 'commit[tag a commit]' 'all[tag everything]' 'push[push tag]' 'push-all[push all tags]' 'delete[delete tag]' 'delete-all[delete all]' 'list[list tags]' 'remote[remote tags]' 'help[show help]'
15778
15970
  ;;
15779
15971
  config|cf|cfg|conf)
15780
- _values 'config key' 'default[default branch]' 'separator[separator char]' 'editor[editor]' 'ticket[ticket prefix]' 'scopes[scope list]' 'ai[AI setup wizard]' 'key[AI API key]' 'provider[AI provider]' 'model[AI model]' 'proxy[AI proxy]' 'history[AI history]' 'diff[AI diff payload]' 'push-size[warn on large pushes]' 'delete[unset a key]' 'user[user name/email]' 'auto[manage tab completion]' 'help[show help]'
15972
+ _values 'config key' 'default[default branch]' 'separator[separator char]' 'editor[editor]' 'ticket[ticket prefix]' 'scopes[scope list]' 'ai[AI setup wizard]' 'key[AI API key]' 'provider[AI provider]' 'model[AI model]' 'proxy[AI proxy]' 'history[AI history]' 'diff[AI diff payload]' 'push-size[warn on large pushes]' 'fast-split[split in fast commit modes]' 'delete[unset a key]' 'user[user name/email]' 'auto[manage tab completion]' 'help[show help]'
15781
15973
  ;;
15782
15974
  undo|un)
15783
15975
  _values 'undo target' 'commit[undo last commit]' 'amend[undo amend]' 'merge[undo merge]' 'rebase[undo rebase]' 'stash[undo stash]' 'help[show help]'
@@ -15979,7 +16171,7 @@ complete -c gitb -n "$__gitb_branch" -a "list remote main new newd delete prev r
15979
16171
  set -l __gitb_tag "__gitb_using_cmd tag t tg; and __gitb_at_position 2"
15980
16172
  complete -c gitb -n "$__gitb_tag" -a "annotated commit all push push-all delete delete-all list remote help"
15981
16173
  set -l __gitb_config "__gitb_using_cmd config cf cfg conf; and __gitb_at_position 2"
15982
- complete -c gitb -n "$__gitb_config" -a "default separator editor ticket scopes ai key provider model proxy history diff push-size delete user auto help"
16174
+ complete -c gitb -n "$__gitb_config" -a "default separator editor ticket scopes ai key provider model proxy history diff push-size fast-split delete user auto help"
15983
16175
  set -l __gitb_undo "__gitb_using_cmd undo un; and __gitb_at_position 2"
15984
16176
  complete -c gitb -n "$__gitb_undo" -a "commit amend merge rebase stash help"
15985
16177
  set -l __gitb_reset "__gitb_using_cmd reset res; and __gitb_at_position 2"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitbasher",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "Simple bash utility that makes git easy to use",
5
5
  "keywords": [
6
6
  "git",