git-jira-shortcuts 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chipallen2
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # git-jira-shortcuts
2
+
3
+ Git + Jira workflow shortcuts for zsh — interactive branch switching, auto-prefixed commits, Jira integration, and more.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g git-jira-shortcuts
9
+ git-jira-shortcuts init
10
+ source ~/.zshrc
11
+ ```
12
+
13
+ The `init` command will walk you through configuration and add the necessary source lines to your `~/.zshrc`.
14
+
15
+ ## Configuration
16
+
17
+ Running `git-jira-shortcuts init` creates `~/.git-jira-shortcuts.env` with:
18
+
19
+ | Variable | Required | Description |
20
+ |----------|----------|-------------|
21
+ | `GJS_TICKET_PREFIX` | Yes | Jira project key (e.g. `MOTOMATE`, `PROJ`) |
22
+ | `GJS_JIRA_DOMAIN` | Yes | Jira domain (e.g. `yourco.atlassian.net`) |
23
+ | `GJS_JIRA_API_TOKEN` | Yes | Base64-encoded Jira API token |
24
+ | `GJS_BRANCH_WEBHOOK_URL` | No | Optional webhook for branch name generation |
25
+
26
+ ### Generating your Jira API token
27
+
28
+ 1. Go to [Atlassian API tokens](https://id.atlassian.com/manage-profile/security/api-tokens)
29
+ 2. Create a new token
30
+ 3. Base64-encode it:
31
+ ```bash
32
+ echo -n "your-email@company.com:your-api-token" | base64
33
+ ```
34
+ 4. Paste the result during `git-jira-shortcuts init`
35
+
36
+ ## Commands
37
+
38
+ ### Status & Info
39
+ | Command | Alias | Description |
40
+ |---------|-------|-------------|
41
+ | `gs` | `gstatus` | Clean git status with remote sync info |
42
+ | `glist` | `gl` | List files pending in this branch |
43
+ | `grecent` | — | Show recently checked out branches (last 10) |
44
+ | `grepos` | `repos` | Show all repo clones and their current branch |
45
+ | `ghelp` | — | Show all available commands |
46
+
47
+ ### Branching
48
+ | Command | Alias | Description |
49
+ |---------|-------|-------------|
50
+ | `gswitch [branch]` | `gw` | Switch branches with interactive picker (↑/↓ arrows) |
51
+ | `gstart <branch\|ticket#>` | `gt`, `gcreate` | Create or switch to branch, auto-names from Jira |
52
+ | `gdelete [branch]` | `gdel` | Delete feature branch if clean and pushed |
53
+ | `gmerge [branch]` | `gm` | Merge branch into current if no conflicts |
54
+
55
+ ### Committing & Pushing
56
+ | Command | Alias | Description |
57
+ |---------|-------|-------------|
58
+ | `gcfast <message>` | `gf` | Stage all, commit (skip hooks), push. Auto-prefixes ticket ID. |
59
+ | `gcommit <message>` | `gc` | Stage all, commit (with hooks), push. Auto-prefixes ticket ID. |
60
+ | `gpush [branch]` | `gpu` | Push with upstream tracking |
61
+ | `gp` | — | Pull without rebase or editor |
62
+
63
+ ### Utilities
64
+ | Command | Alias | Description |
65
+ |---------|-------|-------------|
66
+ | `greset [file]` | `gr` | Reset a file with confirmation (interactive if no file given) |
67
+ | `gdiff [branch]` | — | List files changed vs target branch + GitHub compare link |
68
+ | `testJira` | `tj` | Test Jira API connection |
69
+
70
+ ## Features
71
+
72
+ ### Interactive branch picker
73
+ When you run `gswitch`, `gdelete`, or `gmerge` without a branch name, you get an arrow-key menu of your recent branches:
74
+
75
+ ```
76
+ Switch to branch (↑/↓ select, Enter confirm, q cancel):
77
+ ● feature-branch-1
78
+ ○ feature-branch-2
79
+ ○ develop
80
+ ```
81
+
82
+ ### Ticket number shortcuts
83
+ Type a ticket number instead of a full branch name:
84
+ ```bash
85
+ gw 1234 # Switches to PROJ-1234-* branch
86
+ gstart 1234 # Creates PROJ-1234-<jira-title> branch
87
+ ```
88
+
89
+ ### Auto-prefixed commits
90
+ When on a ticket branch, commit messages are auto-prefixed:
91
+ ```bash
92
+ gc fix the bug # Commits as "PROJ-1234: fix the bug"
93
+ ```
94
+
95
+ ### Branch shorthand
96
+ - `m` → `master`
97
+ - `d` → `develop`
98
+
99
+ ## Update
100
+
101
+ ```bash
102
+ npm update -g git-jira-shortcuts
103
+ ```
104
+
105
+ ## Reconfigure
106
+
107
+ ```bash
108
+ git-jira-shortcuts init
109
+ source ~/.zshrc
110
+ ```
111
+
112
+ ## CLI
113
+
114
+ ```bash
115
+ git-jira-shortcuts init # Interactive setup wizard
116
+ git-jira-shortcuts path # Print path to shell script
117
+ git-jira-shortcuts --version # Print version
118
+ git-jira-shortcuts --help # Show help
119
+ ```
120
+
121
+ ## Requirements
122
+
123
+ - zsh
124
+ - Node.js >= 16
125
+ - `jq` (for Jira JSON parsing)
126
+ - `curl` (for Jira API calls)
127
+
128
+ ## License
129
+
130
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const readline = require('readline');
6
+ const os = require('os');
7
+
8
+ const SHELL_SCRIPT = path.resolve(__dirname, '..', 'shell', 'git-extras.sh');
9
+ const ENV_FILE = path.join(os.homedir(), '.git-jira-shortcuts.env');
10
+ const ZSHRC = path.join(os.homedir(), '.zshrc');
11
+ const SOURCE_MARKER = '# git-jira-shortcuts';
12
+ const PKG = require(path.resolve(__dirname, '..', 'package.json'));
13
+
14
+ // ── helpers ──────────────────────────────────────────────────────────
15
+
16
+ function ask(rl, question, defaultVal) {
17
+ const suffix = defaultVal ? ` (${defaultVal})` : '';
18
+ return new Promise((resolve) => {
19
+ rl.question(`${question}${suffix}: `, (answer) => {
20
+ resolve(answer.trim() || defaultVal || '');
21
+ });
22
+ });
23
+ }
24
+
25
+ function readExistingEnv() {
26
+ if (!fs.existsSync(ENV_FILE)) return {};
27
+ const content = fs.readFileSync(ENV_FILE, 'utf8');
28
+ const vars = {};
29
+ for (const line of content.split('\n')) {
30
+ const m = line.match(/^export\s+(\w+)="(.*)"/);
31
+ if (m) vars[m[1]] = m[2];
32
+ }
33
+ return vars;
34
+ }
35
+
36
+ function writeEnvFile(vars) {
37
+ const lines = [
38
+ '# git-jira-shortcuts configuration',
39
+ `# Generated by git-jira-shortcuts init on ${new Date().toISOString()}`,
40
+ '',
41
+ `export GJS_TICKET_PREFIX="${vars.GJS_TICKET_PREFIX || ''}"`,
42
+ `export GJS_JIRA_DOMAIN="${vars.GJS_JIRA_DOMAIN || ''}"`,
43
+ `export GJS_JIRA_API_TOKEN="${vars.GJS_JIRA_API_TOKEN || ''}"`,
44
+ `export GJS_BRANCH_WEBHOOK_URL="${vars.GJS_BRANCH_WEBHOOK_URL || ''}"`,
45
+ '',
46
+ ];
47
+ fs.writeFileSync(ENV_FILE, lines.join('\n'), 'utf8');
48
+ }
49
+
50
+ function ensureZshrcSourceLines() {
51
+ let content = '';
52
+ if (fs.existsSync(ZSHRC)) {
53
+ content = fs.readFileSync(ZSHRC, 'utf8');
54
+ }
55
+
56
+ if (content.includes(SOURCE_MARKER)) {
57
+ console.log(' ~/.zshrc already has source lines — skipping.');
58
+ return;
59
+ }
60
+
61
+ const block = [
62
+ '',
63
+ SOURCE_MARKER,
64
+ '[ -f ~/.git-jira-shortcuts.env ] && source ~/.git-jira-shortcuts.env',
65
+ 'source "$(git-jira-shortcuts path)"',
66
+ '',
67
+ ].join('\n');
68
+
69
+ fs.appendFileSync(ZSHRC, block, 'utf8');
70
+ console.log(' ✅ Source lines added to ~/.zshrc');
71
+ }
72
+
73
+ // ── commands ─────────────────────────────────────────────────────────
74
+
75
+ async function runInit() {
76
+ const existing = readExistingEnv();
77
+ const hasExisting = Object.keys(existing).length > 0;
78
+
79
+ console.log('');
80
+ console.log('🔧 git-jira-shortcuts init');
81
+ console.log('─'.repeat(40));
82
+
83
+ if (hasExisting) {
84
+ console.log(' Found existing config at ~/.git-jira-shortcuts.env');
85
+ console.log(' Press Enter to keep current values shown in parentheses.');
86
+ console.log('');
87
+ }
88
+
89
+ const rl = readline.createInterface({
90
+ input: process.stdin,
91
+ output: process.stdout,
92
+ });
93
+
94
+ try {
95
+ const ticketPrefix = await ask(
96
+ rl,
97
+ ' Jira project key (e.g. MOTOMATE, PROJ)',
98
+ existing.GJS_TICKET_PREFIX
99
+ );
100
+
101
+ const jiraDomain = await ask(
102
+ rl,
103
+ ' Jira domain (e.g. yourco.atlassian.net)',
104
+ existing.GJS_JIRA_DOMAIN
105
+ );
106
+
107
+ console.log('');
108
+ console.log(' To generate a Jira API token:');
109
+ console.log(' 1. Go to https://id.atlassian.com/manage-profile/security/api-tokens');
110
+ console.log(' 2. Create a token');
111
+ console.log(' 3. Base64-encode it: echo -n "email:token" | base64');
112
+ console.log('');
113
+
114
+ const jiraToken = await ask(
115
+ rl,
116
+ ' Jira API token (base64)',
117
+ existing.GJS_JIRA_API_TOKEN
118
+ );
119
+
120
+ const webhookUrl = await ask(
121
+ rl,
122
+ ' Branch name webhook URL (optional, Enter to skip)',
123
+ existing.GJS_BRANCH_WEBHOOK_URL
124
+ );
125
+
126
+ console.log('');
127
+ console.log(' Writing config...');
128
+
129
+ writeEnvFile({
130
+ GJS_TICKET_PREFIX: ticketPrefix,
131
+ GJS_JIRA_DOMAIN: jiraDomain,
132
+ GJS_JIRA_API_TOKEN: jiraToken,
133
+ GJS_BRANCH_WEBHOOK_URL: webhookUrl,
134
+ });
135
+ console.log(' ✅ Config written to ~/.git-jira-shortcuts.env');
136
+
137
+ ensureZshrcSourceLines();
138
+
139
+ console.log('');
140
+ console.log(' 🎉 Setup complete! Reload your shell:');
141
+ console.log(' source ~/.zshrc');
142
+ console.log('');
143
+ console.log(' Then try:');
144
+ console.log(' ghelp — list all commands');
145
+ console.log(' testJira — verify Jira connection');
146
+ console.log('');
147
+ } finally {
148
+ rl.close();
149
+ }
150
+ }
151
+
152
+ function runPath() {
153
+ console.log(SHELL_SCRIPT);
154
+ }
155
+
156
+ function runHelp() {
157
+ console.log(`
158
+ git-jira-shortcuts v${PKG.version}
159
+ Git + Jira workflow shortcuts for zsh
160
+
161
+ Usage:
162
+ git-jira-shortcuts init Interactive setup wizard
163
+ git-jira-shortcuts path Print path to shell script (used by source)
164
+ git-jira-shortcuts --version Print version
165
+ git-jira-shortcuts --help Show this help
166
+
167
+ After init, these zsh commands are available:
168
+ gs Clean git status gw/gswitch Switch branches
169
+ gf/gcfast Fast commit + push gc/gcommit Commit + push
170
+ gt/gstart Create branch from Jira gm/gmerge Safe merge
171
+ gpu/gpush Push with tracking gdel/gdelete Delete branch
172
+ gl/glist List pending files gr/greset Reset files
173
+ gdiff Diff vs target branch grecent Recent branches
174
+ grepos Show all repo branches testJira Test Jira API
175
+ ghelp Show all commands
176
+ `);
177
+ }
178
+
179
+ // ── main ─────────────────────────────────────────────────────────────
180
+
181
+ const command = process.argv[2];
182
+
183
+ switch (command) {
184
+ case 'init':
185
+ runInit().catch((err) => {
186
+ console.error('Error during init:', err.message);
187
+ process.exit(1);
188
+ });
189
+ break;
190
+ case 'path':
191
+ runPath();
192
+ break;
193
+ case '--version':
194
+ case '-v':
195
+ console.log(PKG.version);
196
+ break;
197
+ case '--help':
198
+ case '-h':
199
+ case undefined:
200
+ runHelp();
201
+ break;
202
+ default:
203
+ console.error(`Unknown command: ${command}`);
204
+ runHelp();
205
+ process.exit(1);
206
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "git-jira-shortcuts",
3
+ "version": "1.0.0",
4
+ "description": "Git + Jira workflow shortcuts for zsh — interactive branch switching, auto-prefixed commits, Jira integration, and more.",
5
+ "author": "chipallen2",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/chipallen2/git-jira-shortcuts.git"
10
+ },
11
+ "bin": {
12
+ "git-jira-shortcuts": "./bin/cli.js"
13
+ },
14
+ "files": [
15
+ "bin/",
16
+ "shell/",
17
+ "README.md"
18
+ ],
19
+ "keywords": [
20
+ "git",
21
+ "jira",
22
+ "shortcuts",
23
+ "zsh",
24
+ "cli",
25
+ "branch",
26
+ "workflow"
27
+ ],
28
+ "engines": {
29
+ "node": ">=16"
30
+ }
31
+ }
@@ -0,0 +1,866 @@
1
+ #!/usr/bin/env zsh
2
+ # git-jira-shortcuts — Git + Jira workflow shortcuts for zsh
3
+ # https://github.com/chipallen2/git-jira-shortcuts
4
+ #
5
+ # Configuration (set in ~/.git-jira-shortcuts.env):
6
+ # GJS_TICKET_PREFIX — Jira project key (e.g. MOTOMATE, PROJ)
7
+ # GJS_JIRA_DOMAIN — Jira domain (e.g. yourco.atlassian.net)
8
+ # GJS_JIRA_API_TOKEN — Base64 Jira API token
9
+ # GJS_BRANCH_WEBHOOK_URL — Optional webhook for branch name generation
10
+ # GJS_REPOS — Optional array of repo paths for grepos
11
+
12
+ # Store path to this script for self-reference (used by ghelp)
13
+ GJS_SHELL_SCRIPT_PATH="${0:A}"
14
+
15
+ ###
16
+ ### INTERNAL HELPERS
17
+ ###
18
+
19
+ _gjs_get_recent_branches() {
20
+ local reflog=$(git reflog --all --format='%gd:%gs' 2>/dev/null)
21
+ [[ -z "$reflog" ]] && return 1
22
+ local current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
23
+ echo "$reflog" | grep "checkout:" | cut -d':' -f3- | \
24
+ sed 's/.*moving from .* to //' | \
25
+ awk -v cur="$current_branch" '!seen[$0]++ && $0 != cur' | \
26
+ head -10
27
+ }
28
+
29
+ _gjs_interactive_menu() {
30
+ local -a options=("$@")
31
+ local selected=1
32
+ local total=${#options[@]}
33
+
34
+ [[ $total -eq 0 ]] && return 1
35
+
36
+ tput civis >&2 2>/dev/null
37
+
38
+ for i in {1..$total}; do
39
+ if [[ $i -eq $selected ]]; then
40
+ printf " \033[36m● %s\033[0m\n" "${options[$i]}" >&2
41
+ else
42
+ printf " ○ %s\n" "${options[$i]}" >&2
43
+ fi
44
+ done
45
+
46
+ while true; do
47
+ read -rsk1 key
48
+ case "$key" in
49
+ $'\e')
50
+ read -rsk1 key2
51
+ read -rsk1 key3
52
+ case "$key3" in
53
+ A) ((selected > 1)) && ((selected--)) ;;
54
+ B) ((selected < total)) && ((selected++)) ;;
55
+ esac
56
+ ;;
57
+ $'\n')
58
+ break
59
+ ;;
60
+ q)
61
+ printf "\033[%dB" "$((total - selected))" >&2
62
+ tput cnorm >&2 2>/dev/null
63
+ return 1
64
+ ;;
65
+ esac
66
+
67
+ printf "\033[%dA" "$total" >&2
68
+ for i in {1..$total}; do
69
+ printf "\r\033[K" >&2
70
+ if [[ $i -eq $selected ]]; then
71
+ printf " \033[36m● %s\033[0m\n" "${options[$i]}" >&2
72
+ else
73
+ printf " ○ %s\n" "${options[$i]}" >&2
74
+ fi
75
+ done
76
+ done
77
+
78
+ tput cnorm >&2 2>/dev/null
79
+ echo "${options[$selected]}"
80
+ }
81
+
82
+ _gjs_is_ticket_number() {
83
+ [[ -n "$GJS_TICKET_PREFIX" ]] && [[ $1 =~ ^[0-9]{3,6}$ ]]
84
+ }
85
+
86
+ _gjs_branch_exists_local() {
87
+ git show-ref --verify --quiet "refs/heads/$1"
88
+ }
89
+
90
+ _gjs_branch_exists_remote() {
91
+ [[ -n "$(git ls-remote --heads origin "$1" 2>/dev/null)" ]]
92
+ }
93
+
94
+ _gjs_resolve_branch_input() {
95
+ local raw_input="$1"
96
+ local scope="${2:-any}"
97
+
98
+ if [[ -z "$raw_input" ]]; then
99
+ echo "$raw_input"
100
+ return 0
101
+ fi
102
+
103
+ if [[ "$raw_input" == "m" ]]; then
104
+ echo "master"
105
+ return 0
106
+ elif [[ "$raw_input" == "d" ]]; then
107
+ echo "develop"
108
+ return 0
109
+ fi
110
+
111
+ if ! _gjs_is_ticket_number "$raw_input"; then
112
+ echo "$raw_input"
113
+ return 0
114
+ fi
115
+
116
+ local prefix="${GJS_TICKET_PREFIX}-$raw_input"
117
+ local -a matches=()
118
+
119
+ if [[ "$scope" == "local" || "$scope" == "any" ]]; then
120
+ while IFS= read -r branch; do
121
+ [[ -z "$branch" ]] && continue
122
+ if [[ "$branch" == ${prefix}* && -z ${matches[(r)$branch]} ]]; then
123
+ matches+=("$branch")
124
+ fi
125
+ done < <(git branch --format="%(refname:short)")
126
+ fi
127
+
128
+ if [[ "$scope" == "remote" || "$scope" == "any" ]]; then
129
+ while IFS= read -r branch; do
130
+ [[ -z "$branch" || "$branch" == *"->"* ]] && continue
131
+ if [[ "$branch" == origin/${prefix}* ]]; then
132
+ local short_branch="${branch#origin/}"
133
+ if [[ -z ${matches[(r)$short_branch]} ]]; then
134
+ matches+=("$short_branch")
135
+ fi
136
+ fi
137
+ done < <(git branch -r --format="%(refname:short)")
138
+ fi
139
+
140
+ if (( ${#matches[@]} == 0 )); then
141
+ echo "❌ No branches found matching $prefix" >&2
142
+ return 1
143
+ fi
144
+
145
+ if (( ${#matches[@]} == 1 )); then
146
+ echo "${matches[1]}"
147
+ return 0
148
+ fi
149
+
150
+ echo "🔢 Multiple branches found for $prefix:" >&2
151
+ local i=1
152
+ for branch in "${matches[@]}"; do
153
+ echo " $i) $branch" >&2
154
+ ((i++))
155
+ done
156
+
157
+ local choice
158
+ while true; do
159
+ read "choice?Select branch [1-${#matches[@]}]: "
160
+ if [[ "$choice" =~ '^[0-9]+$' ]] && (( choice >= 1 && choice <= ${#matches[@]} )); then
161
+ echo "${matches[$choice]}"
162
+ return 0
163
+ fi
164
+ echo "❌ Invalid selection." >&2
165
+ done
166
+ }
167
+
168
+ _gjs_sanitize_branch_name() {
169
+ local title="$1"
170
+ echo "$title" | tr '[:upper:]' '[:lower:]' | \
171
+ sed 's/[^a-z0-9]/-/g' | \
172
+ sed 's/--*/-/g' | \
173
+ sed 's/^-//' | \
174
+ sed 's/-$//' | \
175
+ cut -c1-50
176
+ }
177
+
178
+ _gjs_get_jira_story_title() {
179
+ local story_number="$1"
180
+
181
+ if [[ -z "$GJS_JIRA_API_TOKEN" ]]; then
182
+ echo "❌ GJS_JIRA_API_TOKEN not set. Run: git-jira-shortcuts init" >&2
183
+ return 1
184
+ fi
185
+ if [[ -z "$GJS_JIRA_DOMAIN" ]]; then
186
+ echo "❌ GJS_JIRA_DOMAIN not set. Run: git-jira-shortcuts init" >&2
187
+ return 1
188
+ fi
189
+ if [[ -z "$story_number" ]]; then
190
+ echo "❌ Story number is required" >&2
191
+ return 1
192
+ fi
193
+
194
+ local response
195
+ response=$(curl -sS \
196
+ -H "Authorization: Basic $GJS_JIRA_API_TOKEN" \
197
+ -H "Accept: application/json" \
198
+ "https://$GJS_JIRA_DOMAIN/rest/api/3/issue/${GJS_TICKET_PREFIX}-$story_number?fields=summary")
199
+
200
+ if [[ $? -ne 0 ]]; then
201
+ echo "❌ Jira API call failed." >&2
202
+ return 1
203
+ fi
204
+
205
+ local summary
206
+ summary=$(echo "$response" | jq -r '.fields.summary // empty')
207
+
208
+ if [[ -z "$summary" ]]; then
209
+ echo "❌ Could not find ${GJS_TICKET_PREFIX}-$story_number or extract title" >&2
210
+ return 1
211
+ fi
212
+
213
+ echo "$summary"
214
+ }
215
+
216
+ ###
217
+ ### PUBLIC COMMANDS
218
+ ###
219
+
220
+ grecent() { # grecent | Show recently checked out branches (last 10)
221
+ local reflog=$(git reflog --all --format='%gd:%gs' 2>/dev/null)
222
+ [[ -z "$reflog" ]] && echo "No git history found" && return 1
223
+ local current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
224
+ echo "$reflog" | grep "checkout:" | cut -d':' -f3- | \
225
+ sed 's/.*moving from .* to //' | \
226
+ awk -v cur="$current_branch" '!seen[$0]++ && $0 != cur' | \
227
+ head -10 | \
228
+ nl -nln | \
229
+ sed 's/^/ /'
230
+ }
231
+
232
+ unalias gs 2>/dev/null
233
+ gs() { # gs | Clean git status with remote sync info
234
+ local branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
235
+ [[ -z "$branch" ]] && echo "Not a git repo" && return 1
236
+ echo "\033[1m$branch\033[0m"
237
+ local staged=$(git diff --cached --numstat 2>/dev/null | wc -l | tr -d ' ')
238
+ local unstaged=$(git diff --numstat 2>/dev/null | wc -l | tr -d ' ')
239
+ local untracked=$(git ls-files --others --exclude-standard 2>/dev/null | wc -l | tr -d ' ')
240
+ local pending=$((staged + unstaged + untracked))
241
+ git fetch --quiet 2>/dev/null
242
+ local behind=$(git rev-list --count HEAD..@{u} 2>/dev/null || echo 0)
243
+ local ahead=$(git rev-list --count @{u}..HEAD 2>/dev/null || echo 0)
244
+ if [[ $pending -gt 0 || $ahead -gt 0 ]]; then
245
+ echo "🔴 \033[31mFiles Pending\033[0m"
246
+ elif [[ $behind -gt 0 ]]; then
247
+ echo "🔵 \033[34mBehind\033[0m"
248
+ else
249
+ echo "✅ \033[32mUp to Date\033[0m"
250
+ fi
251
+ }
252
+ alias gstatus='gs' # gstatus | Alias for gs
253
+ alias gp='git pull --no-rebase --no-edit' # gp | Pull without rebase or editor
254
+
255
+ grepos() { # grepos | Show all repo clones and their current branch
256
+ [[ -z "${GJS_REPOS+x}" ]] && echo "GJS_REPOS not defined. Run: git-jira-shortcuts init" && return 1
257
+ local max_len=0
258
+ for entry in "${GJS_REPOS[@]}"; do
259
+ local label="${entry##*:}"
260
+ (( ${#label} > max_len )) && max_len=${#label}
261
+ done
262
+ for entry in "${GJS_REPOS[@]}"; do
263
+ local dir="${entry%:*}"
264
+ local label="${entry##*:}"
265
+ if [[ -d "$dir/.git" ]]; then
266
+ local branch=$(git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null)
267
+ printf "%-${max_len}s %s\n" "$label" "${branch:-???}"
268
+ else
269
+ printf "%-${max_len}s %s\n" "$label" "\033[31m(not found)\033[0m"
270
+ fi
271
+ done
272
+ }
273
+ alias repos='grepos' # grepos | Alias for grepos
274
+
275
+ ghelp() { # ghelp | Show all git-jira-shortcuts commands
276
+ echo "🧠 GIT-JIRA-SHORTCUTS:"
277
+ echo "[arg] = Required [*arg] = Optional [arg=default] = Optional with default"
278
+ echo
279
+ local -a entries=()
280
+ grep -E "^(alias .*# |[a-zA-Z_]+\(\) \{ # )" "$GJS_SHELL_SCRIPT_PATH" | while IFS= read -r line; do
281
+ if [[ "$line" == alias* ]]; then
282
+ name=$(echo "$line" | sed 's/alias \([^=]*\)=.*/\1/')
283
+ comment=$(echo "$line" | sed 's/.*# \(.*\)/\1/')
284
+ entries+=("$name|$comment")
285
+ else
286
+ name=$(echo "$line" | sed 's/\([a-zA-Z_]*\)().*/\1/')
287
+ comment=$(echo "$line" | sed 's/.*# \(.*\)/\1/')
288
+ entries+=("$name|$comment")
289
+ fi
290
+ done
291
+ local max_name_len=0
292
+ local max_cmd_len=0
293
+ for entry in "${entries[@]}"; do
294
+ name="${entry%%|*}"
295
+ rest="${entry#*|}"
296
+ cmd="${rest%%|*}"
297
+ (( ${#name} > max_name_len )) && max_name_len=${#name}
298
+ (( ${#cmd} > max_cmd_len )) && max_cmd_len=${#cmd}
299
+ done
300
+ for entry in "${entries[@]}"; do
301
+ name="${entry%%|*}"
302
+ rest="${entry#*|}"
303
+ cmd="${rest%%|*}"
304
+ desc="${rest#*|}"
305
+ printf "%-*s | %-*s | %s\n" "$max_name_len" "$name" "$max_cmd_len" "$cmd" "$desc"
306
+ done | sort
307
+ }
308
+
309
+ gdiff() { # gdiff [*branch=m] | List files changed for PR to target branch + GitHub compare link
310
+ local target_input="${1:-m}"
311
+ local target
312
+ if ! target=$(_gjs_resolve_branch_input "$target_input" "any"); then
313
+ return 1
314
+ fi
315
+
316
+ local diff_ref="$target"
317
+ if _gjs_branch_exists_remote "$target" && ! _gjs_branch_exists_local "$target"; then
318
+ diff_ref="origin/$target"
319
+ fi
320
+
321
+ local current_branch=$(git rev-parse --abbrev-ref HEAD)
322
+ local changed_files=$(git --no-pager diff --name-only "$diff_ref"...HEAD)
323
+
324
+ if [[ -z "$changed_files" ]]; then
325
+ echo "No files changed vs $target"
326
+ else
327
+ local file_count=$(echo "$changed_files" | wc -l | tr -d ' ')
328
+ echo "$file_count files changed vs $target"
329
+ echo ""
330
+ echo "$changed_files"
331
+ fi
332
+
333
+ local remote_url=$(git remote get-url origin 2>/dev/null)
334
+ if [[ -n "$remote_url" ]]; then
335
+ local repo_path
336
+ if [[ "$remote_url" == git@github.com:* ]]; then
337
+ repo_path="${remote_url#git@github.com:}"
338
+ elif [[ "$remote_url" == https://github.com/* ]]; then
339
+ repo_path="${remote_url#https://github.com/}"
340
+ fi
341
+ repo_path="${repo_path%.git}"
342
+ if [[ -n "$repo_path" ]]; then
343
+ echo ""
344
+ echo "View full diff at:"
345
+ echo "https://github.com/${repo_path}/compare/${target}...${current_branch}?expand=1"
346
+ fi
347
+ fi
348
+ }
349
+
350
+ glist() { # glist | List files pending in this branch
351
+ git --no-pager status --short --untracked-files=all
352
+ }
353
+ alias gl='glist' # glist | Alias for glist
354
+
355
+ greset() { # greset [*file] | Reset a specific file with confirmation (interactive if no file given)
356
+ local file="$1"
357
+
358
+ _greset_file() {
359
+ local f="$1"
360
+ local file_status=$(git status --porcelain -- "$f" 2>/dev/null | head -1)
361
+ local index_status="${file_status:0:1}"
362
+ local worktree_status="${file_status:1:1}"
363
+ if [[ "$index_status" == "?" ]]; then
364
+ rm -rf "$f"
365
+ elif [[ "$index_status" == "A" ]]; then
366
+ git restore --staged "$f" 2>/dev/null
367
+ rm -rf "$f"
368
+ else
369
+ git restore --staged --worktree "$f" 2>/dev/null || git checkout HEAD -- "$f" 2>/dev/null
370
+ fi
371
+ }
372
+
373
+ if [[ -z "$file" ]]; then
374
+ local files=()
375
+ local statuses=()
376
+ while IFS= read -r line; do
377
+ local fname="${line:3}"
378
+ if [[ "$fname" == *" -> "* ]]; then
379
+ fname="${fname##* -> }"
380
+ fi
381
+ files+=("$fname")
382
+ statuses+=("${line:0:2}")
383
+ done < <(git status --porcelain)
384
+
385
+ if [[ ${#files[@]} -eq 0 ]]; then
386
+ echo "No modified files to reset."
387
+ return 0
388
+ fi
389
+
390
+ echo "Select a file to reset:"
391
+ echo " 1) ALL (reset all files)"
392
+ local i=2
393
+ for f in "${files[@]}"; do
394
+ echo " $i) $f"
395
+ ((i++))
396
+ done
397
+
398
+ echo -n "Enter number: "
399
+ read choice
400
+
401
+ if [[ "$choice" == "1" ]]; then
402
+ echo "Are you sure you want to reset ALL files? (Y/N)"
403
+ read -k user_input
404
+ echo
405
+ if [[ $user_input == "y" || $user_input == "Y" ]]; then
406
+ git restore --staged --worktree . 2>/dev/null
407
+ git clean -fd 2>/dev/null
408
+ echo "✅ Reset all files"
409
+ else
410
+ echo "❌ Cancelled"
411
+ fi
412
+ return 0
413
+ fi
414
+
415
+ local idx=$((choice - 1))
416
+ if [[ $idx -lt 1 || $idx -gt ${#files[@]} ]]; then
417
+ echo "❌ Invalid selection"
418
+ return 1
419
+ fi
420
+ file="${files[$idx]}"
421
+ fi
422
+
423
+ echo "Are you sure you want to reset $file? (Y/N)"
424
+ read -k user_input
425
+ echo
426
+ if [[ $user_input == "y" || $user_input == "Y" ]]; then
427
+ _greset_file "$file"
428
+ echo "✅ Reset $file"
429
+ else
430
+ echo "❌ Cancelled"
431
+ fi
432
+ }
433
+ alias gr='greset' # greset [*file] | Alias for greset
434
+
435
+ gswitch() { # gswitch [*branch] [--force|-f] | Switch branches (optionally bypass local-dirty guard)
436
+ local force_switch=0
437
+ local -a positional=()
438
+ local arg
439
+ for arg in "$@"; do
440
+ case "$arg" in
441
+ --force|-f)
442
+ force_switch=1
443
+ ;;
444
+ *)
445
+ positional+=("$arg")
446
+ ;;
447
+ esac
448
+ done
449
+ set -- "${positional[@]}"
450
+
451
+ if [[ $force_switch -eq 0 && -n "$(git status --porcelain)" ]]; then
452
+ echo "⚠️ You have uncommitted changes. Commit or stash before switching."
453
+ echo " Use 'gw --force [branch]' to bypass this check."
454
+ return 1
455
+ fi
456
+
457
+ local branch
458
+ branch=$(git rev-parse --abbrev-ref HEAD)
459
+ local unpushed
460
+ unpushed=$(git log origin/"$branch"..HEAD --oneline 2>/dev/null)
461
+
462
+ if [[ -n "$unpushed" ]]; then
463
+ echo "🚫 You have commits on '$branch' not pushed to origin. Push them first."
464
+ return 1
465
+ fi
466
+
467
+ if [[ -z "$1" ]]; then
468
+ local -a branches=()
469
+ while IFS= read -r b; do
470
+ branches+=("$b")
471
+ done < <(_gjs_get_recent_branches)
472
+
473
+ if [[ ${#branches[@]} -eq 0 ]]; then
474
+ echo "No recent branches found."
475
+ return 1
476
+ fi
477
+
478
+ echo "Switch to branch (↑/↓ select, Enter confirm, q cancel):"
479
+ local picked
480
+ if ! picked=$(_gjs_interactive_menu "${branches[@]}"); then
481
+ echo "Cancelled."
482
+ return 1
483
+ fi
484
+ set -- "$picked"
485
+ fi
486
+
487
+ local target_branch
488
+ if ! target_branch=$(_gjs_resolve_branch_input "$1" "any"); then
489
+ return 1
490
+ fi
491
+
492
+ if git show-ref --verify --quiet refs/heads/"$target_branch"; then
493
+ git switch "$target_branch" || return 1
494
+ else
495
+ echo "📡 Branch not found locally, checking out from remote..."
496
+ git checkout -t origin/"$target_branch" || {
497
+ echo "❌ Failed to checkout branch '$target_branch' from remote."
498
+ return 1
499
+ }
500
+ fi
501
+ echo "⬇️ Pulling latest changes..."
502
+ git pull
503
+ }
504
+ alias gw='gswitch' # gswitch [*branch] | Alias for gswitch
505
+
506
+ gcfast() { # gcfast [message] | Commit all, skip hooks, push (auto-prefix if ticket branch)
507
+ if [[ -z "$1" ]]; then
508
+ echo "Usage: gcfast \"commit message\""
509
+ return 1
510
+ fi
511
+
512
+ local message="$*"
513
+ local branch
514
+ branch=$(git rev-parse --abbrev-ref HEAD)
515
+
516
+ if [[ -n "$GJS_TICKET_PREFIX" ]]; then
517
+ local pattern="^${GJS_TICKET_PREFIX}-[0-9]+"
518
+ if [[ "$branch" =~ $pattern ]]; then
519
+ local jira_prefix="${MATCH}"
520
+ local msg_pattern="^${GJS_TICKET_PREFIX}-[0-9]+:"
521
+ if [[ ! "$message" =~ $msg_pattern ]]; then
522
+ message="$jira_prefix: $message"
523
+ echo "📝 Auto-prefixed commit message: \"$message\""
524
+ fi
525
+ fi
526
+ fi
527
+
528
+ echo "🧩 Staging all changes..."
529
+ git add .
530
+
531
+ echo "💬 Committing with message: \"$message\" (no-verify)"
532
+ if git commit -m "$message" --no-verify; then
533
+ echo "✅ Commit successful."
534
+ else
535
+ echo "ℹ️ Nothing to commit, checking if there are commits to push..."
536
+ if ! git log origin/"$branch".."$branch" --oneline | grep -q .; then
537
+ echo "❌ No commits to push and nothing to commit."
538
+ return 1
539
+ fi
540
+ fi
541
+
542
+ echo "🚀 Pushing branch '$branch' to origin..."
543
+ git push -u origin "$branch"
544
+ }
545
+ alias gf='gcfast' # gcfast [message] | Alias for gcfast
546
+
547
+ gcommit() { # gcommit [message] | Commit all with hooks and auto-prefix, then push
548
+ if [[ -z "$1" ]]; then
549
+ echo "Usage: gcommit \"commit message\""
550
+ return 1
551
+ fi
552
+
553
+ local message="$*"
554
+ local branch
555
+ branch=$(git rev-parse --abbrev-ref HEAD)
556
+
557
+ if [[ -n "$GJS_TICKET_PREFIX" ]]; then
558
+ local pattern="^${GJS_TICKET_PREFIX}-[0-9]+"
559
+ if [[ "$branch" =~ $pattern ]]; then
560
+ local jira_prefix="${MATCH}"
561
+ local msg_pattern="^${GJS_TICKET_PREFIX}-[0-9]+:"
562
+ if [[ ! "$message" =~ $msg_pattern ]]; then
563
+ message="$jira_prefix: $message"
564
+ echo "📝 Auto-prefixed commit message: \"$message\""
565
+ fi
566
+ fi
567
+ fi
568
+
569
+ echo "🧩 Staging all changes..."
570
+ git add .
571
+
572
+ echo "💬 Committing with message: \"$message\""
573
+ if git commit -m "$message"; then
574
+ echo "✅ Commit successful."
575
+ else
576
+ echo "ℹ️ Nothing to commit, checking if there are commits to push..."
577
+ if ! git log origin/"$branch".."$branch" --oneline | grep -q .; then
578
+ echo "❌ No commits to push and nothing to commit."
579
+ return 1
580
+ fi
581
+ fi
582
+
583
+ echo "🚀 Pushing branch '$branch' to origin..."
584
+ git push -u origin "$branch"
585
+ }
586
+ alias gc='gcommit' # gcommit [message] | Alias for gcommit
587
+
588
+ gstart() { # gstart [branch|ticket-number] | Create or switch to branch (Jira-aware)
589
+ local branch_input="$1"
590
+
591
+ if [[ -z "$branch_input" ]]; then
592
+ echo "Usage: gstart <branch-name|ticket-number>"
593
+ echo " branch-name: Full branch name or existing branch"
594
+ if [[ -n "$GJS_TICKET_PREFIX" ]]; then
595
+ echo " ticket-number: Numeric ID to auto-create ${GJS_TICKET_PREFIX}-#####-title branch"
596
+ fi
597
+ return 1
598
+ fi
599
+
600
+ local branch
601
+
602
+ if _gjs_is_ticket_number "$branch_input"; then
603
+ echo "📋 Fetching Jira story title for ${GJS_TICKET_PREFIX}-$branch_input..."
604
+ local story_title
605
+ if ! story_title=$(_gjs_get_jira_story_title "$branch_input"); then
606
+ echo "❌ Failed to get story title. Using simple branch name."
607
+ branch="${GJS_TICKET_PREFIX}-$branch_input"
608
+ else
609
+ echo "📝 Story title: $story_title"
610
+
611
+ if [[ -n "$GJS_BRANCH_WEBHOOK_URL" ]]; then
612
+ echo "📡 Getting branch suffix from webhook..."
613
+ local webhook_response
614
+ webhook_response=$(curl -s -X POST \
615
+ -H "Content-Type: application/json" \
616
+ -d "{\"storyTitle\": \"$story_title\"}" \
617
+ "$GJS_BRANCH_WEBHOOK_URL")
618
+
619
+ if [[ $? -eq 0 ]]; then
620
+ local branch_name=$(echo "$webhook_response" | jq -r '.branchName // ""')
621
+
622
+ if [[ -n "$branch_name" ]]; then
623
+ branch="${GJS_TICKET_PREFIX}-$branch_input-$branch_name"
624
+ echo "✅ Webhook provided branch suffix: $branch_name"
625
+ else
626
+ echo "⚠️ Webhook failed, using sanitized Jira title"
627
+ local sanitized_title
628
+ sanitized_title=$(_gjs_sanitize_branch_name "$story_title")
629
+ branch="${GJS_TICKET_PREFIX}-$branch_input-$sanitized_title"
630
+ fi
631
+ else
632
+ echo "❌ Failed to call webhook, using sanitized Jira title"
633
+ local sanitized_title
634
+ sanitized_title=$(_gjs_sanitize_branch_name "$story_title")
635
+ branch="${GJS_TICKET_PREFIX}-$branch_input-$sanitized_title"
636
+ fi
637
+ else
638
+ local sanitized_title
639
+ sanitized_title=$(_gjs_sanitize_branch_name "$story_title")
640
+ branch="${GJS_TICKET_PREFIX}-$branch_input-$sanitized_title"
641
+ fi
642
+
643
+ echo "🌿 Created branch name: $branch"
644
+ fi
645
+ else
646
+ if ! branch=$(_gjs_resolve_branch_input "$branch_input" "any"); then
647
+ return 1
648
+ fi
649
+ fi
650
+
651
+ if _gjs_branch_exists_local "$branch"; then
652
+ git checkout "$branch" || return 1
653
+ elif _gjs_branch_exists_remote "$branch"; then
654
+ echo "📡 Branch not found locally, checking out from origin..."
655
+ git checkout -t "origin/$branch" || return 1
656
+ else
657
+ git checkout -b "$branch" || return 1
658
+ fi
659
+ }
660
+ alias gt='gstart' # gstart [branch] | Alias for gstart
661
+ alias gcreate='gstart' # gstart [branch] | Alias for gstart
662
+
663
+ gmerge() { # gmerge [*branch] | Merge branch into current branch if no conflicts
664
+ local branch_input="$1"
665
+ local current_branch=$(git rev-parse --abbrev-ref HEAD)
666
+ local branch
667
+
668
+ if [[ -z "$branch_input" ]]; then
669
+ local -a branches=()
670
+ while IFS= read -r b; do
671
+ branches+=("$b")
672
+ done < <(_gjs_get_recent_branches)
673
+
674
+ if [[ ${#branches[@]} -eq 0 ]]; then
675
+ echo "No recent branches found."
676
+ return 1
677
+ fi
678
+
679
+ echo "Merge into $current_branch from (↑/↓ select, Enter confirm, q cancel):"
680
+ local picked
681
+ if ! picked=$(_gjs_interactive_menu "${branches[@]}"); then
682
+ echo "Cancelled."
683
+ return 1
684
+ fi
685
+ branch_input="$picked"
686
+ fi
687
+
688
+ if ! branch=$(_gjs_resolve_branch_input "$branch_input" "any"); then
689
+ return 1
690
+ fi
691
+
692
+ echo "🔍 Checking for potential conflicts with $branch..."
693
+ if [ -n "$(git status --porcelain)" ]; then
694
+ echo "❌ Current branch has uncommitted changes. Commit or stash changes first."
695
+ return 1
696
+ fi
697
+
698
+ echo "🔄 Switching to $branch to pull latest changes..."
699
+ if _gjs_branch_exists_local "$branch"; then
700
+ if ! git checkout "$branch"; then
701
+ echo "❌ Failed to switch to $branch"
702
+ return 1
703
+ fi
704
+ else
705
+ if _gjs_branch_exists_remote "$branch"; then
706
+ echo "📡 Branch not found locally, checking out from origin..."
707
+ if ! git fetch origin "$branch"; then
708
+ echo "❌ Failed to fetch $branch from origin"
709
+ return 1
710
+ fi
711
+ if ! git checkout -t "origin/$branch"; then
712
+ echo "❌ Failed to checkout $branch from origin"
713
+ return 1
714
+ fi
715
+ else
716
+ echo "❌ Branch '$branch' not found locally or on origin."
717
+ return 1
718
+ fi
719
+ fi
720
+
721
+ echo "⬇️ Pulling latest changes for $branch..."
722
+ if ! git pull; then
723
+ echo "❌ Failed to pull changes for $branch"
724
+ git checkout "$current_branch"
725
+ return 1
726
+ fi
727
+
728
+ echo "🔄 Switching back to $current_branch..."
729
+ if ! git checkout "$current_branch"; then
730
+ echo "❌ Failed to switch back to $current_branch"
731
+ return 1
732
+ fi
733
+
734
+ if git merge-tree $(git merge-base HEAD "$branch") HEAD "$branch" 2>/dev/null | grep -q "<<<<<<<"; then
735
+ echo "❌ Merge would create conflicts. Aborting merge."
736
+ return 1
737
+ fi
738
+
739
+ echo "✅ No conflicts detected. Merging $branch into $current_branch..."
740
+ git merge "$branch" --no-edit
741
+ }
742
+ alias gm='gmerge' # gmerge [*branch] | Alias for gmerge
743
+
744
+ gpush() { # gpush [*branch] | Push branch and create upstream tracking
745
+ local current_branch=$(git rev-parse --abbrev-ref HEAD)
746
+ local branch_input="$1"
747
+ local branch
748
+
749
+ if [ -z "$branch_input" ]; then
750
+ branch=$current_branch
751
+ else
752
+ if ! branch=$(_gjs_resolve_branch_input "$branch_input" "local"); then
753
+ return 1
754
+ fi
755
+ fi
756
+
757
+ echo "🚀 Pushing branch '$branch' to origin with upstream tracking..."
758
+ git push -u origin "$branch"
759
+ }
760
+ alias gpu='gpush' # gpush [*branch] | Alias for gpush
761
+
762
+ gdelete() { # gdelete [*branch] | Delete feature branch if clean and pushed
763
+ local current_branch=$(git rev-parse --abbrev-ref HEAD)
764
+ local branch_input="$1"
765
+ local branch
766
+
767
+ if [[ -z "$branch_input" ]]; then
768
+ local -a branches=()
769
+ while IFS= read -r b; do
770
+ branches+=("$b")
771
+ done < <(_gjs_get_recent_branches)
772
+
773
+ if [[ ${#branches[@]} -eq 0 ]]; then
774
+ echo "No recent branches found."
775
+ return 1
776
+ fi
777
+
778
+ echo "Delete branch (↑/↓ select, Enter confirm, q cancel):"
779
+ local picked
780
+ if ! picked=$(_gjs_interactive_menu "${branches[@]}"); then
781
+ echo "Cancelled."
782
+ return 1
783
+ fi
784
+ branch_input="$picked"
785
+ fi
786
+
787
+ if ! branch=$(_gjs_resolve_branch_input "$branch_input" "local"); then
788
+ return 1
789
+ fi
790
+
791
+ if [ "$branch" = "master" ] || [ "$branch" = "develop" ]; then
792
+ echo "❌ Cannot delete master or develop branches."
793
+ return 1
794
+ fi
795
+
796
+ echo "🔍 Checking if branch '$branch' is safe to delete..."
797
+
798
+ if [ "$branch" = "$current_branch" ] && [ -n "$(git status --porcelain)" ]; then
799
+ echo "❌ Branch has uncommitted changes. Commit or stash changes first."
800
+ return 1
801
+ fi
802
+
803
+ if ! git show-ref --verify --quiet refs/heads/"$branch"; then
804
+ echo "❌ Branch '$branch' does not exist locally."
805
+ return 1
806
+ fi
807
+
808
+ if git log origin/"$branch".."$branch" --oneline 2>/dev/null | grep -q .; then
809
+ echo "❌ Branch has unpushed commits. Push changes first."
810
+ return 1
811
+ fi
812
+
813
+ if ! git ls-remote --quiet origin "$branch" 2>/dev/null; then
814
+ echo "❌ Branch '$branch' does not exist on origin. Push branch first."
815
+ return 1
816
+ fi
817
+
818
+ echo "✅ Branch is safe to delete."
819
+
820
+ if [ "$current_branch" != "master" ]; then
821
+ echo "🔄 Switching to master branch..."
822
+ if ! git checkout master; then
823
+ echo "❌ Failed to switch to master branch. Aborting deletion."
824
+ return 1
825
+ fi
826
+ fi
827
+
828
+ echo "🗑️ Deleting local branch '$branch'..."
829
+ git branch -d "$branch"
830
+ }
831
+ alias gdel='gdelete' # gdelete [*branch] | Alias for gdelete
832
+
833
+ testJira() { # testJira | Test Jira API connection
834
+ echo "🧪 Testing Jira API connection..."
835
+
836
+ if [[ -z "$GJS_JIRA_API_TOKEN" || -z "$GJS_JIRA_DOMAIN" || -z "$GJS_TICKET_PREFIX" ]]; then
837
+ echo "❌ Jira not configured. Run: git-jira-shortcuts init"
838
+ return 1
839
+ fi
840
+
841
+ echo "📡 Testing API call to $GJS_JIRA_DOMAIN..."
842
+ local response
843
+ response=$(curl -sS \
844
+ -H "Authorization: Basic $GJS_JIRA_API_TOKEN" \
845
+ -H "Accept: application/json" \
846
+ "https://$GJS_JIRA_DOMAIN/rest/api/3/myself" 2>/dev/null)
847
+
848
+ if [[ $? -ne 0 ]]; then
849
+ echo "❌ Jira API call failed. Token might be expired or malformed."
850
+ echo " Run: git-jira-shortcuts init"
851
+ return 1
852
+ fi
853
+
854
+ local display_name
855
+ display_name=$(echo "$response" | jq -r '.displayName // empty')
856
+
857
+ if [[ -z "$display_name" ]]; then
858
+ echo "❌ Jira API test failed."
859
+ echo "$response"
860
+ return 1
861
+ fi
862
+
863
+ echo "✅ Jira API is working! Authenticated as: $display_name"
864
+ return 0
865
+ }
866
+ alias tj='testJira' # testJira | Test Jira API connection