Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ $ git jira-branch create MYAPP-1234
- [Reset an already existing branch](#Resetanalreadyexistingbranch)
- [Open tickets in your browser](#Openticketsinyourbrowser)
- [Show ticket info on your terminal](#Showticketinfoonyourterminal)
- [Attach GitHub PR links to the current branch ticket](#AttachGitHubPRlinkstothecurrentbranchticket)
- [List branches associated with jira tickets](#Listbranchesassociatedwithjiratickets)
- [`wizard` mode](#wizardmode)
- [Setup](#Setup)
Expand Down Expand Up @@ -126,6 +127,45 @@ Will create output like this:
> Long lines in the description of the ticket are wrapped to fit a line width of<br>
> 80 characters to make it easier to read.

### <a name='AttachGitHubPRlinkstothecurrentbranchticket'></a>Attach GitHub PR links to the current branch ticket

The `link-pr` command resolves the Jira ticket from the current branch, or uses
an explicit Jira key if provided, detects the GitHub repository from your git
remotes, finds pull requests in that repository whose head branch contains the
Jira key, and adds missing Jira remote links for those pull requests.

It prefers a GitHub `upstream` remote, then `origin`, and otherwise uses the
first GitHub remote. You can override that with `--remote <name>` or bypass
remote detection entirely with `--repo <owner/repo>`. Supported remote formats
include SSH (`git@github.com:...`, `ssh://git@github.com/...`) and HTTPS
(`https://github.com/...`).

By default, `link-pr` scans the newest `500` pull requests. Override that with
`--scan-limit` or `LINK_PR_SCAN_LIMIT`, or use `all` to scan full history. The
search currently scans pull requests across all GitHub PR states and matches on
the head branch name.

```bash
$ git jira-branch link-pr
> Linked 1, skipped 1.
> linked #42 https://github.com/my-org/my-repo/pull/42
> skipped #40 https://github.com/my-org/my-repo/pull/40
```

```bash
$ git jira-branch link-pr FOOX-1234
```

Examples:

```bash
git-jira-branch link-pr --scan-limit 1000 FOOX-1234
git-jira-branch link-pr --scan-limit all FOOX-1234
git-jira-branch link-pr --remote upstream FOOX-1234
git-jira-branch link-pr --repo my-org/my-repo FOOX-1234
git-jira-branch link-pr --provider github FOOX-1234
```

### <a name='Listbranchesassociatedwithjiratickets'></a>List branches associated with jira tickets

```bash
Expand Down Expand Up @@ -184,6 +224,25 @@ npm i -g git-jira-branch
export JIRA_KEY_PREFIX="MYAPP"
```

For `link-pr` only, set one GitHub token:

```bash
export GITHUB_TOKEN="YOUR_GITHUB_TOKEN"
# or
export GH_TOKEN="YOUR_GITHUB_TOKEN"
```

If both are present, `GITHUB_TOKEN` takes precedence.

Optional default scan window for `link-pr`:

```bash
export LINK_PR_SCAN_LIMIT="500"
```

`LINK_PR_SCAN_LIMIT` must be a positive integer. It only affects `link-pr` and
can be overridden per invocation with `--scan-limit`.

### <a name='Setupshellcompletions'></a>Setup shell completions

The cli can generate shell completion scripts for `bash`,`zsh` and `fish`. To
Expand Down
20 changes: 13 additions & 7 deletions src/__snapshots__/cli.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -50,31 +50,37 @@ $ git-jira-branch

COMMANDS

- create [(-b, --base text)] [(-t, --type text)] [(-r, --reset)] <jira-key>
- create [(-b, --base text)] [(-t, --type text)] [(-r, --reset)] <jira-key>
Fetches the given Jira ticket and creates an aproriately named branch for it.
The branch type (bug or feat) is determined by the ticket type. The branch name
is based on the ticket summary.

- switch <jira-key>
- switch <jira-key>
Switches to an already existing branch that is associated with the given Jira
ticket.

- delete [--force] <jira-key>
- delete [--force] <jira-key>
Deletes the branch associated with the given Jira Ticket.

- open [<jira-key>]
- open [<jira-key>]
Opens the given Jira ticket in your default browser. If no ticket is given the
jira ticket for the current branch is opened.

- info [<jira-key>]
- info [<jira-key>]
Displays information for the given Jira ticket on your terminal. If no ticket is
provided, it presents information for the Jira ticket associated with the
current branch.

- list
- link-pr [--provider text] [--scan-limit text] [--remote text] [--repo text] [<jira-key>]
Finds GitHub pull requests whose head branch contains the Jira key and adds
missing Jira remote links. Uses the current branch ticket by default, prefers
the GitHub \`upstream\` remote when present, and supports \`--remote\` / \`--repo\`
to override repository selection.

- list
Lists all branches that appear to be associated with a Jira ticket.

- tidy [--force]
- tidy [--force]
Deletes branches for tickets that are done.
"
`;
13 changes: 12 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import packageJson from '../package.json' with {type: 'json'};
import {create} from './commands/create/create.command.js';
import {deleteCommand} from './commands/delete/delete.command.js';
import {info} from './commands/info/info.command.js';
import {linkPr} from './commands/link-pr/link-pr.command.js';
import {list} from './commands/list/list.command.js';
import {open} from './commands/open/open.command.js';
import {switchCommand} from './commands/switch/switch.command.js';
import {tidy} from './commands/tidy/tidy.command.js';
import type {NoAssociatedBranch} from './schema/no-associated-branch.js';
import type {AppConfigService} from './services/app-config.js';
import type {GitClient} from './services/git-client.js';
import type {GitHubClient} from './services/github-client.js';
import type {JiraClient} from './services/jira-client.js';
import type {GitJiraBranchError} from './types.js';

Expand All @@ -30,6 +32,7 @@ const mainCommand = gitJiraBranch.pipe(
deleteCommand,
open,
info,
linkPr,
list,
tidy,
]),
Expand All @@ -48,6 +51,7 @@ export const cliEffect = (
GitJiraBranchError | ValidationError.ValidationError | NoAssociatedBranch,
| CliApp.Environment
| GitClient
| GitHubClient
| AppConfigService
| JiraClient
| CommandExecutor.CommandExecutor
Expand All @@ -61,9 +65,16 @@ export const cliEffect = (
// handled and printed by the cli library already
return Effect.void;
}
return printErrors(HelpDoc.p(Span.error(e.message)));
return printErrors(HelpDoc.p(Span.error(errorMessage(e))));
}),
);

const printErrors = (doc: HelpDoc.HelpDoc): Effect.Effect<void> =>
Console.error(HelpDoc.toAnsiText(doc));

const errorMessage = (error: unknown): string =>
error instanceof Object &&
'message' in error &&
typeof error.message === 'string'
? error.message
: String(error);
51 changes: 41 additions & 10 deletions src/commands/create/create.handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ describe('gitCreateJiraBranch', () => {
live('should create feature branch', () =>
Effect.gen(function* () {
mockAppConfigService.getAppConfig.mockReturnValue(
Effect.succeed({defaultJiraKeyPrefix: Option.none()}),
Effect.succeed({
defaultJiraKeyPrefix: Option.none(),
githubToken: Option.none(),
}),
);
mockGitClient.createGitBranch.mockReturnValue(Effect.succeed(undefined));
mockJiraClient.getJiraIssue.mockReturnValue(
Expand Down Expand Up @@ -68,7 +71,10 @@ describe('gitCreateJiraBranch', () => {
live('should create bugfix branch', () =>
Effect.gen(function* () {
mockAppConfigService.getAppConfig.mockReturnValue(
Effect.succeed({defaultJiraKeyPrefix: Option.none()}),
Effect.succeed({
defaultJiraKeyPrefix: Option.none(),
githubToken: Option.none(),
}),
);
mockGitClient.createGitBranch.mockReturnValue(Effect.succeed(undefined));
mockJiraClient.getJiraIssue.mockReturnValue(
Expand Down Expand Up @@ -124,7 +130,10 @@ describe('gitCreateJiraBranch', () => {
live('should use custom type', () =>
Effect.gen(function* () {
mockAppConfigService.getAppConfig.mockReturnValue(
Effect.succeed({defaultJiraKeyPrefix: Option.none()}),
Effect.succeed({
defaultJiraKeyPrefix: Option.none(),
githubToken: Option.none(),
}),
);
mockGitClient.createGitBranch.mockReturnValue(Effect.succeed(undefined));
mockJiraClient.getJiraIssue.mockReturnValue(
Expand Down Expand Up @@ -160,7 +169,10 @@ describe('gitCreateJiraBranch', () => {
live('should create feature branch from base branch', () =>
Effect.gen(function* () {
mockAppConfigService.getAppConfig.mockReturnValue(
Effect.succeed({defaultJiraKeyPrefix: Option.none()}),
Effect.succeed({
defaultJiraKeyPrefix: Option.none(),
githubToken: Option.none(),
}),
);
mockGitClient.createGitBranchFrom.innerMock.mockSuccessValue(undefined);
mockJiraClient.getJiraIssue.mockReturnValue(
Expand Down Expand Up @@ -211,7 +223,10 @@ describe('gitCreateJiraBranch', () => {
live('should reset existing branch', () =>
Effect.gen(function* () {
mockAppConfigService.getAppConfig.mockReturnValue(
Effect.succeed({defaultJiraKeyPrefix: Option.none()}),
Effect.succeed({
defaultJiraKeyPrefix: Option.none(),
githubToken: Option.none(),
}),
);
mockGitClient.createGitBranchFrom.innerMock.mockSuccessValue(undefined);
mockJiraClient.getJiraIssue.mockReturnValue(
Expand Down Expand Up @@ -251,7 +266,10 @@ describe('gitCreateJiraBranch', () => {
live('should create branch with reset for non existing branch', () =>
Effect.gen(function* () {
mockAppConfigService.getAppConfig.mockReturnValue(
Effect.succeed({defaultJiraKeyPrefix: Option.none()}),
Effect.succeed({
defaultJiraKeyPrefix: Option.none(),
githubToken: Option.none(),
}),
);
mockGitClient.createGitBranchFrom.innerMock.mockSuccessValue(undefined);
mockJiraClient.getJiraIssue.mockReturnValue(
Expand Down Expand Up @@ -292,6 +310,7 @@ describe('gitCreateJiraBranch', () => {
Effect.gen(function* () {
mockAppConfigService.getAppConfig.mockSuccessValue({
defaultJiraKeyPrefix: Option.none(),
githubToken: Option.none(),
});
mockJiraClient.getJiraIssue.mockSuccessValue(dummyJiraIssue);
mockGitClient.listBranches.mockSuccessValue(
Expand Down Expand Up @@ -328,7 +347,10 @@ describe('gitCreateJiraBranch', () => {
live('should consider defaultJiraKeyPrefix', () =>
Effect.gen(function* () {
mockAppConfigService.getAppConfig.mockReturnValue(
Effect.succeed({defaultJiraKeyPrefix: Option.some('DUMMYAPP')}),
Effect.succeed({
defaultJiraKeyPrefix: Option.some('DUMMYAPP'),
githubToken: Option.none(),
}),
);
mockGitClient.createGitBranch.mockReturnValue(Effect.succeed(undefined));
mockJiraClient.getJiraIssue.mockReturnValue(
Expand Down Expand Up @@ -373,7 +395,10 @@ describe('gitCreateJiraBranch', () => {
live('should allow overriding defaultJiraKeyPrefix', () =>
Effect.gen(function* () {
mockAppConfigService.getAppConfig.mockReturnValue(
Effect.succeed({defaultJiraKeyPrefix: Option.some('OTHERAPP')}),
Effect.succeed({
defaultJiraKeyPrefix: Option.some('OTHERAPP'),
githubToken: Option.none(),
}),
);
mockGitClient.createGitBranch.mockReturnValue(Effect.succeed(undefined));
mockJiraClient.getJiraIssue.mockReturnValue(
Expand Down Expand Up @@ -421,7 +446,10 @@ describe('gitCreateJiraBranch', () => {
live('should handle umlauts and other chars in summary', () =>
Effect.gen(function* () {
mockAppConfigService.getAppConfig.mockReturnValue(
Effect.succeed({defaultJiraKeyPrefix: Option.some('OTHERAPP')}),
Effect.succeed({
defaultJiraKeyPrefix: Option.some('OTHERAPP'),
githubToken: Option.none(),
}),
);
mockGitClient.createGitBranch.mockReturnValue(Effect.succeed(undefined));
mockJiraClient.getJiraIssue.mockReturnValue(
Expand Down Expand Up @@ -475,7 +503,10 @@ describe('gitCreateJiraBranch', () => {
live('should create branch with no type', () =>
Effect.gen(function* () {
mockAppConfigService.getAppConfig.mockReturnValue(
Effect.succeed({defaultJiraKeyPrefix: Option.none()}),
Effect.succeed({
defaultJiraKeyPrefix: Option.none(),
githubToken: Option.none(),
}),
);
mockGitClient.createGitBranch.mockReturnValue(Effect.succeed(undefined));
mockJiraClient.getJiraIssue.mockReturnValue(
Expand Down
10 changes: 8 additions & 2 deletions src/commands/delete/delete.handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ describe('deleteBranch', () => {
live('should delete branch', () =>
Effect.gen(function* () {
mockAppConfigService.getAppConfig.mockReturnValue(
Effect.succeed({defaultJiraKeyPrefix: Option.none()}),
Effect.succeed({
defaultJiraKeyPrefix: Option.none(),
githubToken: Option.none(),
}),
);
mockGitClient.deleteBranch.mockReturnValue(Effect.succeed(undefined));
mockGitClient.listBranches.mockSuccessValue(
Expand Down Expand Up @@ -54,7 +57,10 @@ describe('deleteBranch', () => {
live('should error on non merged branch', () =>
Effect.gen(function* () {
mockAppConfigService.getAppConfig.mockReturnValue(
Effect.succeed({defaultJiraKeyPrefix: Option.none()}),
Effect.succeed({
defaultJiraKeyPrefix: Option.none(),
githubToken: Option.none(),
}),
);
mockGitClient.deleteBranch.mockFailValue(
new BranchNotMerged({
Expand Down
2 changes: 2 additions & 0 deletions src/commands/info/info.handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('ticketInfo', () => {
Effect.gen(function* () {
mockAppConfigService.getAppConfig.mockSuccessValue({
defaultJiraKeyPrefix: Option.some('DUMMYAPP'),
githubToken: Option.none(),
});

mockJiraClient.getJiraIssue.mockSuccessValue(dummyJiraIssue);
Expand All @@ -44,6 +45,7 @@ describe('ticketInfoForCurrentBranch', () => {
Effect.gen(function* () {
mockAppConfigService.getAppConfig.mockSuccessValue({
defaultJiraKeyPrefix: Option.some('DUMMYAPP'),
githubToken: Option.none(),
});

mockJiraClient.getJiraIssue.mockSuccessValue(dummyJiraIssue);
Expand Down
Loading
Loading