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 +21 -0
- package/README.md +130 -0
- package/bin/cli.js +206 -0
- package/package.json +31 -0
- package/shell/git-extras.sh +866 -0
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
|