diff --git a/internal/commands/dev.go b/internal/commands/dev.go index de6249f..e0ca753 100644 --- a/internal/commands/dev.go +++ b/internal/commands/dev.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "os/exec" - "strconv" "strings" "github.com/atotto/clipboard" @@ -13,6 +12,7 @@ import ( "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" @@ -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...) @@ -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 } @@ -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 { diff --git a/internal/issue/issue.go b/internal/issue/issue.go new file mode 100644 index 0000000..b222682 --- /dev/null +++ b/internal/issue/issue.go @@ -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 +} diff --git a/internal/issue/issue_test.go b/internal/issue/issue_test.go new file mode 100644 index 0000000..3c30b3a --- /dev/null +++ b/internal/issue/issue_test.go @@ -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) + } + }) + } +} diff --git a/uspecs/changes/2602231024-fix-dev-branch-creation/change.md b/uspecs/changes/archive/2602/2602231419-fix-dev-branch-creation/change.md similarity index 98% rename from uspecs/changes/2602231024-fix-dev-branch-creation/change.md rename to uspecs/changes/archive/2602/2602231419-fix-dev-branch-creation/change.md index 7e592da..1ae47e2 100644 --- a/uspecs/changes/2602231024-fix-dev-branch-creation/change.md +++ b/uspecs/changes/archive/2602/2602231419-fix-dev-branch-creation/change.md @@ -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 diff --git a/uspecs/changes/2602231024-fix-dev-branch-creation/impl.md b/uspecs/changes/archive/2602/2602231419-fix-dev-branch-creation/impl.md similarity index 80% rename from uspecs/changes/2602231024-fix-dev-branch-creation/impl.md rename to uspecs/changes/archive/2602/2602231419-fix-dev-branch-creation/impl.md index 2d14f0c..4f8e062 100644 --- a/uspecs/changes/2602231024-fix-dev-branch-creation/impl.md +++ b/uspecs/changes/archive/2602/2602231419-fix-dev-branch-creation/impl.md @@ -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 diff --git a/uspecs/changes/archive/2602/2602231419-unify-issue-detection/impl.md b/uspecs/changes/archive/2602/2602231419-unify-issue-detection/impl.md new file mode 100644 index 0000000..d56160f --- /dev/null +++ b/uspecs/changes/archive/2602/2602231419-unify-issue-detection/impl.md @@ -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) +