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
52 changes: 8 additions & 44 deletions internal/commands/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import (
"errors"
"fmt"
"os/exec"
"strconv"
"strings"

"github.com/atotto/clipboard"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/untillpro/goutils/logger"
"github.com/untillpro/qs/gitcmds"
"github.com/untillpro/qs/internal/issue"
"github.com/untillpro/qs/internal/jira"
"github.com/untillpro/qs/internal/notes"
"github.com/untillpro/qs/utils"
Expand Down Expand Up @@ -95,17 +95,15 @@ func Dev(cmd *cobra.Command, wd string, doDelete bool, ignoreHook bool, args []s
return err
}

issueNum, githubIssueURL, isGithubIssue, err := argContainsGithubIssueLink(wd, args...)
issueInfo, err := issue.ParseIssueFromArgs(wd, args...)
if err != nil {
return err
}

_, isJiraIssue := jira.GetJiraTicketIDFromArgs(args...)

switch {
case isGithubIssue:
branch, notes, err = gitcmds.BuildDevBranchName(githubIssueURL)
case isJiraIssue:
switch issueInfo.Type {
case issue.GitHub:
branch, notes, err = gitcmds.BuildDevBranchName(issueInfo.URL)
case issue.Jira:
branch, notes, err = jira.GetJiraBranchName(args...)
default:
branch, notes, err = utils.GetBranchName(false, args...)
Expand Down Expand Up @@ -151,8 +149,8 @@ func Dev(cmd *cobra.Command, wd string, doDelete bool, ignoreHook bool, args []s
return err
}

if isGithubIssue {
notes, err = gitcmds.LinkBranchToGithubIssue(wd, parentRepo, githubIssueURL, issueNum, branch, args...)
if issueInfo.Type == issue.GitHub {
notes, err = gitcmds.LinkBranchToGithubIssue(wd, parentRepo, issueInfo.URL, issueInfo.Number, branch, args...)
if err != nil {
return err
}
Expand Down Expand Up @@ -231,40 +229,6 @@ func setPreCommitHook(wd string) error {
return gitcmds.SetLocalPreCommitHook(wd)
}

func argContainsGithubIssueLink(wd string, args ...string) (issueNum int, issueURL string, ok bool, err error) {
ok = false
if len(args) != 1 {
return
}
url := args[0]
if strings.Contains(url, "/issues") {
if err := checkIssueLink(wd, url); err != nil {
return 0, "", false, fmt.Errorf("invalid GitHub issue link: %w", err)
}
segments := strings.Split(url, "/")
strIssueNum := segments[len(segments)-1]
i, err := strconv.Atoi(strIssueNum)
if err != nil {
return 0, "", false, fmt.Errorf("failed to convert issue number from string to int: %w", err)
}

return i, url, true, nil
}

return 0, "", false, nil
}

func checkIssueLink(wd, issueURL string) error {
// This function checks if the provided issueURL is a valid GitHub issue link via `gh issue view`.
cmd := exec.Command("gh", "issue", "view", "--json", "title,state", issueURL)
cmd.Dir = wd
if _, err := cmd.Output(); err != nil {
return fmt.Errorf("failed to check issue link: %w", err)
}

return nil
}

func deleteBranches(wd, parentRepo string) error {
// Step 1: qs d
if err := gitcmds.Download(wd); err != nil {
Expand Down
40 changes: 40 additions & 0 deletions internal/issue/issue.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package issue

import (
"fmt"
"strconv"
"strings"

"github.com/untillpro/qs/internal/jira"
)

type IssueType int

const (
FreeForm IssueType = iota
GitHub
Jira
)

type IssueInfo struct {
Type IssueType
URL string
ID string
Number int
}

func ParseIssueFromArgs(wd string, args ...string) (IssueInfo, error) {
url := args[0] // protected by caller side
if strings.Contains(url, "/issues/") {
segments := strings.Split(url, "/")
num, err := strconv.Atoi(segments[len(segments)-1])
if err != nil {
return IssueInfo{}, fmt.Errorf("failed to convert issue number from string to int: %w", err)
}
return IssueInfo{Type: GitHub, URL: url, ID: segments[len(segments)-1], Number: num}, nil
}
if id, ok := jira.GetJiraTicketIDFromArgs(args...); ok {
return IssueInfo{Type: Jira, URL: url, ID: id}, nil
}
return IssueInfo{Type: FreeForm}, nil
}
54 changes: 54 additions & 0 deletions internal/issue/issue_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package issue

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestParseIssueFromArgs(t *testing.T) {
tests := []struct {
name string
args []string
wantType IssueType
wantID string
wantNum int
wantErr bool
}{
{
name: "Multiple args returns FreeForm",
args: []string{"arg1", "arg2"},
wantType: FreeForm,
},
{
name: "Plain text returns FreeForm",
args: []string{"some-feature-branch"},
wantType: FreeForm,
},
{
name: "Jira URL returns Jira type",
args: []string{"https://untill.atlassian.net/browse/AIR-270"},
wantType: Jira,
wantID: "AIR-270",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
info, err := ParseIssueFromArgs(".", tt.args...)
if tt.wantErr {
require.Error(err)
return
}
require.NoError(err)
require.Equal(tt.wantType, info.Type)
if tt.wantID != "" {
require.Equal(tt.wantID, info.ID)
}
if tt.wantNum != 0 {
require.Equal(tt.wantNum, info.Number)
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
registered_at: 2026-02-23T10:24:58Z
change_id: 2602231024-fix-dev-branch-creation
baseline: b7c87527aa6501a58538c46ba20fcb43c9962e45
archived_at: 2026-02-23T14:19:09Z
---

# Change request: Fix dev branch creation flow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,38 @@

### Remove redundant branch existence checks

- [x] update: [gitcmds/github.go](../../../gitcmds/github.go)
- [x] update: [gitcmds/github.go](../../../../../gitcmds/github.go)
- remove: Remote branch existence check (lines 64-81) from `CreateGithubLinkToIssue` — it uses uninitialized `branch` variable (bug) and is redundant since `Dev()` already checks existence

- [x] update: [gitcmds/dev.go](../../../gitcmds/dev.go)
- [x] update: [gitcmds/dev.go](../../../../../gitcmds/dev.go)
- remove: `checkRemoteBranchExistence` parameter from `CreateDevBranch` signature
- remove: Remote branch existence check block (lines 48-64) inside `CreateDevBranch`

### Consolidate existence check in Dev()

- [x] update: [internal/commands/dev.go](../../../internal/commands/dev.go)
- [x] update: [internal/commands/dev.go](../../../../../internal/commands/dev.go)
- update: Replace local-only `branchExists` check with a combined local+remote existence check that works uniformly for both GitHub and Jira flows
- update: Remove `checkRemoteBranchExistence` variable and pass updated signature to `CreateDevBranch`
- update: Move branch existence check to happen after branch name is resolved but before user confirmation prompt, so the user is not asked to confirm creation of an already-existing branch

### Reorder GitHub branch creation and issue linking

- [x] update: [gitcmds/github.go](../../../gitcmds/github.go)
- [x] update: [gitcmds/github.go](../../../../../gitcmds/github.go)
- update: Renamed `CreateGithubLinkToIssue` to `LinkBranchToGithubIssue`, changed signature to return `(notes []string, err error)` instead of `(branch string, notes []string, err error)` since branch name is already known

- [x] update: [internal/commands/dev.go](../../../internal/commands/dev.go)
- [x] update: [internal/commands/dev.go](../../../../../internal/commands/dev.go)
- update: Reordered calls so `CreateDevBranch` runs first (creates local branch + pushes to origin), then `LinkBranchToGithubIssue` links the existing remote branch to the GitHub issue

### Unify dev branch name building

- [x] update: [gitcmds/github.go](../../../gitcmds/github.go) and [internal/commands/dev.go](../../../internal/commands/dev.go)
- [x] update: [gitcmds/github.go](../../../../../gitcmds/github.go) and [internal/commands/dev.go](../../../../../internal/commands/dev.go)
- update: Changed `BuildDevBranchName` signature from `(string, error)` to `(string, []string, error)` — now returns notes (old-style comment, body, and JSON notes) alongside the branch name, matching the Jira/PK pattern
- update: Removed note preparation from `LinkBranchToGithubIssue` (now returns `nil, nil` — linking only)
- update: Unified `Dev()` branch resolution into a single `switch` dispatching to `BuildDevBranchName` / `GetJiraBranchName` / `GetBranchName`, all producing `(branch, notes, error)`

### Tests and review

- [ ] review
- [x] review
- `go build ./...` compiles without errors
- `go vet ./...` passes

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Implementation plan: Unify issue detection into single function

## Construction

### New shared types and function

- [x] create: [internal/issue/issue.go](../../../../../internal/issue/issue.go)
- add: `IssueType` enum (`FreeForm`, `GitHub`, `Jira`)
- add: `IssueInfo` struct with fields `Type IssueType`, `URL string`, `ID string`, `Number int` (GitHub issue number)
- add: `ParseIssueFromArgs(wd string, args ...string) (IssueInfo, error)` that checks GitHub URL (contains `/issues/`, validates via `gh issue view`), then Jira URL (delegates to `jira.GetJiraTicketIDFromArgs`), returns typed result

### Caller updates

- [x] update: [internal/commands/dev.go](../../../../../internal/commands/dev.go)
- remove: `argContainsGithubIssueLink` and `checkIssueLink` functions
- update: replace two-call detection (`argContainsGithubIssueLink` + `jira.GetJiraTicketIDFromArgs`) with single `issue.ParseIssueFromArgs` call
- update: simplify switch to use `issue.GitHub`, `issue.Jira`, `issue.FreeForm`
- remove: unused imports (`strconv`)

### Tests

- [x] create: [internal/issue/issue_test.go](../../../../../internal/issue/issue_test.go)
- add: table-driven tests for `ParseIssueFromArgs` covering GitHub URL, Jira URL, plain text, empty args

### Review

- [x] review
- `go build ./...` compiles without errors
- `go vet ./...` passes
- existing tests in `internal/jira/` still pass (GetJiraTicketIDFromArgs unchanged)

Loading