diff --git a/.github/workflows/simver.yaml b/.github/workflows/simver.yaml new file mode 100644 index 0000000..13d8d87 --- /dev/null +++ b/.github/workflows/simver.yaml @@ -0,0 +1,48 @@ +{ + name: simver, + concurrency: { group: simver, cancel-in-progress: false }, + permissions: { id-token: write, contents: write, pull-requests: read }, + on: + { + workflow_dispatch: null, + workflow_call: null, + push: { branches: [main] }, + pull_request: { types: [opened, synchronize, reopened, closed] }, + }, + defaults: { run: { shell: bash } }, + jobs: + { + simver: + { + runs-on: ubuntu-latest, + + steps: + [ + { + name: checkout code, + uses: "actions/checkout@v4", + with: { fetch-depth: 0 }, + }, + { + name: setup golang, + uses: "actions/setup-go@v4", + with: { go-version: 1.21.4 }, + }, + { + name: install simver, + run: "go install github.com/walteh/simver/cmd/simver_github_actions@v0.2.0", + }, + { + name: run simver, + id: simver, + env: + { + SIMVER_READ_ONLY: false, + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}", + }, + run: simver_github_actions, + }, + ], + }, + }, +} diff --git a/.vscode/simver.code-workspace b/.vscode/simver.code-workspace index bcac362..15d6484 100644 --- a/.vscode/simver.code-workspace +++ b/.vscode/simver.code-workspace @@ -10,6 +10,7 @@ VSCODE ////////////////////////////////////////////// */ "workbench.tree.indent": 16, + "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "search.useIgnoreFiles": true, "debug.console.wordWrap": false, @@ -25,17 +26,17 @@ /* ////////////////////////////////////////////// YAML ////////////////////////////////////////////// */ - "yaml.format.enable": true, - "[yaml]": { - "editor.defaultFormatter": "redhat.vscode-yaml", - "editor.bracketPairColorization.enabled": true, - "editor.formatOnSave": true, - }, - "yaml.style.flowSequence": "allow", - "yaml.style.flowMapping": "allow", - "yaml.format.proseWrap": "preserve", - "yaml.format.singleQuote": false, - "yaml.extension.recommendations": true, + // "yaml.format.enable": true, + // "[yaml]": { + // "editor.defaultFormatter": "esbenp.prettier-vscode", + // "editor.bracketPairColorization.enabled": true, + // "editor.formatOnSave": true, + // }, + // "yaml.style.flowSequence": "allow", + // "yaml.style.flowMapping": "allow", + // "yaml.format.proseWrap": "preserve", + // "yaml.format.singleQuote": false, + // "yaml.extension.recommendations": true, /* ////////////////////////////////////////////// DOCKER ////////////////////////////////////////////// */ diff --git a/README.md b/README.md index e5b7576..56368fc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,10 @@ simple, pr based, semver git tagging logic # definitions -## when is mmrt valid? + # how can we make sure that a version is reserved - and if it is not reserved we need to bump it + + +## when is mmrt valid? 1. it exists 2. it is higher than the mrlt @@ -33,7 +36,18 @@ inside this commits tree, the highest '*-pr.N+(this)' is the mmrbn. note each of the nvt, mrrt, mrlt, and mmrt are saved as valid semvers, so "X.Y.Z" the mmrbn is an integer, so "N" ------------ +------------ + +two bugs: +- need to make sure merges do not have build numbers +- need to make sure that build nums are picked up + +# probs to test +1. make sure that a new pr to main does a minor bump +2. make sure that a new pr not to main does a patch bump +3. make sure that a new commit to a pr who has been tagged with a version and was last used for it does a patch bump +1. make sure if reserved is set, but others are not that it does not loop infinitely + # process @@ -86,3 +100,10 @@ the mmrbn is an integer, so "N" 1. find the mrrt and mrlt, calculate the nvt 2. create a new tag (based on nvt) on the head commit with no build number or prerelease +when you merge a pr: +- find the mmrt or the pr branch, and we need to start using that for this branch +- the base branch should inherit the mmrt from the pr branch +- so we need to create: + 1. a new "base" tag for the base branch with the mmrt of the pr branch + 2. create a new build tag using the mmrt + diff --git a/calculate.go b/calculate.go index ea8bc63..5998aab 100644 --- a/calculate.go +++ b/calculate.go @@ -1,28 +1,60 @@ package simver import ( + "context" "fmt" + "github.com/rs/zerolog" "github.com/walteh/terrors" "golang.org/x/mod/semver" ) type Calculation struct { - MyMostRecentTag MMRT - MostRecentLiveTag MRLT - MyMostRecentBuild MMRBN - MostRecentReservedTag MRRT - PR int - NextValidTag NVT + MyMostRecentTag MMRT + MostRecentLiveTag MRLT + MyMostRecentBuild MMRBN + PR int + NextValidTag NVT + IsMerge bool + ForcePatch bool } var ( ErrValidatingCalculation = terrors.New("ErrValidatingCalculation") ) -func (me *Calculation) CalculateNewTagsRaw() ([]string, []string) { - baseTags := make([]string, 0) - headTags := make([]string, 0) +type CalculationOutput struct { + BaseTags []string + HeadTags []string + RootTags []string + MergeTags []string +} + +func (out *CalculationOutput) ApplyRefs(opts RefProvider) Tags { + tags := make(Tags, 0) + for _, tag := range out.BaseTags { + tags = append(tags, Tag{Name: tag, Ref: opts.Base()}) + } + + for _, tag := range out.HeadTags { + tags = append(tags, Tag{Name: tag, Ref: opts.Head()}) + } + for _, tag := range out.RootTags { + tags = append(tags, Tag{Name: tag, Ref: opts.Root()}) + } + for _, tag := range out.MergeTags { + tags = append(tags, Tag{Name: tag, Ref: opts.Merge()}) + } + return tags +} + +func (me *Calculation) CalculateNewTagsRaw(ctx context.Context) *CalculationOutput { + out := &CalculationOutput{ + BaseTags: []string{}, + HeadTags: []string{}, + RootTags: []string{}, + MergeTags: []string{}, + } nvt := string(me.NextValidTag) @@ -42,15 +74,42 @@ func (me *Calculation) CalculateNewTagsRaw() ([]string, []string) { validMmrt = true } + // force patch is ignored if this is a merge + if me.ForcePatch && !me.IsMerge { + nvt = BumpPatch(mmrt) + validMmrt = false + } + // if mmrt is invalid, then we need to reserve a new mmrt (which is the same as nvt) if !validMmrt { mmrt = nvt - baseTags = append(baseTags, nvt+"-reserved") - baseTags = append(baseTags, nvt+fmt.Sprintf("-pr%d+base", me.PR)) + // pr will be 0 if this is not a and is a push to the root branch + if me.PR != 0 { + out.RootTags = append(out.RootTags, nvt+"-reserved") + out.BaseTags = append(out.BaseTags, nvt+fmt.Sprintf("-pr%d+base", me.PR)) + } + } + + if me.IsMerge { + out.MergeTags = append(out.MergeTags, mmrt) + } else { + if me.PR == 0 { + out.HeadTags = append(out.HeadTags, mmrt) + } else { + out.HeadTags = append(out.HeadTags, mmrt+fmt.Sprintf("-pr%d+%d", me.PR, int(me.MyMostRecentBuild)+1)) + } } - // then finally we tag mmrt - headTags = append(headTags, mmrt+fmt.Sprintf("-pr%d+%d", me.PR, int(me.MyMostRecentBuild)+1)) + zerolog.Ctx(ctx).Debug(). + Any("calculation", me). + Any("output", out). + Str("mmrt", mmrt). + Str("mrlt", mrlt). + Str("nvt", nvt). + Str("pr", fmt.Sprintf("%d", me.PR)). + Bool("isMerge", me.IsMerge). + Bool("forcePatch", me.ForcePatch). + Msg("CalculateNewTagsRaw") - return baseTags, headTags + return out } diff --git a/calculate_test.go b/calculate_test.go index 188202a..b9b5b9d 100644 --- a/calculate_test.go +++ b/calculate_test.go @@ -1,6 +1,7 @@ package simver_test import ( + "context" "testing" "github.com/stretchr/testify/require" @@ -9,93 +10,212 @@ import ( func TestNewCalculationAndCalculateNewTags(t *testing.T) { testCases := []struct { - name string - calculation *simver.Calculation - expectedBaseTags []string - expectedHeadTags []string + name string + calculation *simver.Calculation + expectedBaseTags []string + expectedHeadTags []string + expectedRootTags []string + expectedMergeTags []string }{ { name: "expired mmrt", calculation: &simver.Calculation{ - MostRecentLiveTag: "v1.10.3", - MostRecentReservedTag: "v1.18.3-reserved", - MyMostRecentTag: "v1.9.9", - MyMostRecentBuild: 33, - PR: 85, - NextValidTag: "v99.99.99", + MostRecentLiveTag: "v1.10.3", + MyMostRecentTag: "v1.9.9", + MyMostRecentBuild: 33, + PR: 85, + NextValidTag: "v99.99.99", + IsMerge: false, + ForcePatch: false, }, expectedBaseTags: []string{ - "v99.99.99-reserved", "v99.99.99-pr85+base", }, expectedHeadTags: []string{ "v99.99.99-pr85+34", }, + expectedRootTags: []string{ + "v99.99.99-reserved", + }, + expectedMergeTags: []string{}, }, { name: "missing all", calculation: &simver.Calculation{ - MostRecentLiveTag: "", - MostRecentReservedTag: "", - MyMostRecentTag: "", - MyMostRecentBuild: 1, - PR: 1, - NextValidTag: "v3.3.3", + MostRecentLiveTag: "", + MyMostRecentTag: "", + MyMostRecentBuild: 1, + PR: 1, + NextValidTag: "v3.3.3", + IsMerge: false, + ForcePatch: false, }, expectedBaseTags: []string{ - "v3.3.3-reserved", "v3.3.3-pr1+base", }, expectedHeadTags: []string{ "v3.3.3-pr1+2", }, + expectedRootTags: []string{ + "v3.3.3-reserved", + }, + expectedMergeTags: []string{}, }, { name: "valid mmrt", calculation: &simver.Calculation{ - MostRecentLiveTag: "v1.2.3", - MostRecentReservedTag: "v1.2.5-reserved", - MyMostRecentTag: "v1.2.4", - MyMostRecentBuild: 33, - PR: 1, - NextValidTag: "v1.2.6", + MostRecentLiveTag: "v1.2.3", + MyMostRecentTag: "v1.2.4", + MyMostRecentBuild: 33, + PR: 1, + NextValidTag: "v1.2.6", + IsMerge: false, + ForcePatch: false, }, expectedBaseTags: []string{}, expectedHeadTags: []string{"v1.2.4-pr1+34"}, + expectedRootTags: []string{}, }, { name: "i have a tag reserved but do not have my own tag", calculation: &simver.Calculation{ - MostRecentLiveTag: "v1.2.3", - MostRecentReservedTag: "v1.2.5-reserved", - MyMostRecentTag: "", - MyMostRecentBuild: 33, - PR: 1, - NextValidTag: "v1.2.6", + MostRecentLiveTag: "v1.2.3", + MyMostRecentTag: "", + MyMostRecentBuild: 33, + PR: 1, + NextValidTag: "v1.2.6", + IsMerge: false, + ForcePatch: false, }, expectedBaseTags: []string{ - "v1.2.6-reserved", "v1.2.6-pr1+base", }, expectedHeadTags: []string{ "v1.2.6-pr1+34", }, + expectedRootTags: []string{ + "v1.2.6-reserved", + }, + expectedMergeTags: []string{}, + }, + { + name: "valid mmrt with merge", + calculation: &simver.Calculation{ + MostRecentLiveTag: "v1.2.3", + MyMostRecentTag: "v1.2.4", + MyMostRecentBuild: 33, + PR: 1, + NextValidTag: "v1.2.6", + IsMerge: true, + ForcePatch: false, + }, + expectedBaseTags: []string{}, + expectedHeadTags: []string{}, + expectedRootTags: []string{}, + expectedMergeTags: []string{"v1.2.4"}, + }, + { + name: "valid mmrt with force patch", + calculation: &simver.Calculation{ + MostRecentLiveTag: "v1.2.3", + MyMostRecentTag: "v1.2.4", + MyMostRecentBuild: 33, + PR: 1, + NextValidTag: "v1.2.6", + IsMerge: false, + ForcePatch: true, + }, + expectedBaseTags: []string{"v1.2.5-pr1+base"}, + expectedHeadTags: []string{"v1.2.5-pr1+34"}, + expectedRootTags: []string{"v1.2.5-reserved"}, + expectedMergeTags: []string{}, + }, + { + name: "valid mmrt with force patch (merge override)", + calculation: &simver.Calculation{ + MostRecentLiveTag: "v1.2.3", + MyMostRecentTag: "v1.2.4", + MyMostRecentBuild: 33, + PR: 1, + NextValidTag: "v1.2.6", + IsMerge: true, + ForcePatch: true, + }, + expectedBaseTags: []string{}, + expectedHeadTags: []string{}, + expectedRootTags: []string{}, + expectedMergeTags: []string{"v1.2.4"}, + }, + { + name: "expired mmrt with force patch", + calculation: &simver.Calculation{ + MostRecentLiveTag: "v1.10.3", + MyMostRecentTag: "v1.9.9", + MyMostRecentBuild: 33, + PR: 85, + NextValidTag: "v99.99.99", + IsMerge: false, + ForcePatch: true, + }, + expectedBaseTags: []string{ + "v1.9.10-pr85+base", + }, + expectedHeadTags: []string{ + "v1.9.10-pr85+34", + }, + expectedRootTags: []string{ + "v1.9.10-reserved", + }, + expectedMergeTags: []string{}, + }, + + { + name: "expired mmrt", + calculation: &simver.Calculation{ + ForcePatch: false, + IsMerge: false, + MostRecentLiveTag: "v0.17.2", + MyMostRecentBuild: 1.000000, + MyMostRecentTag: "v0.17.3", + NextValidTag: "v0.18.0", + PR: 13.000000, + }, + expectedBaseTags: []string{ + // "v0.18.0-pr13+base", + }, + expectedHeadTags: []string{ + "v0.17.3-pr13+2", + }, + expectedRootTags: []string{ + // "v0.18.0-reserved", + }, + expectedMergeTags: []string{}, }, // Add more test cases here... } + ctx := context.Background() + for _, tc := range testCases { - for _, i := range []string{"base", "head"} { + for _, i := range []string{"base", "head", "root", "merge"} { t.Run(tc.name+"_"+i, func(t *testing.T) { - baseTags, headTags := tc.calculation.CalculateNewTagsRaw() + out := tc.calculation.CalculateNewTagsRaw(ctx) if i == "base" { - require.NotContains(t, baseTags, "", "Base tags contain empty string") - require.ElementsMatch(t, tc.expectedBaseTags, baseTags, "Base tags do not match") + require.NotContains(t, out.BaseTags, "", "Base tags contain empty string") + require.ElementsMatch(t, tc.expectedBaseTags, out.BaseTags, "Base tags do not match") + } else if i == "head" { + require.NotContains(t, out.HeadTags, "", "Head tags contain empty string") + require.ElementsMatch(t, tc.expectedHeadTags, out.HeadTags, "Head tags do not match") + } else if i == "root" { + require.NotContains(t, out.RootTags, "", "Root tags contain empty string") + require.ElementsMatch(t, tc.expectedRootTags, out.RootTags, "Root tags do not match") + } else if i == "merge" { + require.NotContains(t, out.MergeTags, "", "Merge tags contain empty string") + require.ElementsMatch(t, tc.expectedMergeTags, out.MergeTags, "Merge tags do not match") } else { - require.NotContains(t, headTags, "", "Head tags contain empty string") - require.ElementsMatch(t, tc.expectedHeadTags, headTags, "Head tags do not match") + require.Fail(t, "invalid test case") } }) } diff --git a/cmd/simver_github_actions/main.go b/cmd/simver_github_actions/main.go index 26835df..6c4d440 100644 --- a/cmd/simver_github_actions/main.go +++ b/cmd/simver_github_actions/main.go @@ -7,7 +7,6 @@ import ( "os" "strconv" "strings" - "time" "github.com/rs/zerolog" "github.com/walteh/simver" @@ -26,6 +25,7 @@ type PullRequestResolver struct { } func (p *PullRequestResolver) CurrentPR(ctx context.Context) (*simver.PRDetails, error) { + head_ref := os.Getenv("GITHUB_REF") if head_ref != "" && strings.HasPrefix(head_ref, "refs/pull/") { @@ -59,14 +59,6 @@ func (p *PullRequestResolver) CurrentPR(ctx context.Context) (*simver.PRDetails, sha := os.Getenv("GITHUB_SHA") - // // this is a push event, we need to find the PR (if any) that this push is for - - // // get the commit hash - // commit, err := p.git.GetHeadRef(ctx) - // if err != nil { - // return nil, Err.Trace(err, "error getting commit hash") - // } - pr, exists, err := p.gh.PRDetailsByCommit(ctx, sha) if err != nil { return nil, Err.Trace(err, "error getting PR details by commit") @@ -76,19 +68,10 @@ func (p *PullRequestResolver) CurrentPR(ctx context.Context) (*simver.PRDetails, return pr, nil } - // // get the branch - // branch, err := p.git.Branch(ctx) - // if err != nil { - // return nil, Err.Trace(err, "error getting branch") - // } - - // pr, exists, err = p.gh.PRDetailsByBranch(ctx, branch) - // if err != nil { - // return nil, Err.Trace(err, "error getting PR details by branch") - // } + isPush := os.Getenv("GITHUB_EVENT_NAME") == "push" - if exists { - return pr, nil + if !isPush { + return nil, errors.New("not a PR event and not a push event") } // get the parent commit @@ -97,16 +80,7 @@ func (p *PullRequestResolver) CurrentPR(ctx context.Context) (*simver.PRDetails, return nil, Err.Trace(err, "error getting parent commit") } - return &simver.PRDetails{ - Number: 0, - HeadBranch: branch, - BaseBranch: branch, - Merged: true, - MergeCommit: sha, - HeadCommit: sha, - BaseCommit: parent, - PotentialMergeCommit: "", - }, nil + return simver.NewPushSimulatedPRDetails(parent, sha, branch), nil } @@ -171,58 +145,35 @@ func main() { os.Exit(1) } - ee, err := simver.LoadExecution(ctx, tagprov, prr) + ee, _, keepgoing, err := simver.LoadExecution(ctx, tagprov, prr) if err != nil { zerolog.Ctx(ctx).Error().Err(err).Msgf("error loading execution") fmt.Println(terrors.FormatErrorCaller(err)) - os.Exit(1) } - tags := simver.NewTags(ee) - - reservedTag, reserved := tags.GetReserved() + if !keepgoing { + zerolog.Ctx(ctx).Debug().Msg("execution is complete, likely because this is a push to a branch that is not main and not related to a PR") + os.Exit(0) + } - tries := 0 + // isPush := os.Getenv("GITHUB_EVENT_NAME") == "push" - for reserved { + // if isPush && prd.HeadBranch != "main" { + // zerolog.Ctx(ctx).Debug().Msg("execution is complete, exiting because this is not a direct push to main") + // os.Exit(0) + // } - err := tagprov.CreateTag(ctx, reservedTag) - if err != nil { - if tries > 5 { - zerolog.Ctx(ctx).Error().Err(err).Msgf("error creating tag: %v", err) - fmt.Println(terrors.FormatErrorCaller(err)) - - os.Exit(1) - } - - time.Sleep(1 * time.Second) - ee, err := simver.LoadExecution(ctx, tagprov, prr) - if err != nil { - zerolog.Ctx(ctx).Error().Err(err).Msgf("error loading execution: %v", err) - fmt.Println(terrors.FormatErrorCaller(err)) - - os.Exit(1) - } - tags := simver.NewTags(ee) - reservedTag, reserved = tags.GetReserved() - } else { - reserved = false - } - } + tt := simver.Calculate(ctx, ee).CalculateNewTagsRaw(ctx) - for _, tag := range tags { - if tag.Name == reservedTag.Name && tag.Ref == reservedTag.Ref { - continue - } + tags := tt.ApplyRefs(ee.ProvideRefs()) - err := tagprov.CreateTag(ctx, tag) - if err != nil { - zerolog.Ctx(ctx).Error().Err(err).Msgf("error creating tag: %v", err) - fmt.Println(terrors.FormatErrorCaller(err)) + err = tagprov.CreateTags(ctx, tags...) + if err != nil { + zerolog.Ctx(ctx).Error().Err(err).Msgf("error creating tag: %v", err) + fmt.Println(terrors.FormatErrorCaller(err)) - os.Exit(1) - } + os.Exit(1) } } diff --git a/exec/gh.go b/exec/gh.go index 5ace92a..428fbfc 100644 --- a/exec/gh.go +++ b/exec/gh.go @@ -102,6 +102,7 @@ type githubPR struct { func (me *githubPR) toPRDetails() *simver.PRDetails { return &simver.PRDetails{ Number: me.Number, + RootBranch: "main", HeadBranch: me.HeadRefName, BaseBranch: me.BaseRefName, Merged: me.State == "MERGED", @@ -124,14 +125,29 @@ func (p *ghProvider) getRelevantPR(ctx context.Context, out []byte) (*simver.PRD return nil, false, err } + ret := func(pr *githubPR) (*simver.PRDetails, bool, error) { + dets := pr.toPRDetails() + dets.BaseCommit, err = p.getBaseCommit(ctx, dets) + if err != nil { + return nil, false, err + } + dets.RootCommit, err = p.getRootCommit(ctx) + if err != nil { + return nil, false, err + } + return dets, true, nil + } + + // first check if there is a merged PR + for _, pr := range dat { + if pr.State == "MERGED" { + return ret(pr) + } + } + for _, pr := range dat { - if pr.State == "MERGED" || pr.State == "OPEN" { - dets := pr.toPRDetails() - dets.BaseCommit, err = p.getBaseCommit(ctx, dets) - if err != nil { - return nil, false, err - } - return dets, true, nil + if pr.State == "OPEN" { + return ret(pr) } } @@ -210,6 +226,33 @@ func (p *ghProvider) getBaseCommit(ctx context.Context, dets *simver.PRDetails) return dat.Parents[0].Sha, nil } +func (p *ghProvider) getRootCommit(ctx context.Context) (string, error) { + zerolog.Ctx(ctx).Debug().Msg("Getting root commit") + + cmd := p.gh(ctx, "api", "-H", "Accept: application/vnd.github+json", fmt.Sprintf("/repos/%s/%s/git/ref/heads/main", p.Org, p.Repo)) + out, err := cmd.Output() + if err != nil { + return "", ErrExecGH.Trace(err) + } + + var dat struct { + Object struct { + Sha string `json:"sha"` + } `json:"object"` + } + + err = json.Unmarshal(out, &dat) + if err != nil { + return "", ErrExecGH.Trace(err) + } + + if dat.Object.Sha == "" { + return "", ErrExecGH.Trace("no sha found") + } + + return dat.Object.Sha, nil +} + func (p *ghProvider) PRDetailsByCommit(ctx context.Context, commitHash string) (*simver.PRDetails, bool, error) { // Implement getting PR details using exec and parsing the output of gh cli diff --git a/exec/tag.go b/exec/tag.go index cf6b51d..154cfbc 100644 --- a/exec/tag.go +++ b/exec/tag.go @@ -15,7 +15,7 @@ var ( _ simver.TagProvider = (*gitProvider)(nil) ) -func (p *gitProvider) TagsFromCommit(ctx context.Context, commitHash string) ([]simver.TagInfo, error) { +func (p *gitProvider) TagsFromCommit(ctx context.Context, commitHash string) (simver.Tags, error) { ctx = zerolog.Ctx(ctx).With().Str("commit", commitHash).Logger().WithContext(ctx) @@ -28,13 +28,13 @@ func (p *gitProvider) TagsFromCommit(ctx context.Context, commitHash string) ([] } lines := strings.Split(string(out), "\n") - var tags []simver.TagInfo + var tags simver.Tags for _, line := range lines { line = strings.TrimSpace(line) if line == "" { continue } - tags = append(tags, simver.TagInfo{Name: line, Ref: commitHash}) + tags = append(tags, simver.Tag{Name: line, Ref: commitHash}) } zerolog.Ctx(ctx).Debug().Int("tags_len", len(tags)).Any("tags", tags).Msg("got tags from commit") @@ -42,22 +42,12 @@ func (p *gitProvider) TagsFromCommit(ctx context.Context, commitHash string) ([] return tags, nil } -func (p *gitProvider) TagsFromBranch(ctx context.Context, branch string) ([]simver.TagInfo, error) { +func (p *gitProvider) TagsFromBranch(ctx context.Context, branch string) (simver.Tags, error) { start := time.Now() ctx = zerolog.Ctx(ctx).With().Str("branch", branch).Logger().WithContext(ctx) - // zerolog.Ctx(ctx).Debug().Msg("getting tags from branch") - - // cmd := p.git(ctx, "pull", "--ff-only", "--tags", "origin", branch) - // cmd.Stdout = os.Stdout - // cmd.Stderr = os.Stderr - // err := cmd.Run() - // if err != nil { - // return nil, ErrExecGit.Trace(err) - // } - cmd := p.git(ctx, "tag", "--merged", "origin/"+branch, "--format='{\"sha\":\"%(objectname)\",\"type\": \"%(objecttype)\", \"ref\": \"%(refname)\"}'") out, err := cmd.Output() if err != nil { @@ -66,7 +56,7 @@ func (p *gitProvider) TagsFromBranch(ctx context.Context, branch string) ([]simv lines := strings.Split(string(out), "\n") - var tags []simver.TagInfo + var tags simver.Tags for _, line := range lines { if line == "" { @@ -102,16 +92,20 @@ func (p *gitProvider) TagsFromBranch(ctx context.Context, branch string) ([]simv continue } - tags = append(tags, simver.TagInfo{Name: name, Ref: dat.Ref}) + tags = append(tags, simver.Tag{Name: name, Ref: dat.Sha}) } zerolog.Ctx(ctx).Debug().Int("tags_len", len(tags)).Any("tags", tags).Dur("dur", time.Since(start)).Msg("got tags from branch") + // tags = tags.ExtractCommitRefs() + + zerolog.Ctx(ctx).Debug().Int("tags_len", len(tags)).Any("tags", tags).Dur("dur", time.Since(start)).Msg("got tags from branch") + return tags, nil } -func (p *gitProvider) FetchTags(ctx context.Context) ([]simver.TagInfo, error) { +func (p *gitProvider) FetchTags(ctx context.Context) (simver.Tags, error) { start := time.Now() @@ -135,7 +129,7 @@ func (p *gitProvider) FetchTags(ctx context.Context) ([]simver.TagInfo, error) { } lines := strings.Split(string(out), "\n") - var tagInfos []simver.TagInfo + var tagInfos simver.Tags for _, line := range lines { parts := strings.Split(line, " ") if len(parts) != 2 { @@ -147,41 +141,39 @@ func (p *gitProvider) FetchTags(ctx context.Context) ([]simver.TagInfo, error) { if name == "" || ref == "" { continue // Skip empty or invalid entries } - tagInfos = append(tagInfos, simver.TagInfo{Name: name, Ref: ref}) + tagInfos = append(tagInfos, simver.Tag{Name: name, Ref: ref}) } + // tagInfos = tagInfos.ExtractCommitRefs() + zerolog.Ctx(ctx).Debug().Int("tags_len", len(tagInfos)).Dur("duration", time.Since(start)).Any("tags", tagInfos).Msg("tags fetched") return tagInfos, nil } -func (p *gitProvider) CreateTag(ctx context.Context, tag simver.TagInfo) error { - - ctx = zerolog.Ctx(ctx).With().Str("name", tag.Name).Str("ref", tag.Ref).Logger().WithContext(ctx) +func (p *gitProvider) CreateTags(ctx context.Context, tag ...simver.Tag) error { if p.ReadOnly { zerolog.Ctx(ctx).Debug().Msg("read only mode, skipping tag creation") return nil } - zerolog.Ctx(ctx).Debug().Msg("creating tag") - - cmd := p.git(ctx, "tag", tag.Name, tag.Ref) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := cmd.Run() - if err != nil { - return ErrExecGit.Trace(err, "name="+tag.Name, "ref="+tag.Ref) + for _, t := range tag { + cmd := p.git(ctx, "tag", t.Name, t.Ref) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return ErrExecGit.Trace(err, "name="+t.Name, "ref="+t.Ref) + } } - zerolog.Ctx(ctx).Debug().Msg("pushing tag") - - cmd = p.git(ctx, "push", "origin", tag.Name) + cmd := p.git(ctx, "push", "origin", "--tags") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - err = cmd.Run() + err := cmd.Run() if err != nil { - return ErrExecGit.Trace(err, "name="+tag.Name, "ref="+tag.Ref) + return ErrExecGit.Trace(err) } zerolog.Ctx(ctx).Debug().Msg("tag created") diff --git a/execution.go b/execution.go index 6cee8f4..8d6961d 100644 --- a/execution.go +++ b/execution.go @@ -4,56 +4,48 @@ import ( "context" "fmt" "regexp" + "slices" "strconv" "strings" + // exp sort + "github.com/rs/zerolog" "golang.org/x/mod/semver" ) type Execution interface { PR() int - HeadCommit() string - BaseCommit() string - - HeadBranch() string - BaseBranch() string - + IsMinor() bool + IsMerge() bool HeadCommitTags() Tags - BaseCommitTags() Tags - HeadBranchTags() Tags BaseBranchTags() Tags + RootBranchTags() Tags + ProvideRefs() RefProvider } const baseTag = "v0.1.0" -func NewCaclulation(ex Execution) *Calculation { +func Calculate(ctx context.Context, ex Execution) *Calculation { mrlt := MostRecentLiveTag(ex) + mrrt := MostRecentReservedTag(ex) - return &Calculation{ - MostRecentLiveTag: mrlt, - MostRecentReservedTag: mrrt, - MyMostRecentTag: MyMostRecentTag(ex), - MyMostRecentBuild: MyMostRecentBuildNumber(ex), - PR: ex.PR(), - NextValidTag: GetNextValidTag(ex.BaseBranch() == "main", mrlt, mrrt), - } -} -func NewTags(me Execution) Tags { - calc := NewCaclulation(me) + mmrt := MyMostRecentTag(ex) - baseTags, headTags := calc.CalculateNewTagsRaw() + maxlr := MaxLiveOrReservedTag(mrlt, mrrt) - tags := make(Tags, 0) - for _, tag := range baseTags { - tags = append(tags, TagInfo{Name: tag, Ref: me.BaseCommit()}) - } - for _, tag := range headTags { - tags = append(tags, TagInfo{Name: tag, Ref: me.HeadCommit()}) - } + // maxmr := MaxMyOrReservedTag(mrrt, mmrt) - return tags + return &Calculation{ + IsMerge: ex.IsMerge(), + MostRecentLiveTag: mrlt, + ForcePatch: ForcePatch(ctx, ex, mmrt), + MyMostRecentTag: mmrt, + MyMostRecentBuild: MyMostRecentBuildNumber(ex), + PR: ex.PR(), + NextValidTag: GetNextValidTag(ctx, ex.IsMinor(), maxlr), + } } type MRLT string // most recent live tag @@ -61,46 +53,131 @@ type MRRT string // most recent reserved tag type NVT string // next valid tag type MMRT string // my most recent tag type MMRBN int // my most recent build number +type MRT string // my reserved tag + +type MAXLR string // max live or reserved tag + +type MAXMR string // max my reserved tag + +func MaxLiveOrReservedTag(mrlt MRLT, mrrt MRRT) MAXLR { + return MAXLR(Max(mrlt, mrrt)) +} + +func MaxMyOrReservedTag(mrrt MRRT, mmrt MMRT) MAXMR { + return MAXMR(Max(mrrt, mmrt)) +} + +func BumpPatch[S ~string](arg S) S { + + maj := semver.MajorMinor(string(arg)) + patch := strings.Split(strings.TrimPrefix(string(arg), maj), "-")[0] + + patch = strings.TrimPrefix(patch, ".") + + if patch == "" { + patch = "0" + } + + patchnum, err := strconv.Atoi(patch) + if err != nil { + panic("patchnum is not a number somehow: " + patch) + } + + patchnum++ + + return S(fmt.Sprintf("%s.%d", maj, patchnum)) + +} + +func ForcePatch(ctx context.Context, ee Execution, mmrt MMRT) bool { + // if our head branch has a + reg := regexp.MustCompile(fmt.Sprintf(`^%s$`, mmrt)) + + // head commit tags matching mmrt + hct := ee.HeadCommitTags().SemversMatching(func(s string) bool { + return reg.MatchString(s) + }) + + if len(hct) > 0 { + return false + } + + // head branch tags matching mmrt + hbt := ee.HeadBranchTags().SemversMatching(func(s string) bool { + return reg.MatchString(s) + }) + + if len(hbt) > 0 { + return true + } + + return false +} func MostRecentLiveTag(e Execution) MRLT { reg := regexp.MustCompile(`^v\d+\.\d+\.\d+$`) - highest, err := e.BaseBranchTags().HighestSemverMatching(reg) - if err != nil { + highest := e.BaseBranchTags().SemversMatching(func(s string) bool { + return reg.MatchString(s) + }) + + if len(highest) == 0 { return "" } - return MRLT(strings.Split(semver.Canonical(highest), "-")[0]) + return MRLT(strings.Split(semver.Canonical(highest[len(highest)-1]), "-")[0]) } func MyMostRecentTag(e Execution) MMRT { - reg := regexp.MustCompile(fmt.Sprintf(`^v\d+\.\d+\.\d+-pr%d+\+base$`, e.PR())) - highest, err := e.BaseCommitTags().HighestSemverMatching(reg) - if err != nil { + reg := regexp.MustCompile(`^v\d+\.\d+\.\d+.*$`) + highest := e.HeadBranchTags().SemversMatching(func(s string) bool { + if strings.Contains(s, "-reserved") || strings.Contains(s, "-base") { + return false + } + return reg.MatchString(s) + }) + + if len(highest) == 0 { return "" } - return MMRT(strings.Split(semver.Canonical(highest), "-")[0]) + return MMRT(strings.Split(semver.Canonical(highest[len(highest)-1]), "-")[0]) } func MostRecentReservedTag(e Execution) MRRT { reg := regexp.MustCompile(`^v\d+\.\d+\.\d+-reserved$`) - highest, err := e.BaseCommitTags().HighestSemverMatching(reg) - if err != nil { + highest := e.RootBranchTags().SemversMatching(func(s string) bool { + return reg.MatchString(s) + }) + + if len(highest) == 0 { return "" } - return MRRT(strings.Split(semver.Canonical(highest), "-")[0]) + return MRRT(strings.Split(semver.Canonical(highest[len(highest)-1]), "-")[0]) } func MyMostRecentBuildNumber(e Execution) MMRBN { - reg := regexp.MustCompile(fmt.Sprintf(`^.*-pr%d+\+\d+$`, e.PR())) - highest, err := e.HeadBranchTags().HighestSemverMatching(reg) - if err != nil { + reg := regexp.MustCompile(fmt.Sprintf(`^.*-pr%d\+\d+$`, e.PR())) + highest := e.HeadBranchTags().SemversMatching(func(s string) bool { + return reg.MatchString(s) + }) + + if len(highest) == 0 { return 0 } + slices.SortFunc(highest, func(a, b string) int { + // because we know the regex matches, we know the split will be len 2 + // and the second element will be a valid number + ai, _ := strconv.Atoi(strings.Split(a, "+")[1]) + bi, _ := strconv.Atoi(strings.Split(b, "+")[1]) + return bi - ai + }) + + high := highest[0] + // get the build number - split := strings.Split(highest, "+") + split := strings.Split(high, "+") if len(split) != 2 { return 0 } @@ -112,27 +189,38 @@ func MyMostRecentBuildNumber(e Execution) MMRBN { return MMRBN(n) } -func GetNextValidTag(minor bool, mrlt MRLT, mrrt MRRT) NVT { - +func Max[A ~string, B ~string](a A, b B) string { var max string - if mrlt == "" || mrrt == "" { - if mrlt != "" { - max = string(mrlt) - } else if mrrt != "" { - max = string(mrrt) + if a == "" || b == "" { + if a != "" { + max = string(a) + } else if b != "" { + max = string(b) } else { max = baseTag } } else { // only compare if both exist - if semver.Compare(string(mrrt), string(mrlt)) > 0 { - max = string(mrrt) + if semver.Compare(string(b), string(a)) > 0 { + max = string(b) } else { - max = string(mrlt) + max = string(a) } } + return max +} + +func GetNextValidTag(ctx context.Context, minor bool, maxt MAXLR) NVT { + + var max string + if maxt == "" { + max = baseTag + } else { + max = string(maxt) + } + maj := semver.Major(max) + "." majmin := semver.MajorMinor(max) @@ -158,102 +246,16 @@ func GetNextValidTag(minor bool, mrlt MRLT, mrrt MRRT) NVT { patchnum++ } - return NVT(fmt.Sprintf("%s.%d.%d", semver.Major(max), minornum, patchnum)) - -} - -var _ Execution = &rawExecution{} - -type rawExecution struct { - pr *PRDetails - baseBranch string - headBranch string - headCommit string - baseCommit string - headCommitTags Tags - baseCommitTags Tags - baseBranchTags Tags - headBranchTags Tags -} - -func (e *rawExecution) BaseCommit() string { - return e.baseCommit -} - -func (e *rawExecution) HeadCommit() string { - return e.headCommit -} + zerolog.Ctx(ctx).Debug(). + Str("max", max). + Str("maj", maj). + Str("majmin", majmin). + Str("patch", patch). + Str("min", min). + Int("minornum", minornum). + Int("patchnum", patchnum). + Msg("calculated next valid tag") -func (e *rawExecution) BaseCommitTags() Tags { - return e.baseCommitTags -} - -func (e *rawExecution) HeadCommitTags() Tags { - return e.headCommitTags -} - -func (e *rawExecution) BaseBranchTags() Tags { - return e.baseBranchTags -} - -func (e *rawExecution) HeadBranchTags() Tags { - return e.headBranchTags -} - -func (e *rawExecution) PR() int { - return e.pr.Number -} - -func (e *rawExecution) BaseBranch() string { - return e.baseBranch -} - -func (e *rawExecution) HeadBranch() string { - return e.headBranch -} - -func LoadExecution(ctx context.Context, tprov TagProvider, prr PRResolver) (Execution, error) { - - pr, err := prr.CurrentPR(ctx) - if err != nil { - return nil, err - } - - _, err = tprov.FetchTags(ctx) - if err != nil { - return nil, err - } - - baseCommitTags, err := tprov.TagsFromCommit(ctx, pr.BaseCommit) - if err != nil { - return nil, err - } - - headTags, err := tprov.TagsFromCommit(ctx, pr.HeadCommit) - if err != nil { - return nil, err - } - - branchTags, err := tprov.TagsFromBranch(ctx, pr.HeadBranch) - if err != nil { - return nil, err - } - - baseBranchTags, err := tprov.TagsFromBranch(ctx, pr.BaseBranch) - if err != nil { - return nil, err - } - - return &rawExecution{ - pr: pr, - baseBranch: pr.BaseBranch, - headBranch: pr.BaseBranch, - headCommit: pr.HeadCommit, - baseCommit: pr.BaseCommit, - headCommitTags: headTags, - baseCommitTags: baseCommitTags, - baseBranchTags: branchTags, - headBranchTags: baseBranchTags, - }, nil + return NVT(fmt.Sprintf("%s.%d.%d", semver.Major(max), minornum, patchnum)) } diff --git a/execution_test.go b/execution_test.go index 05fc743..a5bebf3 100644 --- a/execution_test.go +++ b/execution_test.go @@ -1,6 +1,7 @@ package simver_test import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -16,7 +17,7 @@ func TestMrlt(t *testing.T) { }{ { name: "Valid MRLT", - tags: simver.Tags{simver.TagInfo{Name: "v1.2.3"}, simver.TagInfo{Name: "v1.2.4"}}, + tags: simver.Tags{simver.Tag{Name: "v1.2.3"}, simver.Tag{Name: "v1.2.4"}}, expectedMrlt: "v1.2.4", }, { @@ -26,7 +27,7 @@ func TestMrlt(t *testing.T) { }, { name: "Invalid Semver Format", - tags: simver.Tags{simver.TagInfo{Name: "v1.2"}, simver.TagInfo{Name: "v1.2.x"}}, + tags: simver.Tags{simver.Tag{Name: "v1.2"}, simver.Tag{Name: "v1.2.x"}}, expectedMrlt: "", }, } @@ -52,14 +53,14 @@ func TestMmrt(t *testing.T) { { name: "Valid MMRT", prNum: 1, - tags: simver.Tags{simver.TagInfo{Name: "v1.2.3-pr1+base"}}, + tags: simver.Tags{simver.Tag{Name: "v1.2.3-pr1+base"}}, expectedMmrt: "v1.2.3", }, { name: "Invalid MMRT", prNum: 3, - tags: simver.Tags{simver.TagInfo{Name: "v1.2.3-pr3+0"}}, - expectedMmrt: "", + tags: simver.Tags{simver.Tag{Name: "v1.2.3-pr3+0"}}, + expectedMmrt: "v1.2.3", }, { name: "No MMRT", @@ -70,16 +71,16 @@ func TestMmrt(t *testing.T) { { name: "Non-Matching PR Number", prNum: 3, - tags: simver.Tags{simver.TagInfo{Name: "v1.2.3-pr1+base"}}, - expectedMmrt: "", + tags: simver.Tags{simver.Tag{Name: "v1.2.3-pr1+base"}}, + expectedMmrt: "v1.2.3", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { mockExec := new(mockery.MockExecution_simver) - mockExec.EXPECT().PR().Return(tc.prNum) - mockExec.EXPECT().BaseCommitTags().Return(tc.tags) + // mockExec.EXPECT().PR().Return(tc.prNum) + mockExec.EXPECT().HeadBranchTags().Return(tc.tags) result := simver.MyMostRecentTag(mockExec) mockExec.AssertExpectations(t) assert.Equal(t, tc.expectedMmrt, result) @@ -95,7 +96,7 @@ func TestMrrt(t *testing.T) { }{ { name: "Valid MRRT", - tags: simver.Tags{simver.TagInfo{Name: "v1.2.3-reserved"}}, + tags: simver.Tags{simver.Tag{Name: "v1.2.3-reserved"}}, expectedMrrt: "v1.2.3", }, { @@ -105,7 +106,7 @@ func TestMrrt(t *testing.T) { }, { name: "Invalid Reserved Tag Format", - tags: simver.Tags{simver.TagInfo{Name: "v1.2-reserved"}}, + tags: simver.Tags{simver.Tag{Name: "v1.2-reserved"}}, expectedMrrt: "", }, } @@ -113,7 +114,7 @@ func TestMrrt(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { mockExec := new(mockery.MockExecution_simver) - mockExec.EXPECT().BaseCommitTags().Return(tc.tags) + mockExec.EXPECT().RootBranchTags().Return(tc.tags) result := simver.MostRecentReservedTag(mockExec) mockExec.AssertExpectations(t) assert.Equal(t, tc.expectedMrrt, result) @@ -121,211 +122,420 @@ func TestMrrt(t *testing.T) { } } +func TestMax(t *testing.T) { + testCases := []struct { + name string + a string + b string + expected string + }{ + { + name: "normal", + a: "v1.2.3", + b: "v1.2.4-reserved", + expected: "v1.2.4-reserved", + }, + + { + name: "no a", + a: "", + b: "v1.2.4-reserved", + expected: "v1.2.4-reserved", + }, + { + name: "no b", + a: "v1.2.3", + b: "", + expected: "v1.2.3", + }, + { + name: "no a or b", + a: "", + b: "", + expected: "v0.1.0", // base tag is v0.1.0 + }, + { + name: "invalid a", + a: "x", + b: "v1.2.4-reserved", + expected: "v1.2.4-reserved", + }, + { + name: "invalid b", + a: "v9.8.7-pr33+4444", + b: "x", + expected: "v9.8.7-pr33+4444", + }, + } + + // ctx := context.Background() + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + res := simver.Max(tc.a, tc.b) + assert.Equal(t, tc.expected, res) + }) + } +} + func TestNvt(t *testing.T) { testCases := []struct { name string - mrlt simver.MRLT - mrrt simver.MRRT + max simver.MAXLR minor bool expectedNvt simver.NVT }{ { name: "normal", - mrlt: "v1.2.3", - mrrt: "v1.2.4-reserved", + max: "v1.2.4-reserved", minor: false, expectedNvt: "v1.2.5", }, { name: "minor", - mrlt: "v1.2.3", - mrrt: "v1.2.4-reserved", + max: "v1.2.4-reserved", minor: true, expectedNvt: "v1.3.0", }, { name: "no mrlt", - mrlt: "", - mrrt: "v1.2.4-reserved", + max: "v1.2.4-reserved", minor: false, expectedNvt: "v1.2.5", }, { name: "no mrrt", - mrlt: "v1.2.3", - mrrt: "", + max: "v1.2.3", minor: false, expectedNvt: "v1.2.4", }, { - name: "no mrlt or mrrt", - mrlt: "", - mrrt: "", + name: "no mrlt or mrrt", + max: "", + minor: false, expectedNvt: "v0.1.1", // base tag is v0.1.0 }, { name: "invalid mrlt", - mrlt: "x", - mrrt: "v1.2.4-reserved", + max: "v1.2.4-reserved", minor: false, expectedNvt: "v1.2.5", }, } + ctx := context.Background() + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - result := simver.GetNextValidTag(tc.minor, tc.mrlt, tc.mrrt) + + result := simver.GetNextValidTag(ctx, tc.minor, tc.max) assert.Equal(t, tc.expectedNvt, result) }) } } +const ( + root_ref = "root_ref" + base_ref = "base_ref" + head_ref = "head_ref" + merge_ref = "merge_ref" +) + func TestNewTags(t *testing.T) { testCases := []struct { name string - headCommitTags simver.Tags - baseCommitTags simver.Tags baseBranchTags simver.Tags headBranchTags simver.Tags - headBranch string - baseBranch string - headCommit string - baseCommit string + rootBranchTags simver.Tags + headCommitTags simver.Tags pr int + isMerge bool + isMinor bool expectedTags simver.Tags }{ { name: "Normal Commit on Non-Main Base Branch", - headCommitTags: simver.Tags{}, - baseCommitTags: simver.Tags{}, - baseBranchTags: simver.Tags{simver.TagInfo{Name: "v1.2.3"}}, + baseBranchTags: simver.Tags{simver.Tag{Name: "v1.2.3"}}, headBranchTags: simver.Tags{}, - headCommit: "head123", - baseCommit: "base456", - headBranch: "feature", - baseBranch: "main2", + headCommitTags: simver.Tags{}, + rootBranchTags: simver.Tags{}, pr: 0, + isMerge: false, + isMinor: false, expectedTags: simver.Tags{ - simver.TagInfo{Name: "v1.2.4-pr0+1", Ref: "head123"}, - simver.TagInfo{Name: "v1.2.4-reserved", Ref: "base456"}, - simver.TagInfo{Name: "v1.2.4-pr0+base", Ref: "base456"}, + simver.Tag{Name: "v1.2.4", Ref: head_ref}, + // simver.Tag{Name: "v1.2.4-reserved", Ref: root_ref}, + // simver.Tag{Name: "v1.2.4-pr0+base", Ref: base_ref}, }, }, { name: "Normal Commit on Main Branch", - headCommitTags: simver.Tags{}, - baseCommitTags: simver.Tags{}, - baseBranchTags: simver.Tags{simver.TagInfo{Name: "v1.2.3"}}, + baseBranchTags: simver.Tags{simver.Tag{Name: "v1.2.3"}}, headBranchTags: simver.Tags{}, - headCommit: "head123", - baseCommit: "base456", - headBranch: "feature", - baseBranch: "main", - pr: 99, + headCommitTags: simver.Tags{}, + rootBranchTags: simver.Tags{}, + pr: 0, + isMerge: true, + isMinor: true, expectedTags: simver.Tags{ - simver.TagInfo{Name: "v1.3.0-pr99+1", Ref: "head123"}, - simver.TagInfo{Name: "v1.3.0-reserved", Ref: "base456"}, - simver.TagInfo{Name: "v1.3.0-pr99+base", Ref: "base456"}, + simver.Tag{Name: "v1.3.0", Ref: merge_ref}, + // simver.Tag{Name: "v1.3.0-reserved", Ref: root_ref}, + // simver.Tag{Name: "v1.3.0-pr99+base", Ref: base_ref}, }, }, { name: "PR Merge with Valid MMRT", + baseBranchTags: simver.Tags{simver.Tag{Name: "v1.2.3"}}, + headBranchTags: simver.Tags{simver.Tag{Name: "v1.2.4-pr1+1002"}}, headCommitTags: simver.Tags{}, - baseCommitTags: simver.Tags{simver.TagInfo{Name: "v1.2.4-reserved"}, simver.TagInfo{Name: "v1.2.4-pr1+base"}}, - baseBranchTags: simver.Tags{simver.TagInfo{Name: "v1.2.3"}}, - headBranchTags: simver.Tags{simver.TagInfo{Name: "v1.2.4-pr1+1002"}}, - headCommit: "head123", - baseCommit: "base456", - headBranch: "feature", - baseBranch: "main2", + rootBranchTags: simver.Tags{simver.Tag{Name: "v1.2.4-reserved"}}, pr: 1, - expectedTags: simver.Tags{simver.TagInfo{Name: "v1.2.4-pr1+1003", Ref: "head123"}}, + isMerge: false, + isMinor: false, + expectedTags: simver.Tags{ + simver.Tag{Name: "v1.2.4-pr1+1003", Ref: head_ref}, + }, }, { name: "PR Merge with No MMRT", + baseBranchTags: simver.Tags{simver.Tag{Name: "v1.2.3"}}, + headBranchTags: simver.Tags{simver.Tag{Name: "v1.5.9-pr87+1002"}}, headCommitTags: simver.Tags{}, - baseCommitTags: simver.Tags{simver.TagInfo{Name: "v1.5.9-reserved"}, simver.TagInfo{Name: "v1.5.9-pr87+base"}}, - baseBranchTags: simver.Tags{simver.TagInfo{Name: "v1.2.3"}}, - headBranchTags: simver.Tags{simver.TagInfo{Name: "v1.5.9-pr87+1002"}}, - headCommit: "head123", - baseCommit: "base456", - headBranch: "main", - baseBranch: "main", + rootBranchTags: simver.Tags{simver.Tag{Name: "v1.5.9-reserved"}}, pr: 87, - expectedTags: simver.Tags{simver.TagInfo{Name: "v1.5.9-pr87+1003", Ref: "head123"}}, + isMerge: false, + isMinor: false, + expectedTags: simver.Tags{ + simver.Tag{Name: "v1.5.9-pr87+1003", Ref: head_ref}, + }, }, { name: "PR Merge with Invalid MMRT", - headCommitTags: simver.Tags{simver.TagInfo{Name: "v1.2.99999-pr2+base"}}, - baseCommitTags: simver.Tags{simver.TagInfo{Name: "v1.2.3-reserved"}, simver.TagInfo{Name: "v1.2.4-pr2+base"}}, - baseBranchTags: simver.Tags{simver.TagInfo{Name: "v1.2.3"}}, - headBranchTags: simver.Tags{simver.TagInfo{Name: "v1.2.99999-pr2+base"}}, - headCommit: "head123", - baseCommit: "base456", - headBranch: "feature", - baseBranch: "main", + baseBranchTags: simver.Tags{simver.Tag{Name: "v1.2.3"}}, + headBranchTags: simver.Tags{simver.Tag{Name: "v1.2.4-pr2+5"}}, + headCommitTags: simver.Tags{}, + rootBranchTags: simver.Tags{simver.Tag{Name: "v1.2.3-reserved"}}, pr: 2, - expectedTags: simver.Tags{simver.TagInfo{Name: "v1.2.4-pr2+1", Ref: "head123"}}, + isMerge: false, + isMinor: false, + expectedTags: simver.Tags{ + simver.Tag{Name: "v1.2.4-pr2+6", Ref: head_ref}, + }, }, { name: "No Tags Available for PR Commit", - headCommitTags: simver.Tags{}, - baseCommitTags: simver.Tags{}, baseBranchTags: simver.Tags{}, headBranchTags: simver.Tags{}, - headCommit: "head123", - baseCommit: "base123", - headBranch: "feature", - baseBranch: "main", - pr: 0, + headCommitTags: simver.Tags{}, + rootBranchTags: simver.Tags{}, + pr: 1, + isMerge: false, + isMinor: true, expectedTags: simver.Tags{ // we also need to reserve the next version tag // which should be v0.2.0 since the base branch is main - simver.TagInfo{Name: "v0.2.0-reserved", Ref: "base123"}, - simver.TagInfo{Name: "v0.2.0-pr0+base", Ref: "base123"}, + simver.Tag{Name: "v0.2.0-reserved", Ref: root_ref}, + simver.Tag{Name: "v0.2.0-pr1+base", Ref: base_ref}, // and finally, we need to tag the commit with the PR number // since the base branch is main, we set it to v0.2.0-pr0 - simver.TagInfo{Name: "v0.2.0-pr0+1", Ref: "head123"}, + simver.Tag{Name: "v0.2.0-pr1+1", Ref: head_ref}, }, }, { name: "No Tags Available for PR Merge Commit", - headCommitTags: simver.Tags{}, - baseCommitTags: simver.Tags{}, baseBranchTags: simver.Tags{}, headBranchTags: simver.Tags{}, - headCommit: "head123", - baseCommit: "base123", - headBranch: "main", - baseBranch: "main", + headCommitTags: simver.Tags{}, + rootBranchTags: simver.Tags{}, pr: 2, + isMerge: false, + isMinor: true, expectedTags: simver.Tags{ // since this is a merge we do not need to reserve anything // since the base branch is main, we set it to v0.2.0 - simver.TagInfo{Name: "v0.2.0-pr2+1", Ref: "head123"}, + simver.Tag{Name: "v0.2.0-pr2+1", Ref: head_ref}, // we need to make sure we have a reserved tag for the base branch - simver.TagInfo{Name: "v0.2.0-reserved", Ref: "base123"}, - simver.TagInfo{Name: "v0.2.0-pr2+base", Ref: "base123"}, + simver.Tag{Name: "v0.2.0-reserved", Ref: root_ref}, + simver.Tag{Name: "v0.2.0-pr2+base", Ref: base_ref}, + }, + }, + { + name: "merge", + baseBranchTags: simver.Tags{simver.Tag{Name: "v1.5.9-pr84+12"}}, + headBranchTags: simver.Tags{simver.Tag{Name: "v1.5.10-pr87+1002"}}, + headCommitTags: simver.Tags{}, + rootBranchTags: simver.Tags{ + simver.Tag{Name: "v1.5.9-reserved"}, + simver.Tag{Name: "v1.5.10-reserved"}, + simver.Tag{Name: "v1.5.0"}, + simver.Tag{Name: "v1.5.9-pr84+base"}, + }, + pr: 87, + isMerge: true, + isMinor: false, + expectedTags: simver.Tags{ + simver.Tag{Name: "v1.5.10", Ref: merge_ref}, + }, + }, + { + name: "after merge", + baseBranchTags: simver.Tags{simver.Tag{Name: "v1.2.2"}}, + headBranchTags: simver.Tags{simver.Tag{Name: "v1.5.10-pr84+1002"}, simver.Tag{Name: "v1.5.10"}}, + headCommitTags: simver.Tags{}, + rootBranchTags: simver.Tags{ + simver.Tag{Name: "v1.5.9-reserved"}, + simver.Tag{Name: "v1.5.10-reserved"}, + simver.Tag{Name: "v1.5.0"}, + simver.Tag{Name: "v1.5.9-pr84+base"}, + }, + pr: 84, + isMerge: false, + isMinor: false, + expectedTags: simver.Tags{ + simver.Tag{Name: "v1.5.11-pr84+1003", Ref: head_ref}, + simver.Tag{Name: "v1.5.11-reserved", Ref: root_ref}, + simver.Tag{Name: "v1.5.11-pr84+base", Ref: base_ref}, + }, + }, + { + name: "extra build tags", + baseBranchTags: simver.Tags{simver.Tag{Name: "v1.2.3"}}, + headBranchTags: simver.Tags{ + simver.Tag{Name: "v1.2.4-pr2+4"}, + simver.Tag{Name: "v1.2.4-pr2+5"}, + }, + headCommitTags: simver.Tags{}, + rootBranchTags: simver.Tags{simver.Tag{Name: "v1.2.3-reserved"}}, + pr: 2, + isMerge: false, + isMinor: false, + expectedTags: simver.Tags{ + simver.Tag{Name: "v1.2.4-pr2+6", Ref: head_ref}, + }, + }, + { + name: "ignore other base", + baseBranchTags: simver.Tags{simver.Tag{Name: "v1.2.3"}}, + headBranchTags: simver.Tags{simver.Tag{Name: "v1.2.4-pr2+5"}, simver.Tag{Name: "v1.2.99-base"}}, + headCommitTags: simver.Tags{}, + rootBranchTags: simver.Tags{simver.Tag{Name: "v1.2.3-reserved"}}, + pr: 2, + isMerge: false, + isMinor: false, + expectedTags: simver.Tags{ + simver.Tag{Name: "v1.2.4-pr2+6", Ref: head_ref}, + }, + }, + { + name: "reserved tag already exists", + baseBranchTags: simver.Tags{simver.Tag{Name: "v1.2.3"}}, + headBranchTags: simver.Tags{}, + headCommitTags: simver.Tags{}, + rootBranchTags: simver.Tags{simver.Tag{Name: "v1.2.4-reserved"}}, + pr: 3, + isMerge: false, + isMinor: false, + expectedTags: simver.Tags{ + // if the reserved tag did not exist, we would be using v1.2.4 + // but since it exists, and pr0 does not know it owns it (via its own v1.2.4-pr0+base tag) + // we expect to use the next valid tag, which is v1.2.5 + simver.Tag{Name: "v1.2.5-pr3+1", Ref: head_ref}, + simver.Tag{Name: "v1.2.5-reserved", Ref: root_ref}, + simver.Tag{Name: "v1.2.5-pr3+base", Ref: base_ref}, + }, + }, + { + name: "reserved tag already exists", + baseBranchTags: simver.Tags{ + simver.Tag{Name: "v0.17.3-reserved"}, + simver.Tag{Name: "v0.17.3-pr85+base"}, + simver.Tag{Name: "v0.17.3-pr85+1"}, + }, + headBranchTags: simver.Tags{}, + headCommitTags: simver.Tags{}, + rootBranchTags: simver.Tags{ + simver.Tag{Name: "v0.17.3-reserved"}, + }, + pr: 99, + isMerge: false, + isMinor: false, + expectedTags: simver.Tags{ + // if the reserved tag did not exist, we would be using v1.2.4 + // but since it exists, and pr99 does not know it owns it (via its own v1.2.4-pr99+base tag) + // we expect to use the next valid tag, which is v1.2.5 + simver.Tag{Name: "v0.17.4-pr99+1", Ref: head_ref}, + simver.Tag{Name: "v0.17.4-reserved", Ref: root_ref}, + simver.Tag{Name: "v0.17.4-pr99+base", Ref: base_ref}, }, }, } + ctx := context.Background() + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + + // ctx = zerolog.New(zerolog.NewTestWriter(t)).With().Logger().WithContext(ctx) + mockExec := new(mockery.MockExecution_simver) - mockExec.EXPECT().HeadCommitTags().Return(tc.headCommitTags) - mockExec.EXPECT().BaseCommitTags().Return(tc.baseCommitTags) - mockExec.EXPECT().HeadCommit().Return(tc.headCommit) - mockExec.EXPECT().BaseCommit().Return(tc.baseCommit) mockExec.EXPECT().HeadBranchTags().Return(tc.headBranchTags) + mockExec.EXPECT().HeadCommitTags().Return(tc.headCommitTags) mockExec.EXPECT().BaseBranchTags().Return(tc.baseBranchTags) mockExec.EXPECT().PR().Return(tc.pr) - mockExec.EXPECT().HeadBranch().Return(tc.headBranch) - mockExec.EXPECT().BaseBranch().Return(tc.baseBranch) + mockExec.EXPECT().IsMinor().Return(tc.isMinor) + mockExec.EXPECT().IsMerge().Return(tc.isMerge) + mockExec.EXPECT().RootBranchTags().Return(tc.rootBranchTags) + + got := simver.Calculate(ctx, mockExec). + CalculateNewTagsRaw(ctx). + ApplyRefs(&simver.BasicRefProvider{ + HeadRef: head_ref, + BaseRef: base_ref, + RootRef: root_ref, + MergeRef: merge_ref, + }) + + assert.ElementsMatch(t, tc.expectedTags, got) + }) + } +} +func TestTagString_BumpPatch(t *testing.T) { + testCases := []struct { + name string + input string + expected string + panic bool + }{ + { + name: "BumpPatch with patch version", + input: "v1.2.3", + expected: "v1.2.4", + panic: false, + }, + { + name: "BumpPatch with no patch version", + input: "v1.2", + expected: "v1.2.1", + panic: false, + }, + { + name: "BumpPatch with invalid patch version", + input: "v1.2.x", + panic: true, + }, + } - result := simver.NewTags(mockExec) - assert.ElementsMatch(t, tc.expectedTags, result) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.panic { + assert.Panics(t, func() { + simver.BumpPatch(tc.input) + }) + return + } + result := simver.BumpPatch(tc.input) + assert.Equal(t, tc.expected, result) }) } } diff --git a/gen/mockery/Execution.simver.mockery.go b/gen/mockery/Execution.simver.mockery.go index 66d5b68..ee4f54d 100644 --- a/gen/mockery/Execution.simver.mockery.go +++ b/gen/mockery/Execution.simver.mockery.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.36.1. DO NOT EDIT. +// Code generated by mockery v2.37.1. DO NOT EDIT. package mockery @@ -20,47 +20,6 @@ func (_m *MockExecution_simver) EXPECT() *MockExecution_simver_Expecter { return &MockExecution_simver_Expecter{mock: &_m.Mock} } -// BaseBranch provides a mock function with given fields: -func (_m *MockExecution_simver) BaseBranch() string { - ret := _m.Called() - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// MockExecution_simver_BaseBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BaseBranch' -type MockExecution_simver_BaseBranch_Call struct { - *mock.Call -} - -// BaseBranch is a helper method to define mock.On call -func (_e *MockExecution_simver_Expecter) BaseBranch() *MockExecution_simver_BaseBranch_Call { - return &MockExecution_simver_BaseBranch_Call{Call: _e.mock.On("BaseBranch")} -} - -func (_c *MockExecution_simver_BaseBranch_Call) Run(run func()) *MockExecution_simver_BaseBranch_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockExecution_simver_BaseBranch_Call) Return(_a0 string) *MockExecution_simver_BaseBranch_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockExecution_simver_BaseBranch_Call) RunAndReturn(run func() string) *MockExecution_simver_BaseBranch_Call { - _c.Call.Return(run) - return _c -} - // BaseBranchTags provides a mock function with given fields: func (_m *MockExecution_simver) BaseBranchTags() simver.Tags { ret := _m.Called() @@ -104,49 +63,51 @@ func (_c *MockExecution_simver_BaseBranchTags_Call) RunAndReturn(run func() simv return _c } -// BaseCommit provides a mock function with given fields: -func (_m *MockExecution_simver) BaseCommit() string { +// HeadBranchTags provides a mock function with given fields: +func (_m *MockExecution_simver) HeadBranchTags() simver.Tags { ret := _m.Called() - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { + var r0 simver.Tags + if rf, ok := ret.Get(0).(func() simver.Tags); ok { r0 = rf() } else { - r0 = ret.Get(0).(string) + if ret.Get(0) != nil { + r0 = ret.Get(0).(simver.Tags) + } } return r0 } -// MockExecution_simver_BaseCommit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BaseCommit' -type MockExecution_simver_BaseCommit_Call struct { +// MockExecution_simver_HeadBranchTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HeadBranchTags' +type MockExecution_simver_HeadBranchTags_Call struct { *mock.Call } -// BaseCommit is a helper method to define mock.On call -func (_e *MockExecution_simver_Expecter) BaseCommit() *MockExecution_simver_BaseCommit_Call { - return &MockExecution_simver_BaseCommit_Call{Call: _e.mock.On("BaseCommit")} +// HeadBranchTags is a helper method to define mock.On call +func (_e *MockExecution_simver_Expecter) HeadBranchTags() *MockExecution_simver_HeadBranchTags_Call { + return &MockExecution_simver_HeadBranchTags_Call{Call: _e.mock.On("HeadBranchTags")} } -func (_c *MockExecution_simver_BaseCommit_Call) Run(run func()) *MockExecution_simver_BaseCommit_Call { +func (_c *MockExecution_simver_HeadBranchTags_Call) Run(run func()) *MockExecution_simver_HeadBranchTags_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *MockExecution_simver_BaseCommit_Call) Return(_a0 string) *MockExecution_simver_BaseCommit_Call { +func (_c *MockExecution_simver_HeadBranchTags_Call) Return(_a0 simver.Tags) *MockExecution_simver_HeadBranchTags_Call { _c.Call.Return(_a0) return _c } -func (_c *MockExecution_simver_BaseCommit_Call) RunAndReturn(run func() string) *MockExecution_simver_BaseCommit_Call { +func (_c *MockExecution_simver_HeadBranchTags_Call) RunAndReturn(run func() simver.Tags) *MockExecution_simver_HeadBranchTags_Call { _c.Call.Return(run) return _c } -// BaseCommitTags provides a mock function with given fields: -func (_m *MockExecution_simver) BaseCommitTags() simver.Tags { +// HeadCommitTags provides a mock function with given fields: +func (_m *MockExecution_simver) HeadCommitTags() simver.Tags { ret := _m.Called() var r0 simver.Tags @@ -161,279 +122,238 @@ func (_m *MockExecution_simver) BaseCommitTags() simver.Tags { return r0 } -// MockExecution_simver_BaseCommitTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BaseCommitTags' -type MockExecution_simver_BaseCommitTags_Call struct { +// MockExecution_simver_HeadCommitTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HeadCommitTags' +type MockExecution_simver_HeadCommitTags_Call struct { *mock.Call } -// BaseCommitTags is a helper method to define mock.On call -func (_e *MockExecution_simver_Expecter) BaseCommitTags() *MockExecution_simver_BaseCommitTags_Call { - return &MockExecution_simver_BaseCommitTags_Call{Call: _e.mock.On("BaseCommitTags")} +// HeadCommitTags is a helper method to define mock.On call +func (_e *MockExecution_simver_Expecter) HeadCommitTags() *MockExecution_simver_HeadCommitTags_Call { + return &MockExecution_simver_HeadCommitTags_Call{Call: _e.mock.On("HeadCommitTags")} } -func (_c *MockExecution_simver_BaseCommitTags_Call) Run(run func()) *MockExecution_simver_BaseCommitTags_Call { +func (_c *MockExecution_simver_HeadCommitTags_Call) Run(run func()) *MockExecution_simver_HeadCommitTags_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *MockExecution_simver_BaseCommitTags_Call) Return(_a0 simver.Tags) *MockExecution_simver_BaseCommitTags_Call { +func (_c *MockExecution_simver_HeadCommitTags_Call) Return(_a0 simver.Tags) *MockExecution_simver_HeadCommitTags_Call { _c.Call.Return(_a0) return _c } -func (_c *MockExecution_simver_BaseCommitTags_Call) RunAndReturn(run func() simver.Tags) *MockExecution_simver_BaseCommitTags_Call { +func (_c *MockExecution_simver_HeadCommitTags_Call) RunAndReturn(run func() simver.Tags) *MockExecution_simver_HeadCommitTags_Call { _c.Call.Return(run) return _c } -// HeadBranch provides a mock function with given fields: -func (_m *MockExecution_simver) HeadBranch() string { +// IsMerge provides a mock function with given fields: +func (_m *MockExecution_simver) IsMerge() bool { ret := _m.Called() - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { r0 = rf() } else { - r0 = ret.Get(0).(string) + r0 = ret.Get(0).(bool) } return r0 } -// MockExecution_simver_HeadBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HeadBranch' -type MockExecution_simver_HeadBranch_Call struct { +// MockExecution_simver_IsMerge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsMerge' +type MockExecution_simver_IsMerge_Call struct { *mock.Call } -// HeadBranch is a helper method to define mock.On call -func (_e *MockExecution_simver_Expecter) HeadBranch() *MockExecution_simver_HeadBranch_Call { - return &MockExecution_simver_HeadBranch_Call{Call: _e.mock.On("HeadBranch")} +// IsMerge is a helper method to define mock.On call +func (_e *MockExecution_simver_Expecter) IsMerge() *MockExecution_simver_IsMerge_Call { + return &MockExecution_simver_IsMerge_Call{Call: _e.mock.On("IsMerge")} } -func (_c *MockExecution_simver_HeadBranch_Call) Run(run func()) *MockExecution_simver_HeadBranch_Call { +func (_c *MockExecution_simver_IsMerge_Call) Run(run func()) *MockExecution_simver_IsMerge_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *MockExecution_simver_HeadBranch_Call) Return(_a0 string) *MockExecution_simver_HeadBranch_Call { +func (_c *MockExecution_simver_IsMerge_Call) Return(_a0 bool) *MockExecution_simver_IsMerge_Call { _c.Call.Return(_a0) return _c } -func (_c *MockExecution_simver_HeadBranch_Call) RunAndReturn(run func() string) *MockExecution_simver_HeadBranch_Call { +func (_c *MockExecution_simver_IsMerge_Call) RunAndReturn(run func() bool) *MockExecution_simver_IsMerge_Call { _c.Call.Return(run) return _c } -// HeadBranchTags provides a mock function with given fields: -func (_m *MockExecution_simver) HeadBranchTags() simver.Tags { +// IsMinor provides a mock function with given fields: +func (_m *MockExecution_simver) IsMinor() bool { ret := _m.Called() - var r0 simver.Tags - if rf, ok := ret.Get(0).(func() simver.Tags); ok { + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { r0 = rf() } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(simver.Tags) - } + r0 = ret.Get(0).(bool) } return r0 } -// MockExecution_simver_HeadBranchTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HeadBranchTags' -type MockExecution_simver_HeadBranchTags_Call struct { +// MockExecution_simver_IsMinor_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsMinor' +type MockExecution_simver_IsMinor_Call struct { *mock.Call } -// HeadBranchTags is a helper method to define mock.On call -func (_e *MockExecution_simver_Expecter) HeadBranchTags() *MockExecution_simver_HeadBranchTags_Call { - return &MockExecution_simver_HeadBranchTags_Call{Call: _e.mock.On("HeadBranchTags")} +// IsMinor is a helper method to define mock.On call +func (_e *MockExecution_simver_Expecter) IsMinor() *MockExecution_simver_IsMinor_Call { + return &MockExecution_simver_IsMinor_Call{Call: _e.mock.On("IsMinor")} } -func (_c *MockExecution_simver_HeadBranchTags_Call) Run(run func()) *MockExecution_simver_HeadBranchTags_Call { +func (_c *MockExecution_simver_IsMinor_Call) Run(run func()) *MockExecution_simver_IsMinor_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *MockExecution_simver_HeadBranchTags_Call) Return(_a0 simver.Tags) *MockExecution_simver_HeadBranchTags_Call { +func (_c *MockExecution_simver_IsMinor_Call) Return(_a0 bool) *MockExecution_simver_IsMinor_Call { _c.Call.Return(_a0) return _c } -func (_c *MockExecution_simver_HeadBranchTags_Call) RunAndReturn(run func() simver.Tags) *MockExecution_simver_HeadBranchTags_Call { +func (_c *MockExecution_simver_IsMinor_Call) RunAndReturn(run func() bool) *MockExecution_simver_IsMinor_Call { _c.Call.Return(run) return _c } -// HeadCommit provides a mock function with given fields: -func (_m *MockExecution_simver) HeadCommit() string { +// PR provides a mock function with given fields: +func (_m *MockExecution_simver) PR() int { ret := _m.Called() - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { + var r0 int + if rf, ok := ret.Get(0).(func() int); ok { r0 = rf() } else { - r0 = ret.Get(0).(string) + r0 = ret.Get(0).(int) } return r0 } -// MockExecution_simver_HeadCommit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HeadCommit' -type MockExecution_simver_HeadCommit_Call struct { +// MockExecution_simver_PR_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PR' +type MockExecution_simver_PR_Call struct { *mock.Call } -// HeadCommit is a helper method to define mock.On call -func (_e *MockExecution_simver_Expecter) HeadCommit() *MockExecution_simver_HeadCommit_Call { - return &MockExecution_simver_HeadCommit_Call{Call: _e.mock.On("HeadCommit")} +// PR is a helper method to define mock.On call +func (_e *MockExecution_simver_Expecter) PR() *MockExecution_simver_PR_Call { + return &MockExecution_simver_PR_Call{Call: _e.mock.On("PR")} } -func (_c *MockExecution_simver_HeadCommit_Call) Run(run func()) *MockExecution_simver_HeadCommit_Call { +func (_c *MockExecution_simver_PR_Call) Run(run func()) *MockExecution_simver_PR_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *MockExecution_simver_HeadCommit_Call) Return(_a0 string) *MockExecution_simver_HeadCommit_Call { +func (_c *MockExecution_simver_PR_Call) Return(_a0 int) *MockExecution_simver_PR_Call { _c.Call.Return(_a0) return _c } -func (_c *MockExecution_simver_HeadCommit_Call) RunAndReturn(run func() string) *MockExecution_simver_HeadCommit_Call { +func (_c *MockExecution_simver_PR_Call) RunAndReturn(run func() int) *MockExecution_simver_PR_Call { _c.Call.Return(run) return _c } -// HeadCommitTags provides a mock function with given fields: -func (_m *MockExecution_simver) HeadCommitTags() simver.Tags { +// ProvideRefs provides a mock function with given fields: +func (_m *MockExecution_simver) ProvideRefs() simver.RefProvider { ret := _m.Called() - var r0 simver.Tags - if rf, ok := ret.Get(0).(func() simver.Tags); ok { + var r0 simver.RefProvider + if rf, ok := ret.Get(0).(func() simver.RefProvider); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(simver.Tags) + r0 = ret.Get(0).(simver.RefProvider) } } return r0 } -// MockExecution_simver_HeadCommitTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HeadCommitTags' -type MockExecution_simver_HeadCommitTags_Call struct { - *mock.Call -} - -// HeadCommitTags is a helper method to define mock.On call -func (_e *MockExecution_simver_Expecter) HeadCommitTags() *MockExecution_simver_HeadCommitTags_Call { - return &MockExecution_simver_HeadCommitTags_Call{Call: _e.mock.On("HeadCommitTags")} -} - -func (_c *MockExecution_simver_HeadCommitTags_Call) Run(run func()) *MockExecution_simver_HeadCommitTags_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockExecution_simver_HeadCommitTags_Call) Return(_a0 simver.Tags) *MockExecution_simver_HeadCommitTags_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockExecution_simver_HeadCommitTags_Call) RunAndReturn(run func() simver.Tags) *MockExecution_simver_HeadCommitTags_Call { - _c.Call.Return(run) - return _c -} - -// IsMerge provides a mock function with given fields: -func (_m *MockExecution_simver) IsMerge() bool { - ret := _m.Called() - - var r0 bool - if rf, ok := ret.Get(0).(func() bool); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -// MockExecution_simver_IsMerge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsMerge' -type MockExecution_simver_IsMerge_Call struct { +// MockExecution_simver_ProvideRefs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ProvideRefs' +type MockExecution_simver_ProvideRefs_Call struct { *mock.Call } -// IsMerge is a helper method to define mock.On call -func (_e *MockExecution_simver_Expecter) IsMerge() *MockExecution_simver_IsMerge_Call { - return &MockExecution_simver_IsMerge_Call{Call: _e.mock.On("IsMerge")} +// ProvideRefs is a helper method to define mock.On call +func (_e *MockExecution_simver_Expecter) ProvideRefs() *MockExecution_simver_ProvideRefs_Call { + return &MockExecution_simver_ProvideRefs_Call{Call: _e.mock.On("ProvideRefs")} } -func (_c *MockExecution_simver_IsMerge_Call) Run(run func()) *MockExecution_simver_IsMerge_Call { +func (_c *MockExecution_simver_ProvideRefs_Call) Run(run func()) *MockExecution_simver_ProvideRefs_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *MockExecution_simver_IsMerge_Call) Return(_a0 bool) *MockExecution_simver_IsMerge_Call { +func (_c *MockExecution_simver_ProvideRefs_Call) Return(_a0 simver.RefProvider) *MockExecution_simver_ProvideRefs_Call { _c.Call.Return(_a0) return _c } -func (_c *MockExecution_simver_IsMerge_Call) RunAndReturn(run func() bool) *MockExecution_simver_IsMerge_Call { +func (_c *MockExecution_simver_ProvideRefs_Call) RunAndReturn(run func() simver.RefProvider) *MockExecution_simver_ProvideRefs_Call { _c.Call.Return(run) return _c } -// PR provides a mock function with given fields: -func (_m *MockExecution_simver) PR() int { +// RootBranchTags provides a mock function with given fields: +func (_m *MockExecution_simver) RootBranchTags() simver.Tags { ret := _m.Called() - var r0 int - if rf, ok := ret.Get(0).(func() int); ok { + var r0 simver.Tags + if rf, ok := ret.Get(0).(func() simver.Tags); ok { r0 = rf() } else { - r0 = ret.Get(0).(int) + if ret.Get(0) != nil { + r0 = ret.Get(0).(simver.Tags) + } } return r0 } -// MockExecution_simver_PR_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PR' -type MockExecution_simver_PR_Call struct { +// MockExecution_simver_RootBranchTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RootBranchTags' +type MockExecution_simver_RootBranchTags_Call struct { *mock.Call } -// PR is a helper method to define mock.On call -func (_e *MockExecution_simver_Expecter) PR() *MockExecution_simver_PR_Call { - return &MockExecution_simver_PR_Call{Call: _e.mock.On("PR")} +// RootBranchTags is a helper method to define mock.On call +func (_e *MockExecution_simver_Expecter) RootBranchTags() *MockExecution_simver_RootBranchTags_Call { + return &MockExecution_simver_RootBranchTags_Call{Call: _e.mock.On("RootBranchTags")} } -func (_c *MockExecution_simver_PR_Call) Run(run func()) *MockExecution_simver_PR_Call { +func (_c *MockExecution_simver_RootBranchTags_Call) Run(run func()) *MockExecution_simver_RootBranchTags_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *MockExecution_simver_PR_Call) Return(_a0 int) *MockExecution_simver_PR_Call { +func (_c *MockExecution_simver_RootBranchTags_Call) Return(_a0 simver.Tags) *MockExecution_simver_RootBranchTags_Call { _c.Call.Return(_a0) return _c } -func (_c *MockExecution_simver_PR_Call) RunAndReturn(run func() int) *MockExecution_simver_PR_Call { +func (_c *MockExecution_simver_RootBranchTags_Call) RunAndReturn(run func() simver.Tags) *MockExecution_simver_RootBranchTags_Call { _c.Call.Return(run) return _c } diff --git a/git.go b/git.go index 0c66d89..65b07b9 100644 --- a/git.go +++ b/git.go @@ -7,10 +7,3 @@ type GitProvider interface { CommitFromRef(ctx context.Context, ref string) (string, error) Branch(ctx context.Context) (string, error) } - -type TagProvider interface { - FetchTags(ctx context.Context) ([]TagInfo, error) - CreateTag(ctx context.Context, tag TagInfo) error - TagsFromCommit(ctx context.Context, commitHash string) ([]TagInfo, error) - TagsFromBranch(ctx context.Context, branch string) ([]TagInfo, error) -} diff --git a/pr.go b/pr.go index 481fea2..c4cdf0f 100644 --- a/pr.go +++ b/pr.go @@ -7,15 +7,33 @@ type PRDetails struct { Number int HeadBranch string BaseBranch string + RootBranch string // always main Merged bool MergeCommit string HeadCommit string - BaseCommit string PotentialMergeCommit string + + BaseCommit string + RootCommit string +} + +func NewPushSimulatedPRDetails(parentCommit, headCommit, branch string) *PRDetails { + return &PRDetails{ + Number: 0, + HeadBranch: branch, + BaseBranch: branch, + RootBranch: branch, + Merged: true, + MergeCommit: headCommit, + HeadCommit: headCommit, + RootCommit: parentCommit, + BaseCommit: parentCommit, + PotentialMergeCommit: "", + } } -func (d *PRDetails) IsReal() bool { - return d.Number != 0 +func (dets *PRDetails) IsSimulatedPush() bool { + return dets.Number == 0 } type PRProvider interface { diff --git a/refs.go b/refs.go new file mode 100644 index 0000000..f39ce8a --- /dev/null +++ b/refs.go @@ -0,0 +1,31 @@ +package simver + +type RefProvider interface { + Head() string + Base() string + Root() string + Merge() string +} + +type BasicRefProvider struct { + HeadRef string + BaseRef string + RootRef string + MergeRef string +} + +func (e *BasicRefProvider) Head() string { + return e.HeadRef +} + +func (e *BasicRefProvider) Base() string { + return e.BaseRef +} + +func (e *BasicRefProvider) Root() string { + return e.RootRef +} + +func (e *BasicRefProvider) Merge() string { + return e.MergeRef +} diff --git a/simver.go b/simver.go index c60de98..f8aaf21 100644 --- a/simver.go +++ b/simver.go @@ -1 +1,166 @@ package simver + +import ( + "context" + + "github.com/rs/zerolog" +) + +var _ Execution = &rawExecution{} +var _ RefProvider = &rawExecution{} + +type rawExecution struct { + pr *PRDetails + baseBranch string + headBranch string + rootBranch string + headCommit string + baseCommit string + rootCommit string + mergeCommit string + rootBranchTags Tags + rootCommitTags Tags + headCommitTags Tags + baseCommitTags Tags + baseBranchTags Tags + headBranchTags Tags + isMerged bool + isMinor bool +} + +func (e *rawExecution) Head() string { + return e.headCommit +} + +func (e *rawExecution) Base() string { + return e.baseCommit +} + +func (e *rawExecution) Root() string { + return e.rootCommit +} + +func (e *rawExecution) Merge() string { + return e.mergeCommit +} + +func (e *rawExecution) ProvideRefs() RefProvider { + return e +} + +func (e *rawExecution) BaseBranchTags() Tags { + return e.baseBranchTags +} + +func (e *rawExecution) HeadBranchTags() Tags { + return e.headBranchTags +} + +func (e *rawExecution) PR() int { + return e.pr.Number +} + +func (e *rawExecution) IsMerge() bool { + return e.pr.Merged +} + +func (e *rawExecution) RootBranch() string { + return e.rootBranch +} + +func (e *rawExecution) RootBranchTags() Tags { + return e.rootBranchTags +} + +func (e *rawExecution) IsMinor() bool { + return e.baseBranch == e.rootBranch +} + +func (e *rawExecution) HeadCommitTags() Tags { + return e.headCommitTags +} + +func LoadExecution(ctx context.Context, tprov TagProvider, prr PRResolver) (Execution, *PRDetails, bool, error) { + + pr, err := prr.CurrentPR(ctx) + if err != nil { + return nil, nil, false, err + } + + if pr.IsSimulatedPush() && pr.HeadBranch != "main" { + return nil, nil, false, nil + } + + _, err = tprov.FetchTags(ctx) + if err != nil { + return nil, nil, false, err + } + + baseCommitTags, err := tprov.TagsFromCommit(ctx, pr.BaseCommit) + if err != nil { + return nil, nil, false, err + } + + baseBranchTags, err := tprov.TagsFromBranch(ctx, pr.BaseBranch) + if err != nil { + return nil, nil, false, err + } + + rootCommitTags, err := tprov.TagsFromCommit(ctx, pr.RootCommit) + if err != nil { + return nil, nil, false, err + } + + rootBranchTags, err := tprov.TagsFromBranch(ctx, pr.RootBranch) + if err != nil { + return nil, nil, false, err + } + + var headBranchTags Tags + var headCommit string + + if pr.Merged { + headCommit = pr.MergeCommit + headBranchTags = baseBranchTags + } else { + headCommit = pr.HeadCommit + branchTags, err := tprov.TagsFromBranch(ctx, pr.HeadBranch) + if err != nil { + return nil, nil, false, err + } + headBranchTags = branchTags + } + + headTags, err := tprov.TagsFromCommit(ctx, headCommit) + if err != nil { + return nil, nil, false, err + } + + zerolog.Ctx(ctx).Debug(). + Array("baseCommitTags", baseCommitTags). + Array("baseBranchTags", baseBranchTags). + Array("rootCommitTags", rootCommitTags). + Array("rootBranchTags", rootBranchTags). + Array("headTags", headTags). + Array("headBranchTags", headBranchTags). + Any("pr", pr). + Msg("loaded tags") + + return &rawExecution{ + pr: pr, + baseBranch: pr.BaseBranch, + headBranch: pr.BaseBranch, + headCommit: pr.HeadCommit, + baseCommit: pr.BaseCommit, + headCommitTags: headTags, + baseCommitTags: baseCommitTags, + baseBranchTags: baseBranchTags, + headBranchTags: headBranchTags, + rootBranch: pr.RootBranch, + rootCommit: pr.RootCommit, + rootBranchTags: rootBranchTags, + rootCommitTags: rootCommitTags, + mergeCommit: pr.MergeCommit, + }, pr, true, nil + +} diff --git a/tags.go b/tags.go index fbb0459..7685e32 100644 --- a/tags.go +++ b/tags.go @@ -1,22 +1,60 @@ package simver import ( - "errors" - "regexp" - "sort" + "context" + "slices" "strings" + "github.com/rs/zerolog" "golang.org/x/mod/semver" ) -type TagInfo struct { +type TagProvider interface { + FetchTags(ctx context.Context) (Tags, error) + CreateTags(ctx context.Context, tag ...Tag) error + TagsFromCommit(ctx context.Context, commitHash string) (Tags, error) + TagsFromBranch(ctx context.Context, branch string) (Tags, error) +} + +type Tag struct { Name string Ref string } -type Tags []TagInfo +var _ zerolog.LogArrayMarshaler = (*Tags)(nil) + +type Tags []Tag + +func shortRef(ref string) string { + if len(ref) <= 11 { + return ref + } + + return ref[:4] + "..." + ref[len(ref)-4:] +} + +func (t Tags) Sort() Tags { + tags := make(Tags, len(t)) + copy(tags, t) -func (t Tags) GetReserved() (TagInfo, bool) { + slices.SortFunc(tags, func(a, b Tag) int { + return semver.Compare(a.Name, b.Name) + }) + + return tags +} + +// MarshalZerologArray implements zerolog.LogArrayMarshaler. +func (t Tags) MarshalZerologArray(a *zerolog.Array) { + + tr := t.Sort() + + for _, tag := range tr { + a.Str(shortRef(tag.Ref) + " => " + tag.Name) + } +} + +func (t Tags) GetReserved() (Tag, bool) { for _, tag := range t { if strings.Contains(tag.Name, "-reserved") { @@ -24,31 +62,55 @@ func (t Tags) GetReserved() (TagInfo, bool) { } } - return TagInfo{}, false + return Tag{}, false +} + +func (t Tags) Names() []string { + var names []string + + for _, tag := range t { + names = append(names, tag.Name) + } + + return names } -// HighestSemverContainingString finds the highest semantic version tag that contains the specified string. -func (t Tags) HighestSemverMatching(matcher ...*regexp.Regexp) (string, error) { +func (t Tags) SemversMatching(matcher func(string) bool) []string { var versions []string - for _, m := range matcher { - for _, tag := range t { - if m.MatchString(tag.Name) { - // Attempt to parse the semantic version from the tag - v := semver.Canonical(tag.Name) - if v != "" && semver.IsValid(v) { - versions = append(versions, tag.Name) - } + for _, tag := range t { + if matcher(tag.Name) { + // Attempt to parse the semantic version from the tag + v := semver.Canonical(tag.Name) + if v != "" && semver.IsValid(v) { + versions = append(versions, tag.Name) } } - if len(versions) == 0 { - return "", errors.New("no matching semantic versions found") + } + + semver.Sort(versions) + + return versions +} + +func (t Tags) ExtractCommitRefs() Tags { + var tags Tags + + for _, tag := range t { + if len(tag.Ref) == 40 { + tags = append(tags, tag) } } - // Use sort to find the highest version - sort.Slice(versions, func(i, j int) bool { - return semver.Compare(versions[i], versions[j]) < 0 - }) - return versions[len(versions)-1], nil + return tags +} + +func (t Tags) MappedByName() map[string]string { + m := make(map[string]string) + + for _, tag := range t { + m[tag.Name] = tag.Ref + } + + return m } diff --git a/tags_test.go b/tags_test.go new file mode 100644 index 0000000..30909ba --- /dev/null +++ b/tags_test.go @@ -0,0 +1,57 @@ +package simver_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/walteh/simver" +) + +func TestExtractCommitRefs(t *testing.T) { + testCases := []struct { + name string + tags simver.Tags + expected simver.Tags + }{ + { + name: "No Commit Refs", + tags: simver.Tags{ + simver.Tag{Name: "v1.2.3"}, + simver.Tag{Name: "v1.2.4-pr1+base"}, + simver.Tag{Name: "v1.2.4-reserved"}, + }, + expected: simver.Tags{}, + }, + { + name: "One Commit Ref", + tags: simver.Tags{ + simver.Tag{Name: "v1.2.3"}, + simver.Tag{Name: "v1.2.4-pr1+base", Ref: "1234567890123456789012345678901234567890"}, + simver.Tag{Name: "v1.2.4-reserved"}, + }, + expected: simver.Tags{ + simver.Tag{Name: "v1.2.4-pr1+base", Ref: "1234567890123456789012345678901234567890"}, + }, + }, + { + name: "Multiple Commit Refs", + tags: simver.Tags{ + simver.Tag{Name: "v1.2.3"}, + simver.Tag{Name: "v1.2.4-pr1+base", Ref: "1234567890123456789012345678901234567890"}, + simver.Tag{Name: "v1.2.4-reserved"}, + simver.Tag{Name: "v1.2.5-pr2+base", Ref: "0987654321098765432109876543210987654321"}, + }, + expected: simver.Tags{ + simver.Tag{Name: "v1.2.4-pr1+base", Ref: "1234567890123456789012345678901234567890"}, + simver.Tag{Name: "v1.2.5-pr2+base", Ref: "0987654321098765432109876543210987654321"}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := tc.tags.ExtractCommitRefs() + assert.ElementsMatch(t, tc.expected, result) + }) + } +}