gitbasher 4.0.0 → 4.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.
Files changed (4) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +40 -3
  3. package/dist/gitb +637 -111
  4. package/package.json +1 -1
package/dist/gitb CHANGED
@@ -1,13 +1,28 @@
1
1
  #!/usr/bin/env bash
2
2
  set -o pipefail
3
- GITBASHER_VERSION="4.0.0"
3
+ GITBASHER_VERSION="4.1.0"
4
4
  if [ "$1" == "init" ] || [ "$1" == "i" ]; then
5
5
  git init
6
6
  fi
7
+ GITBASHER_NO_REPO=""
7
8
  git_check=$(git branch --show-current 2>&1)
8
9
  if [[ "$git_check" == *"fatal: not a git repository"* ]]; then
9
- echo "You can use gitb only in a git repository"
10
- exit
10
+ case "${1:-}" in
11
+ ""|help|man|--help|-h|version|--version|-v|\
12
+ config|cf|cfg|conf|\
13
+ update|up|upd|\
14
+ uninstall|uns|uni|\
15
+ clone|cl|clo|\
16
+ init|i)
17
+ GITBASHER_NO_REPO="true"
18
+ export GITBASHER_NO_REPO
19
+ ;;
20
+ *)
21
+ echo "You can use 'gitb $1' only in a git repository."
22
+ echo "Run 'gitb help' to see commands that work anywhere (config, update, uninstall, clone)."
23
+ exit 1
24
+ ;;
25
+ esac
11
26
  fi
12
27
  if ((BASH_VERSINFO[0] < 4)); then
13
28
  _gitb_candidate=$(command -v bash 2>/dev/null)
@@ -27,11 +42,15 @@ if ((BASH_VERSINFO[0] < 4)); then
27
42
  unset _gitb_candidate
28
43
  printf "Sorry, you need at least bash-4.0 to run gitbasher.\n\n"
29
44
  printf "Linux (Debian/Ubuntu):\n sudo apt update && sudo apt install --only-upgrade bash\n\n"
30
- printf "macOS:\n 1) Install Homebrew (if missing):\n /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n 2) Install newer bash:\n brew install bash\n 3) Optional: make it your default shell (then restart Terminal):\n sudo sh -c 'echo /opt/homebrew/bin/bash >> /etc/shells' && chsh -s /opt/homebrew/bin/bash\n\n"
45
+ printf "macOS:\n 1) Install Homebrew (if missing):\n /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n 2) Install newer bash:\n brew install bash\n 3) Optional: make it your default shell (then restart Terminal):\n sudo sh -c 'echo /opt/homebrew/bin/bash >> /etc/shells' && chsh -s /opt/homebrew/bin/bash\n\n"
31
46
  printf "Or run gitb explicitly with the newer bash when installed:\n /opt/homebrew/bin/bash $0 \"\$@\"\n\n"
32
47
  exit 1;
33
48
  fi
34
- git_dir=$(git rev-parse --git-dir 2>/dev/null)
49
+ if [ "$GITBASHER_NO_REPO" = "true" ]; then
50
+ git_dir=""
51
+ else
52
+ git_dir=$(git rev-parse --git-dir 2>/dev/null)
53
+ fi
35
54
  if [ -n "$git_dir" ] && [ -e "$git_dir/index.lock" ]; then
36
55
  printf "\033[33mDetected %s/index.lock from a possibly interrupted git operation.\033[0m\n" "$git_dir"
37
56
  printf "Another git process may be running. Remove the lock and continue?\n"
@@ -383,6 +402,11 @@ function wrong_mode {
383
402
  exit
384
403
  fi
385
404
  }
405
+ function is_network_error {
406
+ local output="$1"
407
+ echo "$output" | grep -qiE \
408
+ "Could not read from remote repository|Could not resolve host|Connection timed out|Operation timed out|connect to host|Connection refused|Connection reset|Network is unreachable|kex_exchange_identification|remote end hung up unexpectedly|unable to access|Failed to connect|early EOF|RPC failed|timed out|ssh: connect|Temporary failure in name resolution|[Bb]roken pipe|[Rr]ecv failure|[Ss]end failure|TLS connection|SSL connection|no route to host"
409
+ }
386
410
  function get_repo {
387
411
  local remote_name=${origin_name:-origin}
388
412
  local repo
@@ -1189,9 +1213,9 @@ ENDCOLOR="\e[0m"
1189
1213
  BOLD="\033[1m"
1190
1214
  NORMAL="\033[0m"
1191
1215
  function get_config_value {
1192
- value=$(git config --local --get "$1")
1216
+ value=$(git config --local --get "$1" 2>/dev/null)
1193
1217
  if [ "$value" == "" ]; then
1194
- value=$(git config --global --get "$1")
1218
+ value=$(git config --global --get "$1" 2>/dev/null)
1195
1219
  if [ "$value" == "" ]; then
1196
1220
  value=$2
1197
1221
  fi
@@ -1199,7 +1223,7 @@ function get_config_value {
1199
1223
  echo -e "$value"
1200
1224
  }
1201
1225
  function set_config_value {
1202
- if [ -z "$3" ]; then
1226
+ if [ -z "$3" ] && [ "$GITBASHER_NO_REPO" != "true" ]; then
1203
1227
  git config --local "$1" "$2"
1204
1228
  else
1205
1229
  git config --global "$1" "$2"
@@ -1207,6 +1231,10 @@ function set_config_value {
1207
1231
  echo -e "$2"
1208
1232
  }
1209
1233
  function unset_config_value {
1234
+ if [ "$GITBASHER_NO_REPO" = "true" ]; then
1235
+ git config --global --unset "$1" 2>/dev/null
1236
+ return
1237
+ fi
1210
1238
  git config --unset "$1"
1211
1239
  local global_config=$(git config --global --get "$1" 2>/dev/null)
1212
1240
  if [ -n "$global_config" ]; then
@@ -1238,9 +1266,16 @@ function validate_git_url {
1238
1266
  fi
1239
1267
  return 1
1240
1268
  }
1269
+ #
1270
+ _gitb_init_run_queries="true"
1241
1271
  if [ -n "$GITBASHER_SKIP_INIT_QUERIES" ]; then
1272
+ _gitb_init_run_queries=""
1242
1273
  return 0
1243
1274
  fi
1275
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
1276
+ _gitb_init_run_queries=""
1277
+ fi
1278
+ if [ -n "$_gitb_init_run_queries" ]; then
1244
1279
  current_branch=$(git branch --show-current)
1245
1280
  main_branch=$(get_config_value gitbasher.branch "main")
1246
1281
  if [[ "$( git branch | grep -E "^[[:space:]*]*main[[:space:]]*$" )" == "" ]] && [[ "$( git branch | grep -E "^[[:space:]*]*master[[:space:]]*$" )" != "" ]]; then
@@ -1252,7 +1287,7 @@ if [ "$(get_config_value gitbasher.branch "")" == "" ]; then
1252
1287
  git config --local gitbasher.branch "$main_branch"
1253
1288
  fi
1254
1289
  origin_name=$(git remote -v | head -n 1 | sed 's/\t.*//')
1255
- if [ "$origin_name" == "" ]; then
1290
+ if [ "$origin_name" == "" ]; then
1256
1291
  if [ -z "$GITBASHER_TEST_MODE" ] && [ -t 0 ]; then
1257
1292
  echo -e "${YELLOW}⚠ No remote is configured in this repo.${ENDCOLOR}"
1258
1293
  echo
@@ -1308,6 +1343,24 @@ if [ -n "$scopes" ]; then
1308
1343
  fi
1309
1344
  is_first=$(get_config_value gitbasher.isfirst "true")
1310
1345
  git config --local gitbasher.isfirst "false"
1346
+ else
1347
+ current_branch=""
1348
+ main_branch=$(get_config_value gitbasher.branch "main")
1349
+ origin_name=""
1350
+ sep=$(get_config_value gitbasher.sep "-")
1351
+ editor=$(get_config_value core.editor "vi")
1352
+ ticket_name=$(get_config_value gitbasher.ticket "")
1353
+ scopes=$(get_config_value gitbasher.scopes "")
1354
+ if [ -n "$scopes" ]; then
1355
+ if ! validate_scope_list "$scopes" >/dev/null 2>&1; then
1356
+ scopes=""
1357
+ else
1358
+ scopes="$validated_scopes"
1359
+ fi
1360
+ fi
1361
+ is_first="false"
1362
+ fi
1363
+ unset _gitb_init_run_queries
1311
1364
  readonly OPENROUTER_API_URL="https://openrouter.ai/api/v1/chat/completions"
1312
1365
  readonly OPENAI_API_URL="https://api.openai.com/v1/chat/completions"
1313
1366
  readonly OLLAMA_API_URL="http://localhost:11434/v1/chat/completions"
@@ -2131,6 +2184,11 @@ function clear_sensitive_vars {
2131
2184
  fi
2132
2185
  }
2133
2186
  function set_default_branch {
2187
+ if [ "$GITBASHER_NO_REPO" = "true" ]; then
2188
+ echo -e "${RED}✗ Cannot pick a default branch outside a git repository.${ENDCOLOR}" >&2
2189
+ echo -e "Run this inside a repo, or set one directly with ${GREEN}git config --global gitbasher.branch <name>${ENDCOLOR}." >&2
2190
+ exit 1
2191
+ fi
2134
2192
  echo -e "${YELLOW}Fetching remote branches...${ENDCOLOR}"
2135
2193
  echo
2136
2194
  fetch_output=$(git fetch 2>&1)
@@ -2144,6 +2202,7 @@ function set_default_branch {
2144
2202
  main_branch=$(set_config_value gitbasher.branch $branch_name)
2145
2203
  echo -e "${GREEN}✓ Set '${branch_name}' as the default gitbasher branch in '${project_name}'${ENDCOLOR}"
2146
2204
  echo
2205
+ [ "$GITBASHER_NO_REPO" = "true" ] && exit
2147
2206
  echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
2148
2207
  yes_no_choice "\nSet '${branch_name}' globally" "true"
2149
2208
  main_branch=$(set_config_value gitbasher.branch $branch_name "true")
@@ -2190,6 +2249,7 @@ function set_sep {
2190
2249
  sep=$(set_config_value gitbasher.sep $new_sep)
2191
2250
  echo -e "${GREEN}✓ Set '${sep}' as the branch name separator in '${project_name}'${ENDCOLOR}"
2192
2251
  echo
2252
+ [ "$GITBASHER_NO_REPO" = "true" ] && exit
2193
2253
  echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
2194
2254
  yes_no_choice "\nSet '${sep}' globally" "true"
2195
2255
  sep=$(set_config_value gitbasher.sep "$new_sep" "true")
@@ -2218,6 +2278,7 @@ function set_editor {
2218
2278
  editor=$(set_config_value core.editor $choice)
2219
2279
  echo -e "${GREEN}✓ Using editor '$editor' (${which_output})${ENDCOLOR}"
2220
2280
  echo
2281
+ [ "$GITBASHER_NO_REPO" = "true" ] && exit
2221
2282
  echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
2222
2283
  yes_no_choice "\nSet '${editor}' globally" "true"
2223
2284
  editor=$(set_config_value core.editor "$choice" "true")
@@ -2250,6 +2311,7 @@ function set_ticket {
2250
2311
  ticket_name=$(set_config_value gitbasher.ticket $ticket_name)
2251
2312
  echo -e "${GREEN}✓ Set '${ticket_name}' as the ticket prefix in '${project_name}'${ENDCOLOR}"
2252
2313
  echo
2314
+ [ "$GITBASHER_NO_REPO" = "true" ] && exit
2253
2315
  echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
2254
2316
  yes_no_choice "\nSet '${ticket_name}' globally" "true"
2255
2317
  ticket_name=$(set_config_value gitbasher.ticket $ticket_name "true")
@@ -2338,6 +2400,7 @@ function configure_ai_key {
2338
2400
  ai_api_key=$(set_ai_api_key "$ai_key_input")
2339
2401
  echo -e "${GREEN}✓ Configured AI API key for '${provider}' in '${project_name}':${ENDCOLOR} ${BLUE}$(mask_api_key "$ai_api_key")${ENDCOLOR}"
2340
2402
  echo
2403
+ [ "$GITBASHER_NO_REPO" = "true" ] && exit
2341
2404
  echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
2342
2405
  echo -e "${RED}⚠ Global API keys are stored in plaintext in ~/.gitconfig.${ENDCOLOR}"
2343
2406
  echo -e "${CYAN}💡 For better security, set ${BLUE}GITB_AI_API_KEY_${provider_upper}${CYAN} as an environment variable instead.${ENDCOLOR}"
@@ -2391,9 +2454,11 @@ function configure_ai_provider {
2391
2454
  ;;
2392
2455
  esac
2393
2456
  echo
2394
- echo -e "Do you want to set this provider ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
2395
- yes_no_choice "\nSet AI provider globally" "true"
2396
- set_config_value gitbasher.ai-provider "$new_provider" "true"
2457
+ if [ "$GITBASHER_NO_REPO" != "true" ]; then
2458
+ echo -e "Do you want to set this provider ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
2459
+ yes_no_choice "\nSet AI provider globally" "true"
2460
+ set_config_value gitbasher.ai-provider "$new_provider" "true"
2461
+ fi
2397
2462
  if ai_provider_requires_api_key && [ -z "$(get_ai_api_key)" ]; then
2398
2463
  echo
2399
2464
  echo -e "${YELLOW}No API key is set for '${new_provider}'.${ENDCOLOR}"
@@ -2470,6 +2535,7 @@ function configure_ai_model {
2470
2535
  set_ai_model "$model_input"
2471
2536
  echo -e "${GREEN}✓ Set AI model to '$model_input' for '${project_name}'${ENDCOLOR}"
2472
2537
  echo
2538
+ [ "$GITBASHER_NO_REPO" = "true" ] && exit
2473
2539
  echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
2474
2540
  yes_no_choice "\nSet AI model globally" "true"
2475
2541
  set_config_value gitbasher.ai-model "$model_input" "true"
@@ -2517,6 +2583,7 @@ function configure_ai_proxy {
2517
2583
  echo -e " ${BLUE}gitb commit ai${ENDCOLOR} - Generate commit with AI through proxy"
2518
2584
  echo -e " ${BLUE}gitb commit aif${ENDCOLOR} - Fast AI commit through proxy"
2519
2585
  echo
2586
+ [ "$GITBASHER_NO_REPO" = "true" ] && exit
2520
2587
  echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
2521
2588
  yes_no_choice "\nSet AI proxy globally" "true"
2522
2589
  set_ai_proxy "$ai_proxy_input"
@@ -2560,6 +2627,7 @@ function configure_ai_history {
2560
2627
  set_ai_commit_history_limit "$limit_input"
2561
2628
  echo -e "${GREEN}✓ Set AI commit history limit to ${limit_input} for '${project_name}'${ENDCOLOR}"
2562
2629
  echo
2630
+ [ "$GITBASHER_NO_REPO" = "true" ] && exit
2563
2631
  echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
2564
2632
  yes_no_choice "\nSet AI commit history limit globally" "true"
2565
2633
  git config --global gitbasher.ai-commit-history-limit "$limit_input"
@@ -2621,6 +2689,7 @@ function configure_ai_diff {
2621
2689
  exit
2622
2690
  fi
2623
2691
  echo
2692
+ [ "$GITBASHER_NO_REPO" = "true" ] && exit
2624
2693
  echo -e "Do you want to apply ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
2625
2694
  yes_no_choice "\nApply AI diff settings globally" "true"
2626
2695
  if [ -n "$lines_input" ]; then
@@ -2658,10 +2727,17 @@ function set_scopes {
2658
2727
  exit 1
2659
2728
  fi
2660
2729
  scopes_raw="$validated_scopes"
2730
+ if [ "$GITBASHER_NO_REPO" = "true" ]; then
2731
+ git config --global --replace-all gitbasher.scopes "$scopes_raw"
2732
+ scopes="$scopes_raw"
2733
+ echo -e "${GREEN}✓ Set '${scopes}' as the global scopes list${ENDCOLOR}"
2734
+ return
2735
+ fi
2661
2736
  git config --local --replace-all gitbasher.scopes "$scopes_raw"
2662
2737
  scopes="$scopes_raw"
2663
2738
  echo -e "${GREEN}✓ Set '${scopes}' as the scopes list in '${project_name}'${ENDCOLOR}"
2664
2739
  echo
2740
+ [ "$GITBASHER_NO_REPO" = "true" ] && exit
2665
2741
  echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
2666
2742
  yes_no_choice "\nSet '${scopes}' globally" "true"
2667
2743
  git config --global --replace-all gitbasher.scopes "$scopes_raw"
@@ -2772,13 +2848,17 @@ function set_user {
2772
2848
  user_email="$validated_email"
2773
2849
  fi
2774
2850
  echo
2851
+ local _user_scope="--local"
2852
+ if [ "$GITBASHER_NO_REPO" = "true" ]; then
2853
+ _user_scope="--global"
2854
+ fi
2775
2855
  if [ "$user_name" != "" ]; then
2776
2856
  echo -e "${GREEN}✓ Set user name to '${user_name}'${ENDCOLOR}"
2777
- git config --local --replace-all user.name "$user_name"
2857
+ git config "$_user_scope" --replace-all user.name "$user_name"
2778
2858
  fi
2779
2859
  if [ "$user_email" != "" ]; then
2780
2860
  echo -e "${GREEN}✓ Set user email to '${user_email}'${ENDCOLOR}"
2781
- git config --local --replace-all user.email "$user_email"
2861
+ git config "$_user_scope" --replace-all user.email "$user_email"
2782
2862
  fi
2783
2863
  }
2784
2864
  function config_script {
@@ -5041,7 +5121,7 @@ function pull_script {
5041
5121
  else
5042
5122
  echo -e "${GREEN}✓ Fetched '$origin_name/$current_branch'${ENDCOLOR}"
5043
5123
  fi
5044
- if [ "$fetch_output" != "" ]; then
5124
+ if [ "$fetch_output" != "" ] && [ -z "$fetch_progress_shown" ]; then
5045
5125
  echo
5046
5126
  echo -e "$fetch_output"
5047
5127
  fi
@@ -5087,19 +5167,40 @@ function pull_script {
5087
5167
  exit
5088
5168
  }
5089
5169
  function fetch {
5090
- if [ -n "$3" ]; then
5091
- fetch_output=$(git fetch --all 2>&1)
5092
- fetch_code=$?
5093
- else
5094
- fetch_output=$(git fetch "$2" "$1" 2>&1)
5095
- fetch_code=$?
5170
+ fetch_progress_shown=""
5171
+ if [ -t 1 ] && [ -t 2 ]; then
5172
+ local tmp_file
5173
+ tmp_file=$(mktemp 2>/dev/null)
5174
+ if [ -n "$tmp_file" ]; then
5175
+ if [ -n "$3" ]; then
5176
+ git fetch --all --progress 2>&1 | tee "$tmp_file"
5177
+ fetch_code=${PIPESTATUS[0]}
5178
+ else
5179
+ git fetch --progress "$2" "$1" 2>&1 | tee "$tmp_file"
5180
+ fetch_code=${PIPESTATUS[0]}
5181
+ fi
5182
+ fetch_output=$(cat "$tmp_file")
5183
+ rm -f "$tmp_file"
5184
+ fetch_progress_shown="true"
5185
+ fi
5186
+ fi
5187
+ if [ -z "$fetch_progress_shown" ]; then
5188
+ if [ -n "$3" ]; then
5189
+ fetch_output=$(git fetch --all 2>&1)
5190
+ fetch_code=$?
5191
+ else
5192
+ fetch_output=$(git fetch "$2" "$1" 2>&1)
5193
+ fetch_code=$?
5194
+ fi
5096
5195
  fi
5097
5196
  if [ $fetch_code == 0 ] ; then
5098
5197
  return
5099
5198
  fi
5100
5199
  if [[ ${fetch_output} != *"couldn't find remote ref"* ]]; then
5101
5200
  echo -e "${RED}✗ Cannot fetch '$1'.${ENDCOLOR}"
5102
- echo -e "${fetch_output}"
5201
+ if [ -z "$fetch_progress_shown" ]; then
5202
+ echo -e "${fetch_output}"
5203
+ fi
5103
5204
  exit $fetch_code
5104
5205
  fi
5105
5206
  echo -e "${YELLOW}Branch '$1' does not exist on $2.${ENDCOLOR}"
@@ -5190,65 +5291,84 @@ function pull {
5190
5291
  fi
5191
5292
  }
5192
5293
  function push {
5193
- push_output=$(git push "$@" "${origin_name}" "${current_branch}" 2>&1)
5194
- push_code=$?
5195
- if [ $push_code -eq 0 ] ; then
5196
- echo -e "${GREEN}✓ Pushed to ${origin_name}/${current_branch}${ENDCOLOR}"
5197
- repo=$(get_repo)
5198
- host=$(get_repo_host "$repo")
5199
- head_hash=$(git rev-parse HEAD 2>/dev/null)
5200
- print_link "Repo" "$repo"
5201
- if [ -n "$head_hash" ]; then
5202
- commit_url=$(get_commit_url "$head_hash" "$repo")
5203
- if [ -n "$commit_url" ]; then
5204
- print_link "Commit" "$commit_url"
5205
- fi
5206
- fi
5207
- if [[ ${current_branch} != ${main_branch} ]]; then
5208
- branch_url=$(get_branch_url "${current_branch}" "$repo")
5209
- if [ -n "$branch_url" ]; then
5210
- print_link "Branch" "$branch_url"
5211
- fi
5212
- link=$(echo "$push_output" | grep -Eo "https?://[^[:space:]]+" | head -n 1 | sed 's|^remote:[[:space:]]*||')
5213
- if [ "$host" = "github" ]; then
5214
- if [ -n "$link" ]; then
5215
- print_link "New PR" "$link"
5216
- else
5217
- new_pr_url=$(get_new_pr_url "${main_branch}" "${current_branch}" "$repo")
5218
- print_link "New PR" "$new_pr_url"
5294
+ local push_max_retries=4
5295
+ local push_retry_delay=2
5296
+ local push_attempt=0
5297
+ while true; do
5298
+ push_output=$(git push "$@" "${origin_name}" "${current_branch}" 2>&1)
5299
+ push_code=$?
5300
+ if [ $push_code -eq 0 ] ; then
5301
+ echo -e "${GREEN}✓ Pushed to ${origin_name}/${current_branch}${ENDCOLOR}"
5302
+ repo=$(get_repo)
5303
+ host=$(get_repo_host "$repo")
5304
+ head_hash=$(git rev-parse HEAD 2>/dev/null)
5305
+ print_link "Repo" "$repo"
5306
+ if [ -n "$head_hash" ]; then
5307
+ commit_url=$(get_commit_url "$head_hash" "$repo")
5308
+ if [ -n "$commit_url" ]; then
5309
+ print_link "Commit" "$commit_url"
5310
+ fi
5311
+ fi
5312
+ if [[ ${current_branch} != ${main_branch} ]]; then
5313
+ branch_url=$(get_branch_url "${current_branch}" "$repo")
5314
+ if [ -n "$branch_url" ]; then
5315
+ print_link "Branch" "$branch_url"
5219
5316
  fi
5220
- elif [ "$host" = "gitlab" ]; then
5221
- if [ -n "$link" ]; then
5222
- is_new=$(echo "$push_output" | grep -i "create a merge request")
5223
- if [ -n "$is_new" ]; then
5224
- print_link "New MR" "$link"
5317
+ link=$(echo "$push_output" | grep -Eo "https?://[^[:space:]]+" | head -n 1 | sed 's|^remote:[[:space:]]*||')
5318
+ if [ "$host" = "github" ]; then
5319
+ if [ -n "$link" ]; then
5320
+ print_link "New PR" "$link"
5225
5321
  else
5226
- print_link "MR" "$link"
5322
+ new_pr_url=$(get_new_pr_url "${main_branch}" "${current_branch}" "$repo")
5323
+ print_link "New PR" "$new_pr_url"
5324
+ fi
5325
+ elif [ "$host" = "gitlab" ]; then
5326
+ if [ -n "$link" ]; then
5327
+ is_new=$(echo "$push_output" | grep -i "create a merge request")
5328
+ if [ -n "$is_new" ]; then
5329
+ print_link "New MR" "$link"
5330
+ else
5331
+ print_link "MR" "$link"
5332
+ fi
5333
+ else
5334
+ new_mr_url=$(get_new_pr_url "${main_branch}" "${current_branch}" "$repo")
5335
+ print_link "New MR" "$new_mr_url"
5336
+ fi
5337
+ elif [ "$host" = "bitbucket" ]; then
5338
+ if [ -n "$link" ]; then
5339
+ print_link "New PR" "$link"
5340
+ else
5341
+ new_pr_url=$(get_new_pr_url "${main_branch}" "${current_branch}" "$repo")
5342
+ print_link "New PR" "$new_pr_url"
5227
5343
  fi
5228
- else
5229
- new_mr_url=$(get_new_pr_url "${main_branch}" "${current_branch}" "$repo")
5230
- print_link "New MR" "$new_mr_url"
5231
- fi
5232
- elif [ "$host" = "bitbucket" ]; then
5233
- if [ -n "$link" ]; then
5234
- print_link "New PR" "$link"
5235
- else
5236
- new_pr_url=$(get_new_pr_url "${main_branch}" "${current_branch}" "$repo")
5237
- print_link "New PR" "$new_pr_url"
5238
5344
  fi
5239
5345
  fi
5346
+ ci_url=$(get_ci_url "${current_branch}" "$repo")
5347
+ if [ -n "$ci_url" ]; then
5348
+ print_link "$(get_ci_label "$repo")" "$ci_url"
5349
+ fi
5350
+ exit
5240
5351
  fi
5241
- ci_url=$(get_ci_url "${current_branch}" "$repo")
5242
- if [ -n "$ci_url" ]; then
5243
- print_link "$(get_ci_label "$repo")" "$ci_url"
5352
+ if [[ $push_output == *"[rejected]"* ]]; then
5353
+ return
5354
+ fi
5355
+ if is_network_error "$push_output" && [ $push_attempt -lt $push_max_retries ]; then
5356
+ push_attempt=$((push_attempt + 1))
5357
+ echo -e "${YELLOW}⚠ Network error while pushing — retrying in ${push_retry_delay}s (attempt ${push_attempt}/${push_max_retries})...${ENDCOLOR}"
5358
+ echo "$push_output" | head -n 2
5359
+ echo
5360
+ sleep $push_retry_delay
5361
+ push_retry_delay=$((push_retry_delay * 2))
5362
+ continue
5244
5363
  fi
5245
- exit
5246
- fi
5247
- if [[ $push_output != *"[rejected]"* ]]; then
5248
5364
  echo -e "${RED}✗ Cannot push.${ENDCOLOR}"
5249
5365
  echo "$push_output"
5366
+ if [ $push_attempt -gt 0 ]; then
5367
+ echo
5368
+ echo -e "${YELLOW}⚠ Still failing after ${push_attempt} retries — check your network/VPN, then run ${GREEN}gitb push${YELLOW} again.${ENDCOLOR}"
5369
+ fi
5250
5370
  exit $push_code
5251
- fi
5371
+ done
5252
5372
  }
5253
5373
  function push_script {
5254
5374
  case "$1" in
@@ -5502,11 +5622,16 @@ function detect_scopes_from_staged_files {
5502
5622
  detected_scopes_sorted=("${filename_sorted[@]}" "${directory_sorted[@]}")
5503
5623
  final_scopes=()
5504
5624
  count=0
5625
+ declare -A seen_norm=()
5505
5626
  for entry in "${detected_scopes_sorted[@]}"; do
5506
5627
  if [ $count -ge 9 ]; then
5507
5628
  break
5508
5629
  fi
5509
5630
  token="${entry##*:}" # Extract token after last colon
5631
+ token=$(normalize_scope_name "$token")
5632
+ [ -z "$token" ] && continue
5633
+ [ -n "${seen_norm[$token]:-}" ] && continue
5634
+ seen_norm["$token"]=1
5510
5635
  final_scopes+=("$token")
5511
5636
  count=$((count + 1))
5512
5637
  done
@@ -5514,6 +5639,12 @@ function detect_scopes_from_staged_files {
5514
5639
  fi
5515
5640
  fi
5516
5641
  }
5642
+ function normalize_scope_name {
5643
+ local s="$1"
5644
+ while [[ "$s" == [._-]* ]]; do s="${s#?}"; done
5645
+ while [[ "$s" == *[._-] ]]; do s="${s%?}"; done
5646
+ printf '%s' "$s"
5647
+ }
5517
5648
  function build_split_groups_from_staged {
5518
5649
  detect_scopes_from_staged_files
5519
5650
  unset split_groups split_group_keys
@@ -5542,6 +5673,8 @@ function build_split_groups_from_staged {
5542
5673
  comp_lower="${comp,,}"
5543
5674
  comp_no_ext_lower="${comp_lower%.*}"
5544
5675
  [ -z "$comp_no_ext_lower" ] && comp_no_ext_lower="$comp_lower"
5676
+ comp_lower=$(normalize_scope_name "$comp_lower")
5677
+ comp_no_ext_lower=$(normalize_scope_name "$comp_no_ext_lower")
5545
5678
  for scope in "${scopes_arr[@]}"; do
5546
5679
  if [ "$comp_lower" = "$scope" ] || [ "$comp_no_ext_lower" = "$scope" ]; then
5547
5680
  assigned="$scope"
@@ -5553,6 +5686,7 @@ function build_split_groups_from_staged {
5553
5686
  local stem="${comps[$last_idx_inner]}"
5554
5687
  stem="${stem%.*}" # drop the extension
5555
5688
  stem="${stem,,}" # lowercase
5689
+ stem=$(normalize_scope_name "$stem")
5556
5690
  if [ -n "$stem" ] && [[ ! "$stem" =~ ^(index|main|mod|init|app|__init__|setup|config|types|utils|util|helpers|helper|common|shared)$ ]]; then
5557
5691
  assigned="$stem"
5558
5692
  fi
@@ -5565,11 +5699,102 @@ function build_split_groups_from_staged {
5565
5699
  split_groups[$assigned]+=$'\n'"$file"
5566
5700
  fi
5567
5701
  done <<< "$staged_files"
5702
+ consolidate_split_groups "$(get_max_split_groups)"
5568
5703
  if [ ${#split_group_keys[@]} -lt 2 ]; then
5569
5704
  return 1
5570
5705
  fi
5571
5706
  return 0
5572
5707
  }
5708
+ function get_max_split_groups {
5709
+ local v
5710
+ v=$(get_config_value gitbasher.commit-max-split-groups "7")
5711
+ case "$v" in
5712
+ ''|*[!0-9]*) v=7 ;;
5713
+ esac
5714
+ [ "$v" -lt 2 ] && v=2
5715
+ [ "$v" -gt 20 ] && v=20
5716
+ printf '%s' "$v"
5717
+ }
5718
+ function consolidate_split_groups {
5719
+ local max="$1"
5720
+ case "$max" in ''|*[!0-9]*) max=7 ;; esac
5721
+ [ "$max" -lt 2 ] && max=2
5722
+ [ ${#split_group_keys[@]} -le "$max" ] && return 0
5723
+ local -A rebucket=()
5724
+ local -a rebucket_keys=()
5725
+ local key file bucket comp_lower i last_idx
5726
+ local -a comps
5727
+ for key in "${split_group_keys[@]}"; do
5728
+ while IFS= read -r file; do
5729
+ [ -z "$file" ] && continue
5730
+ IFS='/' read -r -a comps <<< "$file"
5731
+ last_idx=$((${#comps[@]} - 1))
5732
+ bucket=""
5733
+ for i in "${!comps[@]}"; do
5734
+ [ "$i" -eq "$last_idx" ] && continue # skip the filename
5735
+ comp_lower="${comps[$i],,}"
5736
+ [ -z "$comp_lower" ] && continue
5737
+ if [[ "$comp_lower" =~ ^(src|lib|libs|scripts|bin|node_modules|vendor|tmp|temp|cache|logs|log|services|apps|packages|modules|components|cmd|internal)$ ]]; then
5738
+ continue
5739
+ fi
5740
+ bucket=$(normalize_scope_name "$comp_lower") # .github -> github
5741
+ [ -z "$bucket" ] && continue
5742
+ break
5743
+ done
5744
+ [ -z "$bucket" ] && bucket="misc"
5745
+ if [ -z "${rebucket[$bucket]:-}" ]; then
5746
+ rebucket_keys+=("$bucket")
5747
+ rebucket[$bucket]="$file"
5748
+ else
5749
+ rebucket[$bucket]+=$'\n'"$file"
5750
+ fi
5751
+ done <<< "${split_groups[$key]}"
5752
+ done
5753
+ unset split_groups split_group_keys
5754
+ declare -gA split_groups=()
5755
+ split_group_keys=()
5756
+ for key in "${rebucket_keys[@]}"; do
5757
+ split_groups[$key]="${rebucket[$key]}"
5758
+ split_group_keys+=("$key")
5759
+ done
5760
+ [ ${#split_group_keys[@]} -le "$max" ] && return 0
5761
+ local -a sized=()
5762
+ local n
5763
+ for key in "${split_group_keys[@]}"; do
5764
+ n=$(printf '%s\n' "${split_groups[$key]}" | grep -c .)
5765
+ sized+=("${n}"$'\t'"${key}")
5766
+ done
5767
+ IFS=$'\n' sized=($(printf '%s\n' "${sized[@]}" | sort -t$'\t' -k1,1nr -k2,2))
5768
+ unset IFS
5769
+ local -A kept=()
5770
+ local -a kept_keys=()
5771
+ local keep_limit=$((max - 1))
5772
+ local misc_files="" idx=0 entry
5773
+ for entry in "${sized[@]}"; do
5774
+ key="${entry#*$'\t'}"
5775
+ if [ "$idx" -lt "$keep_limit" ] && [ "$key" != "misc" ]; then
5776
+ kept_keys+=("$key")
5777
+ kept[$key]="${split_groups[$key]}"
5778
+ idx=$((idx + 1))
5779
+ elif [ -z "$misc_files" ]; then
5780
+ misc_files="${split_groups[$key]}"
5781
+ else
5782
+ misc_files+=$'\n'"${split_groups[$key]}"
5783
+ fi
5784
+ done
5785
+ unset split_groups split_group_keys
5786
+ declare -gA split_groups=()
5787
+ split_group_keys=()
5788
+ for key in "${kept_keys[@]}"; do
5789
+ split_groups[$key]="${kept[$key]}"
5790
+ split_group_keys+=("$key")
5791
+ done
5792
+ if [ -n "$misc_files" ]; then
5793
+ split_group_keys+=("misc")
5794
+ split_groups[misc]="$misc_files"
5795
+ fi
5796
+ return 0
5797
+ }
5573
5798
  function is_heuristic_weak {
5574
5799
  local total
5575
5800
  total=$(git -c core.quotePath=false diff --name-only --cached | grep -c .)
@@ -5601,10 +5826,12 @@ function refine_groups_with_ai {
5601
5826
  if [ -n "$detected_scopes" ]; then
5602
5827
  heuristic_hint="$detected_scopes"
5603
5828
  fi
5604
- local system_prompt='You group staged git files into logical scopes for atomic commits.
5829
+ local max_groups
5830
+ max_groups=$(get_max_split_groups)
5831
+ local system_prompt="You group staged git files into logical scopes for atomic commits.
5605
5832
  Input arrives in XML tags: a list of staged files, a diff summary, recent commit messages (for codebase scope conventions), and heuristic-detected candidate scope tokens.
5606
- Task: assign EVERY staged file to a single scope. Use 2-5 groups when there is meaningful separation; use exactly 1 group only if all files truly belong together. Use lowercase scope names that match the style of <recent_commits>. Prefer scope names from <heuristic_candidates> when they fit the file path or change; invent new ones only when none of the candidates apply. Use "misc" for files with no clear scope (e.g., README, top-level config).
5607
- Output format: TSV only. One line per staged file in the form <scope><TAB><file_path>. No header, no prose, no markdown fences, no surrounding quotes. Every file from <staged_files> MUST appear exactly once. The file paths must be byte-identical to those in <staged_files>.'
5833
+ Task: assign EVERY staged file to a single scope. Use 2 to ${max_groups} groups when there is meaningful separation; use exactly 1 group only if all files truly belong together. Never exceed ${max_groups} groups — when many files share a theme, prefer a single broader scope over several tiny ones. Use lowercase scope names that match the style of <recent_commits>. Prefer scope names from <heuristic_candidates> when they fit the file path or change; invent new ones only when none of the candidates apply. Use \"misc\" for files with no clear scope (e.g., README, top-level config).
5834
+ Output format: TSV only. One line per staged file in the form <scope><TAB><file_path>. No header, no prose, no markdown fences, no surrounding quotes. Every file from <staged_files> MUST appear exactly once. The file paths must be byte-identical to those in <staged_files>."
5608
5835
  local user_prompt="<recent_commits>
5609
5836
  ${recent_commits}
5610
5837
  </recent_commits>
@@ -6093,6 +6320,7 @@ function try_offer_commit_split {
6093
6320
  if ! refine_groups_with_ai; then
6094
6321
  echo -e "${YELLOW}AI grouping failed, falling back to heuristic.${ENDCOLOR}"
6095
6322
  fi
6323
+ consolidate_split_groups "$(get_max_split_groups)"
6096
6324
  fi
6097
6325
  if [ ${#split_group_keys[@]} -lt 2 ]; then
6098
6326
  if [ -n "$force_split" ]; then
@@ -6286,8 +6514,8 @@ function set_commit_flag_from_token {
6286
6514
  fixup|fix|x) fixup="true";;
6287
6515
  amend|am|a) amend="true";;
6288
6516
  split|sp|sl) split="true";;
6289
- last|l) last="true";;
6290
6517
  revert|rev) revert="true";;
6518
+ last|l) last="true";; # deprecated alias of `gitb edit`; kept for back-compat
6291
6519
  help|h) help="true";;
6292
6520
  *) return 1;;
6293
6521
  esac
@@ -6295,30 +6523,26 @@ function set_commit_flag_from_token {
6295
6523
  }
6296
6524
  function validate_commit_flag_combo {
6297
6525
  local actions=() invalid=()
6298
- [ -n "$last" ] && actions+=("last")
6299
6526
  [ -n "$revert" ] && actions+=("revert")
6300
6527
  [ -n "$fixup" ] && actions+=("fixup")
6301
6528
  [ -n "$amend" ] && actions+=("amend")
6302
6529
  [ -n "$split" ] && actions+=("split")
6303
6530
  if [ ${#actions[@]} -gt 1 ]; then
6304
6531
  echo -e "${RED}✗ Cannot combine actions: ${actions[*]}${ENDCOLOR}"
6305
- echo -e "Pick one of: last, revert, fixup, amend, split."
6532
+ echo -e "Pick one of: revert, fixup, amend, split."
6306
6533
  exit 1
6307
6534
  fi
6308
- local lone=""
6309
- [ -n "$last" ] && lone="last"
6310
- [ -n "$revert" ] && lone="revert"
6311
- if [ -n "$lone" ]; then
6535
+ if [ -n "$revert" ]; then
6312
6536
  [ -n "$llm" ] && invalid+=("ai")
6313
6537
  [ -n "$fast" ] && invalid+=("fast")
6314
- [ -n "$push" ] && invalid+=("push")
6315
6538
  [ -n "$scope" ] && invalid+=("scope")
6316
6539
  [ -n "$msg" ] && invalid+=("msg")
6317
6540
  [ -n "$ticket" ] && invalid+=("ticket")
6318
6541
  [ -n "$staged" ] && invalid+=("staged")
6319
6542
  [ -n "$no_split" ] && invalid+=("no-split")
6320
6543
  if [ ${#invalid[@]} -gt 0 ]; then
6321
- echo -e "${RED}✗ '${lone}' takes no modifiers (got: ${invalid[*]})${ENDCOLOR}"
6544
+ echo -e "${RED}✗ 'revert' does not use: ${invalid[*]}${ENDCOLOR}"
6545
+ echo -e "Only push applies (the revert commit message is auto-generated)."
6322
6546
  exit 1
6323
6547
  fi
6324
6548
  fi
@@ -6347,8 +6571,6 @@ function summarize_commit_intent {
6347
6571
  local action="commit" mods=()
6348
6572
  if [ -n "$auto_accept" ]; then
6349
6573
  action="ultrafast commit (ai + fast + auto-accept)"
6350
- elif [ -n "$last" ]; then
6351
- action="amend last commit (reuse message)"
6352
6574
  elif [ -n "$revert" ]; then
6353
6575
  action="revert a commit"
6354
6576
  elif [ -n "$amend" ]; then
@@ -6403,8 +6625,9 @@ function commit_script {
6403
6625
  amend|am|a) amend="true";;
6404
6626
  amendst|ast|sta) amend="true"; staged="true";;
6405
6627
  amendf|amf|af|fa) amend="true"; fast="true";;
6406
- last|l) last="true";;
6628
+ last|l) last="true";; # deprecated alias of `gitb edit`; kept for back-compat
6407
6629
  revert|rev) revert="true";;
6630
+ revertp|revp|rp) revert="true"; push="true";;
6408
6631
  llm|ai|i) llm="true";;
6409
6632
  llmf|aif|if) llm="true"; fast="true";;
6410
6633
  llmp|aip|ip) llm="true"; push="true";;
@@ -6421,6 +6644,10 @@ function commit_script {
6421
6644
  wrong_mode "commit" $1
6422
6645
  esac
6423
6646
  fi
6647
+ if [ -n "$last" ]; then
6648
+ edit_script ""
6649
+ exit
6650
+ fi
6424
6651
  validate_commit_flag_combo
6425
6652
  header_msg="GIT COMMIT"
6426
6653
  if [ -n "${llm}" ]; then
@@ -6466,8 +6693,6 @@ function commit_script {
6466
6693
  header_msg="$header_msg TICKET"
6467
6694
  elif [ -n "${amend}" ]; then
6468
6695
  header_msg="$header_msg AMEND LAST"
6469
- elif [ -n "${last}" ]; then
6470
- header_msg="$header_msg LAST"
6471
6696
  elif [ -n "${revert}" ]; then
6472
6697
  header_msg="$header_msg REVERT"
6473
6698
  fi
@@ -6488,7 +6713,6 @@ function commit_script {
6488
6713
  print_help_row $PAD "split" "sp, sl" "Split staged changes into one commit per detected scope"
6489
6714
  print_help_row $PAD "fixup" "x, fix" "Create a ${GREEN}--fixup${ENDCOLOR} commit against an older commit"
6490
6715
  print_help_row $PAD "amend" "a, am" "Add changes into the last commit (no message edit)"
6491
- print_help_row $PAD "last" "l" "Rewrite the last commit message"
6492
6716
  print_help_row $PAD "revert" "rev" "Revert a commit (${GREEN}git revert --no-edit${ENDCOLOR})"
6493
6717
  print_help_row $PAD "ff" "" "Ultrafast: ${BOLD}ai + split + fast${NORMAL} with no prompts (use ${BOLD}ffp${NORMAL} to also push)"
6494
6718
  print_help_row $PAD "help" "h, --help, -h" "Show this help"
@@ -6519,18 +6743,13 @@ function commit_script {
6519
6743
  echo -e " ${BLUE}•${ENDCOLOR} Word order doesn't matter: ${GREEN}ai fast push${ENDCOLOR} == ${GREEN}push fast ai${ENDCOLOR} == ${GREEN}aifp${ENDCOLOR}"
6520
6744
  echo -e " ${BLUE}•${ENDCOLOR} Modifiers stack on actions: ${GREEN}ai+fixup${ENDCOLOR}, ${GREEN}fast+amend${ENDCOLOR}, ${GREEN}split+push${ENDCOLOR}, ${GREEN}ai+staged${ENDCOLOR}, ..."
6521
6745
  echo -e " ${BLUE}•${ENDCOLOR} ${BOLD}fast${NORMAL} and ${BOLD}staged${NORMAL} are mutually exclusive (one stages all, the other uses what's staged)"
6522
- echo -e " ${BLUE}•${ENDCOLOR} ${BOLD}last${NORMAL} and ${BOLD}revert${NORMAL} take no modifiers; ${BOLD}ff${NORMAL} only accepts ${BOLD}push${NORMAL} (as ${BOLD}ffp${NORMAL})"
6746
+ 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}"
6523
6747
  git config --unset gitbasher.cached-git-add 2>/dev/null || true
6524
6748
  exit 0
6525
6749
  fi
6526
6750
  summarize_commit_intent
6527
6751
  echo
6528
6752
  warn_if_detached_head "commit"
6529
- if [ -n "$last" ]; then
6530
- git config --unset gitbasher.cached-git-add 2>/dev/null
6531
- git commit --amend
6532
- exit
6533
- fi
6534
6753
  is_clean=$(LC_ALL=C git status --porcelain)
6535
6754
  if [ -z "$is_clean" ]; then
6536
6755
  if [ -z "${revert}" ]; then
@@ -6570,6 +6789,10 @@ function commit_script {
6570
6789
  result=$(git revert --no-edit ${commit_hash} 2>&1)
6571
6790
  check_code $? "$result" "revert"
6572
6791
  after_commit "revert"
6792
+ if [ -n "${push}" ]; then
6793
+ echo
6794
+ push_script y
6795
+ fi
6573
6796
  exit
6574
6797
  fi
6575
6798
  if [ -z "${fast}" ] && [ -z "${staged}" ]; then
@@ -6773,6 +6996,10 @@ function commit_script {
6773
6996
  git config --unset gitbasher.cached-commit-message 2>/dev/null
6774
6997
  echo
6775
6998
  after_commit "amend"
6999
+ if [ -n "${push}" ]; then
7000
+ echo
7001
+ push_script y
7002
+ fi
6776
7003
  exit
6777
7004
  fi
6778
7005
  echo
@@ -7042,6 +7269,184 @@ ${staged_with_tab}
7042
7269
  push_script y
7043
7270
  fi
7044
7271
  }
7272
+ function edit_script {
7273
+ local mode="amend"
7274
+ case "$1" in
7275
+ help|h|--help|-h)
7276
+ echo -e "${YELLOW}GIT EDIT${ENDCOLOR}"
7277
+ echo
7278
+ echo -e "usage: ${YELLOW}gitb edit [mode] [arg]${ENDCOLOR}"
7279
+ echo
7280
+ echo -e "Rewrite a commit message or rename the current branch."
7281
+ echo
7282
+ echo -e "${YELLOW}Modes${ENDCOLOR}"
7283
+ echo -e " ${BOLD}<empty>${NORMAL} Reword the ${BOLD}last${NORMAL} commit (${GREEN}git commit --amend${ENDCOLOR})"
7284
+ echo -e " ${BOLD}pick${NORMAL} (${BOLD}p${NORMAL}, ${BOLD}c${NORMAL}) Choose any recent commit and reword it via rebase"
7285
+ echo -e " ${BOLD}branch${NORMAL} (${BOLD}b${NORMAL}, ${BOLD}br${NORMAL}, ${BOLD}rename${NORMAL}) Rename the current branch (${GREEN}git branch -m${ENDCOLOR})"
7286
+ echo -e " ${BOLD}help${NORMAL} (${BOLD}h${NORMAL}) Show this help"
7287
+ echo
7288
+ echo -e "${YELLOW}Notes${ENDCOLOR}"
7289
+ echo -e " ${BLUE}•${ENDCOLOR} ${BOLD}pick${NORMAL} requires a clean working tree (the rebase replays commits)."
7290
+ echo -e " ${BLUE}•${ENDCOLOR} Merge commits and the root commit cannot be reworded this way."
7291
+ echo -e " ${BLUE}•${ENDCOLOR} If a reworded commit was already pushed, run ${YELLOW}gitb push force${ENDCOLOR} afterwards."
7292
+ echo -e " ${BLUE}•${ENDCOLOR} ${BOLD}branch${NORMAL} accepts an optional new name: ${YELLOW}gitb edit branch feat/new-name${ENDCOLOR}."
7293
+ echo -e " ${BLUE}•${ENDCOLOR} To add staged changes into the last commit, use ${YELLOW}gitb commit amend${ENDCOLOR}."
7294
+ echo -e " ${BLUE}•${ENDCOLOR} To undo the amend/rebase, use ${YELLOW}gitb undo amend${ENDCOLOR} / ${YELLOW}gitb undo rebase${ENDCOLOR}."
7295
+ exit 0
7296
+ ;;
7297
+ "")
7298
+ ;;
7299
+ pick|p|c|choose)
7300
+ mode="pick"
7301
+ ;;
7302
+ branch|b|br|rename|ren)
7303
+ mode="branch"
7304
+ ;;
7305
+ *)
7306
+ wrong_mode "edit" "$1"
7307
+ ;;
7308
+ esac
7309
+ echo -e "${YELLOW}GIT EDIT${ENDCOLOR}"
7310
+ echo
7311
+ warn_if_detached_head "edit"
7312
+ if [ "$mode" = "branch" ]; then
7313
+ edit_branch_name "$2"
7314
+ return
7315
+ fi
7316
+ if ! git rev-parse --verify HEAD >/dev/null 2>&1; then
7317
+ echo -e "${RED}✗ No commits yet — nothing to edit.${ENDCOLOR}"
7318
+ exit 1
7319
+ fi
7320
+ git config --unset gitbasher.cached-git-add 2>/dev/null
7321
+ if [ "$mode" = "amend" ]; then
7322
+ git commit --amend
7323
+ return
7324
+ fi
7325
+ local dirty
7326
+ dirty=$(LC_ALL=C git status --porcelain)
7327
+ if [ -n "$dirty" ]; then
7328
+ echo -e "${RED}✗ Working tree has uncommitted changes — cannot reword via rebase.${ENDCOLOR}"
7329
+ echo -e "Commit, stash, or save them first (try ${YELLOW}gitb wip up${ENDCOLOR})."
7330
+ exit 1
7331
+ fi
7332
+ echo -e "${YELLOW}Step 1.${ENDCOLOR} Select a commit to ${YELLOW}reword${ENDCOLOR}:"
7333
+ choose_commit 19
7334
+ if [ -z "$commit_hash" ]; then
7335
+ exit
7336
+ fi
7337
+ local head_hash
7338
+ head_hash=$(git rev-parse --short HEAD)
7339
+ if [ "$commit_hash" = "$head_hash" ]; then
7340
+ echo
7341
+ git commit --amend
7342
+ return
7343
+ fi
7344
+ if git rev-parse --verify "${commit_hash}^2" >/dev/null 2>&1; then
7345
+ echo
7346
+ echo -e "${RED}✗ Cannot reword a merge commit via rebase.${ENDCOLOR}"
7347
+ exit 1
7348
+ fi
7349
+ if ! git rev-parse --verify "${commit_hash}^" >/dev/null 2>&1; then
7350
+ echo
7351
+ echo -e "${RED}✗ Cannot reword the root commit this way.${ENDCOLOR}"
7352
+ echo -e "Use ${YELLOW}git rebase -i --root${ENDCOLOR} manually."
7353
+ exit 1
7354
+ fi
7355
+ local seq_script
7356
+ seq_script=$(mktemp "${TMPDIR:-/tmp}/gitb-edit-seq.XXXXXX")
7357
+ chmod 600 "$seq_script"
7358
+ trap 'rm -f "$seq_script"' EXIT INT TERM
7359
+ cat > "$seq_script" <<SEQ_EOF
7360
+ todo="\$1"
7361
+ tmpfile="\$(mktemp "\${todo}.XXXXXX")"
7362
+ awk -v target="${commit_hash}" '
7363
+ \$1 == "pick" && (index(\$2, target) == 1 || index(target, \$2) == 1) { \$1 = "reword" }
7364
+ { print }
7365
+ ' "\$todo" > "\$tmpfile"
7366
+ mv "\$tmpfile" "\$todo"
7367
+ SEQ_EOF
7368
+ echo
7369
+ echo -e "${YELLOW}Rewording ${commit_hash}...${ENDCOLOR}"
7370
+ GIT_SEQUENCE_EDITOR="bash $seq_script" git rebase -i "${commit_hash}^"
7371
+ }
7372
+ function edit_branch_name {
7373
+ local new_name="$1"
7374
+ local old_name="$current_branch"
7375
+ if [ -z "$old_name" ]; then
7376
+ echo -e "${RED}✗ Could not determine the current branch.${ENDCOLOR}"
7377
+ exit 1
7378
+ fi
7379
+ if [ -z "$new_name" ]; then
7380
+ echo -e "${YELLOW}Renaming branch ${BOLD}${old_name}${NORMAL}${ENDCOLOR}"
7381
+ echo -e "Press Enter to keep the current name (no-op), or 0 to cancel"
7382
+ read_editable_input new_name "New name: " "$old_name"
7383
+ echo
7384
+ if [ "$new_name" = "0" ] || [ -z "$new_name" ]; then
7385
+ echo -e "${YELLOW}Cancelled.${ENDCOLOR}"
7386
+ exit
7387
+ fi
7388
+ fi
7389
+ if ! sanitize_git_name "$new_name"; then
7390
+ show_sanitization_error "branch name" "Use only letters, numbers, dots, dashes, underscores, and slashes."
7391
+ exit 1
7392
+ fi
7393
+ new_name="$sanitized_git_name"
7394
+ if [ "$new_name" = "$old_name" ]; then
7395
+ echo -e "${YELLOW}New name matches the current name — nothing to do.${ENDCOLOR}"
7396
+ exit
7397
+ fi
7398
+ if git show-ref --verify --quiet "refs/heads/${new_name}"; then
7399
+ echo -e "${RED}✗ A local branch named ${BOLD}${new_name}${NORMAL} already exists.${ENDCOLOR}"
7400
+ exit 1
7401
+ fi
7402
+ if [ "$old_name" = "$main_branch" ]; then
7403
+ echo -e "${YELLOW}⚠ ${BOLD}${old_name}${NORMAL} is the configured main branch.${ENDCOLOR}"
7404
+ echo -e "Renaming it will leave ${YELLOW}gitbasher.branch${ENDCOLOR} pointing at the old name."
7405
+ printf "Rename anyway? (y/n) "
7406
+ yes_no_choice
7407
+ echo
7408
+ fi
7409
+ local result
7410
+ result=$(git branch -m "$old_name" "$new_name" 2>&1)
7411
+ if [ $? -ne 0 ]; then
7412
+ echo -e "${RED}✗ Could not rename branch.${ENDCOLOR}"
7413
+ echo "$result"
7414
+ exit 1
7415
+ fi
7416
+ current_branch="$new_name"
7417
+ echo -e "${GREEN}✓ Renamed ${BOLD}${old_name}${NORMAL}${GREEN} → ${BOLD}${new_name}${NORMAL}${ENDCOLOR}"
7418
+ if [ -z "$origin_name" ]; then
7419
+ return
7420
+ fi
7421
+ if ! git ls-remote --exit-code --heads "$origin_name" "$old_name" >/dev/null 2>&1; then
7422
+ echo -e "Push the renamed branch when ready: ${YELLOW}gitb push${ENDCOLOR}"
7423
+ return
7424
+ fi
7425
+ echo
7426
+ echo -e "${YELLOW}${BOLD}${old_name}${NORMAL}${YELLOW} also exists on ${origin_name}.${ENDCOLOR}"
7427
+ echo -e "Push ${BOLD}${new_name}${NORMAL} and delete the old remote branch?"
7428
+ printf "(y/n) "
7429
+ yes_no_choice
7430
+ echo
7431
+ local push_out
7432
+ push_out=$(git push -u "$origin_name" "$new_name" 2>&1)
7433
+ if [ $? -ne 0 ]; then
7434
+ echo -e "${RED}✗ Could not push ${BOLD}${new_name}${NORMAL}${RED} to ${origin_name}.${ENDCOLOR}"
7435
+ echo "$push_out"
7436
+ echo -e "Old remote branch ${BOLD}${old_name}${NORMAL} left intact."
7437
+ exit 1
7438
+ fi
7439
+ echo -e "${GREEN}✓ Pushed ${BOLD}${new_name}${NORMAL}${GREEN} to ${origin_name}${ENDCOLOR}"
7440
+ local del_out
7441
+ del_out=$(git push "$origin_name" --delete "$old_name" 2>&1)
7442
+ if [ $? -ne 0 ]; then
7443
+ echo -e "${YELLOW}⚠ Could not delete ${BOLD}${old_name}${NORMAL}${YELLOW} on ${origin_name}.${ENDCOLOR}"
7444
+ echo "$del_out"
7445
+ echo -e "Delete it manually with ${YELLOW}git push ${origin_name} --delete ${old_name}${ENDCOLOR} when ready."
7446
+ return
7447
+ fi
7448
+ echo -e "${GREEN}✓ Deleted ${BOLD}${old_name}${NORMAL}${GREEN} on ${origin_name}${ENDCOLOR}"
7449
+ }
7045
7450
  function branch_script {
7046
7451
  case "$1" in
7047
7452
  list|l) list="true";;
@@ -11993,6 +12398,97 @@ function origin_script {
11993
12398
  exit
11994
12399
  fi
11995
12400
  }
12401
+ function _gitb_clone_dest_from_url {
12402
+ local url="$1"
12403
+ local stripped="${url%.git}"
12404
+ local last="${stripped##*/}"
12405
+ last="${last##*:}"
12406
+ echo "$last"
12407
+ }
12408
+ function _gitb_print_clone_help {
12409
+ local PAD=14
12410
+ echo -e "usage: ${YELLOW}gitb clone <url> [destination]${ENDCOLOR}"
12411
+ echo
12412
+ echo -e "Clone a remote repository, show progress, and initialize gitbasher in it."
12413
+ echo
12414
+ print_help_header $PAD
12415
+ print_help_row $PAD "<url>" "" "Git URL to clone (https, ssh, or git@)"
12416
+ print_help_row $PAD "[destination]" "" "Optional target directory (default: repo name)"
12417
+ print_help_row $PAD "help" "h" "Show this help"
12418
+ echo
12419
+ echo -e "${YELLOW}Examples${ENDCOLOR}"
12420
+ echo -e " ${GREEN}gitb clone https://github.com/user/repo.git${ENDCOLOR}"
12421
+ echo -e " ${GREEN}gitb clone git@github.com:user/repo.git my-repo${ENDCOLOR}"
12422
+ exit
12423
+ }
12424
+ function clone_script {
12425
+ echo -e "${YELLOW}GIT CLONE${ENDCOLOR}"
12426
+ echo
12427
+ case "$1" in
12428
+ ""|help|h) _gitb_print_clone_help ;;
12429
+ esac
12430
+ local raw_url="$1"
12431
+ local dest="$2"
12432
+ if ! validate_git_url "$raw_url"; then
12433
+ echo -e "${RED}✗ Invalid git URL format: $raw_url${ENDCOLOR}" >&2
12434
+ echo -e "${YELLOW}Expected one of:${ENDCOLOR}" >&2
12435
+ echo -e " https://host/user/repo.git" >&2
12436
+ echo -e " git@host:user/repo.git" >&2
12437
+ echo -e " ssh://git@host/repo.git" >&2
12438
+ exit 1
12439
+ fi
12440
+ local url="$validated_url"
12441
+ if [ -z "$dest" ]; then
12442
+ dest=$(_gitb_clone_dest_from_url "$url")
12443
+ fi
12444
+ if [ -z "$dest" ]; then
12445
+ echo -e "${RED}✗ Could not determine target directory from URL.${ENDCOLOR}" >&2
12446
+ echo -e "${YELLOW}Pass a destination explicitly: ${GREEN}gitb clone <url> <dir>${ENDCOLOR}" >&2
12447
+ exit 1
12448
+ fi
12449
+ if ! sanitize_file_path "$dest"; then
12450
+ echo -e "${RED}✗ Invalid destination path: $dest${ENDCOLOR}" >&2
12451
+ exit 1
12452
+ fi
12453
+ dest="$sanitized_file_path"
12454
+ if [ -e "$dest" ]; then
12455
+ echo -e "${RED}✗ Destination '$dest' already exists.${ENDCOLOR}" >&2
12456
+ echo -e "${YELLOW}Remove it first or pass a different destination.${ENDCOLOR}" >&2
12457
+ exit 1
12458
+ fi
12459
+ echo -e "Source: ${BLUE}$url${ENDCOLOR}"
12460
+ echo -e "Target: ${BLUE}$dest${ENDCOLOR}"
12461
+ echo
12462
+ if ! git clone --progress "$url" "$dest"; then
12463
+ echo
12464
+ echo -e "${RED}✗ Clone failed.${ENDCOLOR}" >&2
12465
+ exit 1
12466
+ fi
12467
+ echo
12468
+ echo -e "${GREEN}✓ Cloned into '$dest'${ENDCOLOR}"
12469
+ local target_abs
12470
+ target_abs=$(cd "$dest" 2>/dev/null && pwd)
12471
+ if [ -z "$target_abs" ]; then
12472
+ echo -e "${RED}✗ Failed to enter directory '$dest'.${ENDCOLOR}" >&2
12473
+ exit 1
12474
+ fi
12475
+ if ! cd "$target_abs"; then
12476
+ echo -e "${RED}✗ Failed to cd into '$target_abs'.${ENDCOLOR}" >&2
12477
+ exit 1
12478
+ fi
12479
+ local cloned_branch
12480
+ cloned_branch=$(git branch --show-current 2>/dev/null)
12481
+ if [ -z "$cloned_branch" ]; then
12482
+ cloned_branch="main"
12483
+ fi
12484
+ git config --local gitbasher.branch "$cloned_branch" 2>/dev/null
12485
+ git config --local gitbasher.scopes "" 2>/dev/null
12486
+ git config --local gitbasher.isfirst "true" 2>/dev/null
12487
+ echo -e "${GREEN}✓ Initialized gitbasher in '$dest'${ENDCOLOR}"
12488
+ echo
12489
+ echo -e "${CYAN}💡 Enter the new repo with:${ENDCOLOR}"
12490
+ echo -e " ${GREEN}cd $target_abs${ENDCOLOR}"
12491
+ }
11996
12492
  GITBASHER_REPO="maxbolgarin/gitbasher"
11997
12493
  GITBASHER_RELEASES_URL="https://github.com/${GITBASHER_REPO}/releases"
11998
12494
  function _normalize_version {
@@ -12480,6 +12976,7 @@ function _gitb_bash_completion_content {
12480
12976
  cat <<'GITB_BASH_EOF'
12481
12977
  _gitb_commands="
12482
12978
  commit c co com
12979
+ edit ed ee
12483
12980
  push p ps pus
12484
12981
  pull pu pl pul
12485
12982
  merge m me
@@ -12508,8 +13005,8 @@ update up upd
12508
13005
  uninstall uns uni
12509
13006
  help man
12510
13007
  "
12511
- _gitb_sub_commit="ai llm i fast f fasts fs sf ff ffp ffpush push pu p fastp fp pf fastsp fsp fps scope s msg m ticket jira j t staged st no-split nosplit nsp nsl fixup fix x fixupp fixp xp px fixupst xst stx fastfix fx xf fastfixp fxp xfp amend am a amendst ast sta amendf amf af fa split sp sl splitp spp slp aisplit isplit aispl ispl aisplitp isplitp aisplp isplp llmf aif if llmp aip ip llmst aist ist llmfp aifp ifp ipf llms ais is llmsf aisf isf llmsfp aisfp isfp llmm aim im llmmf aimf imf llmmfp aimfp imfp last l revert rev help h"
12512
- _gitb_sub_commit_simple="ai llm i fast f push pu p scope s msg m ticket jira j t staged no-split nosplit nsp nsl fixup fix x amend am a split sp sl last l revert rev help h"
13008
+ _gitb_sub_commit="ai llm i fast f fasts fs sf ff ffp ffpush push pu p fastp fp pf fastsp fsp fps scope s msg m ticket jira j t staged st no-split nosplit nsp nsl fixup fix x fixupp fixp xp px fixupst xst stx fastfix fx xf fastfixp fxp xfp amend am a amendst ast sta amendf amf af fa split sp sl splitp spp slp aisplit isplit aispl ispl aisplitp isplitp aisplp isplp llmf aif if llmp aip ip llmst aist ist llmfp aifp ifp ipf llms ais is llmsf aisf isf llmsfp aisfp isfp llmm aim im llmmf aimf imf llmmfp aimfp imfp revert rev revertp revp rp help h"
13009
+ _gitb_sub_commit_simple="ai llm i fast f push pu p scope s msg m ticket jira j t staged no-split nosplit nsp nsl fixup fix x amend am a split sp sl revert rev help h"
12513
13010
  _gitb_sub_wip_backend="stash s branch b worktree w wt tree nopush np n"
12514
13011
  _gitb_sub_push="yes y force f list log l help h"
12515
13012
  _gitb_sub_pull="fetch fe all fa upd u ffonly ff merge m rebase r interactive ri rs dry d dr help h"
@@ -12530,10 +13027,12 @@ _gitb_sub_origin="set add new a change update c u set-url rename mv ren remove d
12530
13027
  _gitb_sub_log="branch b compare comp c search s help h"
12531
13028
  _gitb_sub_hook="list create edit toggle test show remove select install help"
12532
13029
  _gitb_sub_log_branch="local l remote r all a help h"
13030
+ _gitb_sub_edit="pick p c choose branch b br rename ren help h"
12533
13031
  _gitb_sub_config_auto="up u on install enable down d off uninstall disable remove status st print cat p help h"
12534
13032
  _gitb_canonical() {
12535
13033
  case "$1" in
12536
13034
  commit|c|co|com) echo commit ;;
13035
+ edit|ed|ee) echo edit ;;
12537
13036
  push|p|ps|pus) echo push ;;
12538
13037
  pull|pu|pl|pul) echo pull ;;
12539
13038
  merge|m|me) echo merge ;;
@@ -12564,7 +13063,6 @@ _gitb_commit_state_aware() {
12564
13063
  for ((i=2; i<COMP_CWORD; i++)); do
12565
13064
  tok="${COMP_WORDS[$i]}"
12566
13065
  case "$tok" in
12567
- last|l) has_action="last" ;;
12568
13066
  revert|rev) has_action="revert" ;;
12569
13067
  fixup|fix|x) has_action="fixup" ;;
12570
13068
  amend|am|a) has_action="amend" ;;
@@ -12581,8 +13079,8 @@ _gitb_commit_state_aware() {
12581
13079
  done
12582
13080
  local candidates=()
12583
13081
  case "$has_action" in
12584
- last|revert)
12585
- :
13082
+ revert)
13083
+ [ -z "$has_push" ] && candidates+=("push")
12586
13084
  ;;
12587
13085
  amend|fixup)
12588
13086
  [ -z "${has_fast}${has_staged}" ] && candidates+=("fast" "staged")
@@ -12594,7 +13092,7 @@ _gitb_commit_state_aware() {
12594
13092
  if [ -z "$has_action" ]; then
12595
13093
  candidates+=("split")
12596
13094
  [ -z "$has_strict_mod" ] && candidates+=("amend" "fixup")
12597
- [ -z "$has_any_mod" ] && candidates+=("last" "revert")
13095
+ [ -z "$has_any_mod" ] && candidates+=("revert")
12598
13096
  fi
12599
13097
  [ -z "$has_ai" ] && candidates+=("ai")
12600
13098
  [ -z "${has_fast}${has_staged}" ] && candidates+=("fast" "staged")
@@ -12690,7 +13188,7 @@ _gitb() {
12690
13188
  'staged[only staged files]' 'no-split[disable split]'
12691
13189
  'fixup[create fixup commit]' 'amend[amend last commit]'
12692
13190
  'split[split into atomic commits]'
12693
- 'last[show last commit]' 'revert[revert a commit]' 'help[show help]'
13191
+ 'revert[revert a commit]' 'help[show help]'
12694
13192
  )
12695
13193
  _values 'commit flag' $flags
12696
13194
  return
@@ -12702,7 +13200,6 @@ _gitb() {
12702
13200
  for ((i=3; i<CURRENT; i++)); do
12703
13201
  tok="${words[$i]}"
12704
13202
  case "$tok" in
12705
- last|l) has_action="last" ;;
12706
13203
  revert|rev) has_action="revert" ;;
12707
13204
  fixup|fix|x) has_action="fixup" ;;
12708
13205
  amend|am|a) has_action="amend" ;;
@@ -12719,7 +13216,8 @@ _gitb() {
12719
13216
  done
12720
13217
  flags=()
12721
13218
  case "$has_action" in
12722
- last|revert)
13219
+ revert)
13220
+ [ -z "$has_push" ] && flags+=('push[commit and push]')
12723
13221
  ;;
12724
13222
  amend|fixup)
12725
13223
  [ -z "${has_fast}${has_staged}" ] && flags+=('fast[fast commit (git add .)]' 'staged[only staged files]')
@@ -12731,7 +13229,7 @@ _gitb() {
12731
13229
  if [ -z "$has_action" ]; then
12732
13230
  flags+=('split[split into atomic commits]')
12733
13231
  [ -z "$has_strict_mod" ] && flags+=('amend[amend last commit]' 'fixup[create fixup commit]')
12734
- [ -z "$has_any_mod" ] && flags+=('last[reuse last message]' 'revert[revert a commit]')
13232
+ [ -z "$has_any_mod" ] && flags+=('revert[revert a commit]')
12735
13233
  fi
12736
13234
  [ -z "$has_ai" ] && flags+=('ai[AI-generated message]')
12737
13235
  [ -z "${has_fast}${has_staged}" ] && flags+=('fast[fast commit (git add .)]' 'staged[only staged files]')
@@ -12770,6 +13268,7 @@ _gitb() {
12770
13268
  local -a cmds
12771
13269
  cmds=(
12772
13270
  'commit:Interactive commit (alias: c, co, com)'
13271
+ 'edit:Rewrite the last commit message (alias: ed, ee)'
12773
13272
  'push:Push current branch (alias: p, ps, pus)'
12774
13273
  'pull:Pull updates (alias: pu, pl, pul)'
12775
13274
  'merge:Merge a branch (alias: m, me)'
@@ -12810,7 +13309,10 @@ _gitb() {
12810
13309
  'no-split[disable split]' 'fixup[create fixup commit]' \
12811
13310
  'amend[amend last commit]' 'split[split into atomic commits]' \
12812
13311
  'splitp[split and push]' \
12813
- 'last[show last commit]' 'revert[revert a commit]' 'help[show help]'
13312
+ 'revert[revert a commit]' 'help[show help]'
13313
+ ;;
13314
+ edit|ed|ee)
13315
+ _values 'edit mode' 'pick[reword any recent commit]' 'branch[rename current branch]' 'help[show help]'
12814
13316
  ;;
12815
13317
  push|p|ps|pus)
12816
13318
  _values 'push mode' 'yes[skip confirmation]' 'force[force push]' 'list[show pending pushes]' 'help[show help]'
@@ -12944,6 +13446,9 @@ complete -c gitb -n __gitb_no_subcmd -a commit -d 'Interactive commit'
12944
13446
  complete -c gitb -n __gitb_no_subcmd -a c -d 'Alias of commit'
12945
13447
  complete -c gitb -n __gitb_no_subcmd -a co -d 'Alias of commit'
12946
13448
  complete -c gitb -n __gitb_no_subcmd -a com -d 'Alias of commit'
13449
+ complete -c gitb -n __gitb_no_subcmd -a edit -d 'Rewrite the last commit message'
13450
+ complete -c gitb -n __gitb_no_subcmd -a ed -d 'Alias of edit'
13451
+ complete -c gitb -n __gitb_no_subcmd -a ee -d 'Alias of edit'
12947
13452
  complete -c gitb -n __gitb_no_subcmd -a push -d 'Push current branch'
12948
13453
  complete -c gitb -n __gitb_no_subcmd -a p -d 'Alias of push'
12949
13454
  complete -c gitb -n __gitb_no_subcmd -a pull -d 'Pull updates'
@@ -13004,8 +13509,10 @@ function __gitb_at_commit_extra
13004
13509
  test (count $tokens) -ge 3; or return 1
13005
13510
  contains -- $tokens[2] commit c co com
13006
13511
  end
13007
- complete -c gitb -n __gitb_at_commit_pos2 -a "ai fast fasts ff ffp push fastp scope msg ticket staged no-split fixup amend split splitp aisplit aisplitp aip aif aifp last revert help"
13008
- complete -c gitb -n __gitb_at_commit_extra -a "ai fast push scope msg ticket staged no-split fixup amend split last revert help"
13512
+ complete -c gitb -n __gitb_at_commit_pos2 -a "ai fast fasts ff ffp push fastp scope msg ticket staged no-split fixup amend split splitp aisplit aisplitp aip aif aifp revert revertp revp rp help"
13513
+ complete -c gitb -n __gitb_at_commit_extra -a "ai fast push scope msg ticket staged no-split fixup amend split revert help"
13514
+ set -l __gitb_edit "__gitb_using_cmd edit ed ee; and __gitb_at_position 2"
13515
+ complete -c gitb -n "$__gitb_edit" -a "pick branch help"
13009
13516
  set -l __gitb_push "__gitb_using_cmd push p ps pus; and __gitb_at_position 2"
13010
13517
  complete -c gitb -n "$__gitb_push" -a "yes force list help"
13011
13518
  set -l __gitb_pull "__gitb_using_cmd pull pu pl pul; and __gitb_at_position 2"
@@ -13330,12 +13837,19 @@ function print_help {
13330
13837
  echo
13331
13838
  echo -e "usage: ${YELLOW}gitb <command> [mode]${ENDCOLOR}"
13332
13839
  echo
13840
+ if [ "$GITBASHER_NO_REPO" = "true" ]; then
13841
+ echo -e "${YELLOW}⚠ Not inside a git repository.${ENDCOLOR}"
13842
+ echo -e "Only ${BOLD}clone${NORMAL}, ${BOLD}init${NORMAL}, ${BOLD}config${NORMAL}, ${BOLD}update${NORMAL}, and ${BOLD}uninstall${NORMAL} are available here."
13843
+ echo -e "Run ${GREEN}gitb clone <url>${ENDCOLOR} to clone a repo, ${GREEN}gitb init${ENDCOLOR} to start a new one, or ${GREEN}cd${ENDCOLOR} into an existing one."
13844
+ echo
13845
+ fi
13333
13846
  local CMD=26
13334
13847
  local hdr="${YELLOW}%s${ENDCOLOR}\n"
13335
13848
  local row=" ${BOLD}%-*s${NORMAL} %s\n"
13336
13849
  printf "$hdr" "DAILY"
13337
13850
  printf "$row" "$CMD" "status (s)" "Show repo state and changed files"
13338
13851
  printf "$row" "$CMD" "commit (c, co, com)" "Create commits — interactive, AI, amend, revert"
13852
+ printf "$row" "$CMD" "edit (ed, ee)" "Rewrite the last commit message (${GREEN}git commit --amend${ENDCOLOR})"
13339
13853
  printf "$row" "$CMD" "push (p, ps, pus)" "Push current branch safely"
13340
13854
  printf "$row" "$CMD" "pull (pu, pl, pul)" "Pull from remote"
13341
13855
  printf "$row" "$CMD" "sync (sy)" "Sync current branch with $main_branch"
@@ -13362,6 +13876,7 @@ function print_help {
13362
13876
  printf "$row" "$CMD" "wip (w)" "Stash work-in-progress, optionally back up remote"
13363
13877
  echo
13364
13878
  printf "$hdr" "SETUP"
13879
+ printf "$row" "$CMD" "clone (cl, clo)" "Clone a remote repo and initialize gitbasher in it"
13365
13880
  printf "$row" "$CMD" "origin (or, o, remote)" "Manage remotes"
13366
13881
  printf "$row" "$CMD" "hook (ho, hk)" "Manage git hooks"
13367
13882
  printf "$row" "$CMD" "worktree (wt, tree)" "Manage git worktrees"
@@ -13372,9 +13887,14 @@ function print_help {
13372
13887
  echo -e "Run ${YELLOW}gitb <command> help${ENDCOLOR} for modes and examples"
13373
13888
  exit
13374
13889
  }
13375
- project_name="$(get_repo_name)"
13376
- repo_url="$(get_repo)"
13377
- if [[ $is_first == "true" ]]; then
13890
+ if [ "$GITBASHER_NO_REPO" = "true" ]; then
13891
+ project_name="global config"
13892
+ repo_url=""
13893
+ else
13894
+ project_name="$(get_repo_name)"
13895
+ repo_url="$(get_repo)"
13896
+ fi
13897
+ if [[ $is_first == "true" ]]; then
13378
13898
  git config --local gitbasher.scopes ""
13379
13899
  echo -e "${GREEN}Thanks for using gitbasher in project '$project_name'${ENDCOLOR}"
13380
13900
  print_configuration
@@ -13406,6 +13926,9 @@ case "$1" in
13406
13926
  commit|c|co|com)
13407
13927
  commit_script "${@:2}"
13408
13928
  ;;
13929
+ edit|ed|ee)
13930
+ edit_script "${@:2}"
13931
+ ;;
13409
13932
  push|p|ps|pus)
13410
13933
  push_script $2
13411
13934
  ;;
@@ -13457,6 +13980,9 @@ case "$1" in
13457
13980
  origin|or|o|remote)
13458
13981
  origin_script "$2" "$3"
13459
13982
  ;;
13983
+ clone|cl|clo)
13984
+ clone_script "$2" "$3"
13985
+ ;;
13460
13986
  update|up|upd)
13461
13987
  update_script "$2"
13462
13988
  ;;