feat(mr): delete the source branch on merge from the web and the CLI
- commit
- 4ea27ba3c2ec2048a29b656d03affce6e70dcb45
- author
- brice · · 2026-10-11 05:39 UTC
- parent
- d205ffd
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
25 files · +688 -254
M README.md +1 -1
| @@ -41,7 +41,7 @@ ermite token create <label> [--preset cli|agent|ci] [--scope repo:read …] [--r | |||
| 41 | 41 | ermite token list | revoke <id> | |
| 42 | 42 | ermite mr create <repo> --source <branch> --title … | list <repo> | show <repo> <n> [--diff] | |
| 43 | 43 | ermite mr comment <repo> <n> -m … [--path … --line … --side old|new] | resolve <repo> <n> <comment> | |
| 44 | - | ermite mr merge <repo> <n> --head <id> [--squash] | close <repo> <n> | |
| 44 | + | ermite mr merge <repo> <n> --head <id> [--squash] [--delete-branch] | close <repo> <n> | |
| 45 | 45 | ermite web login # prints a one-time sign-in link (60 s) | |
| 46 | 46 | ermite web logout --all # signs every browser out | |
| 47 | 47 | ermite signing-key show # public key and allowed_signers line of squash commits | |
| @@ -41,7 +41,7 @@ ermite token create <label> [--preset cli|agent|ci] [--scope repo:read …] [--r | |||||
| 41 | ermite token list | revoke <id> | 41 | ermite token list | revoke <id> | ||
| 42 | ermite mr create <repo> --source <branch> --title … | list <repo> | show <repo> <n> [--diff] | 42 | ermite mr create <repo> --source <branch> --title … | list <repo> | show <repo> <n> [--diff] | ||
| 43 | ermite mr comment <repo> <n> -m … [--path … --line … --side old|new] | resolve <repo> <n> <comment> | 43 | ermite mr comment <repo> <n> -m … [--path … --line … --side old|new] | resolve <repo> <n> <comment> | ||
| 44 | - | ermite mr merge <repo> <n> --head <id> [--squash] | close <repo> <n> | 44 | + | ermite mr merge <repo> <n> --head <id> [--squash] [--delete-branch] | close <repo> <n> |
| 45 | ermite web login # prints a one-time sign-in link (60 s) | 45 | ermite web login # prints a one-time sign-in link (60 s) | ||
| 46 | ermite web logout --all # signs every browser out | 46 | ermite web logout --all # signs every browser out | ||
| 47 | ermite signing-key show # public key and allowed_signers line of squash commits | 47 | ermite signing-key show # public key and allowed_signers line of squash commits | ||
M cmd/ermite/api_test.go +1 -1
| @@ -158,7 +158,7 @@ func TestAC2ReadAccess(t *testing.T) { | |||
| 158 | 158 | for _, who := range allPrincipals { | |
| 159 | 159 | resp, err := r.who[who].server.GetVersion(ctx, connect.NewRequest(&pb.GetVersionRequest{})) | |
| 160 | 160 | require.NoError(t, err, who) | |
| 161 | - | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "signing_key", "session"}, resp.Msg.GetFeatures(), who) | |
| 161 | + | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "signing_key", "session"}, resp.Msg.GetFeatures(), who) | |
| 162 | 162 | assert.NotEmpty(t, resp.Msg.GetVersion()) | |
| 163 | 163 | } | |
| 164 | 164 | }) | |
| @@ -158,7 +158,7 @@ func TestAC2ReadAccess(t *testing.T) { | |||||
| 158 | for _, who := range allPrincipals { | 158 | for _, who := range allPrincipals { | ||
| 159 | resp, err := r.who[who].server.GetVersion(ctx, connect.NewRequest(&pb.GetVersionRequest{})) | 159 | resp, err := r.who[who].server.GetVersion(ctx, connect.NewRequest(&pb.GetVersionRequest{})) | ||
| 160 | require.NoError(t, err, who) | 160 | require.NoError(t, err, who) | ||
| 161 | - | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "signing_key", "session"}, resp.Msg.GetFeatures(), who) | 161 | + | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "signing_key", "session"}, resp.Msg.GetFeatures(), who) |
| 162 | assert.NotEmpty(t, resp.Msg.GetVersion()) | 162 | assert.NotEmpty(t, resp.Msg.GetVersion()) | ||
| 163 | } | 163 | } | ||
| 164 | }) | 164 | }) | ||
M cmd/ermite/cli_e2e_test.go +1 -1
| @@ -324,7 +324,7 @@ func TestAC6CLIEndToEnd(t *testing.T) { | |||
| 324 | 324 | ver := decode[versionOut](t, res.stdout) | |
| 325 | 325 | assert.Equal(t, c.url, ver.Server.URL) | |
| 326 | 326 | assert.NotEmpty(t, ver.Server.Version) | |
| 327 | - | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "signing_key", "session"}, ver.Server.Features) | |
| 327 | + | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "signing_key", "session"}, ver.Server.Features) | |
| 328 | 328 | assert.Equal(t, []string{}, ver.Server.MissingFeatures) | |
| 329 | 329 | stdout, stderr, code = c.bin(t, secret, "version") | |
| 330 | 330 | require.Equal(t, 0, code, stderr) | |
| @@ -324,7 +324,7 @@ func TestAC6CLIEndToEnd(t *testing.T) { | |||||
| 324 | ver := decode[versionOut](t, res.stdout) | 324 | ver := decode[versionOut](t, res.stdout) | ||
| 325 | assert.Equal(t, c.url, ver.Server.URL) | 325 | assert.Equal(t, c.url, ver.Server.URL) | ||
| 326 | assert.NotEmpty(t, ver.Server.Version) | 326 | assert.NotEmpty(t, ver.Server.Version) | ||
| 327 | - | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "signing_key", "session"}, ver.Server.Features) | 327 | + | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "signing_key", "session"}, ver.Server.Features) |
| 328 | assert.Equal(t, []string{}, ver.Server.MissingFeatures) | 328 | assert.Equal(t, []string{}, ver.Server.MissingFeatures) | ||
| 329 | stdout, stderr, code = c.bin(t, secret, "version") | 329 | stdout, stderr, code = c.bin(t, secret, "version") | ||
| 330 | require.Equal(t, 0, code, stderr) | 330 | require.Equal(t, 0, code, stderr) | ||
M cmd/ermite/cli_mr.go +23 -4
| @@ -115,6 +115,12 @@ type jsonMergeRequest struct { | |||
| 115 | 115 | MergeMode string `json:"merge_mode"` | |
| 116 | 116 | } | |
| 117 | 117 | ||
| 118 | + | // jsonMerged is the output of `mr merge --json`. | |
| 119 | + | type jsonMerged struct { | |
| 120 | + | jsonMergeRequest | |
| 121 | + | SourceBranchDeleted bool `json:"source_branch_deleted"` | |
| 122 | + | } | |
| 123 | + | ||
| 118 | 124 | func toJSONMergeRequest(m *ermite.MergeRequest) jsonMergeRequest { | |
| 119 | 125 | return jsonMergeRequest{ | |
| 120 | 126 | Repo: m.GetRepo(), Number: m.GetNumber(), Title: m.GetTitle(), Description: m.GetDescription(), | |
| @@ -740,7 +746,7 @@ func newMRResolveCmd(env *cliEnv) *cobra.Command { | |||
| 740 | 746 | } | |
| 741 | 747 | ||
| 742 | 748 | func newMRMergeCmd(env *cliEnv) *cobra.Command { | |
| 743 | - | var squash bool | |
| 749 | + | var squash, deleteBranch bool | |
| 744 | 750 | var head string | |
| 745 | 751 | cmd := &cobra.Command{ | |
| 746 | 752 | Use: "merge <repo> <number>", | |
| @@ -753,12 +759,17 @@ func newMRMergeCmd(env *cliEnv) *cobra.Command { | |||
| 753 | 759 | "The server merges only if the source branch still points to the head you reviewed, so --head is\n" + | |
| 754 | 760 | "required: give the full id of the head that `ermite mr show <repo> <number>` printed when you\n" + | |
| 755 | 761 | "reviewed the change. If the branch moved since, the merge is refused: review again. Needs the\n" + | |
| 756 | - | "mr:merge scope." + jsonMRHelp, | |
| 762 | + | "mr:merge scope.\n\n" + | |
| 763 | + | "--delete-branch deletes the source branch after the merge. The merge is refused when the branch\n" + | |
| 764 | + | "is the default branch or used by another open merge request. If the deletion fails after the\n" + | |
| 765 | + | "merge, the merge stands, the branch is kept and a warning goes to standard error." + jsonMRHelp + | |
| 766 | + | "\n--json adds source_branch_deleted (true when the branch was deleted).", | |
| 757 | 767 | Args: cobra.ExactArgs(2), | |
| 758 | 768 | ValidArgsFunction: completeMRRepoThenNumber(env), | |
| 759 | 769 | } | |
| 760 | 770 | cmd.Flags().BoolVar(&squash, "squash", false, "squash the branch into one signed commit instead of a fast-forward") | |
| 761 | 771 | cmd.Flags().StringVar(&head, "head", "", "full id of the head you reviewed, from `ermite mr show` (required)") | |
| 772 | + | cmd.Flags().BoolVar(&deleteBranch, "delete-branch", false, "delete the source branch after the merge") | |
| 762 | 773 | asJSON := addJSONFlag(cmd) | |
| 763 | 774 | cmd.RunE = func(cmd *cobra.Command, args []string) error { | |
| 764 | 775 | n, err := parseNumber(args[1]) | |
| @@ -780,17 +791,25 @@ func newMRMergeCmd(env *cliEnv) *cobra.Command { | |||
| 780 | 791 | mode = ermite.MergeMode_MERGE_MODE_SQUASH | |
| 781 | 792 | } | |
| 782 | 793 | res, err := c.MergeRequests().MergeMergeRequest(ctx, connect.NewRequest(&ermite.MergeMergeRequestRequest{ | |
| 783 | - | Repo: args[0], Number: n, Mode: mode, HeadId: head, | |
| 794 | + | Repo: args[0], Number: n, Mode: mode, HeadId: head, DeleteSourceBranch: deleteBranch, | |
| 784 | 795 | })) | |
| 785 | 796 | if err != nil { | |
| 786 | 797 | return c.Describe(err) | |
| 787 | 798 | } | |
| 788 | 799 | m := res.Msg.GetMergeRequest() | |
| 800 | + | deleted := res.Msg.GetSourceBranchDeleted() | |
| 801 | + | if deleteBranch && !deleted { | |
| 802 | + | _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "ermite: warning: the branch %s was kept (it moved after the merge, or the server cannot delete it)\n", | |
| 803 | + | clean(m.GetSourceBranch())) | |
| 804 | + | } | |
| 789 | 805 | if *asJSON { | |
| 790 | - | return writeJSON(cmd.OutOrStdout(), toJSONMergeRequest(m)) | |
| 806 | + | return writeJSON(cmd.OutOrStdout(), jsonMerged{jsonMergeRequest: toJSONMergeRequest(m), SourceBranchDeleted: deleted}) | |
| 791 | 807 | } | |
| 792 | 808 | _, err = fmt.Fprintf(cmd.OutOrStdout(), "merged %s!%d into %s (%s) at %s\n", clean(m.GetRepo()), m.GetNumber(), | |
| 793 | 809 | clean(m.GetTargetBranch()), modeName(m.GetMergeMode()), shortID(m.GetMergedCommitId())) | |
| 810 | + | if err == nil && deleted { | |
| 811 | + | _, err = fmt.Fprintf(cmd.OutOrStdout(), "deleted branch %s\n", clean(m.GetSourceBranch())) | |
| 812 | + | } | |
| 794 | 813 | return err | |
| 795 | 814 | } | |
| 796 | 815 | return cmd | |
| @@ -115,6 +115,12 @@ type jsonMergeRequest struct { | |||||
| 115 | MergeMode string `json:"merge_mode"` | 115 | MergeMode string `json:"merge_mode"` | ||
| 116 | } | 116 | } | ||
| 117 | 117 | ||||
| 118 | + | // jsonMerged is the output of `mr merge --json`. | |||
| 119 | + | type jsonMerged struct { | |||
| 120 | + | jsonMergeRequest | |||
| 121 | + | SourceBranchDeleted bool `json:"source_branch_deleted"` | |||
| 122 | + | } | |||
| 123 | + | ||||
| 118 | func toJSONMergeRequest(m *ermite.MergeRequest) jsonMergeRequest { | 124 | func toJSONMergeRequest(m *ermite.MergeRequest) jsonMergeRequest { | ||
| 119 | return jsonMergeRequest{ | 125 | return jsonMergeRequest{ | ||
| 120 | Repo: m.GetRepo(), Number: m.GetNumber(), Title: m.GetTitle(), Description: m.GetDescription(), | 126 | Repo: m.GetRepo(), Number: m.GetNumber(), Title: m.GetTitle(), Description: m.GetDescription(), | ||
| @@ -740,7 +746,7 @@ func newMRResolveCmd(env *cliEnv) *cobra.Command { | |||||
| 740 | } | 746 | } | ||
| 741 | 747 | ||||
| 742 | func newMRMergeCmd(env *cliEnv) *cobra.Command { | 748 | func newMRMergeCmd(env *cliEnv) *cobra.Command { | ||
| 743 | - | var squash bool | 749 | + | var squash, deleteBranch bool |
| 744 | var head string | 750 | var head string | ||
| 745 | cmd := &cobra.Command{ | 751 | cmd := &cobra.Command{ | ||
| 746 | Use: "merge <repo> <number>", | 752 | Use: "merge <repo> <number>", | ||
| @@ -753,12 +759,17 @@ func newMRMergeCmd(env *cliEnv) *cobra.Command { | |||||
| 753 | "The server merges only if the source branch still points to the head you reviewed, so --head is\n" + | 759 | "The server merges only if the source branch still points to the head you reviewed, so --head is\n" + | ||
| 754 | "required: give the full id of the head that `ermite mr show <repo> <number>` printed when you\n" + | 760 | "required: give the full id of the head that `ermite mr show <repo> <number>` printed when you\n" + | ||
| 755 | "reviewed the change. If the branch moved since, the merge is refused: review again. Needs the\n" + | 761 | "reviewed the change. If the branch moved since, the merge is refused: review again. Needs the\n" + | ||
| 756 | - | "mr:merge scope." + jsonMRHelp, | 762 | + | "mr:merge scope.\n\n" + |
| 763 | + | "--delete-branch deletes the source branch after the merge. The merge is refused when the branch\n" + | |||
| 764 | + | "is the default branch or used by another open merge request. If the deletion fails after the\n" + | |||
| 765 | + | "merge, the merge stands, the branch is kept and a warning goes to standard error." + jsonMRHelp + | |||
| 766 | + | "\n--json adds source_branch_deleted (true when the branch was deleted).", | |||
| 757 | Args: cobra.ExactArgs(2), | 767 | Args: cobra.ExactArgs(2), | ||
| 758 | ValidArgsFunction: completeMRRepoThenNumber(env), | 768 | ValidArgsFunction: completeMRRepoThenNumber(env), | ||
| 759 | } | 769 | } | ||
| 760 | cmd.Flags().BoolVar(&squash, "squash", false, "squash the branch into one signed commit instead of a fast-forward") | 770 | cmd.Flags().BoolVar(&squash, "squash", false, "squash the branch into one signed commit instead of a fast-forward") | ||
| 761 | cmd.Flags().StringVar(&head, "head", "", "full id of the head you reviewed, from `ermite mr show` (required)") | 771 | cmd.Flags().StringVar(&head, "head", "", "full id of the head you reviewed, from `ermite mr show` (required)") | ||
| 772 | + | cmd.Flags().BoolVar(&deleteBranch, "delete-branch", false, "delete the source branch after the merge") | |||
| 762 | asJSON := addJSONFlag(cmd) | 773 | asJSON := addJSONFlag(cmd) | ||
| 763 | cmd.RunE = func(cmd *cobra.Command, args []string) error { | 774 | cmd.RunE = func(cmd *cobra.Command, args []string) error { | ||
| 764 | n, err := parseNumber(args[1]) | 775 | n, err := parseNumber(args[1]) | ||
| @@ -780,17 +791,25 @@ func newMRMergeCmd(env *cliEnv) *cobra.Command { | |||||
| 780 | mode = ermite.MergeMode_MERGE_MODE_SQUASH | 791 | mode = ermite.MergeMode_MERGE_MODE_SQUASH | ||
| 781 | } | 792 | } | ||
| 782 | res, err := c.MergeRequests().MergeMergeRequest(ctx, connect.NewRequest(&ermite.MergeMergeRequestRequest{ | 793 | res, err := c.MergeRequests().MergeMergeRequest(ctx, connect.NewRequest(&ermite.MergeMergeRequestRequest{ | ||
| 783 | - | Repo: args[0], Number: n, Mode: mode, HeadId: head, | 794 | + | Repo: args[0], Number: n, Mode: mode, HeadId: head, DeleteSourceBranch: deleteBranch, |
| 784 | })) | 795 | })) | ||
| 785 | if err != nil { | 796 | if err != nil { | ||
| 786 | return c.Describe(err) | 797 | return c.Describe(err) | ||
| 787 | } | 798 | } | ||
| 788 | m := res.Msg.GetMergeRequest() | 799 | m := res.Msg.GetMergeRequest() | ||
| 800 | + | deleted := res.Msg.GetSourceBranchDeleted() | |||
| 801 | + | if deleteBranch && !deleted { | |||
| 802 | + | _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "ermite: warning: the branch %s was kept (it moved after the merge, or the server cannot delete it)\n", | |||
| 803 | + | clean(m.GetSourceBranch())) | |||
| 804 | + | } | |||
| 789 | if *asJSON { | 805 | if *asJSON { | ||
| 790 | - | return writeJSON(cmd.OutOrStdout(), toJSONMergeRequest(m)) | 806 | + | return writeJSON(cmd.OutOrStdout(), jsonMerged{jsonMergeRequest: toJSONMergeRequest(m), SourceBranchDeleted: deleted}) |
| 791 | } | 807 | } | ||
| 792 | _, err = fmt.Fprintf(cmd.OutOrStdout(), "merged %s!%d into %s (%s) at %s\n", clean(m.GetRepo()), m.GetNumber(), | 808 | _, err = fmt.Fprintf(cmd.OutOrStdout(), "merged %s!%d into %s (%s) at %s\n", clean(m.GetRepo()), m.GetNumber(), | ||
| 793 | clean(m.GetTargetBranch()), modeName(m.GetMergeMode()), shortID(m.GetMergedCommitId())) | 809 | clean(m.GetTargetBranch()), modeName(m.GetMergeMode()), shortID(m.GetMergedCommitId())) | ||
| 810 | + | if err == nil && deleted { | |||
| 811 | + | _, err = fmt.Fprintf(cmd.OutOrStdout(), "deleted branch %s\n", clean(m.GetSourceBranch())) | |||
| 812 | + | } | |||
| 794 | return err | 813 | return err | ||
| 795 | } | 814 | } | ||
| 796 | return cmd | 815 | return cmd | ||
M cmd/ermite/cli_mr_test.go +32 -0
| @@ -309,6 +309,38 @@ func TestMRMergeNeedsTheHead(t *testing.T) { | |||
| 309 | 309 | assert.NotContains(t, res.stdout, "default: the current head") | |
| 310 | 310 | } | |
| 311 | 311 | ||
| 312 | + | func TestMRMergeDeleteBranch(t *testing.T) { | |
| 313 | + | merged := proto.Clone(mrOne).(*ermite.MergeRequest) | |
| 314 | + | merged.State = ermite.MergeRequestState_MERGE_REQUEST_STATE_MERGED | |
| 315 | + | merged.MergedCommitId, merged.MergeMode = headA, ermite.MergeMode_MERGE_MODE_FAST_FORWARD | |
| 316 | + | deleted := true | |
| 317 | + | f := &fakeAPI{mergeMR: func(*ermite.MergeMergeRequestRequest) (*ermite.MergeMergeRequestResponse, error) { | |
| 318 | + | return &ermite.MergeMergeRequestResponse{MergeRequest: merged, SourceBranchDeleted: deleted}, nil | |
| 319 | + | }} | |
| 320 | + | url := f.serve(t) | |
| 321 | + | ||
| 322 | + | res := runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--head", headA, "--delete-branch").ok(t) | |
| 323 | + | assert.Equal(t, "merged alpha!1 into main (fast-forward) at "+headA[:10]+"\ndeleted branch feat/x\n", res.stdout) | |
| 324 | + | assert.Empty(t, res.stderr) | |
| 325 | + | _, reqs := f.snapshot() | |
| 326 | + | assert.True(t, reqs[0].(*ermite.MergeMergeRequestRequest).GetDeleteSourceBranch()) | |
| 327 | + | ||
| 328 | + | res = runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--head", headA, "--delete-branch", "--json").ok(t) | |
| 329 | + | assert.Contains(t, res.stdout, `"source_branch_deleted": true`) | |
| 330 | + | ||
| 331 | + | // The merge stands when the branch is kept: a warning, no failure. | |
| 332 | + | deleted = false | |
| 333 | + | res = runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--head", headA, "--delete-branch").ok(t) | |
| 334 | + | assert.Equal(t, "merged alpha!1 into main (fast-forward) at "+headA[:10]+"\n", res.stdout) | |
| 335 | + | assert.Contains(t, res.stderr, "ermite: warning: the branch feat/x was kept") | |
| 336 | + | ||
| 337 | + | // Without the flag nothing is asked and nothing is said. | |
| 338 | + | res = runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--head", headA).ok(t) | |
| 339 | + | assert.Empty(t, res.stderr) | |
| 340 | + | _, reqs = f.snapshot() | |
| 341 | + | assert.False(t, reqs[len(reqs)-1].(*ermite.MergeMergeRequestRequest).GetDeleteSourceBranch()) | |
| 342 | + | } | |
| 343 | + | ||
| 312 | 344 | func TestMRErrorsAreFriendly(t *testing.T) { | |
| 313 | 345 | f := &fakeAPI{ | |
| 314 | 346 | listMR: func(*ermite.ListMergeRequestsRequest) (*ermite.ListMergeRequestsResponse, error) { | |
| @@ -309,6 +309,38 @@ func TestMRMergeNeedsTheHead(t *testing.T) { | |||||
| 309 | assert.NotContains(t, res.stdout, "default: the current head") | 309 | assert.NotContains(t, res.stdout, "default: the current head") | ||
| 310 | } | 310 | } | ||
| 311 | 311 | ||||
| 312 | + | func TestMRMergeDeleteBranch(t *testing.T) { | |||
| 313 | + | merged := proto.Clone(mrOne).(*ermite.MergeRequest) | |||
| 314 | + | merged.State = ermite.MergeRequestState_MERGE_REQUEST_STATE_MERGED | |||
| 315 | + | merged.MergedCommitId, merged.MergeMode = headA, ermite.MergeMode_MERGE_MODE_FAST_FORWARD | |||
| 316 | + | deleted := true | |||
| 317 | + | f := &fakeAPI{mergeMR: func(*ermite.MergeMergeRequestRequest) (*ermite.MergeMergeRequestResponse, error) { | |||
| 318 | + | return &ermite.MergeMergeRequestResponse{MergeRequest: merged, SourceBranchDeleted: deleted}, nil | |||
| 319 | + | }} | |||
| 320 | + | url := f.serve(t) | |||
| 321 | + | ||||
| 322 | + | res := runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--head", headA, "--delete-branch").ok(t) | |||
| 323 | + | assert.Equal(t, "merged alpha!1 into main (fast-forward) at "+headA[:10]+"\ndeleted branch feat/x\n", res.stdout) | |||
| 324 | + | assert.Empty(t, res.stderr) | |||
| 325 | + | _, reqs := f.snapshot() | |||
| 326 | + | assert.True(t, reqs[0].(*ermite.MergeMergeRequestRequest).GetDeleteSourceBranch()) | |||
| 327 | + | ||||
| 328 | + | res = runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--head", headA, "--delete-branch", "--json").ok(t) | |||
| 329 | + | assert.Contains(t, res.stdout, `"source_branch_deleted": true`) | |||
| 330 | + | ||||
| 331 | + | // The merge stands when the branch is kept: a warning, no failure. | |||
| 332 | + | deleted = false | |||
| 333 | + | res = runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--head", headA, "--delete-branch").ok(t) | |||
| 334 | + | assert.Equal(t, "merged alpha!1 into main (fast-forward) at "+headA[:10]+"\n", res.stdout) | |||
| 335 | + | assert.Contains(t, res.stderr, "ermite: warning: the branch feat/x was kept") | |||
| 336 | + | ||||
| 337 | + | // Without the flag nothing is asked and nothing is said. | |||
| 338 | + | res = runCLI(t, envFor(url), "", "mr", "merge", "alpha", "1", "--head", headA).ok(t) | |||
| 339 | + | assert.Empty(t, res.stderr) | |||
| 340 | + | _, reqs = f.snapshot() | |||
| 341 | + | assert.False(t, reqs[len(reqs)-1].(*ermite.MergeMergeRequestRequest).GetDeleteSourceBranch()) | |||
| 342 | + | } | |||
| 343 | + | ||||
| 312 | func TestMRErrorsAreFriendly(t *testing.T) { | 344 | func TestMRErrorsAreFriendly(t *testing.T) { | ||
| 313 | f := &fakeAPI{ | 345 | f := &fakeAPI{ | ||
| 314 | listMR: func(*ermite.ListMergeRequestsRequest) (*ermite.ListMergeRequestsResponse, error) { | 346 | listMR: func(*ermite.ListMergeRequestsRequest) (*ermite.ListMergeRequestsResponse, error) { | ||
M cmd/ermite/cli_version.go +6 -5
| @@ -100,9 +100,10 @@ func newVersionCmd(env *cliEnv) *cobra.Command { | |||
| 100 | 100 | ||
| 101 | 101 | // featureUse says what breaks without a feature, for the version warning. | |
| 102 | 102 | var featureUse = map[string]string{ | |
| 103 | - | client.FeatureRepoUpdate: "`ermite repo edit` needs: the command will fail on this server", | |
| 104 | - | client.FeatureEventsResume: "`ermite watch` uses to resume after a reconnection: events during an outage may be missed", | |
| 105 | - | client.FeatureMergeRequests: "`ermite mr` needs: the commands will fail on this server", | |
| 106 | - | client.FeatureSigningKey: "`ermite signing-key show` needs: the command will fail on this server", | |
| 107 | - | client.FeatureSession: "`ermite web login` needs: the command will fail on this server", | |
| 103 | + | client.FeatureRepoUpdate: "`ermite repo edit` needs: the command will fail on this server", | |
| 104 | + | client.FeatureEventsResume: "`ermite watch` uses to resume after a reconnection: events during an outage may be missed", | |
| 105 | + | client.FeatureMergeRequests: "`ermite mr` needs: the commands will fail on this server", | |
| 106 | + | client.FeatureMergeDeleteBranch: "`ermite mr merge --delete-branch` needs: the merge works but the branch is kept", | |
| 107 | + | client.FeatureSigningKey: "`ermite signing-key show` needs: the command will fail on this server", | |
| 108 | + | client.FeatureSession: "`ermite web login` needs: the command will fail on this server", | |
| 108 | 109 | } | |
| @@ -100,9 +100,10 @@ func newVersionCmd(env *cliEnv) *cobra.Command { | |||||
| 100 | 100 | ||||
| 101 | // featureUse says what breaks without a feature, for the version warning. | 101 | // featureUse says what breaks without a feature, for the version warning. | ||
| 102 | var featureUse = map[string]string{ | 102 | var featureUse = map[string]string{ | ||
| 103 | - | client.FeatureRepoUpdate: "`ermite repo edit` needs: the command will fail on this server", | 103 | + | client.FeatureRepoUpdate: "`ermite repo edit` needs: the command will fail on this server", |
| 104 | - | client.FeatureEventsResume: "`ermite watch` uses to resume after a reconnection: events during an outage may be missed", | 104 | + | client.FeatureEventsResume: "`ermite watch` uses to resume after a reconnection: events during an outage may be missed", |
| 105 | - | client.FeatureMergeRequests: "`ermite mr` needs: the commands will fail on this server", | 105 | + | client.FeatureMergeRequests: "`ermite mr` needs: the commands will fail on this server", |
| 106 | - | client.FeatureSigningKey: "`ermite signing-key show` needs: the command will fail on this server", | 106 | + | client.FeatureMergeDeleteBranch: "`ermite mr merge --delete-branch` needs: the merge works but the branch is kept", |
| 107 | - | client.FeatureSession: "`ermite web login` needs: the command will fail on this server", | 107 | + | client.FeatureSigningKey: "`ermite signing-key show` needs: the command will fail on this server", |
| 108 | + | client.FeatureSession: "`ermite web login` needs: the command will fail on this server", | |||
| 108 | } | 109 | } | ||
M cmd/ermite/cli_version_test.go +5 -5
| @@ -14,7 +14,7 @@ import ( | |||
| 14 | 14 | ) | |
| 15 | 15 | ||
| 16 | 16 | // allFeatures are the features this client uses. | |
| 17 | - | var allFeatures = []string{"repo.update", "events.resume", "merge_requests", "signing_key", "session"} | |
| 17 | + | var allFeatures = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "signing_key", "session"} | |
| 18 | 18 | ||
| 19 | 19 | func versionFake(features ...string) *fakeAPI { | |
| 20 | 20 | return &fakeAPI{getVersion: func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error) { | |
| @@ -35,15 +35,15 @@ func TestVersionWithServer(t *testing.T) { | |||
| 35 | 35 | f := versionFake(allFeatures...) | |
| 36 | 36 | url := f.serve(t) | |
| 37 | 37 | res := runCLI(t, envFor(url), "", "version").ok(t) | |
| 38 | - | assert.Equal(t, "client ermite v0.0.1\nserver v0.4.0 at "+url+"\nfeatures repo.update, events.resume, merge_requests, signing_key, session\n", res.stdout) | |
| 38 | + | assert.Equal(t, "client ermite v0.0.1\nserver v0.4.0 at "+url+"\nfeatures repo.update, events.resume, merge_requests, merge_requests.delete_branch, signing_key, session\n", res.stdout) | |
| 39 | 39 | assert.Empty(t, res.stderr, "no warning when every feature is there") | |
| 40 | 40 | ||
| 41 | 41 | res = runCLI(t, envFor(url), "", "version", "--json").ok(t) | |
| 42 | - | assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","events.resume","merge_requests","signing_key","session"],"missing_features":[]}}`, res.stdout) | |
| 42 | + | assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","events.resume","merge_requests","merge_requests.delete_branch","signing_key","session"],"missing_features":[]}}`, res.stdout) | |
| 43 | 43 | } | |
| 44 | 44 | ||
| 45 | 45 | func TestVersionWarnsAboutMissingFeatures(t *testing.T) { | |
| 46 | - | f := versionFake("repo.update", "merge_requests", "signing_key", "session") | |
| 46 | + | f := versionFake("repo.update", "merge_requests", "merge_requests.delete_branch", "signing_key", "session") | |
| 47 | 47 | url := f.serve(t) | |
| 48 | 48 | res := runCLI(t, envFor(url), "", "version").ok(t) | |
| 49 | 49 | assert.Contains(t, res.stdout, "v0.4.0") | |
| @@ -51,7 +51,7 @@ func TestVersionWarnsAboutMissingFeatures(t *testing.T) { | |||
| 51 | 51 | assert.NotContains(t, res.stderr, "repo.update") | |
| 52 | 52 | ||
| 53 | 53 | res = runCLI(t, envFor(url), "", "version", "--json").ok(t) | |
| 54 | - | assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","merge_requests","signing_key","session"],"missing_features":["events.resume"]}}`, res.stdout) | |
| 54 | + | assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","merge_requests","merge_requests.delete_branch","signing_key","session"],"missing_features":["events.resume"]}}`, res.stdout) | |
| 55 | 55 | assert.Contains(t, res.stderr, "events.resume", "the warning also goes to stderr with --json") | |
| 56 | 56 | ||
| 57 | 57 | f = versionFake() | |
| @@ -14,7 +14,7 @@ import ( | |||||
| 14 | ) | 14 | ) | ||
| 15 | 15 | ||||
| 16 | // allFeatures are the features this client uses. | 16 | // allFeatures are the features this client uses. | ||
| 17 | - | var allFeatures = []string{"repo.update", "events.resume", "merge_requests", "signing_key", "session"} | 17 | + | var allFeatures = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "signing_key", "session"} |
| 18 | 18 | ||||
| 19 | func versionFake(features ...string) *fakeAPI { | 19 | func versionFake(features ...string) *fakeAPI { | ||
| 20 | return &fakeAPI{getVersion: func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error) { | 20 | return &fakeAPI{getVersion: func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error) { | ||
| @@ -35,15 +35,15 @@ func TestVersionWithServer(t *testing.T) { | |||||
| 35 | f := versionFake(allFeatures...) | 35 | f := versionFake(allFeatures...) | ||
| 36 | url := f.serve(t) | 36 | url := f.serve(t) | ||
| 37 | res := runCLI(t, envFor(url), "", "version").ok(t) | 37 | res := runCLI(t, envFor(url), "", "version").ok(t) | ||
| 38 | - | assert.Equal(t, "client ermite v0.0.1\nserver v0.4.0 at "+url+"\nfeatures repo.update, events.resume, merge_requests, signing_key, session\n", res.stdout) | 38 | + | assert.Equal(t, "client ermite v0.0.1\nserver v0.4.0 at "+url+"\nfeatures repo.update, events.resume, merge_requests, merge_requests.delete_branch, signing_key, session\n", res.stdout) |
| 39 | assert.Empty(t, res.stderr, "no warning when every feature is there") | 39 | assert.Empty(t, res.stderr, "no warning when every feature is there") | ||
| 40 | 40 | ||||
| 41 | res = runCLI(t, envFor(url), "", "version", "--json").ok(t) | 41 | res = runCLI(t, envFor(url), "", "version", "--json").ok(t) | ||
| 42 | - | assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","events.resume","merge_requests","signing_key","session"],"missing_features":[]}}`, res.stdout) | 42 | + | assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","events.resume","merge_requests","merge_requests.delete_branch","signing_key","session"],"missing_features":[]}}`, res.stdout) |
| 43 | } | 43 | } | ||
| 44 | 44 | ||||
| 45 | func TestVersionWarnsAboutMissingFeatures(t *testing.T) { | 45 | func TestVersionWarnsAboutMissingFeatures(t *testing.T) { | ||
| 46 | - | f := versionFake("repo.update", "merge_requests", "signing_key", "session") | 46 | + | f := versionFake("repo.update", "merge_requests", "merge_requests.delete_branch", "signing_key", "session") |
| 47 | url := f.serve(t) | 47 | url := f.serve(t) | ||
| 48 | res := runCLI(t, envFor(url), "", "version").ok(t) | 48 | res := runCLI(t, envFor(url), "", "version").ok(t) | ||
| 49 | assert.Contains(t, res.stdout, "v0.4.0") | 49 | assert.Contains(t, res.stdout, "v0.4.0") | ||
| @@ -51,7 +51,7 @@ func TestVersionWarnsAboutMissingFeatures(t *testing.T) { | |||||
| 51 | assert.NotContains(t, res.stderr, "repo.update") | 51 | assert.NotContains(t, res.stderr, "repo.update") | ||
| 52 | 52 | ||||
| 53 | res = runCLI(t, envFor(url), "", "version", "--json").ok(t) | 53 | res = runCLI(t, envFor(url), "", "version", "--json").ok(t) | ||
| 54 | - | assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","merge_requests","signing_key","session"],"missing_features":["events.resume"]}}`, res.stdout) | 54 | + | assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","merge_requests","merge_requests.delete_branch","signing_key","session"],"missing_features":["events.resume"]}}`, res.stdout) |
| 55 | assert.Contains(t, res.stderr, "events.resume", "the warning also goes to stderr with --json") | 55 | assert.Contains(t, res.stderr, "events.resume", "the warning also goes to stderr with --json") | ||
| 56 | 56 | ||||
| 57 | f = versionFake() | 57 | f = versionFake() | ||
M cmd/ermite/web_mr_test.go +5 -1
| @@ -160,6 +160,8 @@ func TestWebMergeRequestPagesRender(t *testing.T) { | |||
| 160 | 160 | assert.Contains(t, own.body, `<option value="fast-forward">`) | |
| 161 | 161 | assert.Contains(t, own.body, `<option value="squash">`) | |
| 162 | 162 | assert.Contains(t, own.body, "ermite mr merge proj 1 --head "+head) | |
| 163 | + | assert.Contains(t, own.body, `name="delete_branch" value="1"> delete feat/search after the merge`) | |
| 164 | + | assert.NotContains(t, own.body, `name="delete_branch" value="1" checked>`, "unchecked by default") | |
| 163 | 165 | assert.Contains(t, own.body, "ermite mr merge proj 1 --head "+head+" --squash") | |
| 164 | 166 | assert.Contains(t, own.body, `action="/proj/merge-requests/1/comments"`) | |
| 165 | 167 | assert.Contains(t, own.body, `action="/proj/merge-requests/1/close"`) | |
| @@ -377,8 +379,10 @@ func TestWebMergeRequestSquashCloseAndList(t *testing.T) { | |||
| 377 | 379 | "source": {"feat/search"}, "target": {"main"}, "title": {"Squash me"}, "description": {""}, | |
| 378 | 380 | }) | |
| 379 | 381 | require.Equal(t, http.StatusSeeOther, res.status, res.body) | |
| 380 | - | res = b.form("/proj/merge-requests/1/merge", token, url.Values{"mode": {"squash"}, "head_id": {head}}) | |
| 382 | + | assert.Contains(t, b.get("/proj/merge-requests/1", as(token)).body, `name="delete_branch" value="1"`, "the box offers to delete the branch") | |
| 383 | + | res = b.form("/proj/merge-requests/1/merge", token, url.Values{"mode": {"squash"}, "head_id": {head}, "delete_branch": {"1"}}) | |
| 381 | 384 | require.Equal(t, http.StatusSeeOther, res.status, res.body) | |
| 385 | + | assert.Empty(t, r.bareGit("proj", "for-each-ref", "refs/heads/feat/search"), "the source branch is deleted") | |
| 382 | 386 | merged := r.headOf("main") | |
| 383 | 387 | assert.NotEqual(t, head, merged, "squash makes a new commit") | |
| 384 | 388 | assert.Equal(t, main0, r.bareGit("proj", "rev-parse", "main^"), "on top of the old target") | |
| @@ -160,6 +160,8 @@ func TestWebMergeRequestPagesRender(t *testing.T) { | |||||
| 160 | assert.Contains(t, own.body, `<option value="fast-forward">`) | 160 | assert.Contains(t, own.body, `<option value="fast-forward">`) | ||
| 161 | assert.Contains(t, own.body, `<option value="squash">`) | 161 | assert.Contains(t, own.body, `<option value="squash">`) | ||
| 162 | assert.Contains(t, own.body, "ermite mr merge proj 1 --head "+head) | 162 | assert.Contains(t, own.body, "ermite mr merge proj 1 --head "+head) | ||
| 163 | + | assert.Contains(t, own.body, `name="delete_branch" value="1"> delete feat/search after the merge`) | |||
| 164 | + | assert.NotContains(t, own.body, `name="delete_branch" value="1" checked>`, "unchecked by default") | |||
| 163 | assert.Contains(t, own.body, "ermite mr merge proj 1 --head "+head+" --squash") | 165 | assert.Contains(t, own.body, "ermite mr merge proj 1 --head "+head+" --squash") | ||
| 164 | assert.Contains(t, own.body, `action="/proj/merge-requests/1/comments"`) | 166 | assert.Contains(t, own.body, `action="/proj/merge-requests/1/comments"`) | ||
| 165 | assert.Contains(t, own.body, `action="/proj/merge-requests/1/close"`) | 167 | assert.Contains(t, own.body, `action="/proj/merge-requests/1/close"`) | ||
| @@ -377,8 +379,10 @@ func TestWebMergeRequestSquashCloseAndList(t *testing.T) { | |||||
| 377 | "source": {"feat/search"}, "target": {"main"}, "title": {"Squash me"}, "description": {""}, | 379 | "source": {"feat/search"}, "target": {"main"}, "title": {"Squash me"}, "description": {""}, | ||
| 378 | }) | 380 | }) | ||
| 379 | require.Equal(t, http.StatusSeeOther, res.status, res.body) | 381 | require.Equal(t, http.StatusSeeOther, res.status, res.body) | ||
| 380 | - | res = b.form("/proj/merge-requests/1/merge", token, url.Values{"mode": {"squash"}, "head_id": {head}}) | 382 | + | assert.Contains(t, b.get("/proj/merge-requests/1", as(token)).body, `name="delete_branch" value="1"`, "the box offers to delete the branch") |
| 383 | + | res = b.form("/proj/merge-requests/1/merge", token, url.Values{"mode": {"squash"}, "head_id": {head}, "delete_branch": {"1"}}) | |||
| 381 | require.Equal(t, http.StatusSeeOther, res.status, res.body) | 384 | require.Equal(t, http.StatusSeeOther, res.status, res.body) | ||
| 385 | + | assert.Empty(t, r.bareGit("proj", "for-each-ref", "refs/heads/feat/search"), "the source branch is deleted") | |||
| 382 | merged := r.headOf("main") | 386 | merged := r.headOf("main") | ||
| 383 | assert.NotEqual(t, head, merged, "squash makes a new commit") | 387 | assert.NotEqual(t, head, merged, "squash makes a new commit") | ||
| 384 | assert.Equal(t, main0, r.bareGit("proj", "rev-parse", "main^"), "on top of the old target") | 388 | assert.Equal(t, main0, r.bareGit("proj", "rev-parse", "main^"), "on top of the old target") | ||
M internal/adapters/client/client_test.go +2 -2
| @@ -139,10 +139,10 @@ func TestDescribeBoundsLongMessages(t *testing.T) { | |||
| 139 | 139 | func TestMissingFeatures(t *testing.T) { | |
| 140 | 140 | assert.Empty(t, client.MissingFeatures(client.UsedFeatures())) | |
| 141 | 141 | assert.Equal(t, | |
| 142 | - | []string{client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureSigningKey}, | |
| 142 | + | []string{client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureSigningKey}, | |
| 143 | 143 | client.MissingFeatures([]string{client.FeatureRepoUpdate, client.FeatureSession, "other"})) | |
| 144 | 144 | assert.Equal(t, []string{client.FeatureSigningKey}, client.MissingFeatures([]string{ | |
| 145 | - | client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureSession, | |
| 145 | + | client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureSession, | |
| 146 | 146 | })) | |
| 147 | 147 | assert.Equal(t, client.UsedFeatures(), client.MissingFeatures(nil)) | |
| 148 | 148 | } | |
| @@ -139,10 +139,10 @@ func TestDescribeBoundsLongMessages(t *testing.T) { | |||||
| 139 | func TestMissingFeatures(t *testing.T) { | 139 | func TestMissingFeatures(t *testing.T) { | ||
| 140 | assert.Empty(t, client.MissingFeatures(client.UsedFeatures())) | 140 | assert.Empty(t, client.MissingFeatures(client.UsedFeatures())) | ||
| 141 | assert.Equal(t, | 141 | assert.Equal(t, | ||
| 142 | - | []string{client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureSigningKey}, | 142 | + | []string{client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureSigningKey}, |
| 143 | client.MissingFeatures([]string{client.FeatureRepoUpdate, client.FeatureSession, "other"})) | 143 | client.MissingFeatures([]string{client.FeatureRepoUpdate, client.FeatureSession, "other"})) | ||
| 144 | assert.Equal(t, []string{client.FeatureSigningKey}, client.MissingFeatures([]string{ | 144 | assert.Equal(t, []string{client.FeatureSigningKey}, client.MissingFeatures([]string{ | ||
| 145 | - | client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureSession, | 145 | + | client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureSession, |
| 146 | })) | 146 | })) | ||
| 147 | assert.Equal(t, client.UsedFeatures(), client.MissingFeatures(nil)) | 147 | assert.Equal(t, client.UsedFeatures(), client.MissingFeatures(nil)) | ||
| 148 | } | 148 | } | ||
M internal/adapters/client/features.go +4 -1
| @@ -15,6 +15,9 @@ const ( | |||
| 15 | 15 | FeatureEventsResume = "events.resume" | |
| 16 | 16 | // FeatureMergeRequests is MergeRequestService (`ermite mr`). | |
| 17 | 17 | FeatureMergeRequests = "merge_requests" | |
| 18 | + | // FeatureMergeDeleteBranch is MergeMergeRequest with delete_source_branch | |
| 19 | + | // (`ermite mr merge --delete-branch`). | |
| 20 | + | FeatureMergeDeleteBranch = "merge_requests.delete_branch" | |
| 18 | 21 | // FeatureSigningKey is ServerService.GetSigningKey (`ermite signing-key`). | |
| 19 | 22 | FeatureSigningKey = "signing_key" | |
| 20 | 23 | // FeatureSession is SessionService (`ermite web login`, `web logout`). | |
| @@ -23,7 +26,7 @@ const ( | |||
| 23 | 26 | ||
| 24 | 27 | // UsedFeatures lists the features the CLI uses, in the order they are reported. | |
| 25 | 28 | func UsedFeatures() []string { | |
| 26 | - | return []string{FeatureRepoUpdate, FeatureEventsResume, FeatureMergeRequests, FeatureSigningKey, FeatureSession} | |
| 29 | + | return []string{FeatureRepoUpdate, FeatureEventsResume, FeatureMergeRequests, FeatureMergeDeleteBranch, FeatureSigningKey, FeatureSession} | |
| 27 | 30 | } | |
| 28 | 31 | ||
| 29 | 32 | // MissingFeatures returns the features the CLI uses that have is lacking. | |
| @@ -15,6 +15,9 @@ const ( | |||||
| 15 | FeatureEventsResume = "events.resume" | 15 | FeatureEventsResume = "events.resume" | ||
| 16 | // FeatureMergeRequests is MergeRequestService (`ermite mr`). | 16 | // FeatureMergeRequests is MergeRequestService (`ermite mr`). | ||
| 17 | FeatureMergeRequests = "merge_requests" | 17 | FeatureMergeRequests = "merge_requests" | ||
| 18 | + | // FeatureMergeDeleteBranch is MergeMergeRequest with delete_source_branch | |||
| 19 | + | // (`ermite mr merge --delete-branch`). | |||
| 20 | + | FeatureMergeDeleteBranch = "merge_requests.delete_branch" | |||
| 18 | // FeatureSigningKey is ServerService.GetSigningKey (`ermite signing-key`). | 21 | // FeatureSigningKey is ServerService.GetSigningKey (`ermite signing-key`). | ||
| 19 | FeatureSigningKey = "signing_key" | 22 | FeatureSigningKey = "signing_key" | ||
| 20 | // FeatureSession is SessionService (`ermite web login`, `web logout`). | 23 | // FeatureSession is SessionService (`ermite web login`, `web logout`). | ||
| @@ -23,7 +26,7 @@ const ( | |||||
| 23 | 26 | ||||
| 24 | // UsedFeatures lists the features the CLI uses, in the order they are reported. | 27 | // UsedFeatures lists the features the CLI uses, in the order they are reported. | ||
| 25 | func UsedFeatures() []string { | 28 | func UsedFeatures() []string { | ||
| 26 | - | return []string{FeatureRepoUpdate, FeatureEventsResume, FeatureMergeRequests, FeatureSigningKey, FeatureSession} | 29 | + | return []string{FeatureRepoUpdate, FeatureEventsResume, FeatureMergeRequests, FeatureMergeDeleteBranch, FeatureSigningKey, FeatureSession} |
| 27 | } | 30 | } | ||
| 28 | 31 | ||||
| 29 | // MissingFeatures returns the features the CLI uses that have is lacking. | 32 | // MissingFeatures returns the features the CLI uses that have is lacking. | ||
M internal/adapters/gitcli/merge.go +26 -0
| @@ -235,3 +235,29 @@ func (g *Git) UpdateBranch(ctx context.Context, repo domain.RepoName, branch str | |||
| 235 | 235 | } | |
| 236 | 236 | return fmt.Errorf("update branch %q of %q: %w", branch, repo, err) | |
| 237 | 237 | } | |
| 238 | + | ||
| 239 | + | // DeleteBranch runs `git update-ref -d refs/heads/<branch> <old>`, which takes | |
| 240 | + | // the ref lock and refuses when the ref is not at old. A failure is told from | |
| 241 | + | // a moved or deleted branch by reading the branch again. | |
| 242 | + | // | |
| 243 | + | //hexcheck:ignore suspicious-business-logic-in-adapter maps a refused update-ref to the conflict error of the port | |
| 244 | + | func (g *Git) DeleteBranch(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID) error { | |
| 245 | + | dir, err := g.repoDir(repo) | |
| 246 | + | if err != nil { | |
| 247 | + | return err | |
| 248 | + | } | |
| 249 | + | if _, err := domain.ParseBranchName(branch); err != nil { | |
| 250 | + | return fmt.Errorf("delete branch of %q: %w", repo, err) | |
| 251 | + | } | |
| 252 | + | _, err = g.run(ctx, "--git-dir="+dir, "update-ref", "-d", headsRefPrefix+branch, old.String()) | |
| 253 | + | if err == nil { | |
| 254 | + | return nil | |
| 255 | + | } | |
| 256 | + | switch cur, herr := g.BranchHead(ctx, repo, branch); { | |
| 257 | + | case errors.Is(herr, domain.ErrNotFound): | |
| 258 | + | return fmt.Errorf("branch %q of %q is gone: %w", branch, repo, domain.ErrConflict) | |
| 259 | + | case herr == nil && cur != old: | |
| 260 | + | return fmt.Errorf("branch %q of %q is at %s, not %s: %w", branch, repo, cur, old, domain.ErrConflict) | |
| 261 | + | } | |
| 262 | + | return fmt.Errorf("delete branch %q of %q: %w", branch, repo, err) | |
| 263 | + | } | |
| @@ -235,3 +235,29 @@ func (g *Git) UpdateBranch(ctx context.Context, repo domain.RepoName, branch str | |||||
| 235 | } | 235 | } | ||
| 236 | return fmt.Errorf("update branch %q of %q: %w", branch, repo, err) | 236 | return fmt.Errorf("update branch %q of %q: %w", branch, repo, err) | ||
| 237 | } | 237 | } | ||
| 238 | + | ||||
| 239 | + | // DeleteBranch runs `git update-ref -d refs/heads/<branch> <old>`, which takes | |||
| 240 | + | // the ref lock and refuses when the ref is not at old. A failure is told from | |||
| 241 | + | // a moved or deleted branch by reading the branch again. | |||
| 242 | + | // | |||
| 243 | + | //hexcheck:ignore suspicious-business-logic-in-adapter maps a refused update-ref to the conflict error of the port | |||
| 244 | + | func (g *Git) DeleteBranch(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID) error { | |||
| 245 | + | dir, err := g.repoDir(repo) | |||
| 246 | + | if err != nil { | |||
| 247 | + | return err | |||
| 248 | + | } | |||
| 249 | + | if _, err := domain.ParseBranchName(branch); err != nil { | |||
| 250 | + | return fmt.Errorf("delete branch of %q: %w", repo, err) | |||
| 251 | + | } | |||
| 252 | + | _, err = g.run(ctx, "--git-dir="+dir, "update-ref", "-d", headsRefPrefix+branch, old.String()) | |||
| 253 | + | if err == nil { | |||
| 254 | + | return nil | |||
| 255 | + | } | |||
| 256 | + | switch cur, herr := g.BranchHead(ctx, repo, branch); { | |||
| 257 | + | case errors.Is(herr, domain.ErrNotFound): | |||
| 258 | + | return fmt.Errorf("branch %q of %q is gone: %w", branch, repo, domain.ErrConflict) | |||
| 259 | + | case herr == nil && cur != old: | |||
| 260 | + | return fmt.Errorf("branch %q of %q is at %s, not %s: %w", branch, repo, cur, old, domain.ErrConflict) | |||
| 261 | + | } | |||
| 262 | + | return fmt.Errorf("delete branch %q of %q: %w", branch, repo, err) | |||
| 263 | + | } | |||
M internal/adapters/gitcli/merge_test.go +25 -0
| @@ -206,3 +206,28 @@ func TestWriteCommitAndUpdateBranch(t *testing.T) { | |||
| 206 | 206 | err = g.UpdateBranch(ctx, name(t, "missing"), "main", id, m.mainTip) | |
| 207 | 207 | assert.ErrorIs(t, err, domain.ErrNotFound) | |
| 208 | 208 | } | |
| 209 | + | ||
| 210 | + | func TestDeleteBranch(t *testing.T) { | |
| 211 | + | g, data := newGit(t) | |
| 212 | + | ctx := context.Background() | |
| 213 | + | m := newMergeFixture(t, data) | |
| 214 | + | m.f.Git("pack-refs", "--all") // the ref is in packed-refs too | |
| 215 | + | ||
| 216 | + | // The old value is a precondition. | |
| 217 | + | err := g.DeleteBranch(ctx, m.f.Name, "feat", m.mainTip) | |
| 218 | + | assert.ErrorIs(t, err, domain.ErrConflict, "feat is not at main") | |
| 219 | + | assert.Equal(t, m.featTip.String(), m.f.Git("rev-parse", "refs/heads/feat")) | |
| 220 | + | ||
| 221 | + | require.NoError(t, g.DeleteBranch(ctx, m.f.Name, "feat", m.featTip)) | |
| 222 | + | _, err = g.BranchHead(ctx, m.f.Name, "feat") | |
| 223 | + | assert.ErrorIs(t, err, domain.ErrNotFound) | |
| 224 | + | assert.Empty(t, m.f.Git("for-each-ref", "refs/heads/feat"), "gone from packed-refs too") | |
| 225 | + | ||
| 226 | + | err = g.DeleteBranch(ctx, m.f.Name, "feat", m.featTip) | |
| 227 | + | assert.ErrorIs(t, err, domain.ErrConflict, "already gone") | |
| 228 | + | err = g.DeleteBranch(ctx, m.f.Name, "refs/heads/main", m.mainTip) | |
| 229 | + | assert.Error(t, err, "only short branch names") | |
| 230 | + | assert.Equal(t, m.mainTip.String(), m.f.Git("rev-parse", "refs/heads/main")) | |
| 231 | + | err = g.DeleteBranch(ctx, name(t, "missing"), "main", m.mainTip) | |
| 232 | + | assert.ErrorIs(t, err, domain.ErrNotFound) | |
| 233 | + | } | |
| @@ -206,3 +206,28 @@ func TestWriteCommitAndUpdateBranch(t *testing.T) { | |||||
| 206 | err = g.UpdateBranch(ctx, name(t, "missing"), "main", id, m.mainTip) | 206 | err = g.UpdateBranch(ctx, name(t, "missing"), "main", id, m.mainTip) | ||
| 207 | assert.ErrorIs(t, err, domain.ErrNotFound) | 207 | assert.ErrorIs(t, err, domain.ErrNotFound) | ||
| 208 | } | 208 | } | ||
| 209 | + | ||||
| 210 | + | func TestDeleteBranch(t *testing.T) { | |||
| 211 | + | g, data := newGit(t) | |||
| 212 | + | ctx := context.Background() | |||
| 213 | + | m := newMergeFixture(t, data) | |||
| 214 | + | m.f.Git("pack-refs", "--all") // the ref is in packed-refs too | |||
| 215 | + | ||||
| 216 | + | // The old value is a precondition. | |||
| 217 | + | err := g.DeleteBranch(ctx, m.f.Name, "feat", m.mainTip) | |||
| 218 | + | assert.ErrorIs(t, err, domain.ErrConflict, "feat is not at main") | |||
| 219 | + | assert.Equal(t, m.featTip.String(), m.f.Git("rev-parse", "refs/heads/feat")) | |||
| 220 | + | ||||
| 221 | + | require.NoError(t, g.DeleteBranch(ctx, m.f.Name, "feat", m.featTip)) | |||
| 222 | + | _, err = g.BranchHead(ctx, m.f.Name, "feat") | |||
| 223 | + | assert.ErrorIs(t, err, domain.ErrNotFound) | |||
| 224 | + | assert.Empty(t, m.f.Git("for-each-ref", "refs/heads/feat"), "gone from packed-refs too") | |||
| 225 | + | ||||
| 226 | + | err = g.DeleteBranch(ctx, m.f.Name, "feat", m.featTip) | |||
| 227 | + | assert.ErrorIs(t, err, domain.ErrConflict, "already gone") | |||
| 228 | + | err = g.DeleteBranch(ctx, m.f.Name, "refs/heads/main", m.mainTip) | |||
| 229 | + | assert.Error(t, err, "only short branch names") | |||
| 230 | + | assert.Equal(t, m.mainTip.String(), m.f.Git("rev-parse", "refs/heads/main")) | |||
| 231 | + | err = g.DeleteBranch(ctx, name(t, "missing"), "main", m.mainTip) | |||
| 232 | + | assert.ErrorIs(t, err, domain.ErrNotFound) | |||
| 233 | + | } | |||
M internal/adapters/rpc/api.go +1 -1
| @@ -27,7 +27,7 @@ const MaxRequestBytes = 1 << 20 | |||
| 27 | 27 | ||
| 28 | 28 | // Features are the optional capabilities of the contract that this server | |
| 29 | 29 | // implements, as GetVersion lists them. | |
| 30 | - | var Features = []string{"repo.update", "events.resume", "merge_requests", "signing_key", "session"} | |
| 30 | + | var Features = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "signing_key", "session"} | |
| 31 | 31 | ||
| 32 | 32 | // Authenticator turns a token secret into a principal, or | |
| 33 | 33 | // domain.ErrUnauthenticated. app.Auth fits. | |
| @@ -27,7 +27,7 @@ const MaxRequestBytes = 1 << 20 | |||||
| 27 | 27 | ||||
| 28 | // Features are the optional capabilities of the contract that this server | 28 | // Features are the optional capabilities of the contract that this server | ||
| 29 | // implements, as GetVersion lists them. | 29 | // implements, as GetVersion lists them. | ||
| 30 | - | var Features = []string{"repo.update", "events.resume", "merge_requests", "signing_key", "session"} | 30 | + | var Features = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "signing_key", "session"} |
| 31 | 31 | ||||
| 32 | // Authenticator turns a token secret into a principal, or | 32 | // Authenticator turns a token secret into a principal, or | ||
| 33 | // domain.ErrUnauthenticated. app.Auth fits. | 33 | // domain.ErrUnauthenticated. app.Auth fits. | ||
M internal/adapters/rpc/mergerequest.go +6 -2
| @@ -245,11 +245,15 @@ func (s *mergeRequestService) MergeMergeRequest(ctx context.Context, req *connec | |||
| 245 | 245 | if err != nil { | |
| 246 | 246 | return nil, connectError(ctx, err) | |
| 247 | 247 | } | |
| 248 | - | mr, err := s.mrs.Merge(ctx, caller(ctx), name, req.Msg.GetNumber(), app.MergeParams{Mode: mode, HeadID: head}) | |
| 248 | + | res, err := s.mrs.Merge(ctx, caller(ctx), name, req.Msg.GetNumber(), app.MergeParams{ | |
| 249 | + | Mode: mode, HeadID: head, DeleteSource: req.Msg.GetDeleteSourceBranch(), | |
| 250 | + | }) | |
| 249 | 251 | if err != nil { | |
| 250 | 252 | return nil, connectError(ctx, err) | |
| 251 | 253 | } | |
| 252 | - | return connect.NewResponse(&pb.MergeMergeRequestResponse{MergeRequest: mergeRequestToPB(mr)}), nil | |
| 254 | + | return connect.NewResponse(&pb.MergeMergeRequestResponse{ | |
| 255 | + | MergeRequest: mergeRequestToPB(res.MergeRequest), SourceBranchDeleted: res.SourceDeleted, | |
| 256 | + | }), nil | |
| 253 | 257 | } | |
| 254 | 258 | ||
| 255 | 259 | func (s *mergeRequestService) MarkRead(ctx context.Context, req *connect.Request[pb.MarkReadRequest]) (*connect.Response[pb.MarkReadResponse], error) { | |
| @@ -245,11 +245,15 @@ func (s *mergeRequestService) MergeMergeRequest(ctx context.Context, req *connec | |||||
| 245 | if err != nil { | 245 | if err != nil { | ||
| 246 | return nil, connectError(ctx, err) | 246 | return nil, connectError(ctx, err) | ||
| 247 | } | 247 | } | ||
| 248 | - | mr, err := s.mrs.Merge(ctx, caller(ctx), name, req.Msg.GetNumber(), app.MergeParams{Mode: mode, HeadID: head}) | 248 | + | res, err := s.mrs.Merge(ctx, caller(ctx), name, req.Msg.GetNumber(), app.MergeParams{ |
| 249 | + | Mode: mode, HeadID: head, DeleteSource: req.Msg.GetDeleteSourceBranch(), | |||
| 250 | + | }) | |||
| 249 | if err != nil { | 251 | if err != nil { | ||
| 250 | return nil, connectError(ctx, err) | 252 | return nil, connectError(ctx, err) | ||
| 251 | } | 253 | } | ||
| 252 | - | return connect.NewResponse(&pb.MergeMergeRequestResponse{MergeRequest: mergeRequestToPB(mr)}), nil | 254 | + | return connect.NewResponse(&pb.MergeMergeRequestResponse{ |
| 255 | + | MergeRequest: mergeRequestToPB(res.MergeRequest), SourceBranchDeleted: res.SourceDeleted, | |||
| 256 | + | }), nil | |||
| 253 | } | 257 | } | ||
| 254 | 258 | ||||
| 255 | func (s *mergeRequestService) MarkRead(ctx context.Context, req *connect.Request[pb.MarkReadRequest]) (*connect.Response[pb.MarkReadResponse], error) { | 259 | func (s *mergeRequestService) MarkRead(ctx context.Context, req *connect.Request[pb.MarkReadRequest]) (*connect.Response[pb.MarkReadResponse], error) { | ||
M internal/adapters/web/mr.go +7 -3
| @@ -147,6 +147,9 @@ type MergeBox struct { | |||
| 147 | 147 | CLI Clone | |
| 148 | 148 | // CanMarkRead is true when the head differs from the one last read. | |
| 149 | 149 | CanMarkRead bool | |
| 150 | + | // CanDeleteBranch offers to delete the source after the merge: it is not | |
| 151 | + | // the default branch. The server checks again, with the open requests. | |
| 152 | + | CanDeleteBranch bool | |
| 150 | 153 | } | |
| 151 | 154 | ||
| 152 | 155 | // ModeOption is a merge mode the repository allows. | |
| @@ -528,7 +531,7 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||
| 528 | 531 | p.Invisible = invisible | |
| 529 | 532 | p.DiffView = newDiffView(r, lines, self+"?view=unified", self+"?view=split") | |
| 530 | 533 | if signed && open { | |
| 531 | - | p.Box = s.mergeBox(d, n, head, lastRead) | |
| 534 | + | p.Box = s.mergeBox(d, n, m.GetSourceBranch(), head, lastRead) | |
| 532 | 535 | } | |
| 533 | 536 | if canWrite { | |
| 534 | 537 | p.GeneralForm = &CommentForm{ | |
| @@ -573,11 +576,12 @@ func (d *repoData) hasBranch(name string) bool { | |||
| 573 | 576 | } | |
| 574 | 577 | ||
| 575 | 578 | // mergeBox is what the owner can do with an open request. | |
| 576 | - | func (s *Server) mergeBox(d *repoData, n int64, head, lastRead string) *MergeBox { | |
| 579 | + | func (s *Server) mergeBox(d *repoData, n int64, source, head, lastRead string) *MergeBox { | |
| 577 | 580 | b := &MergeBox{ | |
| 578 | 581 | MergeAction: mrHref(d.name(), n, "merge"), CloseAction: mrHref(d.name(), n, "close"), | |
| 579 | 582 | ReadAction: mrHref(d.name(), n, "read"), Head: head, HeadGone: head == "", | |
| 580 | - | CanMarkRead: head != "" && head != lastRead, | |
| 583 | + | CanMarkRead: head != "" && head != lastRead, | |
| 584 | + | CanDeleteBranch: source != d.repo.GetDefaultBranch(), | |
| 581 | 585 | } | |
| 582 | 586 | ff, sq := false, false | |
| 583 | 587 | if st := d.repo.GetSettings(); st != nil { | |
| @@ -147,6 +147,9 @@ type MergeBox struct { | |||||
| 147 | CLI Clone | 147 | CLI Clone | ||
| 148 | // CanMarkRead is true when the head differs from the one last read. | 148 | // CanMarkRead is true when the head differs from the one last read. | ||
| 149 | CanMarkRead bool | 149 | CanMarkRead bool | ||
| 150 | + | // CanDeleteBranch offers to delete the source after the merge: it is not | |||
| 151 | + | // the default branch. The server checks again, with the open requests. | |||
| 152 | + | CanDeleteBranch bool | |||
| 150 | } | 153 | } | ||
| 151 | 154 | ||||
| 152 | // ModeOption is a merge mode the repository allows. | 155 | // ModeOption is a merge mode the repository allows. | ||
| @@ -528,7 +531,7 @@ func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ | |||||
| 528 | p.Invisible = invisible | 531 | p.Invisible = invisible | ||
| 529 | p.DiffView = newDiffView(r, lines, self+"?view=unified", self+"?view=split") | 532 | p.DiffView = newDiffView(r, lines, self+"?view=unified", self+"?view=split") | ||
| 530 | if signed && open { | 533 | if signed && open { | ||
| 531 | - | p.Box = s.mergeBox(d, n, head, lastRead) | 534 | + | p.Box = s.mergeBox(d, n, m.GetSourceBranch(), head, lastRead) |
| 532 | } | 535 | } | ||
| 533 | if canWrite { | 536 | if canWrite { | ||
| 534 | p.GeneralForm = &CommentForm{ | 537 | p.GeneralForm = &CommentForm{ | ||
| @@ -573,11 +576,12 @@ func (d *repoData) hasBranch(name string) bool { | |||||
| 573 | } | 576 | } | ||
| 574 | 577 | ||||
| 575 | // mergeBox is what the owner can do with an open request. | 578 | // mergeBox is what the owner can do with an open request. | ||
| 576 | - | func (s *Server) mergeBox(d *repoData, n int64, head, lastRead string) *MergeBox { | 579 | + | func (s *Server) mergeBox(d *repoData, n int64, source, head, lastRead string) *MergeBox { |
| 577 | b := &MergeBox{ | 580 | b := &MergeBox{ | ||
| 578 | MergeAction: mrHref(d.name(), n, "merge"), CloseAction: mrHref(d.name(), n, "close"), | 581 | MergeAction: mrHref(d.name(), n, "merge"), CloseAction: mrHref(d.name(), n, "close"), | ||
| 579 | ReadAction: mrHref(d.name(), n, "read"), Head: head, HeadGone: head == "", | 582 | ReadAction: mrHref(d.name(), n, "read"), Head: head, HeadGone: head == "", | ||
| 580 | - | CanMarkRead: head != "" && head != lastRead, | 583 | + | CanMarkRead: head != "" && head != lastRead, |
| 584 | + | CanDeleteBranch: source != d.repo.GetDefaultBranch(), | |||
| 581 | } | 585 | } | ||
| 582 | ff, sq := false, false | 586 | ff, sq := false, false | ||
| 583 | if st := d.repo.GetSettings(); st != nil { | 587 | if st := d.repo.GetSettings(); st != nil { | ||
M internal/adapters/web/mr.templ +3 -0
| @@ -382,6 +382,9 @@ templ mergeBox(p MRPage) { | |||
| 382 | 382 | <option value={ m.Value }>{ m.Label }</option> | |
| 383 | 383 | } | |
| 384 | 384 | </select> | |
| 385 | + | if p.Box.CanDeleteBranch { | |
| 386 | + | <label class="d-check"><input type="checkbox" name="delete_branch" value="1"/> delete { p.Source } after the merge</label> | |
| 387 | + | } | |
| 385 | 388 | <div class="d-btns"> | |
| 386 | 389 | <button type="submit" class="d-main">[merge]</button> | |
| 387 | 390 | </div> | |
| @@ -382,6 +382,9 @@ templ mergeBox(p MRPage) { | |||||
| 382 | <option value={ m.Value }>{ m.Label }</option> | 382 | <option value={ m.Value }>{ m.Label }</option> | ||
| 383 | } | 383 | } | ||
| 384 | </select> | 384 | </select> | ||
| 385 | + | if p.Box.CanDeleteBranch { | |||
| 386 | + | <label class="d-check"><input type="checkbox" name="delete_branch" value="1"/> delete { p.Source } after the merge</label> | |||
| 387 | + | } | |||
| 385 | <div class="d-btns"> | 388 | <div class="d-btns"> | ||
| 386 | <button type="submit" class="d-main">[merge]</button> | 389 | <button type="submit" class="d-main">[merge]</button> | ||
| 387 | </div> | 390 | </div> | ||
M internal/adapters/web/mr_post.go +5 -3
| @@ -205,9 +205,10 @@ func (s *Server) mrClose(ctx context.Context, r *http.Request) (string, string, | |||
| 205 | 205 | return back, back, nil | |
| 206 | 206 | } | |
| 207 | 207 | ||
| 208 | - | // mrMerge is POST /<repo>/merge-requests/<n>/merge. The form carries the mode | |
| 209 | - | // and the head the owner was shown (head_id): the server merges only if the | |
| 210 | - | // branch still points to it. | |
| 208 | + | // mrMerge is POST /<repo>/merge-requests/<n>/merge. The form carries the mode, | |
| 209 | + | // the head the owner was shown (head_id): the server merges only if the | |
| 210 | + | // branch still points to it, and delete_branch when the owner asked to delete | |
| 211 | + | // the source branch after the merge. | |
| 211 | 212 | func (s *Server) mrMerge(ctx context.Context, r *http.Request) (string, string, error) { | |
| 212 | 213 | repo := r.PathValue("repo") | |
| 213 | 214 | n, err := number(r) | |
| @@ -226,6 +227,7 @@ func (s *Server) mrMerge(ctx context.Context, r *http.Request) (string, string, | |||
| 226 | 227 | } | |
| 227 | 228 | if _, err := s.cfg.MergeRequests.MergeMergeRequest(ctx, connect.NewRequest(&pb.MergeMergeRequestRequest{ | |
| 228 | 229 | Repo: repo, Number: n, Mode: mode, HeadId: r.PostFormValue("head_id"), | |
| 230 | + | DeleteSourceBranch: r.PostFormValue("delete_branch") == "1", | |
| 229 | 231 | })); err != nil { | |
| 230 | 232 | return "", back, err | |
| 231 | 233 | } | |
| @@ -205,9 +205,10 @@ func (s *Server) mrClose(ctx context.Context, r *http.Request) (string, string, | |||||
| 205 | return back, back, nil | 205 | return back, back, nil | ||
| 206 | } | 206 | } | ||
| 207 | 207 | ||||
| 208 | - | // mrMerge is POST /<repo>/merge-requests/<n>/merge. The form carries the mode | 208 | + | // mrMerge is POST /<repo>/merge-requests/<n>/merge. The form carries the mode, |
| 209 | - | // and the head the owner was shown (head_id): the server merges only if the | 209 | + | // the head the owner was shown (head_id): the server merges only if the |
| 210 | - | // branch still points to it. | 210 | + | // branch still points to it, and delete_branch when the owner asked to delete |
| 211 | + | // the source branch after the merge. | |||
| 211 | func (s *Server) mrMerge(ctx context.Context, r *http.Request) (string, string, error) { | 212 | func (s *Server) mrMerge(ctx context.Context, r *http.Request) (string, string, error) { | ||
| 212 | repo := r.PathValue("repo") | 213 | repo := r.PathValue("repo") | ||
| 213 | n, err := number(r) | 214 | n, err := number(r) | ||
| @@ -226,6 +227,7 @@ func (s *Server) mrMerge(ctx context.Context, r *http.Request) (string, string, | |||||
| 226 | } | 227 | } | ||
| 227 | if _, err := s.cfg.MergeRequests.MergeMergeRequest(ctx, connect.NewRequest(&pb.MergeMergeRequestRequest{ | 228 | if _, err := s.cfg.MergeRequests.MergeMergeRequest(ctx, connect.NewRequest(&pb.MergeMergeRequestRequest{ | ||
| 228 | Repo: repo, Number: n, Mode: mode, HeadId: r.PostFormValue("head_id"), | 229 | Repo: repo, Number: n, Mode: mode, HeadId: r.PostFormValue("head_id"), | ||
| 230 | + | DeleteSourceBranch: r.PostFormValue("delete_branch") == "1", | |||
| 229 | })); err != nil { | 231 | })); err != nil { | ||
| 230 | return "", back, err | 232 | return "", back, err | ||
| 231 | } | 233 | } | ||
M internal/adapters/web/mr_templ.go +223 -200
| @@ -2042,66 +2042,89 @@ func mergeBox(p MRPage) templ.Component { | |||
| 2042 | 2042 | return templ_7745c5c3_Err | |
| 2043 | 2043 | } | |
| 2044 | 2044 | } | |
| 2045 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "</select><div class=\"d-btns\"><button type=\"submit\" class=\"d-main\">[merge]</button></div></form>") | |
| 2045 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "</select> ") | |
| 2046 | + | if templ_7745c5c3_Err != nil { | |
| 2047 | + | return templ_7745c5c3_Err | |
| 2048 | + | } | |
| 2049 | + | if p.Box.CanDeleteBranch { | |
| 2050 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ") | |
| 2051 | + | if templ_7745c5c3_Err != nil { | |
| 2052 | + | return templ_7745c5c3_Err | |
| 2053 | + | } | |
| 2054 | + | var templ_7745c5c3_Var105 string | |
| 2055 | + | templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |
| 2056 | + | if templ_7745c5c3_Err != nil { | |
| 2057 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 386, Col: 101} | |
| 2058 | + | } | |
| 2059 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105)) | |
| 2060 | + | if templ_7745c5c3_Err != nil { | |
| 2061 | + | return templ_7745c5c3_Err | |
| 2062 | + | } | |
| 2063 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, " after the merge</label>") | |
| 2064 | + | if templ_7745c5c3_Err != nil { | |
| 2065 | + | return templ_7745c5c3_Err | |
| 2066 | + | } | |
| 2067 | + | } | |
| 2068 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "<div class=\"d-btns\"><button type=\"submit\" class=\"d-main\">[merge]</button></div></form>") | |
| 2046 | 2069 | if templ_7745c5c3_Err != nil { | |
| 2047 | 2070 | return templ_7745c5c3_Err | |
| 2048 | 2071 | } | |
| 2049 | 2072 | } | |
| 2050 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "<div class=\"d-btns\">") | |
| 2073 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "<div class=\"d-btns\">") | |
| 2051 | 2074 | if templ_7745c5c3_Err != nil { | |
| 2052 | 2075 | return templ_7745c5c3_Err | |
| 2053 | 2076 | } | |
| 2054 | 2077 | if p.Box.CanMarkRead { | |
| 2055 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "<form method=\"post\" action=\"") | |
| 2078 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "<form method=\"post\" action=\"") | |
| 2056 | 2079 | if templ_7745c5c3_Err != nil { | |
| 2057 | 2080 | return templ_7745c5c3_Err | |
| 2058 | 2081 | } | |
| 2059 | - | var templ_7745c5c3_Var105 templ.SafeURL | |
| 2060 | - | templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction)) | |
| 2082 | + | var templ_7745c5c3_Var106 templ.SafeURL | |
| 2083 | + | templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction)) | |
| 2061 | 2084 | if templ_7745c5c3_Err != nil { | |
| 2062 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 392, Col: 64} | |
| 2085 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 395, Col: 64} | |
| 2063 | 2086 | } | |
| 2064 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105)) | |
| 2087 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106)) | |
| 2065 | 2088 | if templ_7745c5c3_Err != nil { | |
| 2066 | 2089 | return templ_7745c5c3_Err | |
| 2067 | 2090 | } | |
| 2068 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "\"><input type=\"hidden\" name=\"head_id\" value=\"") | |
| 2091 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "\"><input type=\"hidden\" name=\"head_id\" value=\"") | |
| 2069 | 2092 | if templ_7745c5c3_Err != nil { | |
| 2070 | 2093 | return templ_7745c5c3_Err | |
| 2071 | 2094 | } | |
| 2072 | - | var templ_7745c5c3_Var106 string | |
| 2073 | - | templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head) | |
| 2095 | + | var templ_7745c5c3_Var107 string | |
| 2096 | + | templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head) | |
| 2074 | 2097 | if templ_7745c5c3_Err != nil { | |
| 2075 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 393, Col: 59} | |
| 2098 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 59} | |
| 2076 | 2099 | } | |
| 2077 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var106) | |
| 2100 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var107) | |
| 2078 | 2101 | if templ_7745c5c3_Err != nil { | |
| 2079 | 2102 | return templ_7745c5c3_Err | |
| 2080 | 2103 | } | |
| 2081 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "\"> <button type=\"submit\" title=\"the next visit can show only what changed after this head\">[mark read]</button></form>") | |
| 2104 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "\"> <button type=\"submit\" title=\"the next visit can show only what changed after this head\">[mark read]</button></form>") | |
| 2082 | 2105 | if templ_7745c5c3_Err != nil { | |
| 2083 | 2106 | return templ_7745c5c3_Err | |
| 2084 | 2107 | } | |
| 2085 | 2108 | } | |
| 2086 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "<form method=\"post\" action=\"") | |
| 2109 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "<form method=\"post\" action=\"") | |
| 2087 | 2110 | if templ_7745c5c3_Err != nil { | |
| 2088 | 2111 | return templ_7745c5c3_Err | |
| 2089 | 2112 | } | |
| 2090 | - | var templ_7745c5c3_Var107 templ.SafeURL | |
| 2091 | - | templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction)) | |
| 2113 | + | var templ_7745c5c3_Var108 templ.SafeURL | |
| 2114 | + | templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction)) | |
| 2092 | 2115 | if templ_7745c5c3_Err != nil { | |
| 2093 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 397, Col: 64} | |
| 2116 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 400, Col: 64} | |
| 2094 | 2117 | } | |
| 2095 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107)) | |
| 2118 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108)) | |
| 2096 | 2119 | if templ_7745c5c3_Err != nil { | |
| 2097 | 2120 | return templ_7745c5c3_Err | |
| 2098 | 2121 | } | |
| 2099 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\">[close]</button></form></div>") | |
| 2122 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\">[close]</button></form></div>") | |
| 2100 | 2123 | if templ_7745c5c3_Err != nil { | |
| 2101 | 2124 | return templ_7745c5c3_Err | |
| 2102 | 2125 | } | |
| 2103 | 2126 | if !p.Box.HeadGone { | |
| 2104 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "<p class=\"mute d-or\">or from a terminal</p>") | |
| 2127 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "<p class=\"mute d-or\">or from a terminal</p>") | |
| 2105 | 2128 | if templ_7745c5c3_Err != nil { | |
| 2106 | 2129 | return templ_7745c5c3_Err | |
| 2107 | 2130 | } | |
| @@ -2110,7 +2133,7 @@ func mergeBox(p MRPage) templ.Component { | |||
| 2110 | 2133 | return templ_7745c5c3_Err | |
| 2111 | 2134 | } | |
| 2112 | 2135 | } | |
| 2113 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "</section>") | |
| 2136 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "</section>") | |
| 2114 | 2137 | if templ_7745c5c3_Err != nil { | |
| 2115 | 2138 | return templ_7745c5c3_Err | |
| 2116 | 2139 | } | |
| @@ -2136,12 +2159,12 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2136 | 2159 | }() | |
| 2137 | 2160 | } | |
| 2138 | 2161 | ctx = templ.InitializeContext(ctx) | |
| 2139 | - | templ_7745c5c3_Var108 := templ.GetChildren(ctx) | |
| 2140 | - | if templ_7745c5c3_Var108 == nil { | |
| 2141 | - | templ_7745c5c3_Var108 = templ.NopComponent | |
| 2162 | + | templ_7745c5c3_Var109 := templ.GetChildren(ctx) | |
| 2163 | + | if templ_7745c5c3_Var109 == nil { | |
| 2164 | + | templ_7745c5c3_Var109 = templ.NopComponent | |
| 2142 | 2165 | } | |
| 2143 | 2166 | ctx = templ.ClearChildren(ctx) | |
| 2144 | - | templ_7745c5c3_Var109 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 2167 | + | templ_7745c5c3_Var110 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 2145 | 2168 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 2146 | 2169 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 2147 | 2170 | if !templ_7745c5c3_IsBuffer { | |
| @@ -2153,263 +2176,263 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2153 | 2176 | }() | |
| 2154 | 2177 | } | |
| 2155 | 2178 | ctx = templ.InitializeContext(ctx) | |
| 2156 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "<header class=\"show mr-h\"><h1>") | |
| 2157 | - | if templ_7745c5c3_Err != nil { | |
| 2158 | - | return templ_7745c5c3_Err | |
| 2159 | - | } | |
| 2160 | - | var templ_7745c5c3_Var110 string | |
| 2161 | - | templ_7745c5c3_Var110, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |
| 2162 | - | if templ_7745c5c3_Err != nil { | |
| 2163 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 413, Col: 17} | |
| 2164 | - | } | |
| 2165 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var110)) | |
| 2166 | - | if templ_7745c5c3_Err != nil { | |
| 2167 | - | return templ_7745c5c3_Err | |
| 2168 | - | } | |
| 2169 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, " → ") | |
| 2179 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "<header class=\"show mr-h\"><h1>") | |
| 2170 | 2180 | if templ_7745c5c3_Err != nil { | |
| 2171 | 2181 | return templ_7745c5c3_Err | |
| 2172 | 2182 | } | |
| 2173 | 2183 | var templ_7745c5c3_Var111 string | |
| 2174 | - | templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2184 | + | templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |
| 2175 | 2185 | if templ_7745c5c3_Err != nil { | |
| 2176 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 413, Col: 34} | |
| 2186 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 17} | |
| 2177 | 2187 | } | |
| 2178 | 2188 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111)) | |
| 2179 | 2189 | if templ_7745c5c3_Err != nil { | |
| 2180 | 2190 | return templ_7745c5c3_Err | |
| 2181 | 2191 | } | |
| 2182 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"") | |
| 2192 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, " → ") | |
| 2183 | 2193 | if templ_7745c5c3_Err != nil { | |
| 2184 | 2194 | return templ_7745c5c3_Err | |
| 2185 | 2195 | } | |
| 2186 | - | var templ_7745c5c3_Var112 templ.SafeURL | |
| 2187 | - | templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref)) | |
| 2196 | + | var templ_7745c5c3_Var112 string | |
| 2197 | + | templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2188 | 2198 | if templ_7745c5c3_Err != nil { | |
| 2189 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 45} | |
| 2199 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 34} | |
| 2190 | 2200 | } | |
| 2191 | 2201 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112)) | |
| 2192 | 2202 | if templ_7745c5c3_Err != nil { | |
| 2193 | 2203 | return templ_7745c5c3_Err | |
| 2194 | 2204 | } | |
| 2195 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "\">") | |
| 2205 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"") | |
| 2196 | 2206 | if templ_7745c5c3_Err != nil { | |
| 2197 | 2207 | return templ_7745c5c3_Err | |
| 2198 | 2208 | } | |
| 2199 | - | var templ_7745c5c3_Var113 string | |
| 2200 | - | templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |
| 2209 | + | var templ_7745c5c3_Var113 templ.SafeURL | |
| 2210 | + | templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref)) | |
| 2201 | 2211 | if templ_7745c5c3_Err != nil { | |
| 2202 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 58} | |
| 2212 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 419, Col: 45} | |
| 2203 | 2213 | } | |
| 2204 | 2214 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113)) | |
| 2205 | 2215 | if templ_7745c5c3_Err != nil { | |
| 2206 | 2216 | return templ_7745c5c3_Err | |
| 2207 | 2217 | } | |
| 2208 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, "</a></dd><dt>target</dt><dd><a href=\"") | |
| 2218 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "\">") | |
| 2209 | 2219 | if templ_7745c5c3_Err != nil { | |
| 2210 | 2220 | return templ_7745c5c3_Err | |
| 2211 | 2221 | } | |
| 2212 | - | var templ_7745c5c3_Var114 templ.SafeURL | |
| 2213 | - | templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref)) | |
| 2222 | + | var templ_7745c5c3_Var114 string | |
| 2223 | + | templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |
| 2214 | 2224 | if templ_7745c5c3_Err != nil { | |
| 2215 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 45} | |
| 2225 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 419, Col: 58} | |
| 2216 | 2226 | } | |
| 2217 | 2227 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var114)) | |
| 2218 | 2228 | if templ_7745c5c3_Err != nil { | |
| 2219 | 2229 | return templ_7745c5c3_Err | |
| 2220 | 2230 | } | |
| 2221 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "\">") | |
| 2231 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "</a></dd><dt>target</dt><dd><a href=\"") | |
| 2222 | 2232 | if templ_7745c5c3_Err != nil { | |
| 2223 | 2233 | return templ_7745c5c3_Err | |
| 2224 | 2234 | } | |
| 2225 | - | var templ_7745c5c3_Var115 string | |
| 2226 | - | templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2235 | + | var templ_7745c5c3_Var115 templ.SafeURL | |
| 2236 | + | templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref)) | |
| 2227 | 2237 | if templ_7745c5c3_Err != nil { | |
| 2228 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 58} | |
| 2238 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 421, Col: 45} | |
| 2229 | 2239 | } | |
| 2230 | 2240 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115)) | |
| 2231 | 2241 | if templ_7745c5c3_Err != nil { | |
| 2232 | 2242 | return templ_7745c5c3_Err | |
| 2233 | 2243 | } | |
| 2234 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "</a></dd><dt>ahead</dt><dd>") | |
| 2244 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "\">") | |
| 2235 | 2245 | if templ_7745c5c3_Err != nil { | |
| 2236 | 2246 | return templ_7745c5c3_Err | |
| 2237 | 2247 | } | |
| 2238 | 2248 | var templ_7745c5c3_Var116 string | |
| 2239 | - | templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText) | |
| 2249 | + | templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2240 | 2250 | if templ_7745c5c3_Err != nil { | |
| 2241 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 420, Col: 21} | |
| 2251 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 421, Col: 58} | |
| 2242 | 2252 | } | |
| 2243 | 2253 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116)) | |
| 2244 | 2254 | if templ_7745c5c3_Err != nil { | |
| 2245 | 2255 | return templ_7745c5c3_Err | |
| 2246 | 2256 | } | |
| 2247 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "</dd></dl></header>") | |
| 2257 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "</a></dd><dt>ahead</dt><dd>") | |
| 2258 | + | if templ_7745c5c3_Err != nil { | |
| 2259 | + | return templ_7745c5c3_Err | |
| 2260 | + | } | |
| 2261 | + | var templ_7745c5c3_Var117 string | |
| 2262 | + | templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText) | |
| 2263 | + | if templ_7745c5c3_Err != nil { | |
| 2264 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 423, Col: 21} | |
| 2265 | + | } | |
| 2266 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var117)) | |
| 2267 | + | if templ_7745c5c3_Err != nil { | |
| 2268 | + | return templ_7745c5c3_Err | |
| 2269 | + | } | |
| 2270 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "</dd></dl></header>") | |
| 2248 | 2271 | if templ_7745c5c3_Err != nil { | |
| 2249 | 2272 | return templ_7745c5c3_Err | |
| 2250 | 2273 | } | |
| 2251 | 2274 | if p.Note != "" { | |
| 2252 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "<p class=\"notice\">") | |
| 2275 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "<p class=\"notice\">") | |
| 2253 | 2276 | if templ_7745c5c3_Err != nil { | |
| 2254 | 2277 | return templ_7745c5c3_Err | |
| 2255 | 2278 | } | |
| 2256 | - | var templ_7745c5c3_Var117 string | |
| 2257 | - | templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | |
| 2279 | + | var templ_7745c5c3_Var118 string | |
| 2280 | + | templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | |
| 2258 | 2281 | if templ_7745c5c3_Err != nil { | |
| 2259 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 424, Col: 29} | |
| 2282 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 427, Col: 29} | |
| 2260 | 2283 | } | |
| 2261 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var117)) | |
| 2284 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118)) | |
| 2262 | 2285 | if templ_7745c5c3_Err != nil { | |
| 2263 | 2286 | return templ_7745c5c3_Err | |
| 2264 | 2287 | } | |
| 2265 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "</p>") | |
| 2288 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "</p>") | |
| 2266 | 2289 | if templ_7745c5c3_Err != nil { | |
| 2267 | 2290 | return templ_7745c5c3_Err | |
| 2268 | 2291 | } | |
| 2269 | 2292 | } | |
| 2270 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, " ") | |
| 2293 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, " ") | |
| 2271 | 2294 | if templ_7745c5c3_Err != nil { | |
| 2272 | 2295 | return templ_7745c5c3_Err | |
| 2273 | 2296 | } | |
| 2274 | 2297 | if p.Existing != nil { | |
| 2275 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"") | |
| 2298 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"") | |
| 2276 | 2299 | if templ_7745c5c3_Err != nil { | |
| 2277 | 2300 | return templ_7745c5c3_Err | |
| 2278 | 2301 | } | |
| 2279 | - | var templ_7745c5c3_Var118 templ.SafeURL | |
| 2280 | - | templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href)) | |
| 2302 | + | var templ_7745c5c3_Var119 templ.SafeURL | |
| 2303 | + | templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href)) | |
| 2281 | 2304 | if templ_7745c5c3_Err != nil { | |
| 2282 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 427, Col: 114} | |
| 2305 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 430, Col: 114} | |
| 2283 | 2306 | } | |
| 2284 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118)) | |
| 2307 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119)) | |
| 2285 | 2308 | if templ_7745c5c3_Err != nil { | |
| 2286 | 2309 | return templ_7745c5c3_Err | |
| 2287 | 2310 | } | |
| 2288 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "\">") | |
| 2311 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "\">") | |
| 2289 | 2312 | if templ_7745c5c3_Err != nil { | |
| 2290 | 2313 | return templ_7745c5c3_Err | |
| 2291 | 2314 | } | |
| 2292 | - | var templ_7745c5c3_Var119 string | |
| 2293 | - | templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label) | |
| 2315 | + | var templ_7745c5c3_Var120 string | |
| 2316 | + | templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label) | |
| 2294 | 2317 | if templ_7745c5c3_Err != nil { | |
| 2295 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 427, Col: 135} | |
| 2318 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 430, Col: 135} | |
| 2296 | 2319 | } | |
| 2297 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119)) | |
| 2320 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120)) | |
| 2298 | 2321 | if templ_7745c5c3_Err != nil { | |
| 2299 | 2322 | return templ_7745c5c3_Err | |
| 2300 | 2323 | } | |
| 2301 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "</a></p>") | |
| 2324 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "</a></p>") | |
| 2302 | 2325 | if templ_7745c5c3_Err != nil { | |
| 2303 | 2326 | return templ_7745c5c3_Err | |
| 2304 | 2327 | } | |
| 2305 | 2328 | } | |
| 2306 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, " ") | |
| 2329 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, " ") | |
| 2307 | 2330 | if templ_7745c5c3_Err != nil { | |
| 2308 | 2331 | return templ_7745c5c3_Err | |
| 2309 | 2332 | } | |
| 2310 | 2333 | if p.Create != nil { | |
| 2311 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "<form class=\"cform create\" method=\"post\" action=\"") | |
| 2334 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "<form class=\"cform create\" method=\"post\" action=\"") | |
| 2312 | 2335 | if templ_7745c5c3_Err != nil { | |
| 2313 | 2336 | return templ_7745c5c3_Err | |
| 2314 | 2337 | } | |
| 2315 | - | var templ_7745c5c3_Var120 templ.SafeURL | |
| 2316 | - | templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action)) | |
| 2338 | + | var templ_7745c5c3_Var121 templ.SafeURL | |
| 2339 | + | templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action)) | |
| 2317 | 2340 | if templ_7745c5c3_Err != nil { | |
| 2318 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 430, Col: 83} | |
| 2341 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 433, Col: 83} | |
| 2319 | 2342 | } | |
| 2320 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120)) | |
| 2343 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121)) | |
| 2321 | 2344 | if templ_7745c5c3_Err != nil { | |
| 2322 | 2345 | return templ_7745c5c3_Err | |
| 2323 | 2346 | } | |
| 2324 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "\"><input type=\"hidden\" name=\"source\" value=\"") | |
| 2347 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "\"><input type=\"hidden\" name=\"source\" value=\"") | |
| 2325 | 2348 | if templ_7745c5c3_Err != nil { | |
| 2326 | 2349 | return templ_7745c5c3_Err | |
| 2327 | 2350 | } | |
| 2328 | - | var templ_7745c5c3_Var121 string | |
| 2329 | - | templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source) | |
| 2351 | + | var templ_7745c5c3_Var122 string | |
| 2352 | + | templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source) | |
| 2330 | 2353 | if templ_7745c5c3_Err != nil { | |
| 2331 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 431, Col: 62} | |
| 2354 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 434, Col: 62} | |
| 2332 | 2355 | } | |
| 2333 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var121) | |
| 2356 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var122) | |
| 2334 | 2357 | if templ_7745c5c3_Err != nil { | |
| 2335 | 2358 | return templ_7745c5c3_Err | |
| 2336 | 2359 | } | |
| 2337 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "\"> <input type=\"hidden\" name=\"target\" value=\"") | |
| 2360 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "\"> <input type=\"hidden\" name=\"target\" value=\"") | |
| 2338 | 2361 | if templ_7745c5c3_Err != nil { | |
| 2339 | 2362 | return templ_7745c5c3_Err | |
| 2340 | 2363 | } | |
| 2341 | - | var templ_7745c5c3_Var122 string | |
| 2342 | - | templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target) | |
| 2364 | + | var templ_7745c5c3_Var123 string | |
| 2365 | + | templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target) | |
| 2343 | 2366 | if templ_7745c5c3_Err != nil { | |
| 2344 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 432, Col: 62} | |
| 2367 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 62} | |
| 2345 | 2368 | } | |
| 2346 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var122) | |
| 2369 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var123) | |
| 2347 | 2370 | if templ_7745c5c3_Err != nil { | |
| 2348 | 2371 | return templ_7745c5c3_Err | |
| 2349 | 2372 | } | |
| 2350 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"") | |
| 2373 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"") | |
| 2351 | 2374 | if templ_7745c5c3_Err != nil { | |
| 2352 | 2375 | return templ_7745c5c3_Err | |
| 2353 | 2376 | } | |
| 2354 | - | var templ_7745c5c3_Var123 string | |
| 2355 | - | templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title) | |
| 2377 | + | var templ_7745c5c3_Var124 string | |
| 2378 | + | templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title) | |
| 2356 | 2379 | if templ_7745c5c3_Err != nil { | |
| 2357 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 434, Col: 97} | |
| 2380 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 437, Col: 97} | |
| 2358 | 2381 | } | |
| 2359 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var123) | |
| 2382 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var124) | |
| 2360 | 2383 | if templ_7745c5c3_Err != nil { | |
| 2361 | 2384 | return templ_7745c5c3_Err | |
| 2362 | 2385 | } | |
| 2363 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "\" autocomplete=\"off\"> <label for=\"mr-desc\">description</label> <textarea id=\"mr-desc\" name=\"description\" rows=\"4\" maxlength=\"8000\" placeholder=\"plain text, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"f-main\">[create merge request]</button></div></form>") | |
| 2386 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "\" autocomplete=\"off\"> <label for=\"mr-desc\">description</label> <textarea id=\"mr-desc\" name=\"description\" rows=\"4\" maxlength=\"8000\" placeholder=\"plain text, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"f-main\">[create merge request]</button></div></form>") | |
| 2364 | 2387 | if templ_7745c5c3_Err != nil { | |
| 2365 | 2388 | return templ_7745c5c3_Err | |
| 2366 | 2389 | } | |
| 2367 | 2390 | } | |
| 2368 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, " ") | |
| 2391 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, " ") | |
| 2369 | 2392 | if templ_7745c5c3_Err != nil { | |
| 2370 | 2393 | return templ_7745c5c3_Err | |
| 2371 | 2394 | } | |
| 2372 | 2395 | if p.HasDiff { | |
| 2373 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "<div class=\"diff-tools\" role=\"toolbar\" aria-label=\"Diff view\"><span class=\"dim\">") | |
| 2396 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "<div class=\"diff-tools\" role=\"toolbar\" aria-label=\"Diff view\"><span class=\"dim\">") | |
| 2374 | 2397 | if templ_7745c5c3_Err != nil { | |
| 2375 | 2398 | return templ_7745c5c3_Err | |
| 2376 | 2399 | } | |
| 2377 | - | var templ_7745c5c3_Var124 string | |
| 2378 | - | templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files")) | |
| 2400 | + | var templ_7745c5c3_Var125 string | |
| 2401 | + | templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files")) | |
| 2379 | 2402 | if templ_7745c5c3_Err != nil { | |
| 2380 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 444, Col: 57} | |
| 2403 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 447, Col: 57} | |
| 2381 | 2404 | } | |
| 2382 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var124)) | |
| 2405 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125)) | |
| 2383 | 2406 | if templ_7745c5c3_Err != nil { | |
| 2384 | 2407 | return templ_7745c5c3_Err | |
| 2385 | 2408 | } | |
| 2386 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, " · <span class=\"plus\">+") | |
| 2409 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, " · <span class=\"plus\">+") | |
| 2387 | 2410 | if templ_7745c5c3_Err != nil { | |
| 2388 | 2411 | return templ_7745c5c3_Err | |
| 2389 | 2412 | } | |
| 2390 | - | var templ_7745c5c3_Var125 string | |
| 2391 | - | templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds)) | |
| 2413 | + | var templ_7745c5c3_Var126 string | |
| 2414 | + | templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds)) | |
| 2392 | 2415 | if templ_7745c5c3_Err != nil { | |
| 2393 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 444, Col: 97} | |
| 2416 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 447, Col: 97} | |
| 2394 | 2417 | } | |
| 2395 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125)) | |
| 2418 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126)) | |
| 2396 | 2419 | if templ_7745c5c3_Err != nil { | |
| 2397 | 2420 | return templ_7745c5c3_Err | |
| 2398 | 2421 | } | |
| 2399 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, "</span> <span class=\"minus\">-") | |
| 2422 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "</span> <span class=\"minus\">-") | |
| 2400 | 2423 | if templ_7745c5c3_Err != nil { | |
| 2401 | 2424 | return templ_7745c5c3_Err | |
| 2402 | 2425 | } | |
| 2403 | - | var templ_7745c5c3_Var126 string | |
| 2404 | - | templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels)) | |
| 2426 | + | var templ_7745c5c3_Var127 string | |
| 2427 | + | templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels)) | |
| 2405 | 2428 | if templ_7745c5c3_Err != nil { | |
| 2406 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 444, Col: 142} | |
| 2429 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 447, Col: 142} | |
| 2407 | 2430 | } | |
| 2408 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126)) | |
| 2431 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127)) | |
| 2409 | 2432 | if templ_7745c5c3_Err != nil { | |
| 2410 | 2433 | return templ_7745c5c3_Err | |
| 2411 | 2434 | } | |
| 2412 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "</span></span> <span class=\"grow\"></span>") | |
| 2435 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, "</span></span> <span class=\"grow\"></span>") | |
| 2413 | 2436 | if templ_7745c5c3_Err != nil { | |
| 2414 | 2437 | return templ_7745c5c3_Err | |
| 2415 | 2438 | } | |
| @@ -2421,64 +2444,64 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2421 | 2444 | if templ_7745c5c3_Err != nil { | |
| 2422 | 2445 | return templ_7745c5c3_Err | |
| 2423 | 2446 | } | |
| 2424 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "<button type=\"button\" aria-keyshortcuts=\"x\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-collapse'))\">[x] collapse all</button></div>") | |
| 2447 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "<button type=\"button\" aria-keyshortcuts=\"x\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-collapse'))\">[x] collapse all</button></div>") | |
| 2425 | 2448 | if templ_7745c5c3_Err != nil { | |
| 2426 | 2449 | return templ_7745c5c3_Err | |
| 2427 | 2450 | } | |
| 2428 | 2451 | if p.Invisible { | |
| 2429 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "<p class=\"notice\">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class=\"inv\"><U+XXXX></span> markers, never applied.</p>") | |
| 2452 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "<p class=\"notice\">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class=\"inv\"><U+XXXX></span> markers, never applied.</p>") | |
| 2430 | 2453 | if templ_7745c5c3_Err != nil { | |
| 2431 | 2454 | return templ_7745c5c3_Err | |
| 2432 | 2455 | } | |
| 2433 | 2456 | } | |
| 2434 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, " <div id=\"diffs\"") | |
| 2457 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, " <div id=\"diffs\"") | |
| 2435 | 2458 | if templ_7745c5c3_Err != nil { | |
| 2436 | 2459 | return templ_7745c5c3_Err | |
| 2437 | 2460 | } | |
| 2438 | 2461 | if p.Both { | |
| 2439 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, " data-diff=\"") | |
| 2462 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, " data-diff=\"") | |
| 2440 | 2463 | if templ_7745c5c3_Err != nil { | |
| 2441 | 2464 | return templ_7745c5c3_Err | |
| 2442 | 2465 | } | |
| 2443 | - | var templ_7745c5c3_Var127 string | |
| 2444 | - | templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 2466 | + | var templ_7745c5c3_Var128 string | |
| 2467 | + | templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 2445 | 2468 | if templ_7745c5c3_Err != nil { | |
| 2446 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 459, Col: 23} | |
| 2469 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 462, Col: 23} | |
| 2447 | 2470 | } | |
| 2448 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var127) | |
| 2471 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var128) | |
| 2449 | 2472 | if templ_7745c5c3_Err != nil { | |
| 2450 | 2473 | return templ_7745c5c3_Err | |
| 2451 | 2474 | } | |
| 2452 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "\"") | |
| 2475 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "\"") | |
| 2453 | 2476 | if templ_7745c5c3_Err != nil { | |
| 2454 | 2477 | return templ_7745c5c3_Err | |
| 2455 | 2478 | } | |
| 2456 | 2479 | } else { | |
| 2457 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, " data-view=\"") | |
| 2480 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, " data-view=\"") | |
| 2458 | 2481 | if templ_7745c5c3_Err != nil { | |
| 2459 | 2482 | return templ_7745c5c3_Err | |
| 2460 | 2483 | } | |
| 2461 | - | var templ_7745c5c3_Var128 string | |
| 2462 | - | templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 2484 | + | var templ_7745c5c3_Var129 string | |
| 2485 | + | templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 2463 | 2486 | if templ_7745c5c3_Err != nil { | |
| 2464 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 461, Col: 23} | |
| 2487 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 464, Col: 23} | |
| 2465 | 2488 | } | |
| 2466 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var128) | |
| 2489 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var129) | |
| 2467 | 2490 | if templ_7745c5c3_Err != nil { | |
| 2468 | 2491 | return templ_7745c5c3_Err | |
| 2469 | 2492 | } | |
| 2470 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "\"") | |
| 2493 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "\"") | |
| 2471 | 2494 | if templ_7745c5c3_Err != nil { | |
| 2472 | 2495 | return templ_7745c5c3_Err | |
| 2473 | 2496 | } | |
| 2474 | 2497 | } | |
| 2475 | 2498 | if p.Both && !p.Pinned { | |
| 2476 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"") | |
| 2499 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"") | |
| 2477 | 2500 | if templ_7745c5c3_Err != nil { | |
| 2478 | 2501 | return templ_7745c5c3_Err | |
| 2479 | 2502 | } | |
| 2480 | 2503 | } | |
| 2481 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, ">") | |
| 2504 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, ">") | |
| 2482 | 2505 | if templ_7745c5c3_Err != nil { | |
| 2483 | 2506 | return templ_7745c5c3_Err | |
| 2484 | 2507 | } | |
| @@ -2488,14 +2511,14 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||
| 2488 | 2511 | return templ_7745c5c3_Err | |
| 2489 | 2512 | } | |
| 2490 | 2513 | } | |
| 2491 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "</div>") | |
| 2514 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "</div>") | |
| 2492 | 2515 | if templ_7745c5c3_Err != nil { | |
| 2493 | 2516 | return templ_7745c5c3_Err | |
| 2494 | 2517 | } | |
| 2495 | 2518 | } | |
| 2496 | 2519 | return nil | |
| 2497 | 2520 | }) | |
| 2498 | - | templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var109), templ_7745c5c3_Buffer) | |
| 2521 | + | templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var110), templ_7745c5c3_Buffer) | |
| 2499 | 2522 | if templ_7745c5c3_Err != nil { | |
| 2500 | 2523 | return templ_7745c5c3_Err | |
| 2501 | 2524 | } | |
| @@ -2519,173 +2542,173 @@ func compareRail(p ComparePage) templ.Component { | |||
| 2519 | 2542 | }() | |
| 2520 | 2543 | } | |
| 2521 | 2544 | ctx = templ.InitializeContext(ctx) | |
| 2522 | - | templ_7745c5c3_Var129 := templ.GetChildren(ctx) | |
| 2523 | - | if templ_7745c5c3_Var129 == nil { | |
| 2524 | - | templ_7745c5c3_Var129 = templ.NopComponent | |
| 2545 | + | templ_7745c5c3_Var130 := templ.GetChildren(ctx) | |
| 2546 | + | if templ_7745c5c3_Var130 == nil { | |
| 2547 | + | templ_7745c5c3_Var130 = templ.NopComponent | |
| 2525 | 2548 | } | |
| 2526 | 2549 | ctx = templ.ClearChildren(ctx) | |
| 2527 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">commits <span>") | |
| 2550 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">commits <span>") | |
| 2528 | 2551 | if templ_7745c5c3_Err != nil { | |
| 2529 | 2552 | return templ_7745c5c3_Err | |
| 2530 | 2553 | } | |
| 2531 | - | var templ_7745c5c3_Var130 string | |
| 2532 | - | templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead))) | |
| 2554 | + | var templ_7745c5c3_Var131 string | |
| 2555 | + | templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead))) | |
| 2533 | 2556 | if templ_7745c5c3_Err != nil { | |
| 2534 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 477, Col: 53} | |
| 2557 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 480, Col: 53} | |
| 2535 | 2558 | } | |
| 2536 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var130)) | |
| 2559 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var131)) | |
| 2537 | 2560 | if templ_7745c5c3_Err != nil { | |
| 2538 | 2561 | return templ_7745c5c3_Err | |
| 2539 | 2562 | } | |
| 2540 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, "</span></h2><ul class=\"pcommits\">") | |
| 2563 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "</span></h2><ul class=\"pcommits\">") | |
| 2541 | 2564 | if templ_7745c5c3_Err != nil { | |
| 2542 | 2565 | return templ_7745c5c3_Err | |
| 2543 | 2566 | } | |
| 2544 | 2567 | for _, c := range p.Ahead { | |
| 2545 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "<li><a href=\"") | |
| 2568 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "<li><a href=\"") | |
| 2546 | 2569 | if templ_7745c5c3_Err != nil { | |
| 2547 | 2570 | return templ_7745c5c3_Err | |
| 2548 | 2571 | } | |
| 2549 | - | var templ_7745c5c3_Var131 templ.SafeURL | |
| 2550 | - | templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) | |
| 2572 | + | var templ_7745c5c3_Var132 templ.SafeURL | |
| 2573 | + | templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) | |
| 2551 | 2574 | if templ_7745c5c3_Err != nil { | |
| 2552 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 481, Col: 37} | |
| 2575 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 37} | |
| 2553 | 2576 | } | |
| 2554 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var131)) | |
| 2577 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132)) | |
| 2555 | 2578 | if templ_7745c5c3_Err != nil { | |
| 2556 | 2579 | return templ_7745c5c3_Err | |
| 2557 | 2580 | } | |
| 2558 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "\">") | |
| 2581 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, "\">") | |
| 2559 | 2582 | if templ_7745c5c3_Err != nil { | |
| 2560 | 2583 | return templ_7745c5c3_Err | |
| 2561 | 2584 | } | |
| 2562 | - | var templ_7745c5c3_Var132 string | |
| 2563 | - | templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | |
| 2585 | + | var templ_7745c5c3_Var133 string | |
| 2586 | + | templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | |
| 2564 | 2587 | if templ_7745c5c3_Err != nil { | |
| 2565 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 481, Col: 49} | |
| 2588 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 49} | |
| 2566 | 2589 | } | |
| 2567 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132)) | |
| 2590 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133)) | |
| 2568 | 2591 | if templ_7745c5c3_Err != nil { | |
| 2569 | 2592 | return templ_7745c5c3_Err | |
| 2570 | 2593 | } | |
| 2571 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "</a> <span class=\"s\">") | |
| 2594 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "</a> <span class=\"s\">") | |
| 2572 | 2595 | if templ_7745c5c3_Err != nil { | |
| 2573 | 2596 | return templ_7745c5c3_Err | |
| 2574 | 2597 | } | |
| 2575 | - | var templ_7745c5c3_Var133 string | |
| 2576 | - | templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | |
| 2598 | + | var templ_7745c5c3_Var134 string | |
| 2599 | + | templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | |
| 2577 | 2600 | if templ_7745c5c3_Err != nil { | |
| 2578 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 482, Col: 32} | |
| 2601 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 485, Col: 32} | |
| 2579 | 2602 | } | |
| 2580 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133)) | |
| 2603 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134)) | |
| 2581 | 2604 | if templ_7745c5c3_Err != nil { | |
| 2582 | 2605 | return templ_7745c5c3_Err | |
| 2583 | 2606 | } | |
| 2584 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "</span> <span class=\"mute\">") | |
| 2607 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "</span> <span class=\"mute\">") | |
| 2585 | 2608 | if templ_7745c5c3_Err != nil { | |
| 2586 | 2609 | return templ_7745c5c3_Err | |
| 2587 | 2610 | } | |
| 2588 | - | var templ_7745c5c3_Var134 string | |
| 2589 | - | templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 2611 | + | var templ_7745c5c3_Var135 string | |
| 2612 | + | templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 2590 | 2613 | if templ_7745c5c3_Err != nil { | |
| 2591 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 16} | |
| 2614 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 487, Col: 16} | |
| 2592 | 2615 | } | |
| 2593 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134)) | |
| 2616 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var135)) | |
| 2594 | 2617 | if templ_7745c5c3_Err != nil { | |
| 2595 | 2618 | return templ_7745c5c3_Err | |
| 2596 | 2619 | } | |
| 2597 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, " ") | |
| 2620 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, " ") | |
| 2598 | 2621 | if templ_7745c5c3_Err != nil { | |
| 2599 | 2622 | return templ_7745c5c3_Err | |
| 2600 | 2623 | } | |
| 2601 | 2624 | if c.WhenAt != "" { | |
| 2602 | - | var templ_7745c5c3_Var135 string | |
| 2603 | - | templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 2625 | + | var templ_7745c5c3_Var136 string | |
| 2626 | + | templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 2604 | 2627 | if templ_7745c5c3_Err != nil { | |
| 2605 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 15} | |
| 2628 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 489, Col: 15} | |
| 2606 | 2629 | } | |
| 2607 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var135)) | |
| 2630 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136)) | |
| 2608 | 2631 | if templ_7745c5c3_Err != nil { | |
| 2609 | 2632 | return templ_7745c5c3_Err | |
| 2610 | 2633 | } | |
| 2611 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "<time datetime=\"") | |
| 2634 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "<time datetime=\"") | |
| 2612 | 2635 | if templ_7745c5c3_Err != nil { | |
| 2613 | 2636 | return templ_7745c5c3_Err | |
| 2614 | 2637 | } | |
| 2615 | - | var templ_7745c5c3_Var136 string | |
| 2616 | - | templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 2638 | + | var templ_7745c5c3_Var137 string | |
| 2639 | + | templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 2617 | 2640 | if templ_7745c5c3_Err != nil { | |
| 2618 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 42} | |
| 2641 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 489, Col: 42} | |
| 2619 | 2642 | } | |
| 2620 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var136) | |
| 2643 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var137) | |
| 2621 | 2644 | if templ_7745c5c3_Err != nil { | |
| 2622 | 2645 | return templ_7745c5c3_Err | |
| 2623 | 2646 | } | |
| 2624 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "\">") | |
| 2647 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "\">") | |
| 2625 | 2648 | if templ_7745c5c3_Err != nil { | |
| 2626 | 2649 | return templ_7745c5c3_Err | |
| 2627 | 2650 | } | |
| 2628 | - | var templ_7745c5c3_Var137 string | |
| 2629 | - | templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 2651 | + | var templ_7745c5c3_Var138 string | |
| 2652 | + | templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 2630 | 2653 | if templ_7745c5c3_Err != nil { | |
| 2631 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 53} | |
| 2654 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 489, Col: 53} | |
| 2632 | 2655 | } | |
| 2633 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137)) | |
| 2656 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138)) | |
| 2634 | 2657 | if templ_7745c5c3_Err != nil { | |
| 2635 | 2658 | return templ_7745c5c3_Err | |
| 2636 | 2659 | } | |
| 2637 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "</time>") | |
| 2660 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, "</time>") | |
| 2638 | 2661 | if templ_7745c5c3_Err != nil { | |
| 2639 | 2662 | return templ_7745c5c3_Err | |
| 2640 | 2663 | } | |
| 2641 | 2664 | } | |
| 2642 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "</span></li>") | |
| 2665 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, "</span></li>") | |
| 2643 | 2666 | if templ_7745c5c3_Err != nil { | |
| 2644 | 2667 | return templ_7745c5c3_Err | |
| 2645 | 2668 | } | |
| 2646 | 2669 | } | |
| 2647 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "</ul>") | |
| 2670 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "</ul>") | |
| 2648 | 2671 | if templ_7745c5c3_Err != nil { | |
| 2649 | 2672 | return templ_7745c5c3_Err | |
| 2650 | 2673 | } | |
| 2651 | 2674 | if len(p.Ahead) == 0 { | |
| 2652 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, "<p class=\"dim\">no commit ahead of ") | |
| 2675 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "<p class=\"dim\">no commit ahead of ") | |
| 2653 | 2676 | if templ_7745c5c3_Err != nil { | |
| 2654 | 2677 | return templ_7745c5c3_Err | |
| 2655 | 2678 | } | |
| 2656 | - | var templ_7745c5c3_Var138 string | |
| 2657 | - | templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2679 | + | var templ_7745c5c3_Var139 string | |
| 2680 | + | templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2658 | 2681 | if templ_7745c5c3_Err != nil { | |
| 2659 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 493, Col: 47} | |
| 2682 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 496, Col: 47} | |
| 2660 | 2683 | } | |
| 2661 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138)) | |
| 2684 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139)) | |
| 2662 | 2685 | if templ_7745c5c3_Err != nil { | |
| 2663 | 2686 | return templ_7745c5c3_Err | |
| 2664 | 2687 | } | |
| 2665 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, ".</p>") | |
| 2688 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, ".</p>") | |
| 2666 | 2689 | if templ_7745c5c3_Err != nil { | |
| 2667 | 2690 | return templ_7745c5c3_Err | |
| 2668 | 2691 | } | |
| 2669 | 2692 | } | |
| 2670 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "</section>") | |
| 2693 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "</section>") | |
| 2671 | 2694 | if templ_7745c5c3_Err != nil { | |
| 2672 | 2695 | return templ_7745c5c3_Err | |
| 2673 | 2696 | } | |
| 2674 | 2697 | if p.HasDiff { | |
| 2675 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">files <span>") | |
| 2698 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">files <span>") | |
| 2676 | 2699 | if templ_7745c5c3_Err != nil { | |
| 2677 | 2700 | return templ_7745c5c3_Err | |
| 2678 | 2701 | } | |
| 2679 | - | var templ_7745c5c3_Var139 string | |
| 2680 | - | templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | |
| 2702 | + | var templ_7745c5c3_Var140 string | |
| 2703 | + | templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | |
| 2681 | 2704 | if templ_7745c5c3_Err != nil { | |
| 2682 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 498, Col: 52} | |
| 2705 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 501, Col: 52} | |
| 2683 | 2706 | } | |
| 2684 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139)) | |
| 2707 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140)) | |
| 2685 | 2708 | if templ_7745c5c3_Err != nil { | |
| 2686 | 2709 | return templ_7745c5c3_Err | |
| 2687 | 2710 | } | |
| 2688 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, "</span></h2>") | |
| 2711 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "</span></h2>") | |
| 2689 | 2712 | if templ_7745c5c3_Err != nil { | |
| 2690 | 2713 | return templ_7745c5c3_Err | |
| 2691 | 2714 | } | |
| @@ -2693,12 +2716,12 @@ func compareRail(p ComparePage) templ.Component { | |||
| 2693 | 2716 | if templ_7745c5c3_Err != nil { | |
| 2694 | 2717 | return templ_7745c5c3_Err | |
| 2695 | 2718 | } | |
| 2696 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "</section>") | |
| 2719 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, "</section>") | |
| 2697 | 2720 | if templ_7745c5c3_Err != nil { | |
| 2698 | 2721 | return templ_7745c5c3_Err | |
| 2699 | 2722 | } | |
| 2700 | 2723 | } | |
| 2701 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">from a terminal</h2>") | |
| 2724 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">from a terminal</h2>") | |
| 2702 | 2725 | if templ_7745c5c3_Err != nil { | |
| 2703 | 2726 | return templ_7745c5c3_Err | |
| 2704 | 2727 | } | |
| @@ -2706,7 +2729,7 @@ func compareRail(p ComparePage) templ.Component { | |||
| 2706 | 2729 | if templ_7745c5c3_Err != nil { | |
| 2707 | 2730 | return templ_7745c5c3_Err | |
| 2708 | 2731 | } | |
| 2709 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "</section>") | |
| 2732 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, "</section>") | |
| 2710 | 2733 | if templ_7745c5c3_Err != nil { | |
| 2711 | 2734 | return templ_7745c5c3_Err | |
| 2712 | 2735 | } | |
| @@ -2042,66 +2042,89 @@ func mergeBox(p MRPage) templ.Component { | |||||
| 2042 | return templ_7745c5c3_Err | 2042 | return templ_7745c5c3_Err | ||
| 2043 | } | 2043 | } | ||
| 2044 | } | 2044 | } | ||
| 2045 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "</select><div class=\"d-btns\"><button type=\"submit\" class=\"d-main\">[merge]</button></div></form>") | 2045 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "</select> ") |
| 2046 | + | if templ_7745c5c3_Err != nil { | |||
| 2047 | + | return templ_7745c5c3_Err | |||
| 2048 | + | } | |||
| 2049 | + | if p.Box.CanDeleteBranch { | |||
| 2050 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "<label class=\"d-check\"><input type=\"checkbox\" name=\"delete_branch\" value=\"1\"> delete ") | |||
| 2051 | + | if templ_7745c5c3_Err != nil { | |||
| 2052 | + | return templ_7745c5c3_Err | |||
| 2053 | + | } | |||
| 2054 | + | var templ_7745c5c3_Var105 string | |||
| 2055 | + | templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |||
| 2056 | + | if templ_7745c5c3_Err != nil { | |||
| 2057 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 386, Col: 101} | |||
| 2058 | + | } | |||
| 2059 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105)) | |||
| 2060 | + | if templ_7745c5c3_Err != nil { | |||
| 2061 | + | return templ_7745c5c3_Err | |||
| 2062 | + | } | |||
| 2063 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, " after the merge</label>") | |||
| 2064 | + | if templ_7745c5c3_Err != nil { | |||
| 2065 | + | return templ_7745c5c3_Err | |||
| 2066 | + | } | |||
| 2067 | + | } | |||
| 2068 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "<div class=\"d-btns\"><button type=\"submit\" class=\"d-main\">[merge]</button></div></form>") | |||
| 2046 | if templ_7745c5c3_Err != nil { | 2069 | if templ_7745c5c3_Err != nil { | ||
| 2047 | return templ_7745c5c3_Err | 2070 | return templ_7745c5c3_Err | ||
| 2048 | } | 2071 | } | ||
| 2049 | } | 2072 | } | ||
| 2050 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "<div class=\"d-btns\">") | 2073 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "<div class=\"d-btns\">") |
| 2051 | if templ_7745c5c3_Err != nil { | 2074 | if templ_7745c5c3_Err != nil { | ||
| 2052 | return templ_7745c5c3_Err | 2075 | return templ_7745c5c3_Err | ||
| 2053 | } | 2076 | } | ||
| 2054 | if p.Box.CanMarkRead { | 2077 | if p.Box.CanMarkRead { | ||
| 2055 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "<form method=\"post\" action=\"") | 2078 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "<form method=\"post\" action=\"") |
| 2056 | if templ_7745c5c3_Err != nil { | 2079 | if templ_7745c5c3_Err != nil { | ||
| 2057 | return templ_7745c5c3_Err | 2080 | return templ_7745c5c3_Err | ||
| 2058 | } | 2081 | } | ||
| 2059 | - | var templ_7745c5c3_Var105 templ.SafeURL | 2082 | + | var templ_7745c5c3_Var106 templ.SafeURL |
| 2060 | - | templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction)) | 2083 | + | templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction)) |
| 2061 | if templ_7745c5c3_Err != nil { | 2084 | if templ_7745c5c3_Err != nil { | ||
| 2062 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 392, Col: 64} | 2085 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 395, Col: 64} |
| 2063 | } | 2086 | } | ||
| 2064 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105)) | 2087 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var106)) |
| 2065 | if templ_7745c5c3_Err != nil { | 2088 | if templ_7745c5c3_Err != nil { | ||
| 2066 | return templ_7745c5c3_Err | 2089 | return templ_7745c5c3_Err | ||
| 2067 | } | 2090 | } | ||
| 2068 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "\"><input type=\"hidden\" name=\"head_id\" value=\"") | 2091 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "\"><input type=\"hidden\" name=\"head_id\" value=\"") |
| 2069 | if templ_7745c5c3_Err != nil { | 2092 | if templ_7745c5c3_Err != nil { | ||
| 2070 | return templ_7745c5c3_Err | 2093 | return templ_7745c5c3_Err | ||
| 2071 | } | 2094 | } | ||
| 2072 | - | var templ_7745c5c3_Var106 string | 2095 | + | var templ_7745c5c3_Var107 string |
| 2073 | - | templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head) | 2096 | + | templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head) |
| 2074 | if templ_7745c5c3_Err != nil { | 2097 | if templ_7745c5c3_Err != nil { | ||
| 2075 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 393, Col: 59} | 2098 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 396, Col: 59} |
| 2076 | } | 2099 | } | ||
| 2077 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var106) | 2100 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var107) |
| 2078 | if templ_7745c5c3_Err != nil { | 2101 | if templ_7745c5c3_Err != nil { | ||
| 2079 | return templ_7745c5c3_Err | 2102 | return templ_7745c5c3_Err | ||
| 2080 | } | 2103 | } | ||
| 2081 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "\"> <button type=\"submit\" title=\"the next visit can show only what changed after this head\">[mark read]</button></form>") | 2104 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "\"> <button type=\"submit\" title=\"the next visit can show only what changed after this head\">[mark read]</button></form>") |
| 2082 | if templ_7745c5c3_Err != nil { | 2105 | if templ_7745c5c3_Err != nil { | ||
| 2083 | return templ_7745c5c3_Err | 2106 | return templ_7745c5c3_Err | ||
| 2084 | } | 2107 | } | ||
| 2085 | } | 2108 | } | ||
| 2086 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "<form method=\"post\" action=\"") | 2109 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "<form method=\"post\" action=\"") |
| 2087 | if templ_7745c5c3_Err != nil { | 2110 | if templ_7745c5c3_Err != nil { | ||
| 2088 | return templ_7745c5c3_Err | 2111 | return templ_7745c5c3_Err | ||
| 2089 | } | 2112 | } | ||
| 2090 | - | var templ_7745c5c3_Var107 templ.SafeURL | 2113 | + | var templ_7745c5c3_Var108 templ.SafeURL |
| 2091 | - | templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction)) | 2114 | + | templ_7745c5c3_Var108, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction)) |
| 2092 | if templ_7745c5c3_Err != nil { | 2115 | if templ_7745c5c3_Err != nil { | ||
| 2093 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 397, Col: 64} | 2116 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 400, Col: 64} |
| 2094 | } | 2117 | } | ||
| 2095 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107)) | 2118 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var108)) |
| 2096 | if templ_7745c5c3_Err != nil { | 2119 | if templ_7745c5c3_Err != nil { | ||
| 2097 | return templ_7745c5c3_Err | 2120 | return templ_7745c5c3_Err | ||
| 2098 | } | 2121 | } | ||
| 2099 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\">[close]</button></form></div>") | 2122 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\">[close]</button></form></div>") |
| 2100 | if templ_7745c5c3_Err != nil { | 2123 | if templ_7745c5c3_Err != nil { | ||
| 2101 | return templ_7745c5c3_Err | 2124 | return templ_7745c5c3_Err | ||
| 2102 | } | 2125 | } | ||
| 2103 | if !p.Box.HeadGone { | 2126 | if !p.Box.HeadGone { | ||
| 2104 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "<p class=\"mute d-or\">or from a terminal</p>") | 2127 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, "<p class=\"mute d-or\">or from a terminal</p>") |
| 2105 | if templ_7745c5c3_Err != nil { | 2128 | if templ_7745c5c3_Err != nil { | ||
| 2106 | return templ_7745c5c3_Err | 2129 | return templ_7745c5c3_Err | ||
| 2107 | } | 2130 | } | ||
| @@ -2110,7 +2133,7 @@ func mergeBox(p MRPage) templ.Component { | |||||
| 2110 | return templ_7745c5c3_Err | 2133 | return templ_7745c5c3_Err | ||
| 2111 | } | 2134 | } | ||
| 2112 | } | 2135 | } | ||
| 2113 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "</section>") | 2136 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "</section>") |
| 2114 | if templ_7745c5c3_Err != nil { | 2137 | if templ_7745c5c3_Err != nil { | ||
| 2115 | return templ_7745c5c3_Err | 2138 | return templ_7745c5c3_Err | ||
| 2116 | } | 2139 | } | ||
| @@ -2136,12 +2159,12 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||||
| 2136 | }() | 2159 | }() | ||
| 2137 | } | 2160 | } | ||
| 2138 | ctx = templ.InitializeContext(ctx) | 2161 | ctx = templ.InitializeContext(ctx) | ||
| 2139 | - | templ_7745c5c3_Var108 := templ.GetChildren(ctx) | 2162 | + | templ_7745c5c3_Var109 := templ.GetChildren(ctx) |
| 2140 | - | if templ_7745c5c3_Var108 == nil { | 2163 | + | if templ_7745c5c3_Var109 == nil { |
| 2141 | - | templ_7745c5c3_Var108 = templ.NopComponent | 2164 | + | templ_7745c5c3_Var109 = templ.NopComponent |
| 2142 | } | 2165 | } | ||
| 2143 | ctx = templ.ClearChildren(ctx) | 2166 | ctx = templ.ClearChildren(ctx) | ||
| 2144 | - | templ_7745c5c3_Var109 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | 2167 | + | templ_7745c5c3_Var110 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { |
| 2145 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | 2168 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | ||
| 2146 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | 2169 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | ||
| 2147 | if !templ_7745c5c3_IsBuffer { | 2170 | if !templ_7745c5c3_IsBuffer { | ||
| @@ -2153,263 +2176,263 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||||
| 2153 | }() | 2176 | }() | ||
| 2154 | } | 2177 | } | ||
| 2155 | ctx = templ.InitializeContext(ctx) | 2178 | ctx = templ.InitializeContext(ctx) | ||
| 2156 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "<header class=\"show mr-h\"><h1>") | 2179 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "<header class=\"show mr-h\"><h1>") |
| 2157 | - | if templ_7745c5c3_Err != nil { | |||
| 2158 | - | return templ_7745c5c3_Err | |||
| 2159 | - | } | |||
| 2160 | - | var templ_7745c5c3_Var110 string | |||
| 2161 | - | templ_7745c5c3_Var110, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |||
| 2162 | - | if templ_7745c5c3_Err != nil { | |||
| 2163 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 413, Col: 17} | |||
| 2164 | - | } | |||
| 2165 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var110)) | |||
| 2166 | - | if templ_7745c5c3_Err != nil { | |||
| 2167 | - | return templ_7745c5c3_Err | |||
| 2168 | - | } | |||
| 2169 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, " → ") | |||
| 2170 | if templ_7745c5c3_Err != nil { | 2180 | if templ_7745c5c3_Err != nil { | ||
| 2171 | return templ_7745c5c3_Err | 2181 | return templ_7745c5c3_Err | ||
| 2172 | } | 2182 | } | ||
| 2173 | var templ_7745c5c3_Var111 string | 2183 | var templ_7745c5c3_Var111 string | ||
| 2174 | - | templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | 2184 | + | templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) |
| 2175 | if templ_7745c5c3_Err != nil { | 2185 | if templ_7745c5c3_Err != nil { | ||
| 2176 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 413, Col: 34} | 2186 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 17} |
| 2177 | } | 2187 | } | ||
| 2178 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111)) | 2188 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111)) | ||
| 2179 | if templ_7745c5c3_Err != nil { | 2189 | if templ_7745c5c3_Err != nil { | ||
| 2180 | return templ_7745c5c3_Err | 2190 | return templ_7745c5c3_Err | ||
| 2181 | } | 2191 | } | ||
| 2182 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"") | 2192 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, " → ") |
| 2183 | if templ_7745c5c3_Err != nil { | 2193 | if templ_7745c5c3_Err != nil { | ||
| 2184 | return templ_7745c5c3_Err | 2194 | return templ_7745c5c3_Err | ||
| 2185 | } | 2195 | } | ||
| 2186 | - | var templ_7745c5c3_Var112 templ.SafeURL | 2196 | + | var templ_7745c5c3_Var112 string |
| 2187 | - | templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref)) | 2197 | + | templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) |
| 2188 | if templ_7745c5c3_Err != nil { | 2198 | if templ_7745c5c3_Err != nil { | ||
| 2189 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 45} | 2199 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 34} |
| 2190 | } | 2200 | } | ||
| 2191 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112)) | 2201 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112)) | ||
| 2192 | if templ_7745c5c3_Err != nil { | 2202 | if templ_7745c5c3_Err != nil { | ||
| 2193 | return templ_7745c5c3_Err | 2203 | return templ_7745c5c3_Err | ||
| 2194 | } | 2204 | } | ||
| 2195 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "\">") | 2205 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"") |
| 2196 | if templ_7745c5c3_Err != nil { | 2206 | if templ_7745c5c3_Err != nil { | ||
| 2197 | return templ_7745c5c3_Err | 2207 | return templ_7745c5c3_Err | ||
| 2198 | } | 2208 | } | ||
| 2199 | - | var templ_7745c5c3_Var113 string | 2209 | + | var templ_7745c5c3_Var113 templ.SafeURL |
| 2200 | - | templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | 2210 | + | templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref)) |
| 2201 | if templ_7745c5c3_Err != nil { | 2211 | if templ_7745c5c3_Err != nil { | ||
| 2202 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 58} | 2212 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 419, Col: 45} |
| 2203 | } | 2213 | } | ||
| 2204 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113)) | 2214 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113)) | ||
| 2205 | if templ_7745c5c3_Err != nil { | 2215 | if templ_7745c5c3_Err != nil { | ||
| 2206 | return templ_7745c5c3_Err | 2216 | return templ_7745c5c3_Err | ||
| 2207 | } | 2217 | } | ||
| 2208 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, "</a></dd><dt>target</dt><dd><a href=\"") | 2218 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "\">") |
| 2209 | if templ_7745c5c3_Err != nil { | 2219 | if templ_7745c5c3_Err != nil { | ||
| 2210 | return templ_7745c5c3_Err | 2220 | return templ_7745c5c3_Err | ||
| 2211 | } | 2221 | } | ||
| 2212 | - | var templ_7745c5c3_Var114 templ.SafeURL | 2222 | + | var templ_7745c5c3_Var114 string |
| 2213 | - | templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref)) | 2223 | + | templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) |
| 2214 | if templ_7745c5c3_Err != nil { | 2224 | if templ_7745c5c3_Err != nil { | ||
| 2215 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 45} | 2225 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 419, Col: 58} |
| 2216 | } | 2226 | } | ||
| 2217 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var114)) | 2227 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var114)) | ||
| 2218 | if templ_7745c5c3_Err != nil { | 2228 | if templ_7745c5c3_Err != nil { | ||
| 2219 | return templ_7745c5c3_Err | 2229 | return templ_7745c5c3_Err | ||
| 2220 | } | 2230 | } | ||
| 2221 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "\">") | 2231 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "</a></dd><dt>target</dt><dd><a href=\"") |
| 2222 | if templ_7745c5c3_Err != nil { | 2232 | if templ_7745c5c3_Err != nil { | ||
| 2223 | return templ_7745c5c3_Err | 2233 | return templ_7745c5c3_Err | ||
| 2224 | } | 2234 | } | ||
| 2225 | - | var templ_7745c5c3_Var115 string | 2235 | + | var templ_7745c5c3_Var115 templ.SafeURL |
| 2226 | - | templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | 2236 | + | templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref)) |
| 2227 | if templ_7745c5c3_Err != nil { | 2237 | if templ_7745c5c3_Err != nil { | ||
| 2228 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 58} | 2238 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 421, Col: 45} |
| 2229 | } | 2239 | } | ||
| 2230 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115)) | 2240 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115)) | ||
| 2231 | if templ_7745c5c3_Err != nil { | 2241 | if templ_7745c5c3_Err != nil { | ||
| 2232 | return templ_7745c5c3_Err | 2242 | return templ_7745c5c3_Err | ||
| 2233 | } | 2243 | } | ||
| 2234 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "</a></dd><dt>ahead</dt><dd>") | 2244 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "\">") |
| 2235 | if templ_7745c5c3_Err != nil { | 2245 | if templ_7745c5c3_Err != nil { | ||
| 2236 | return templ_7745c5c3_Err | 2246 | return templ_7745c5c3_Err | ||
| 2237 | } | 2247 | } | ||
| 2238 | var templ_7745c5c3_Var116 string | 2248 | var templ_7745c5c3_Var116 string | ||
| 2239 | - | templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText) | 2249 | + | templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) |
| 2240 | if templ_7745c5c3_Err != nil { | 2250 | if templ_7745c5c3_Err != nil { | ||
| 2241 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 420, Col: 21} | 2251 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 421, Col: 58} |
| 2242 | } | 2252 | } | ||
| 2243 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116)) | 2253 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116)) | ||
| 2244 | if templ_7745c5c3_Err != nil { | 2254 | if templ_7745c5c3_Err != nil { | ||
| 2245 | return templ_7745c5c3_Err | 2255 | return templ_7745c5c3_Err | ||
| 2246 | } | 2256 | } | ||
| 2247 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "</dd></dl></header>") | 2257 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "</a></dd><dt>ahead</dt><dd>") |
| 2258 | + | if templ_7745c5c3_Err != nil { | |||
| 2259 | + | return templ_7745c5c3_Err | |||
| 2260 | + | } | |||
| 2261 | + | var templ_7745c5c3_Var117 string | |||
| 2262 | + | templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText) | |||
| 2263 | + | if templ_7745c5c3_Err != nil { | |||
| 2264 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 423, Col: 21} | |||
| 2265 | + | } | |||
| 2266 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var117)) | |||
| 2267 | + | if templ_7745c5c3_Err != nil { | |||
| 2268 | + | return templ_7745c5c3_Err | |||
| 2269 | + | } | |||
| 2270 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, "</dd></dl></header>") | |||
| 2248 | if templ_7745c5c3_Err != nil { | 2271 | if templ_7745c5c3_Err != nil { | ||
| 2249 | return templ_7745c5c3_Err | 2272 | return templ_7745c5c3_Err | ||
| 2250 | } | 2273 | } | ||
| 2251 | if p.Note != "" { | 2274 | if p.Note != "" { | ||
| 2252 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "<p class=\"notice\">") | 2275 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "<p class=\"notice\">") |
| 2253 | if templ_7745c5c3_Err != nil { | 2276 | if templ_7745c5c3_Err != nil { | ||
| 2254 | return templ_7745c5c3_Err | 2277 | return templ_7745c5c3_Err | ||
| 2255 | } | 2278 | } | ||
| 2256 | - | var templ_7745c5c3_Var117 string | 2279 | + | var templ_7745c5c3_Var118 string |
| 2257 | - | templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | 2280 | + | templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) |
| 2258 | if templ_7745c5c3_Err != nil { | 2281 | if templ_7745c5c3_Err != nil { | ||
| 2259 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 424, Col: 29} | 2282 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 427, Col: 29} |
| 2260 | } | 2283 | } | ||
| 2261 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var117)) | 2284 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118)) |
| 2262 | if templ_7745c5c3_Err != nil { | 2285 | if templ_7745c5c3_Err != nil { | ||
| 2263 | return templ_7745c5c3_Err | 2286 | return templ_7745c5c3_Err | ||
| 2264 | } | 2287 | } | ||
| 2265 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "</p>") | 2288 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "</p>") |
| 2266 | if templ_7745c5c3_Err != nil { | 2289 | if templ_7745c5c3_Err != nil { | ||
| 2267 | return templ_7745c5c3_Err | 2290 | return templ_7745c5c3_Err | ||
| 2268 | } | 2291 | } | ||
| 2269 | } | 2292 | } | ||
| 2270 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, " ") | 2293 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, " ") |
| 2271 | if templ_7745c5c3_Err != nil { | 2294 | if templ_7745c5c3_Err != nil { | ||
| 2272 | return templ_7745c5c3_Err | 2295 | return templ_7745c5c3_Err | ||
| 2273 | } | 2296 | } | ||
| 2274 | if p.Existing != nil { | 2297 | if p.Existing != nil { | ||
| 2275 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"") | 2298 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"") |
| 2276 | if templ_7745c5c3_Err != nil { | 2299 | if templ_7745c5c3_Err != nil { | ||
| 2277 | return templ_7745c5c3_Err | 2300 | return templ_7745c5c3_Err | ||
| 2278 | } | 2301 | } | ||
| 2279 | - | var templ_7745c5c3_Var118 templ.SafeURL | 2302 | + | var templ_7745c5c3_Var119 templ.SafeURL |
| 2280 | - | templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href)) | 2303 | + | templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href)) |
| 2281 | if templ_7745c5c3_Err != nil { | 2304 | if templ_7745c5c3_Err != nil { | ||
| 2282 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 427, Col: 114} | 2305 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 430, Col: 114} |
| 2283 | } | 2306 | } | ||
| 2284 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118)) | 2307 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119)) |
| 2285 | if templ_7745c5c3_Err != nil { | 2308 | if templ_7745c5c3_Err != nil { | ||
| 2286 | return templ_7745c5c3_Err | 2309 | return templ_7745c5c3_Err | ||
| 2287 | } | 2310 | } | ||
| 2288 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "\">") | 2311 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "\">") |
| 2289 | if templ_7745c5c3_Err != nil { | 2312 | if templ_7745c5c3_Err != nil { | ||
| 2290 | return templ_7745c5c3_Err | 2313 | return templ_7745c5c3_Err | ||
| 2291 | } | 2314 | } | ||
| 2292 | - | var templ_7745c5c3_Var119 string | 2315 | + | var templ_7745c5c3_Var120 string |
| 2293 | - | templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label) | 2316 | + | templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label) |
| 2294 | if templ_7745c5c3_Err != nil { | 2317 | if templ_7745c5c3_Err != nil { | ||
| 2295 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 427, Col: 135} | 2318 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 430, Col: 135} |
| 2296 | } | 2319 | } | ||
| 2297 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119)) | 2320 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120)) |
| 2298 | if templ_7745c5c3_Err != nil { | 2321 | if templ_7745c5c3_Err != nil { | ||
| 2299 | return templ_7745c5c3_Err | 2322 | return templ_7745c5c3_Err | ||
| 2300 | } | 2323 | } | ||
| 2301 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "</a></p>") | 2324 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "</a></p>") |
| 2302 | if templ_7745c5c3_Err != nil { | 2325 | if templ_7745c5c3_Err != nil { | ||
| 2303 | return templ_7745c5c3_Err | 2326 | return templ_7745c5c3_Err | ||
| 2304 | } | 2327 | } | ||
| 2305 | } | 2328 | } | ||
| 2306 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, " ") | 2329 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, " ") |
| 2307 | if templ_7745c5c3_Err != nil { | 2330 | if templ_7745c5c3_Err != nil { | ||
| 2308 | return templ_7745c5c3_Err | 2331 | return templ_7745c5c3_Err | ||
| 2309 | } | 2332 | } | ||
| 2310 | if p.Create != nil { | 2333 | if p.Create != nil { | ||
| 2311 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "<form class=\"cform create\" method=\"post\" action=\"") | 2334 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "<form class=\"cform create\" method=\"post\" action=\"") |
| 2312 | if templ_7745c5c3_Err != nil { | 2335 | if templ_7745c5c3_Err != nil { | ||
| 2313 | return templ_7745c5c3_Err | 2336 | return templ_7745c5c3_Err | ||
| 2314 | } | 2337 | } | ||
| 2315 | - | var templ_7745c5c3_Var120 templ.SafeURL | 2338 | + | var templ_7745c5c3_Var121 templ.SafeURL |
| 2316 | - | templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action)) | 2339 | + | templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action)) |
| 2317 | if templ_7745c5c3_Err != nil { | 2340 | if templ_7745c5c3_Err != nil { | ||
| 2318 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 430, Col: 83} | 2341 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 433, Col: 83} |
| 2319 | } | 2342 | } | ||
| 2320 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120)) | 2343 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var121)) |
| 2321 | if templ_7745c5c3_Err != nil { | 2344 | if templ_7745c5c3_Err != nil { | ||
| 2322 | return templ_7745c5c3_Err | 2345 | return templ_7745c5c3_Err | ||
| 2323 | } | 2346 | } | ||
| 2324 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "\"><input type=\"hidden\" name=\"source\" value=\"") | 2347 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "\"><input type=\"hidden\" name=\"source\" value=\"") |
| 2325 | if templ_7745c5c3_Err != nil { | 2348 | if templ_7745c5c3_Err != nil { | ||
| 2326 | return templ_7745c5c3_Err | 2349 | return templ_7745c5c3_Err | ||
| 2327 | } | 2350 | } | ||
| 2328 | - | var templ_7745c5c3_Var121 string | 2351 | + | var templ_7745c5c3_Var122 string |
| 2329 | - | templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source) | 2352 | + | templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source) |
| 2330 | if templ_7745c5c3_Err != nil { | 2353 | if templ_7745c5c3_Err != nil { | ||
| 2331 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 431, Col: 62} | 2354 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 434, Col: 62} |
| 2332 | } | 2355 | } | ||
| 2333 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var121) | 2356 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var122) |
| 2334 | if templ_7745c5c3_Err != nil { | 2357 | if templ_7745c5c3_Err != nil { | ||
| 2335 | return templ_7745c5c3_Err | 2358 | return templ_7745c5c3_Err | ||
| 2336 | } | 2359 | } | ||
| 2337 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "\"> <input type=\"hidden\" name=\"target\" value=\"") | 2360 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, "\"> <input type=\"hidden\" name=\"target\" value=\"") |
| 2338 | if templ_7745c5c3_Err != nil { | 2361 | if templ_7745c5c3_Err != nil { | ||
| 2339 | return templ_7745c5c3_Err | 2362 | return templ_7745c5c3_Err | ||
| 2340 | } | 2363 | } | ||
| 2341 | - | var templ_7745c5c3_Var122 string | 2364 | + | var templ_7745c5c3_Var123 string |
| 2342 | - | templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target) | 2365 | + | templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target) |
| 2343 | if templ_7745c5c3_Err != nil { | 2366 | if templ_7745c5c3_Err != nil { | ||
| 2344 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 432, Col: 62} | 2367 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 435, Col: 62} |
| 2345 | } | 2368 | } | ||
| 2346 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var122) | 2369 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var123) |
| 2347 | if templ_7745c5c3_Err != nil { | 2370 | if templ_7745c5c3_Err != nil { | ||
| 2348 | return templ_7745c5c3_Err | 2371 | return templ_7745c5c3_Err | ||
| 2349 | } | 2372 | } | ||
| 2350 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"") | 2373 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"") |
| 2351 | if templ_7745c5c3_Err != nil { | 2374 | if templ_7745c5c3_Err != nil { | ||
| 2352 | return templ_7745c5c3_Err | 2375 | return templ_7745c5c3_Err | ||
| 2353 | } | 2376 | } | ||
| 2354 | - | var templ_7745c5c3_Var123 string | 2377 | + | var templ_7745c5c3_Var124 string |
| 2355 | - | templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title) | 2378 | + | templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title) |
| 2356 | if templ_7745c5c3_Err != nil { | 2379 | if templ_7745c5c3_Err != nil { | ||
| 2357 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 434, Col: 97} | 2380 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 437, Col: 97} |
| 2358 | } | 2381 | } | ||
| 2359 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var123) | 2382 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var124) |
| 2360 | if templ_7745c5c3_Err != nil { | 2383 | if templ_7745c5c3_Err != nil { | ||
| 2361 | return templ_7745c5c3_Err | 2384 | return templ_7745c5c3_Err | ||
| 2362 | } | 2385 | } | ||
| 2363 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "\" autocomplete=\"off\"> <label for=\"mr-desc\">description</label> <textarea id=\"mr-desc\" name=\"description\" rows=\"4\" maxlength=\"8000\" placeholder=\"plain text, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"f-main\">[create merge request]</button></div></form>") | 2386 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, "\" autocomplete=\"off\"> <label for=\"mr-desc\">description</label> <textarea id=\"mr-desc\" name=\"description\" rows=\"4\" maxlength=\"8000\" placeholder=\"plain text, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"f-main\">[create merge request]</button></div></form>") |
| 2364 | if templ_7745c5c3_Err != nil { | 2387 | if templ_7745c5c3_Err != nil { | ||
| 2365 | return templ_7745c5c3_Err | 2388 | return templ_7745c5c3_Err | ||
| 2366 | } | 2389 | } | ||
| 2367 | } | 2390 | } | ||
| 2368 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, " ") | 2391 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, " ") |
| 2369 | if templ_7745c5c3_Err != nil { | 2392 | if templ_7745c5c3_Err != nil { | ||
| 2370 | return templ_7745c5c3_Err | 2393 | return templ_7745c5c3_Err | ||
| 2371 | } | 2394 | } | ||
| 2372 | if p.HasDiff { | 2395 | if p.HasDiff { | ||
| 2373 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "<div class=\"diff-tools\" role=\"toolbar\" aria-label=\"Diff view\"><span class=\"dim\">") | 2396 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "<div class=\"diff-tools\" role=\"toolbar\" aria-label=\"Diff view\"><span class=\"dim\">") |
| 2374 | if templ_7745c5c3_Err != nil { | 2397 | if templ_7745c5c3_Err != nil { | ||
| 2375 | return templ_7745c5c3_Err | 2398 | return templ_7745c5c3_Err | ||
| 2376 | } | 2399 | } | ||
| 2377 | - | var templ_7745c5c3_Var124 string | 2400 | + | var templ_7745c5c3_Var125 string |
| 2378 | - | templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files")) | 2401 | + | templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files")) |
| 2379 | if templ_7745c5c3_Err != nil { | 2402 | if templ_7745c5c3_Err != nil { | ||
| 2380 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 444, Col: 57} | 2403 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 447, Col: 57} |
| 2381 | } | 2404 | } | ||
| 2382 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var124)) | 2405 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125)) |
| 2383 | if templ_7745c5c3_Err != nil { | 2406 | if templ_7745c5c3_Err != nil { | ||
| 2384 | return templ_7745c5c3_Err | 2407 | return templ_7745c5c3_Err | ||
| 2385 | } | 2408 | } | ||
| 2386 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, " · <span class=\"plus\">+") | 2409 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, " · <span class=\"plus\">+") |
| 2387 | if templ_7745c5c3_Err != nil { | 2410 | if templ_7745c5c3_Err != nil { | ||
| 2388 | return templ_7745c5c3_Err | 2411 | return templ_7745c5c3_Err | ||
| 2389 | } | 2412 | } | ||
| 2390 | - | var templ_7745c5c3_Var125 string | 2413 | + | var templ_7745c5c3_Var126 string |
| 2391 | - | templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds)) | 2414 | + | templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds)) |
| 2392 | if templ_7745c5c3_Err != nil { | 2415 | if templ_7745c5c3_Err != nil { | ||
| 2393 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 444, Col: 97} | 2416 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 447, Col: 97} |
| 2394 | } | 2417 | } | ||
| 2395 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125)) | 2418 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126)) |
| 2396 | if templ_7745c5c3_Err != nil { | 2419 | if templ_7745c5c3_Err != nil { | ||
| 2397 | return templ_7745c5c3_Err | 2420 | return templ_7745c5c3_Err | ||
| 2398 | } | 2421 | } | ||
| 2399 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, "</span> <span class=\"minus\">-") | 2422 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "</span> <span class=\"minus\">-") |
| 2400 | if templ_7745c5c3_Err != nil { | 2423 | if templ_7745c5c3_Err != nil { | ||
| 2401 | return templ_7745c5c3_Err | 2424 | return templ_7745c5c3_Err | ||
| 2402 | } | 2425 | } | ||
| 2403 | - | var templ_7745c5c3_Var126 string | 2426 | + | var templ_7745c5c3_Var127 string |
| 2404 | - | templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels)) | 2427 | + | templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels)) |
| 2405 | if templ_7745c5c3_Err != nil { | 2428 | if templ_7745c5c3_Err != nil { | ||
| 2406 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 444, Col: 142} | 2429 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 447, Col: 142} |
| 2407 | } | 2430 | } | ||
| 2408 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126)) | 2431 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var127)) |
| 2409 | if templ_7745c5c3_Err != nil { | 2432 | if templ_7745c5c3_Err != nil { | ||
| 2410 | return templ_7745c5c3_Err | 2433 | return templ_7745c5c3_Err | ||
| 2411 | } | 2434 | } | ||
| 2412 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "</span></span> <span class=\"grow\"></span>") | 2435 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, "</span></span> <span class=\"grow\"></span>") |
| 2413 | if templ_7745c5c3_Err != nil { | 2436 | if templ_7745c5c3_Err != nil { | ||
| 2414 | return templ_7745c5c3_Err | 2437 | return templ_7745c5c3_Err | ||
| 2415 | } | 2438 | } | ||
| @@ -2421,64 +2444,64 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||||
| 2421 | if templ_7745c5c3_Err != nil { | 2444 | if templ_7745c5c3_Err != nil { | ||
| 2422 | return templ_7745c5c3_Err | 2445 | return templ_7745c5c3_Err | ||
| 2423 | } | 2446 | } | ||
| 2424 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "<button type=\"button\" aria-keyshortcuts=\"x\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-collapse'))\">[x] collapse all</button></div>") | 2447 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, "<button type=\"button\" aria-keyshortcuts=\"x\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-collapse'))\">[x] collapse all</button></div>") |
| 2425 | if templ_7745c5c3_Err != nil { | 2448 | if templ_7745c5c3_Err != nil { | ||
| 2426 | return templ_7745c5c3_Err | 2449 | return templ_7745c5c3_Err | ||
| 2427 | } | 2450 | } | ||
| 2428 | if p.Invisible { | 2451 | if p.Invisible { | ||
| 2429 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "<p class=\"notice\">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class=\"inv\"><U+XXXX></span> markers, never applied.</p>") | 2452 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "<p class=\"notice\">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class=\"inv\"><U+XXXX></span> markers, never applied.</p>") |
| 2430 | if templ_7745c5c3_Err != nil { | 2453 | if templ_7745c5c3_Err != nil { | ||
| 2431 | return templ_7745c5c3_Err | 2454 | return templ_7745c5c3_Err | ||
| 2432 | } | 2455 | } | ||
| 2433 | } | 2456 | } | ||
| 2434 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, " <div id=\"diffs\"") | 2457 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, " <div id=\"diffs\"") |
| 2435 | if templ_7745c5c3_Err != nil { | 2458 | if templ_7745c5c3_Err != nil { | ||
| 2436 | return templ_7745c5c3_Err | 2459 | return templ_7745c5c3_Err | ||
| 2437 | } | 2460 | } | ||
| 2438 | if p.Both { | 2461 | if p.Both { | ||
| 2439 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, " data-diff=\"") | 2462 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, " data-diff=\"") |
| 2440 | if templ_7745c5c3_Err != nil { | 2463 | if templ_7745c5c3_Err != nil { | ||
| 2441 | return templ_7745c5c3_Err | 2464 | return templ_7745c5c3_Err | ||
| 2442 | } | 2465 | } | ||
| 2443 | - | var templ_7745c5c3_Var127 string | 2466 | + | var templ_7745c5c3_Var128 string |
| 2444 | - | templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | 2467 | + | templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) |
| 2445 | if templ_7745c5c3_Err != nil { | 2468 | if templ_7745c5c3_Err != nil { | ||
| 2446 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 459, Col: 23} | 2469 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 462, Col: 23} |
| 2447 | } | 2470 | } | ||
| 2448 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var127) | 2471 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var128) |
| 2449 | if templ_7745c5c3_Err != nil { | 2472 | if templ_7745c5c3_Err != nil { | ||
| 2450 | return templ_7745c5c3_Err | 2473 | return templ_7745c5c3_Err | ||
| 2451 | } | 2474 | } | ||
| 2452 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "\"") | 2475 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, "\"") |
| 2453 | if templ_7745c5c3_Err != nil { | 2476 | if templ_7745c5c3_Err != nil { | ||
| 2454 | return templ_7745c5c3_Err | 2477 | return templ_7745c5c3_Err | ||
| 2455 | } | 2478 | } | ||
| 2456 | } else { | 2479 | } else { | ||
| 2457 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, " data-view=\"") | 2480 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, " data-view=\"") |
| 2458 | if templ_7745c5c3_Err != nil { | 2481 | if templ_7745c5c3_Err != nil { | ||
| 2459 | return templ_7745c5c3_Err | 2482 | return templ_7745c5c3_Err | ||
| 2460 | } | 2483 | } | ||
| 2461 | - | var templ_7745c5c3_Var128 string | 2484 | + | var templ_7745c5c3_Var129 string |
| 2462 | - | templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | 2485 | + | templ_7745c5c3_Var129, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) |
| 2463 | if templ_7745c5c3_Err != nil { | 2486 | if templ_7745c5c3_Err != nil { | ||
| 2464 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 461, Col: 23} | 2487 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 464, Col: 23} |
| 2465 | } | 2488 | } | ||
| 2466 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var128) | 2489 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var129) |
| 2467 | if templ_7745c5c3_Err != nil { | 2490 | if templ_7745c5c3_Err != nil { | ||
| 2468 | return templ_7745c5c3_Err | 2491 | return templ_7745c5c3_Err | ||
| 2469 | } | 2492 | } | ||
| 2470 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "\"") | 2493 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "\"") |
| 2471 | if templ_7745c5c3_Err != nil { | 2494 | if templ_7745c5c3_Err != nil { | ||
| 2472 | return templ_7745c5c3_Err | 2495 | return templ_7745c5c3_Err | ||
| 2473 | } | 2496 | } | ||
| 2474 | } | 2497 | } | ||
| 2475 | if p.Both && !p.Pinned { | 2498 | if p.Both && !p.Pinned { | ||
| 2476 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"") | 2499 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"") |
| 2477 | if templ_7745c5c3_Err != nil { | 2500 | if templ_7745c5c3_Err != nil { | ||
| 2478 | return templ_7745c5c3_Err | 2501 | return templ_7745c5c3_Err | ||
| 2479 | } | 2502 | } | ||
| 2480 | } | 2503 | } | ||
| 2481 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, ">") | 2504 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, ">") |
| 2482 | if templ_7745c5c3_Err != nil { | 2505 | if templ_7745c5c3_Err != nil { | ||
| 2483 | return templ_7745c5c3_Err | 2506 | return templ_7745c5c3_Err | ||
| 2484 | } | 2507 | } | ||
| @@ -2488,14 +2511,14 @@ func Compare(l Layout, p ComparePage) templ.Component { | |||||
| 2488 | return templ_7745c5c3_Err | 2511 | return templ_7745c5c3_Err | ||
| 2489 | } | 2512 | } | ||
| 2490 | } | 2513 | } | ||
| 2491 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "</div>") | 2514 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "</div>") |
| 2492 | if templ_7745c5c3_Err != nil { | 2515 | if templ_7745c5c3_Err != nil { | ||
| 2493 | return templ_7745c5c3_Err | 2516 | return templ_7745c5c3_Err | ||
| 2494 | } | 2517 | } | ||
| 2495 | } | 2518 | } | ||
| 2496 | return nil | 2519 | return nil | ||
| 2497 | }) | 2520 | }) | ||
| 2498 | - | templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var109), templ_7745c5c3_Buffer) | 2521 | + | templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var110), templ_7745c5c3_Buffer) |
| 2499 | if templ_7745c5c3_Err != nil { | 2522 | if templ_7745c5c3_Err != nil { | ||
| 2500 | return templ_7745c5c3_Err | 2523 | return templ_7745c5c3_Err | ||
| 2501 | } | 2524 | } | ||
| @@ -2519,173 +2542,173 @@ func compareRail(p ComparePage) templ.Component { | |||||
| 2519 | }() | 2542 | }() | ||
| 2520 | } | 2543 | } | ||
| 2521 | ctx = templ.InitializeContext(ctx) | 2544 | ctx = templ.InitializeContext(ctx) | ||
| 2522 | - | templ_7745c5c3_Var129 := templ.GetChildren(ctx) | 2545 | + | templ_7745c5c3_Var130 := templ.GetChildren(ctx) |
| 2523 | - | if templ_7745c5c3_Var129 == nil { | 2546 | + | if templ_7745c5c3_Var130 == nil { |
| 2524 | - | templ_7745c5c3_Var129 = templ.NopComponent | 2547 | + | templ_7745c5c3_Var130 = templ.NopComponent |
| 2525 | } | 2548 | } | ||
| 2526 | ctx = templ.ClearChildren(ctx) | 2549 | ctx = templ.ClearChildren(ctx) | ||
| 2527 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">commits <span>") | 2550 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">commits <span>") |
| 2528 | if templ_7745c5c3_Err != nil { | 2551 | if templ_7745c5c3_Err != nil { | ||
| 2529 | return templ_7745c5c3_Err | 2552 | return templ_7745c5c3_Err | ||
| 2530 | } | 2553 | } | ||
| 2531 | - | var templ_7745c5c3_Var130 string | 2554 | + | var templ_7745c5c3_Var131 string |
| 2532 | - | templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead))) | 2555 | + | templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead))) |
| 2533 | if templ_7745c5c3_Err != nil { | 2556 | if templ_7745c5c3_Err != nil { | ||
| 2534 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 477, Col: 53} | 2557 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 480, Col: 53} |
| 2535 | } | 2558 | } | ||
| 2536 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var130)) | 2559 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var131)) |
| 2537 | if templ_7745c5c3_Err != nil { | 2560 | if templ_7745c5c3_Err != nil { | ||
| 2538 | return templ_7745c5c3_Err | 2561 | return templ_7745c5c3_Err | ||
| 2539 | } | 2562 | } | ||
| 2540 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, "</span></h2><ul class=\"pcommits\">") | 2563 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "</span></h2><ul class=\"pcommits\">") |
| 2541 | if templ_7745c5c3_Err != nil { | 2564 | if templ_7745c5c3_Err != nil { | ||
| 2542 | return templ_7745c5c3_Err | 2565 | return templ_7745c5c3_Err | ||
| 2543 | } | 2566 | } | ||
| 2544 | for _, c := range p.Ahead { | 2567 | for _, c := range p.Ahead { | ||
| 2545 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "<li><a href=\"") | 2568 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "<li><a href=\"") |
| 2546 | if templ_7745c5c3_Err != nil { | 2569 | if templ_7745c5c3_Err != nil { | ||
| 2547 | return templ_7745c5c3_Err | 2570 | return templ_7745c5c3_Err | ||
| 2548 | } | 2571 | } | ||
| 2549 | - | var templ_7745c5c3_Var131 templ.SafeURL | 2572 | + | var templ_7745c5c3_Var132 templ.SafeURL |
| 2550 | - | templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) | 2573 | + | templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) |
| 2551 | if templ_7745c5c3_Err != nil { | 2574 | if templ_7745c5c3_Err != nil { | ||
| 2552 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 481, Col: 37} | 2575 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 37} |
| 2553 | } | 2576 | } | ||
| 2554 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var131)) | 2577 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132)) |
| 2555 | if templ_7745c5c3_Err != nil { | 2578 | if templ_7745c5c3_Err != nil { | ||
| 2556 | return templ_7745c5c3_Err | 2579 | return templ_7745c5c3_Err | ||
| 2557 | } | 2580 | } | ||
| 2558 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "\">") | 2581 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, "\">") |
| 2559 | if templ_7745c5c3_Err != nil { | 2582 | if templ_7745c5c3_Err != nil { | ||
| 2560 | return templ_7745c5c3_Err | 2583 | return templ_7745c5c3_Err | ||
| 2561 | } | 2584 | } | ||
| 2562 | - | var templ_7745c5c3_Var132 string | 2585 | + | var templ_7745c5c3_Var133 string |
| 2563 | - | templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | 2586 | + | templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) |
| 2564 | if templ_7745c5c3_Err != nil { | 2587 | if templ_7745c5c3_Err != nil { | ||
| 2565 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 481, Col: 49} | 2588 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 49} |
| 2566 | } | 2589 | } | ||
| 2567 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132)) | 2590 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133)) |
| 2568 | if templ_7745c5c3_Err != nil { | 2591 | if templ_7745c5c3_Err != nil { | ||
| 2569 | return templ_7745c5c3_Err | 2592 | return templ_7745c5c3_Err | ||
| 2570 | } | 2593 | } | ||
| 2571 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "</a> <span class=\"s\">") | 2594 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "</a> <span class=\"s\">") |
| 2572 | if templ_7745c5c3_Err != nil { | 2595 | if templ_7745c5c3_Err != nil { | ||
| 2573 | return templ_7745c5c3_Err | 2596 | return templ_7745c5c3_Err | ||
| 2574 | } | 2597 | } | ||
| 2575 | - | var templ_7745c5c3_Var133 string | 2598 | + | var templ_7745c5c3_Var134 string |
| 2576 | - | templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | 2599 | + | templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) |
| 2577 | if templ_7745c5c3_Err != nil { | 2600 | if templ_7745c5c3_Err != nil { | ||
| 2578 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 482, Col: 32} | 2601 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 485, Col: 32} |
| 2579 | } | 2602 | } | ||
| 2580 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133)) | 2603 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134)) |
| 2581 | if templ_7745c5c3_Err != nil { | 2604 | if templ_7745c5c3_Err != nil { | ||
| 2582 | return templ_7745c5c3_Err | 2605 | return templ_7745c5c3_Err | ||
| 2583 | } | 2606 | } | ||
| 2584 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "</span> <span class=\"mute\">") | 2607 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "</span> <span class=\"mute\">") |
| 2585 | if templ_7745c5c3_Err != nil { | 2608 | if templ_7745c5c3_Err != nil { | ||
| 2586 | return templ_7745c5c3_Err | 2609 | return templ_7745c5c3_Err | ||
| 2587 | } | 2610 | } | ||
| 2588 | - | var templ_7745c5c3_Var134 string | 2611 | + | var templ_7745c5c3_Var135 string |
| 2589 | - | templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | 2612 | + | templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) |
| 2590 | if templ_7745c5c3_Err != nil { | 2613 | if templ_7745c5c3_Err != nil { | ||
| 2591 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 16} | 2614 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 487, Col: 16} |
| 2592 | } | 2615 | } | ||
| 2593 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134)) | 2616 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var135)) |
| 2594 | if templ_7745c5c3_Err != nil { | 2617 | if templ_7745c5c3_Err != nil { | ||
| 2595 | return templ_7745c5c3_Err | 2618 | return templ_7745c5c3_Err | ||
| 2596 | } | 2619 | } | ||
| 2597 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, " ") | 2620 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, " ") |
| 2598 | if templ_7745c5c3_Err != nil { | 2621 | if templ_7745c5c3_Err != nil { | ||
| 2599 | return templ_7745c5c3_Err | 2622 | return templ_7745c5c3_Err | ||
| 2600 | } | 2623 | } | ||
| 2601 | if c.WhenAt != "" { | 2624 | if c.WhenAt != "" { | ||
| 2602 | - | var templ_7745c5c3_Var135 string | 2625 | + | var templ_7745c5c3_Var136 string |
| 2603 | - | templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | 2626 | + | templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") |
| 2604 | if templ_7745c5c3_Err != nil { | 2627 | if templ_7745c5c3_Err != nil { | ||
| 2605 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 15} | 2628 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 489, Col: 15} |
| 2606 | } | 2629 | } | ||
| 2607 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var135)) | 2630 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var136)) |
| 2608 | if templ_7745c5c3_Err != nil { | 2631 | if templ_7745c5c3_Err != nil { | ||
| 2609 | return templ_7745c5c3_Err | 2632 | return templ_7745c5c3_Err | ||
| 2610 | } | 2633 | } | ||
| 2611 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "<time datetime=\"") | 2634 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "<time datetime=\"") |
| 2612 | if templ_7745c5c3_Err != nil { | 2635 | if templ_7745c5c3_Err != nil { | ||
| 2613 | return templ_7745c5c3_Err | 2636 | return templ_7745c5c3_Err | ||
| 2614 | } | 2637 | } | ||
| 2615 | - | var templ_7745c5c3_Var136 string | 2638 | + | var templ_7745c5c3_Var137 string |
| 2616 | - | templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | 2639 | + | templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) |
| 2617 | if templ_7745c5c3_Err != nil { | 2640 | if templ_7745c5c3_Err != nil { | ||
| 2618 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 42} | 2641 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 489, Col: 42} |
| 2619 | } | 2642 | } | ||
| 2620 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var136) | 2643 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var137) |
| 2621 | if templ_7745c5c3_Err != nil { | 2644 | if templ_7745c5c3_Err != nil { | ||
| 2622 | return templ_7745c5c3_Err | 2645 | return templ_7745c5c3_Err | ||
| 2623 | } | 2646 | } | ||
| 2624 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "\">") | 2647 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "\">") |
| 2625 | if templ_7745c5c3_Err != nil { | 2648 | if templ_7745c5c3_Err != nil { | ||
| 2626 | return templ_7745c5c3_Err | 2649 | return templ_7745c5c3_Err | ||
| 2627 | } | 2650 | } | ||
| 2628 | - | var templ_7745c5c3_Var137 string | 2651 | + | var templ_7745c5c3_Var138 string |
| 2629 | - | templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | 2652 | + | templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) |
| 2630 | if templ_7745c5c3_Err != nil { | 2653 | if templ_7745c5c3_Err != nil { | ||
| 2631 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 53} | 2654 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 489, Col: 53} |
| 2632 | } | 2655 | } | ||
| 2633 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137)) | 2656 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138)) |
| 2634 | if templ_7745c5c3_Err != nil { | 2657 | if templ_7745c5c3_Err != nil { | ||
| 2635 | return templ_7745c5c3_Err | 2658 | return templ_7745c5c3_Err | ||
| 2636 | } | 2659 | } | ||
| 2637 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "</time>") | 2660 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, "</time>") |
| 2638 | if templ_7745c5c3_Err != nil { | 2661 | if templ_7745c5c3_Err != nil { | ||
| 2639 | return templ_7745c5c3_Err | 2662 | return templ_7745c5c3_Err | ||
| 2640 | } | 2663 | } | ||
| 2641 | } | 2664 | } | ||
| 2642 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "</span></li>") | 2665 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, "</span></li>") |
| 2643 | if templ_7745c5c3_Err != nil { | 2666 | if templ_7745c5c3_Err != nil { | ||
| 2644 | return templ_7745c5c3_Err | 2667 | return templ_7745c5c3_Err | ||
| 2645 | } | 2668 | } | ||
| 2646 | } | 2669 | } | ||
| 2647 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "</ul>") | 2670 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "</ul>") |
| 2648 | if templ_7745c5c3_Err != nil { | 2671 | if templ_7745c5c3_Err != nil { | ||
| 2649 | return templ_7745c5c3_Err | 2672 | return templ_7745c5c3_Err | ||
| 2650 | } | 2673 | } | ||
| 2651 | if len(p.Ahead) == 0 { | 2674 | if len(p.Ahead) == 0 { | ||
| 2652 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, "<p class=\"dim\">no commit ahead of ") | 2675 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "<p class=\"dim\">no commit ahead of ") |
| 2653 | if templ_7745c5c3_Err != nil { | 2676 | if templ_7745c5c3_Err != nil { | ||
| 2654 | return templ_7745c5c3_Err | 2677 | return templ_7745c5c3_Err | ||
| 2655 | } | 2678 | } | ||
| 2656 | - | var templ_7745c5c3_Var138 string | 2679 | + | var templ_7745c5c3_Var139 string |
| 2657 | - | templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | 2680 | + | templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) |
| 2658 | if templ_7745c5c3_Err != nil { | 2681 | if templ_7745c5c3_Err != nil { | ||
| 2659 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 493, Col: 47} | 2682 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 496, Col: 47} |
| 2660 | } | 2683 | } | ||
| 2661 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138)) | 2684 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139)) |
| 2662 | if templ_7745c5c3_Err != nil { | 2685 | if templ_7745c5c3_Err != nil { | ||
| 2663 | return templ_7745c5c3_Err | 2686 | return templ_7745c5c3_Err | ||
| 2664 | } | 2687 | } | ||
| 2665 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, ".</p>") | 2688 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, ".</p>") |
| 2666 | if templ_7745c5c3_Err != nil { | 2689 | if templ_7745c5c3_Err != nil { | ||
| 2667 | return templ_7745c5c3_Err | 2690 | return templ_7745c5c3_Err | ||
| 2668 | } | 2691 | } | ||
| 2669 | } | 2692 | } | ||
| 2670 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "</section>") | 2693 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "</section>") |
| 2671 | if templ_7745c5c3_Err != nil { | 2694 | if templ_7745c5c3_Err != nil { | ||
| 2672 | return templ_7745c5c3_Err | 2695 | return templ_7745c5c3_Err | ||
| 2673 | } | 2696 | } | ||
| 2674 | if p.HasDiff { | 2697 | if p.HasDiff { | ||
| 2675 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">files <span>") | 2698 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">files <span>") |
| 2676 | if templ_7745c5c3_Err != nil { | 2699 | if templ_7745c5c3_Err != nil { | ||
| 2677 | return templ_7745c5c3_Err | 2700 | return templ_7745c5c3_Err | ||
| 2678 | } | 2701 | } | ||
| 2679 | - | var templ_7745c5c3_Var139 string | 2702 | + | var templ_7745c5c3_Var140 string |
| 2680 | - | templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | 2703 | + | templ_7745c5c3_Var140, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) |
| 2681 | if templ_7745c5c3_Err != nil { | 2704 | if templ_7745c5c3_Err != nil { | ||
| 2682 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 498, Col: 52} | 2705 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 501, Col: 52} |
| 2683 | } | 2706 | } | ||
| 2684 | - | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139)) | 2707 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var140)) |
| 2685 | if templ_7745c5c3_Err != nil { | 2708 | if templ_7745c5c3_Err != nil { | ||
| 2686 | return templ_7745c5c3_Err | 2709 | return templ_7745c5c3_Err | ||
| 2687 | } | 2710 | } | ||
| 2688 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, "</span></h2>") | 2711 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "</span></h2>") |
| 2689 | if templ_7745c5c3_Err != nil { | 2712 | if templ_7745c5c3_Err != nil { | ||
| 2690 | return templ_7745c5c3_Err | 2713 | return templ_7745c5c3_Err | ||
| 2691 | } | 2714 | } | ||
| @@ -2693,12 +2716,12 @@ func compareRail(p ComparePage) templ.Component { | |||||
| 2693 | if templ_7745c5c3_Err != nil { | 2716 | if templ_7745c5c3_Err != nil { | ||
| 2694 | return templ_7745c5c3_Err | 2717 | return templ_7745c5c3_Err | ||
| 2695 | } | 2718 | } | ||
| 2696 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "</section>") | 2719 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 244, "</section>") |
| 2697 | if templ_7745c5c3_Err != nil { | 2720 | if templ_7745c5c3_Err != nil { | ||
| 2698 | return templ_7745c5c3_Err | 2721 | return templ_7745c5c3_Err | ||
| 2699 | } | 2722 | } | ||
| 2700 | } | 2723 | } | ||
| 2701 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">from a terminal</h2>") | 2724 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 245, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">from a terminal</h2>") |
| 2702 | if templ_7745c5c3_Err != nil { | 2725 | if templ_7745c5c3_Err != nil { | ||
| 2703 | return templ_7745c5c3_Err | 2726 | return templ_7745c5c3_Err | ||
| 2704 | } | 2727 | } | ||
| @@ -2706,7 +2729,7 @@ func compareRail(p ComparePage) templ.Component { | |||||
| 2706 | if templ_7745c5c3_Err != nil { | 2729 | if templ_7745c5c3_Err != nil { | ||
| 2707 | return templ_7745c5c3_Err | 2730 | return templ_7745c5c3_Err | ||
| 2708 | } | 2731 | } | ||
| 2709 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "</section>") | 2732 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 246, "</section>") |
| 2710 | if templ_7745c5c3_Err != nil { | 2733 | if templ_7745c5c3_Err != nil { | ||
| 2711 | return templ_7745c5c3_Err | 2734 | return templ_7745c5c3_Err | ||
| 2712 | } | 2735 | } | ||
M internal/adapters/web/static/css/60-mr.css +2 -0
| @@ -24,6 +24,8 @@ | |||
| 24 | 24 | display: block; width: 100%; font: inherit; color: inherit; background: var(--paper-alt); | |
| 25 | 25 | border: 1px solid var(--rule); border-radius: 0; padding: 0 1ch; | |
| 26 | 26 | } | |
| 27 | + | & .d-check { display: block; margin-top: 4px; color: var(--ink-dim); cursor: pointer; } | |
| 28 | + | & .d-check input { accent-color: var(--ink); margin: 0 1ch 0 0; vertical-align: middle; } | |
| 27 | 29 | & .d-btns { display: flex; gap: 2ch; margin-top: 4px; flex-wrap: wrap; } | |
| 28 | 30 | & .d-btns button { color: var(--ink-dim); } | |
| 29 | 31 | & .d-btns button:hover { color: var(--ink); text-decoration: underline; } | |
| @@ -24,6 +24,8 @@ | |||||
| 24 | display: block; width: 100%; font: inherit; color: inherit; background: var(--paper-alt); | 24 | display: block; width: 100%; font: inherit; color: inherit; background: var(--paper-alt); | ||
| 25 | border: 1px solid var(--rule); border-radius: 0; padding: 0 1ch; | 25 | border: 1px solid var(--rule); border-radius: 0; padding: 0 1ch; | ||
| 26 | } | 26 | } | ||
| 27 | + | & .d-check { display: block; margin-top: 4px; color: var(--ink-dim); cursor: pointer; } | |||
| 28 | + | & .d-check input { accent-color: var(--ink); margin: 0 1ch 0 0; vertical-align: middle; } | |||
| 27 | & .d-btns { display: flex; gap: 2ch; margin-top: 4px; flex-wrap: wrap; } | 29 | & .d-btns { display: flex; gap: 2ch; margin-top: 4px; flex-wrap: wrap; } | ||
| 28 | & .d-btns button { color: var(--ink-dim); } | 30 | & .d-btns button { color: var(--ink-dim); } | ||
| 29 | & .d-btns button:hover { color: var(--ink); text-decoration: underline; } | 31 | & .d-btns button:hover { color: var(--ink); text-decoration: underline; } | ||
M internal/app/mergerequests.go +85 -22
| @@ -703,6 +703,16 @@ type MergeParams struct { | |||
| 703 | 703 | // HeadID is the commit the caller reviewed; the source branch must still | |
| 704 | 704 | // point to it. | |
| 705 | 705 | HeadID domain.ObjectID | |
| 706 | + | // DeleteSource deletes the source branch after the merge. | |
| 707 | + | DeleteSource bool | |
| 708 | + | } | |
| 709 | + | ||
| 710 | + | // MergeResult is a merged request and what happened to its source branch. | |
| 711 | + | type MergeResult struct { | |
| 712 | + | domain.MergeRequest | |
| 713 | + | // SourceDeleted is true when DeleteSource was asked and the branch is | |
| 714 | + | // gone. | |
| 715 | + | SourceDeleted bool | |
| 706 | 716 | } | |
| 707 | 717 | ||
| 708 | 718 | // Merge merges an open merge request, publishes MergeRequestMerged then a | |
| @@ -722,10 +732,21 @@ type MergeParams struct { | |||
| 722 | 732 | // precondition. A squash writes one commit with the tree of the head, the | |
| 723 | 733 | // target as parent and the author and message of domain.NewSquashCommit, | |
| 724 | 734 | // signed by the instance key, and moves the target to it the same way. | |
| 725 | - | func (m *MergeRequests) Merge(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params MergeParams) (domain.MergeRequest, error) { | |
| 735 | + | // | |
| 736 | + | // With DeleteSource the merge is also refused, before anything changes, when | |
| 737 | + | // the source is the default branch or the source or target of another open | |
| 738 | + | // request. After the merge the source is deleted if it still points to the | |
| 739 | + | // head; the PushEvent then carries the deletion too. A failed deletion is | |
| 740 | + | // logged and leaves the branch: the merge stands and SourceDeleted is false. | |
| 741 | + | func (m *MergeRequests) Merge(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params MergeParams) (MergeResult, error) { | |
| 742 | + | mr, deleted, err := m.merge(ctx, p, name, number, params) | |
| 743 | + | return MergeResult{MergeRequest: mr, SourceDeleted: deleted}, err | |
| 744 | + | } | |
| 745 | + | ||
| 746 | + | func (m *MergeRequests) merge(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params MergeParams) (domain.MergeRequest, bool, error) { | |
| 726 | 747 | repo, err := m.writable(ctx, p, name, domain.ScopeMRMerge) | |
| 727 | 748 | if err != nil { | |
| 728 | - | return domain.MergeRequest{}, err | |
| 749 | + | return domain.MergeRequest{}, false, err | |
| 729 | 750 | } | |
| 730 | 751 | mode := params.Mode | |
| 731 | 752 | if mode == "" { | |
| @@ -739,83 +760,125 @@ func (m *MergeRequests) Merge(ctx context.Context, p domain.Principal, name doma | |||
| 739 | 760 | v.Add("head_id", "is required") | |
| 740 | 761 | } | |
| 741 | 762 | if err := v.Err(); err != nil { | |
| 742 | - | return domain.MergeRequest{}, err | |
| 763 | + | return domain.MergeRequest{}, false, err | |
| 743 | 764 | } | |
| 744 | 765 | ||
| 745 | 766 | unlock, err := m.lockRepo(ctx, name) | |
| 746 | 767 | if err != nil { | |
| 747 | - | return domain.MergeRequest{}, err | |
| 768 | + | return domain.MergeRequest{}, false, err | |
| 748 | 769 | } | |
| 749 | 770 | defer unlock() | |
| 750 | 771 | ||
| 751 | 772 | mr, err := m.store.Get(ctx, name, number) | |
| 752 | 773 | if err != nil { | |
| 753 | - | return domain.MergeRequest{}, err | |
| 774 | + | return domain.MergeRequest{}, false, err | |
| 754 | 775 | } | |
| 755 | 776 | if !mr.IsOpen() { | |
| 756 | - | return domain.MergeRequest{}, fmt.Errorf("%w: !%d is %s", domain.ErrPreconditionFailed, number, mr.State) | |
| 777 | + | return domain.MergeRequest{}, false, fmt.Errorf("%w: !%d is %s", domain.ErrPreconditionFailed, number, mr.State) | |
| 757 | 778 | } | |
| 758 | 779 | // The settings are read again under the lock: a change applies at once. | |
| 759 | 780 | repo, err = m.meta.Get(ctx, name) | |
| 760 | 781 | if err != nil { | |
| 761 | 782 | if errors.Is(err, domain.ErrNotFound) { | |
| 762 | - | return domain.MergeRequest{}, err // deleted meanwhile | |
| 783 | + | return domain.MergeRequest{}, false, err // deleted meanwhile | |
| 763 | 784 | } | |
| 764 | - | return domain.MergeRequest{}, fmt.Errorf("get repository %q: %w", name, err) | |
| 785 | + | return domain.MergeRequest{}, false, fmt.Errorf("get repository %q: %w", name, err) | |
| 765 | 786 | } | |
| 766 | 787 | settings := repo.Settings | |
| 767 | 788 | if settings.IsZero() { | |
| 768 | 789 | settings = domain.DefaultRepoSettings() | |
| 769 | 790 | } | |
| 770 | 791 | if !settings.Allows(mode) { | |
| 771 | - | return domain.MergeRequest{}, fmt.Errorf("%w: the %s merge mode is not allowed in %s", domain.ErrPreconditionFailed, mode, name) | |
| 792 | + | return domain.MergeRequest{}, false, fmt.Errorf("%w: the %s merge mode is not allowed in %s", domain.ErrPreconditionFailed, mode, name) | |
| 772 | 793 | } | |
| 773 | 794 | ||
| 774 | 795 | head, err := m.branchOrZero(ctx, name, mr.Source) | |
| 775 | 796 | switch { | |
| 776 | 797 | case err != nil: | |
| 777 | - | return domain.MergeRequest{}, err | |
| 798 | + | return domain.MergeRequest{}, false, err | |
| 778 | 799 | case head.IsZero(): | |
| 779 | - | return domain.MergeRequest{}, fmt.Errorf("%w: the source branch %q no longer exists", domain.ErrPreconditionFailed, mr.Source) | |
| 800 | + | return domain.MergeRequest{}, false, fmt.Errorf("%w: the source branch %q no longer exists", domain.ErrPreconditionFailed, mr.Source) | |
| 780 | 801 | case head != params.HeadID: | |
| 781 | - | return domain.MergeRequest{}, fmt.Errorf("%w: branch moved, read again: %q is at %s, not %s", domain.ErrPreconditionFailed, mr.Source, head, params.HeadID) | |
| 802 | + | return domain.MergeRequest{}, false, fmt.Errorf("%w: branch moved, read again: %q is at %s, not %s", domain.ErrPreconditionFailed, mr.Source, head, params.HeadID) | |
| 782 | 803 | } | |
| 783 | 804 | tip, err := m.branchOrZero(ctx, name, mr.Target) | |
| 784 | 805 | switch { | |
| 785 | 806 | case err != nil: | |
| 786 | - | return domain.MergeRequest{}, err | |
| 807 | + | return domain.MergeRequest{}, false, err | |
| 787 | 808 | case tip.IsZero(): | |
| 788 | - | return domain.MergeRequest{}, fmt.Errorf("%w: the target branch %q no longer exists", domain.ErrPreconditionFailed, mr.Target) | |
| 809 | + | return domain.MergeRequest{}, false, fmt.Errorf("%w: the target branch %q no longer exists", domain.ErrPreconditionFailed, mr.Target) | |
| 789 | 810 | case tip == head: | |
| 790 | - | return domain.MergeRequest{}, fmt.Errorf("%w: nothing to merge, %q already is at %s", domain.ErrPreconditionFailed, mr.Target, head) | |
| 811 | + | return domain.MergeRequest{}, false, fmt.Errorf("%w: nothing to merge, %q already is at %s", domain.ErrPreconditionFailed, mr.Target, head) | |
| 791 | 812 | } | |
| 792 | 813 | switch ok, err := m.git.IsAncestor(ctx, name, tip, head); { | |
| 793 | 814 | case err != nil: | |
| 794 | - | return domain.MergeRequest{}, err | |
| 815 | + | return domain.MergeRequest{}, false, err | |
| 795 | 816 | case !ok: | |
| 796 | - | return domain.MergeRequest{}, fmt.Errorf("%w: %q is not an ancestor of %q: rebase it on %q and push again", domain.ErrPreconditionFailed, mr.Target, mr.Source, mr.Target) | |
| 817 | + | return domain.MergeRequest{}, false, fmt.Errorf("%w: %q is not an ancestor of %q: rebase it on %q and push again", domain.ErrPreconditionFailed, mr.Target, mr.Source, mr.Target) | |
| 818 | + | } | |
| 819 | + | if params.DeleteSource { | |
| 820 | + | if err := m.checkDeletable(ctx, name, mr); err != nil { | |
| 821 | + | return domain.MergeRequest{}, false, err | |
| 822 | + | } | |
| 797 | 823 | } | |
| 798 | 824 | ||
| 799 | 825 | merged, err := m.moveTarget(ctx, mr, mode, tip, head) | |
| 800 | 826 | if err != nil { | |
| 801 | - | return domain.MergeRequest{}, err | |
| 827 | + | return domain.MergeRequest{}, false, err | |
| 802 | 828 | } | |
| 803 | 829 | m.recordPush(ctx, name) // the target moved, whatever happens next | |
| 804 | 830 | mr, err = m.markMerged(ctx, name, number, mode, tip, head, merged) | |
| 805 | 831 | if err != nil { | |
| 806 | - | return domain.MergeRequest{}, err | |
| 832 | + | return domain.MergeRequest{}, false, err | |
| 807 | 833 | } | |
| 808 | 834 | log := zerowrap.FromCtx(ctx) | |
| 809 | 835 | log.Info().Str("repo", name.String()).Int64("number", number).Str("mode", mode.String()). | |
| 810 | 836 | Str("target", mr.Target).Str("commit", merged.String()). | |
| 811 | 837 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("merge request merged") | |
| 838 | + | updates := []domain.RefUpdate{{Old: tip.String(), New: merged.String(), Ref: "refs/heads/" + mr.Target}} | |
| 839 | + | deleted := false | |
| 840 | + | if params.DeleteSource { | |
| 841 | + | // The merge is done and recorded: a failure here keeps the branch. | |
| 842 | + | switch err := m.git.DeleteBranch(context.WithoutCancel(ctx), name, mr.Source, head); { | |
| 843 | + | case err != nil: | |
| 844 | + | log.Warn().Err(err).Str("repo", name.String()).Int64("number", number).Str("branch", mr.Source). | |
| 845 | + | Msg("source branch kept after the merge") | |
| 846 | + | default: | |
| 847 | + | deleted = true | |
| 848 | + | updates = append(updates, domain.RefUpdate{Old: head.String(), New: domain.ZeroOID, Ref: "refs/heads/" + mr.Source}) | |
| 849 | + | log.Info().Str("repo", name.String()).Int64("number", number).Str("branch", mr.Source). | |
| 850 | + | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("source branch deleted after the merge") | |
| 851 | + | } | |
| 852 | + | } | |
| 812 | 853 | at := mr.UpdatedAt | |
| 813 | 854 | m.bus.Publish(ctx, domain.MergeRequestMerged{MergeRequest: mr, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | |
| 814 | 855 | m.bus.Publish(ctx, domain.PushEvent{ | |
| 815 | - | Repo: name, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, | |
| 816 | - | Updates: []domain.RefUpdate{{Old: tip.String(), New: merged.String(), Ref: "refs/heads/" + mr.Target}}, | |
| 856 | + | Repo: name, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, Updates: updates, | |
| 817 | 857 | }) | |
| 818 | - | return mr, nil | |
| 858 | + | return mr, deleted, nil | |
| 859 | + | } | |
| 860 | + | ||
| 861 | + | // checkDeletable refuses to delete the source branch of mr when it is the | |
| 862 | + | // default branch or the source or target of another open request. It runs | |
| 863 | + | // under the merge lock, which also guards the creation of requests. | |
| 864 | + | func (m *MergeRequests) checkDeletable(ctx context.Context, name domain.RepoName, mr domain.MergeRequest) error { | |
| 865 | + | def, _, err := m.repos.DefaultBranch(ctx, name) | |
| 866 | + | if err != nil { | |
| 867 | + | return fmt.Errorf("default branch of %q: %w", name, err) | |
| 868 | + | } | |
| 869 | + | if mr.Source == def { | |
| 870 | + | return fmt.Errorf("%w: %q is the default branch of %s: it cannot be deleted", domain.ErrPreconditionFailed, mr.Source, name) | |
| 871 | + | } | |
| 872 | + | all, err := m.store.List(ctx, name) | |
| 873 | + | if err != nil { | |
| 874 | + | return fmt.Errorf("list merge requests of %q: %w", name, err) | |
| 875 | + | } | |
| 876 | + | for _, o := range all { | |
| 877 | + | if o.Number != mr.Number && o.IsOpen() && (o.Source == mr.Source || o.Target == mr.Source) { | |
| 878 | + | return fmt.Errorf("%w: !%d still uses %q: it cannot be deleted", domain.ErrPreconditionFailed, o.Number, mr.Source) | |
| 879 | + | } | |
| 880 | + | } | |
| 881 | + | return nil | |
| 819 | 882 | } | |
| 820 | 883 | ||
| 821 | 884 | // recordPush does for a merge what Pushes.Record does for a push, as the merge | |
| @@ -703,6 +703,16 @@ type MergeParams struct { | |||||
| 703 | // HeadID is the commit the caller reviewed; the source branch must still | 703 | // HeadID is the commit the caller reviewed; the source branch must still | ||
| 704 | // point to it. | 704 | // point to it. | ||
| 705 | HeadID domain.ObjectID | 705 | HeadID domain.ObjectID | ||
| 706 | + | // DeleteSource deletes the source branch after the merge. | |||
| 707 | + | DeleteSource bool | |||
| 708 | + | } | |||
| 709 | + | ||||
| 710 | + | // MergeResult is a merged request and what happened to its source branch. | |||
| 711 | + | type MergeResult struct { | |||
| 712 | + | domain.MergeRequest | |||
| 713 | + | // SourceDeleted is true when DeleteSource was asked and the branch is | |||
| 714 | + | // gone. | |||
| 715 | + | SourceDeleted bool | |||
| 706 | } | 716 | } | ||
| 707 | 717 | ||||
| 708 | // Merge merges an open merge request, publishes MergeRequestMerged then a | 718 | // Merge merges an open merge request, publishes MergeRequestMerged then a | ||
| @@ -722,10 +732,21 @@ type MergeParams struct { | |||||
| 722 | // precondition. A squash writes one commit with the tree of the head, the | 732 | // precondition. A squash writes one commit with the tree of the head, the | ||
| 723 | // target as parent and the author and message of domain.NewSquashCommit, | 733 | // target as parent and the author and message of domain.NewSquashCommit, | ||
| 724 | // signed by the instance key, and moves the target to it the same way. | 734 | // signed by the instance key, and moves the target to it the same way. | ||
| 725 | - | func (m *MergeRequests) Merge(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params MergeParams) (domain.MergeRequest, error) { | 735 | + | // |
| 736 | + | // With DeleteSource the merge is also refused, before anything changes, when | |||
| 737 | + | // the source is the default branch or the source or target of another open | |||
| 738 | + | // request. After the merge the source is deleted if it still points to the | |||
| 739 | + | // head; the PushEvent then carries the deletion too. A failed deletion is | |||
| 740 | + | // logged and leaves the branch: the merge stands and SourceDeleted is false. | |||
| 741 | + | func (m *MergeRequests) Merge(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params MergeParams) (MergeResult, error) { | |||
| 742 | + | mr, deleted, err := m.merge(ctx, p, name, number, params) | |||
| 743 | + | return MergeResult{MergeRequest: mr, SourceDeleted: deleted}, err | |||
| 744 | + | } | |||
| 745 | + | ||||
| 746 | + | func (m *MergeRequests) merge(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, params MergeParams) (domain.MergeRequest, bool, error) { | |||
| 726 | repo, err := m.writable(ctx, p, name, domain.ScopeMRMerge) | 747 | repo, err := m.writable(ctx, p, name, domain.ScopeMRMerge) | ||
| 727 | if err != nil { | 748 | if err != nil { | ||
| 728 | - | return domain.MergeRequest{}, err | 749 | + | return domain.MergeRequest{}, false, err |
| 729 | } | 750 | } | ||
| 730 | mode := params.Mode | 751 | mode := params.Mode | ||
| 731 | if mode == "" { | 752 | if mode == "" { | ||
| @@ -739,83 +760,125 @@ func (m *MergeRequests) Merge(ctx context.Context, p domain.Principal, name doma | |||||
| 739 | v.Add("head_id", "is required") | 760 | v.Add("head_id", "is required") | ||
| 740 | } | 761 | } | ||
| 741 | if err := v.Err(); err != nil { | 762 | if err := v.Err(); err != nil { | ||
| 742 | - | return domain.MergeRequest{}, err | 763 | + | return domain.MergeRequest{}, false, err |
| 743 | } | 764 | } | ||
| 744 | 765 | ||||
| 745 | unlock, err := m.lockRepo(ctx, name) | 766 | unlock, err := m.lockRepo(ctx, name) | ||
| 746 | if err != nil { | 767 | if err != nil { | ||
| 747 | - | return domain.MergeRequest{}, err | 768 | + | return domain.MergeRequest{}, false, err |
| 748 | } | 769 | } | ||
| 749 | defer unlock() | 770 | defer unlock() | ||
| 750 | 771 | ||||
| 751 | mr, err := m.store.Get(ctx, name, number) | 772 | mr, err := m.store.Get(ctx, name, number) | ||
| 752 | if err != nil { | 773 | if err != nil { | ||
| 753 | - | return domain.MergeRequest{}, err | 774 | + | return domain.MergeRequest{}, false, err |
| 754 | } | 775 | } | ||
| 755 | if !mr.IsOpen() { | 776 | if !mr.IsOpen() { | ||
| 756 | - | return domain.MergeRequest{}, fmt.Errorf("%w: !%d is %s", domain.ErrPreconditionFailed, number, mr.State) | 777 | + | return domain.MergeRequest{}, false, fmt.Errorf("%w: !%d is %s", domain.ErrPreconditionFailed, number, mr.State) |
| 757 | } | 778 | } | ||
| 758 | // The settings are read again under the lock: a change applies at once. | 779 | // The settings are read again under the lock: a change applies at once. | ||
| 759 | repo, err = m.meta.Get(ctx, name) | 780 | repo, err = m.meta.Get(ctx, name) | ||
| 760 | if err != nil { | 781 | if err != nil { | ||
| 761 | if errors.Is(err, domain.ErrNotFound) { | 782 | if errors.Is(err, domain.ErrNotFound) { | ||
| 762 | - | return domain.MergeRequest{}, err // deleted meanwhile | 783 | + | return domain.MergeRequest{}, false, err // deleted meanwhile |
| 763 | } | 784 | } | ||
| 764 | - | return domain.MergeRequest{}, fmt.Errorf("get repository %q: %w", name, err) | 785 | + | return domain.MergeRequest{}, false, fmt.Errorf("get repository %q: %w", name, err) |
| 765 | } | 786 | } | ||
| 766 | settings := repo.Settings | 787 | settings := repo.Settings | ||
| 767 | if settings.IsZero() { | 788 | if settings.IsZero() { | ||
| 768 | settings = domain.DefaultRepoSettings() | 789 | settings = domain.DefaultRepoSettings() | ||
| 769 | } | 790 | } | ||
| 770 | if !settings.Allows(mode) { | 791 | if !settings.Allows(mode) { | ||
| 771 | - | return domain.MergeRequest{}, fmt.Errorf("%w: the %s merge mode is not allowed in %s", domain.ErrPreconditionFailed, mode, name) | 792 | + | return domain.MergeRequest{}, false, fmt.Errorf("%w: the %s merge mode is not allowed in %s", domain.ErrPreconditionFailed, mode, name) |
| 772 | } | 793 | } | ||
| 773 | 794 | ||||
| 774 | head, err := m.branchOrZero(ctx, name, mr.Source) | 795 | head, err := m.branchOrZero(ctx, name, mr.Source) | ||
| 775 | switch { | 796 | switch { | ||
| 776 | case err != nil: | 797 | case err != nil: | ||
| 777 | - | return domain.MergeRequest{}, err | 798 | + | return domain.MergeRequest{}, false, err |
| 778 | case head.IsZero(): | 799 | case head.IsZero(): | ||
| 779 | - | return domain.MergeRequest{}, fmt.Errorf("%w: the source branch %q no longer exists", domain.ErrPreconditionFailed, mr.Source) | 800 | + | return domain.MergeRequest{}, false, fmt.Errorf("%w: the source branch %q no longer exists", domain.ErrPreconditionFailed, mr.Source) |
| 780 | case head != params.HeadID: | 801 | case head != params.HeadID: | ||
| 781 | - | return domain.MergeRequest{}, fmt.Errorf("%w: branch moved, read again: %q is at %s, not %s", domain.ErrPreconditionFailed, mr.Source, head, params.HeadID) | 802 | + | return domain.MergeRequest{}, false, fmt.Errorf("%w: branch moved, read again: %q is at %s, not %s", domain.ErrPreconditionFailed, mr.Source, head, params.HeadID) |
| 782 | } | 803 | } | ||
| 783 | tip, err := m.branchOrZero(ctx, name, mr.Target) | 804 | tip, err := m.branchOrZero(ctx, name, mr.Target) | ||
| 784 | switch { | 805 | switch { | ||
| 785 | case err != nil: | 806 | case err != nil: | ||
| 786 | - | return domain.MergeRequest{}, err | 807 | + | return domain.MergeRequest{}, false, err |
| 787 | case tip.IsZero(): | 808 | case tip.IsZero(): | ||
| 788 | - | return domain.MergeRequest{}, fmt.Errorf("%w: the target branch %q no longer exists", domain.ErrPreconditionFailed, mr.Target) | 809 | + | return domain.MergeRequest{}, false, fmt.Errorf("%w: the target branch %q no longer exists", domain.ErrPreconditionFailed, mr.Target) |
| 789 | case tip == head: | 810 | case tip == head: | ||
| 790 | - | return domain.MergeRequest{}, fmt.Errorf("%w: nothing to merge, %q already is at %s", domain.ErrPreconditionFailed, mr.Target, head) | 811 | + | return domain.MergeRequest{}, false, fmt.Errorf("%w: nothing to merge, %q already is at %s", domain.ErrPreconditionFailed, mr.Target, head) |
| 791 | } | 812 | } | ||
| 792 | switch ok, err := m.git.IsAncestor(ctx, name, tip, head); { | 813 | switch ok, err := m.git.IsAncestor(ctx, name, tip, head); { | ||
| 793 | case err != nil: | 814 | case err != nil: | ||
| 794 | - | return domain.MergeRequest{}, err | 815 | + | return domain.MergeRequest{}, false, err |
| 795 | case !ok: | 816 | case !ok: | ||
| 796 | - | return domain.MergeRequest{}, fmt.Errorf("%w: %q is not an ancestor of %q: rebase it on %q and push again", domain.ErrPreconditionFailed, mr.Target, mr.Source, mr.Target) | 817 | + | return domain.MergeRequest{}, false, fmt.Errorf("%w: %q is not an ancestor of %q: rebase it on %q and push again", domain.ErrPreconditionFailed, mr.Target, mr.Source, mr.Target) |
| 818 | + | } | |||
| 819 | + | if params.DeleteSource { | |||
| 820 | + | if err := m.checkDeletable(ctx, name, mr); err != nil { | |||
| 821 | + | return domain.MergeRequest{}, false, err | |||
| 822 | + | } | |||
| 797 | } | 823 | } | ||
| 798 | 824 | ||||
| 799 | merged, err := m.moveTarget(ctx, mr, mode, tip, head) | 825 | merged, err := m.moveTarget(ctx, mr, mode, tip, head) | ||
| 800 | if err != nil { | 826 | if err != nil { | ||
| 801 | - | return domain.MergeRequest{}, err | 827 | + | return domain.MergeRequest{}, false, err |
| 802 | } | 828 | } | ||
| 803 | m.recordPush(ctx, name) // the target moved, whatever happens next | 829 | m.recordPush(ctx, name) // the target moved, whatever happens next | ||
| 804 | mr, err = m.markMerged(ctx, name, number, mode, tip, head, merged) | 830 | mr, err = m.markMerged(ctx, name, number, mode, tip, head, merged) | ||
| 805 | if err != nil { | 831 | if err != nil { | ||
| 806 | - | return domain.MergeRequest{}, err | 832 | + | return domain.MergeRequest{}, false, err |
| 807 | } | 833 | } | ||
| 808 | log := zerowrap.FromCtx(ctx) | 834 | log := zerowrap.FromCtx(ctx) | ||
| 809 | log.Info().Str("repo", name.String()).Int64("number", number).Str("mode", mode.String()). | 835 | log.Info().Str("repo", name.String()).Int64("number", number).Str("mode", mode.String()). | ||
| 810 | Str("target", mr.Target).Str("commit", merged.String()). | 836 | Str("target", mr.Target).Str("commit", merged.String()). | ||
| 811 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("merge request merged") | 837 | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("merge request merged") | ||
| 838 | + | updates := []domain.RefUpdate{{Old: tip.String(), New: merged.String(), Ref: "refs/heads/" + mr.Target}} | |||
| 839 | + | deleted := false | |||
| 840 | + | if params.DeleteSource { | |||
| 841 | + | // The merge is done and recorded: a failure here keeps the branch. | |||
| 842 | + | switch err := m.git.DeleteBranch(context.WithoutCancel(ctx), name, mr.Source, head); { | |||
| 843 | + | case err != nil: | |||
| 844 | + | log.Warn().Err(err).Str("repo", name.String()).Int64("number", number).Str("branch", mr.Source). | |||
| 845 | + | Msg("source branch kept after the merge") | |||
| 846 | + | default: | |||
| 847 | + | deleted = true | |||
| 848 | + | updates = append(updates, domain.RefUpdate{Old: head.String(), New: domain.ZeroOID, Ref: "refs/heads/" + mr.Source}) | |||
| 849 | + | log.Info().Str("repo", name.String()).Int64("number", number).Str("branch", mr.Source). | |||
| 850 | + | Str("principal", p.Label()).Str("token_id", p.TokenID()).Msg("source branch deleted after the merge") | |||
| 851 | + | } | |||
| 852 | + | } | |||
| 812 | at := mr.UpdatedAt | 853 | at := mr.UpdatedAt | ||
| 813 | m.bus.Publish(ctx, domain.MergeRequestMerged{MergeRequest: mr, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | 854 | m.bus.Publish(ctx, domain.MergeRequestMerged{MergeRequest: mr, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt}) | ||
| 814 | m.bus.Publish(ctx, domain.PushEvent{ | 855 | m.bus.Publish(ctx, domain.PushEvent{ | ||
| 815 | - | Repo: name, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, | 856 | + | Repo: name, At: at, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, Updates: updates, |
| 816 | - | Updates: []domain.RefUpdate{{Old: tip.String(), New: merged.String(), Ref: "refs/heads/" + mr.Target}}, | |||
| 817 | }) | 857 | }) | ||
| 818 | - | return mr, nil | 858 | + | return mr, deleted, nil |
| 859 | + | } | |||
| 860 | + | ||||
| 861 | + | // checkDeletable refuses to delete the source branch of mr when it is the | |||
| 862 | + | // default branch or the source or target of another open request. It runs | |||
| 863 | + | // under the merge lock, which also guards the creation of requests. | |||
| 864 | + | func (m *MergeRequests) checkDeletable(ctx context.Context, name domain.RepoName, mr domain.MergeRequest) error { | |||
| 865 | + | def, _, err := m.repos.DefaultBranch(ctx, name) | |||
| 866 | + | if err != nil { | |||
| 867 | + | return fmt.Errorf("default branch of %q: %w", name, err) | |||
| 868 | + | } | |||
| 869 | + | if mr.Source == def { | |||
| 870 | + | return fmt.Errorf("%w: %q is the default branch of %s: it cannot be deleted", domain.ErrPreconditionFailed, mr.Source, name) | |||
| 871 | + | } | |||
| 872 | + | all, err := m.store.List(ctx, name) | |||
| 873 | + | if err != nil { | |||
| 874 | + | return fmt.Errorf("list merge requests of %q: %w", name, err) | |||
| 875 | + | } | |||
| 876 | + | for _, o := range all { | |||
| 877 | + | if o.Number != mr.Number && o.IsOpen() && (o.Source == mr.Source || o.Target == mr.Source) { | |||
| 878 | + | return fmt.Errorf("%w: !%d still uses %q: it cannot be deleted", domain.ErrPreconditionFailed, o.Number, mr.Source) | |||
| 879 | + | } | |||
| 880 | + | } | |||
| 881 | + | return nil | |||
| 819 | } | 882 | } | ||
| 820 | 883 | ||||
| 821 | // recordPush does for a merge what Pushes.Record does for a push, as the merge | 884 | // recordPush does for a merge what Pushes.Record does for a push, as the merge | ||
A internal/app/mergerequests_delete_test.go +151 -0
| @@ -0,0 +1,151 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package app_test | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "testing" | |
| 8 | + | ||
| 9 | + | "github.com/stretchr/testify/assert" | |
| 10 | + | "github.com/stretchr/testify/require" | |
| 11 | + | ||
| 12 | + | "bnema.dev/ermite/internal/adapters/gitcli" | |
| 13 | + | "bnema.dev/ermite/internal/app" | |
| 14 | + | "bnema.dev/ermite/internal/domain" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | // A merge can delete its source branch; the push event carries both refs. | |
| 18 | + | func TestMergeDeletesTheSourceBranch(t *testing.T) { | |
| 19 | + | for _, mode := range []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash} { | |
| 20 | + | t.Run(string(mode), func(t *testing.T) { | |
| 21 | + | r := newMRRig(t) | |
| 22 | + | main := r.seed() | |
| 23 | + | head := r.feature("feat") | |
| 24 | + | mr := r.open("feat") | |
| 25 | + | r.drain() | |
| 26 | + | ||
| 27 | + | res, err := r.mrs.Merge(r.ctx, mrM, r.repo, mr.Number, app.MergeParams{Mode: mode, HeadID: head, DeleteSource: true}) | |
| 28 | + | require.NoError(t, err) | |
| 29 | + | assert.True(t, res.SourceDeleted) | |
| 30 | + | assert.Equal(t, domain.MRStateMerged, res.State) | |
| 31 | + | _, err = r.git.BranchHead(r.ctx, r.repo, "feat") | |
| 32 | + | assert.ErrorIs(t, err, domain.ErrNotFound, "the branch is gone") | |
| 33 | + | ||
| 34 | + | evs := r.drain() | |
| 35 | + | require.Len(t, evs, 2) | |
| 36 | + | assert.Equal(t, []domain.RefUpdate{ | |
| 37 | + | {Old: main.String(), New: res.MergedCommitID.String(), Ref: "refs/heads/main"}, | |
| 38 | + | {Old: head.String(), New: domain.ZeroOID, Ref: "refs/heads/feat"}, | |
| 39 | + | }, evs[1].(domain.PushEvent).Updates) | |
| 40 | + | ||
| 41 | + | // The merged request still reads, with its commits and diff. | |
| 42 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, false) | |
| 43 | + | require.NoError(t, err) | |
| 44 | + | assert.Len(t, d.Commits, 2) | |
| 45 | + | assert.Len(t, d.Diff.Files, 2) | |
| 46 | + | }) | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | t.Run("without the option the branch stays", func(t *testing.T) { | |
| 50 | + | r := newMRRig(t) | |
| 51 | + | r.seed() | |
| 52 | + | head := r.feature("feat") | |
| 53 | + | mr := r.open("feat") | |
| 54 | + | res, err := r.mrs.Merge(r.ctx, mrM, r.repo, mr.Number, app.MergeParams{HeadID: head}) | |
| 55 | + | require.NoError(t, err) | |
| 56 | + | assert.False(t, res.SourceDeleted) | |
| 57 | + | assert.Equal(t, head, r.branchHead("feat")) | |
| 58 | + | }) | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | // Deleting the source is refused before the merge when the branch is still | |
| 62 | + | // needed: nothing changes. | |
| 63 | + | func TestMergeRefusesToDeleteANeededBranch(t *testing.T) { | |
| 64 | + | t.Run("another open request uses it as its source", func(t *testing.T) { | |
| 65 | + | r := newMRRig(t) | |
| 66 | + | main := r.seed() | |
| 67 | + | head := r.feature("feat") | |
| 68 | + | r.sh("checkout", "-q", "-B", "release", "main") | |
| 69 | + | r.push("release") | |
| 70 | + | mr := r.open("feat") | |
| 71 | + | _, err := r.mrs.Create(r.ctx, root, r.repo, app.CreateMergeRequestParams{Title: "to release", Source: "feat", Target: "release"}) | |
| 72 | + | require.NoError(t, err) | |
| 73 | + | ||
| 74 | + | _, err = r.mrs.Merge(r.ctx, mrM, r.repo, mr.Number, app.MergeParams{HeadID: head, DeleteSource: true}) | |
| 75 | + | require.ErrorIs(t, err, domain.ErrPreconditionFailed) | |
| 76 | + | assert.Contains(t, err.Error(), "!2 still uses") | |
| 77 | + | assert.Equal(t, main, r.branchHead("main"), "not merged") | |
| 78 | + | assert.Equal(t, head, r.branchHead("feat")) | |
| 79 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, false) | |
| 80 | + | require.NoError(t, err) | |
| 81 | + | assert.Equal(t, domain.MRStateOpen, d.MergeRequest.State) | |
| 82 | + | }) | |
| 83 | + | ||
| 84 | + | t.Run("another open request targets it", func(t *testing.T) { | |
| 85 | + | r := newMRRig(t) | |
| 86 | + | r.seed() | |
| 87 | + | head := r.feature("feat") | |
| 88 | + | mr := r.open("feat") | |
| 89 | + | r.sh("checkout", "-q", "-B", "sub", "feat") | |
| 90 | + | r.commit(author, "d.txt", "dee\n", "add d") | |
| 91 | + | r.push("sub") | |
| 92 | + | _, err := r.mrs.Create(r.ctx, root, r.repo, app.CreateMergeRequestParams{Title: "stacked", Source: "sub", Target: "feat"}) | |
| 93 | + | require.NoError(t, err) | |
| 94 | + | _, err = r.mrs.Merge(r.ctx, mrM, r.repo, mr.Number, app.MergeParams{HeadID: head, DeleteSource: true}) | |
| 95 | + | require.ErrorIs(t, err, domain.ErrPreconditionFailed) | |
| 96 | + | assert.Equal(t, head, r.branchHead("feat")) | |
| 97 | + | }) | |
| 98 | + | ||
| 99 | + | t.Run("the default branch", func(t *testing.T) { | |
| 100 | + | r := newMRRig(t) | |
| 101 | + | main := r.seed() | |
| 102 | + | r.sh("checkout", "-q", "-B", "old", "main~1") | |
| 103 | + | r.push("old") | |
| 104 | + | old := r.branchHead("old") | |
| 105 | + | mr, err := r.mrs.Create(r.ctx, root, r.repo, app.CreateMergeRequestParams{Title: "back", Source: "main", Target: "old"}) | |
| 106 | + | require.NoError(t, err) | |
| 107 | + | _, err = r.mrs.Merge(r.ctx, mrM, r.repo, mr.Number, app.MergeParams{HeadID: main, DeleteSource: true}) | |
| 108 | + | require.ErrorIs(t, err, domain.ErrPreconditionFailed) | |
| 109 | + | assert.Contains(t, err.Error(), "default branch") | |
| 110 | + | assert.Equal(t, old, r.branchHead("old"), "not merged") | |
| 111 | + | assert.Equal(t, main, r.branchHead("main")) | |
| 112 | + | }) | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | // movingGit pushes to the source branch between the merge and the deletion. | |
| 116 | + | type movingGit struct { | |
| 117 | + | *gitcli.Git | |
| 118 | + | move func() | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | func (g movingGit) DeleteBranch(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID) error { | |
| 122 | + | g.move() | |
| 123 | + | return g.Git.DeleteBranch(ctx, repo, branch, old) | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | // A branch that moves after the merge is kept: the merge stands. | |
| 127 | + | func TestMergeKeepsASourceThatMovedAfterTheMerge(t *testing.T) { | |
| 128 | + | r := newMRRig(t) | |
| 129 | + | r.seed() | |
| 130 | + | head := r.feature("feat") | |
| 131 | + | mr := r.open("feat") | |
| 132 | + | var moved domain.ObjectID | |
| 133 | + | mrs := app.NewMergeRequests(app.MergeRequestsConfig{ | |
| 134 | + | Meta: r.store, Store: r.store.MergeRequests(), Repos: r.git, Bus: r.bus, Clock: fixedClock{}, Maint: r.sched, | |
| 135 | + | Git: movingGit{Git: r.git, move: func() { | |
| 136 | + | moved = r.commit(author, "late.txt", "late\n", "pushed during the merge") | |
| 137 | + | r.push("feat") | |
| 138 | + | }}, | |
| 139 | + | }) | |
| 140 | + | r.drain() | |
| 141 | + | ||
| 142 | + | res, err := mrs.Merge(r.ctx, mrM, r.repo, mr.Number, app.MergeParams{HeadID: head, DeleteSource: true}) | |
| 143 | + | require.NoError(t, err, "the merge stands") | |
| 144 | + | assert.False(t, res.SourceDeleted) | |
| 145 | + | assert.Equal(t, domain.MRStateMerged, res.State) | |
| 146 | + | assert.Equal(t, head, r.branchHead("main")) | |
| 147 | + | assert.Equal(t, moved, r.branchHead("feat"), "the late commit is kept") | |
| 148 | + | evs := r.drain() | |
| 149 | + | require.Len(t, evs, 2) | |
| 150 | + | assert.Len(t, evs[1].(domain.PushEvent).Updates, 1, "only the target moved") | |
| 151 | + | } | |
| @@ -0,0 +1,151 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | ||||
| 3 | + | package app_test | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "context" | |||
| 7 | + | "testing" | |||
| 8 | + | ||||
| 9 | + | "github.com/stretchr/testify/assert" | |||
| 10 | + | "github.com/stretchr/testify/require" | |||
| 11 | + | ||||
| 12 | + | "bnema.dev/ermite/internal/adapters/gitcli" | |||
| 13 | + | "bnema.dev/ermite/internal/app" | |||
| 14 | + | "bnema.dev/ermite/internal/domain" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | // A merge can delete its source branch; the push event carries both refs. | |||
| 18 | + | func TestMergeDeletesTheSourceBranch(t *testing.T) { | |||
| 19 | + | for _, mode := range []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash} { | |||
| 20 | + | t.Run(string(mode), func(t *testing.T) { | |||
| 21 | + | r := newMRRig(t) | |||
| 22 | + | main := r.seed() | |||
| 23 | + | head := r.feature("feat") | |||
| 24 | + | mr := r.open("feat") | |||
| 25 | + | r.drain() | |||
| 26 | + | ||||
| 27 | + | res, err := r.mrs.Merge(r.ctx, mrM, r.repo, mr.Number, app.MergeParams{Mode: mode, HeadID: head, DeleteSource: true}) | |||
| 28 | + | require.NoError(t, err) | |||
| 29 | + | assert.True(t, res.SourceDeleted) | |||
| 30 | + | assert.Equal(t, domain.MRStateMerged, res.State) | |||
| 31 | + | _, err = r.git.BranchHead(r.ctx, r.repo, "feat") | |||
| 32 | + | assert.ErrorIs(t, err, domain.ErrNotFound, "the branch is gone") | |||
| 33 | + | ||||
| 34 | + | evs := r.drain() | |||
| 35 | + | require.Len(t, evs, 2) | |||
| 36 | + | assert.Equal(t, []domain.RefUpdate{ | |||
| 37 | + | {Old: main.String(), New: res.MergedCommitID.String(), Ref: "refs/heads/main"}, | |||
| 38 | + | {Old: head.String(), New: domain.ZeroOID, Ref: "refs/heads/feat"}, | |||
| 39 | + | }, evs[1].(domain.PushEvent).Updates) | |||
| 40 | + | ||||
| 41 | + | // The merged request still reads, with its commits and diff. | |||
| 42 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, false) | |||
| 43 | + | require.NoError(t, err) | |||
| 44 | + | assert.Len(t, d.Commits, 2) | |||
| 45 | + | assert.Len(t, d.Diff.Files, 2) | |||
| 46 | + | }) | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | t.Run("without the option the branch stays", func(t *testing.T) { | |||
| 50 | + | r := newMRRig(t) | |||
| 51 | + | r.seed() | |||
| 52 | + | head := r.feature("feat") | |||
| 53 | + | mr := r.open("feat") | |||
| 54 | + | res, err := r.mrs.Merge(r.ctx, mrM, r.repo, mr.Number, app.MergeParams{HeadID: head}) | |||
| 55 | + | require.NoError(t, err) | |||
| 56 | + | assert.False(t, res.SourceDeleted) | |||
| 57 | + | assert.Equal(t, head, r.branchHead("feat")) | |||
| 58 | + | }) | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | // Deleting the source is refused before the merge when the branch is still | |||
| 62 | + | // needed: nothing changes. | |||
| 63 | + | func TestMergeRefusesToDeleteANeededBranch(t *testing.T) { | |||
| 64 | + | t.Run("another open request uses it as its source", func(t *testing.T) { | |||
| 65 | + | r := newMRRig(t) | |||
| 66 | + | main := r.seed() | |||
| 67 | + | head := r.feature("feat") | |||
| 68 | + | r.sh("checkout", "-q", "-B", "release", "main") | |||
| 69 | + | r.push("release") | |||
| 70 | + | mr := r.open("feat") | |||
| 71 | + | _, err := r.mrs.Create(r.ctx, root, r.repo, app.CreateMergeRequestParams{Title: "to release", Source: "feat", Target: "release"}) | |||
| 72 | + | require.NoError(t, err) | |||
| 73 | + | ||||
| 74 | + | _, err = r.mrs.Merge(r.ctx, mrM, r.repo, mr.Number, app.MergeParams{HeadID: head, DeleteSource: true}) | |||
| 75 | + | require.ErrorIs(t, err, domain.ErrPreconditionFailed) | |||
| 76 | + | assert.Contains(t, err.Error(), "!2 still uses") | |||
| 77 | + | assert.Equal(t, main, r.branchHead("main"), "not merged") | |||
| 78 | + | assert.Equal(t, head, r.branchHead("feat")) | |||
| 79 | + | d, err := r.mrs.Get(r.ctx, reader, r.repo, mr.Number, false) | |||
| 80 | + | require.NoError(t, err) | |||
| 81 | + | assert.Equal(t, domain.MRStateOpen, d.MergeRequest.State) | |||
| 82 | + | }) | |||
| 83 | + | ||||
| 84 | + | t.Run("another open request targets it", func(t *testing.T) { | |||
| 85 | + | r := newMRRig(t) | |||
| 86 | + | r.seed() | |||
| 87 | + | head := r.feature("feat") | |||
| 88 | + | mr := r.open("feat") | |||
| 89 | + | r.sh("checkout", "-q", "-B", "sub", "feat") | |||
| 90 | + | r.commit(author, "d.txt", "dee\n", "add d") | |||
| 91 | + | r.push("sub") | |||
| 92 | + | _, err := r.mrs.Create(r.ctx, root, r.repo, app.CreateMergeRequestParams{Title: "stacked", Source: "sub", Target: "feat"}) | |||
| 93 | + | require.NoError(t, err) | |||
| 94 | + | _, err = r.mrs.Merge(r.ctx, mrM, r.repo, mr.Number, app.MergeParams{HeadID: head, DeleteSource: true}) | |||
| 95 | + | require.ErrorIs(t, err, domain.ErrPreconditionFailed) | |||
| 96 | + | assert.Equal(t, head, r.branchHead("feat")) | |||
| 97 | + | }) | |||
| 98 | + | ||||
| 99 | + | t.Run("the default branch", func(t *testing.T) { | |||
| 100 | + | r := newMRRig(t) | |||
| 101 | + | main := r.seed() | |||
| 102 | + | r.sh("checkout", "-q", "-B", "old", "main~1") | |||
| 103 | + | r.push("old") | |||
| 104 | + | old := r.branchHead("old") | |||
| 105 | + | mr, err := r.mrs.Create(r.ctx, root, r.repo, app.CreateMergeRequestParams{Title: "back", Source: "main", Target: "old"}) | |||
| 106 | + | require.NoError(t, err) | |||
| 107 | + | _, err = r.mrs.Merge(r.ctx, mrM, r.repo, mr.Number, app.MergeParams{HeadID: main, DeleteSource: true}) | |||
| 108 | + | require.ErrorIs(t, err, domain.ErrPreconditionFailed) | |||
| 109 | + | assert.Contains(t, err.Error(), "default branch") | |||
| 110 | + | assert.Equal(t, old, r.branchHead("old"), "not merged") | |||
| 111 | + | assert.Equal(t, main, r.branchHead("main")) | |||
| 112 | + | }) | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | // movingGit pushes to the source branch between the merge and the deletion. | |||
| 116 | + | type movingGit struct { | |||
| 117 | + | *gitcli.Git | |||
| 118 | + | move func() | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | func (g movingGit) DeleteBranch(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID) error { | |||
| 122 | + | g.move() | |||
| 123 | + | return g.Git.DeleteBranch(ctx, repo, branch, old) | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | // A branch that moves after the merge is kept: the merge stands. | |||
| 127 | + | func TestMergeKeepsASourceThatMovedAfterTheMerge(t *testing.T) { | |||
| 128 | + | r := newMRRig(t) | |||
| 129 | + | r.seed() | |||
| 130 | + | head := r.feature("feat") | |||
| 131 | + | mr := r.open("feat") | |||
| 132 | + | var moved domain.ObjectID | |||
| 133 | + | mrs := app.NewMergeRequests(app.MergeRequestsConfig{ | |||
| 134 | + | Meta: r.store, Store: r.store.MergeRequests(), Repos: r.git, Bus: r.bus, Clock: fixedClock{}, Maint: r.sched, | |||
| 135 | + | Git: movingGit{Git: r.git, move: func() { | |||
| 136 | + | moved = r.commit(author, "late.txt", "late\n", "pushed during the merge") | |||
| 137 | + | r.push("feat") | |||
| 138 | + | }}, | |||
| 139 | + | }) | |||
| 140 | + | r.drain() | |||
| 141 | + | ||||
| 142 | + | res, err := mrs.Merge(r.ctx, mrM, r.repo, mr.Number, app.MergeParams{HeadID: head, DeleteSource: true}) | |||
| 143 | + | require.NoError(t, err, "the merge stands") | |||
| 144 | + | assert.False(t, res.SourceDeleted) | |||
| 145 | + | assert.Equal(t, domain.MRStateMerged, res.State) | |||
| 146 | + | assert.Equal(t, head, r.branchHead("main")) | |||
| 147 | + | assert.Equal(t, moved, r.branchHead("feat"), "the late commit is kept") | |||
| 148 | + | evs := r.drain() | |||
| 149 | + | require.Len(t, evs, 2) | |||
| 150 | + | assert.Len(t, evs[1].(domain.PushEvent).Updates, 1, "only the target moved") | |||
| 151 | + | } | |||
M internal/ports/mocks/MergeGit.go +69 -0
| @@ -191,6 +191,75 @@ func (_c *MergeGit_CommitTree_Call) RunAndReturn(run func(ctx context.Context, r | |||
| 191 | 191 | return _c | |
| 192 | 192 | } | |
| 193 | 193 | ||
| 194 | + | // DeleteBranch provides a mock function for the type MergeGit | |
| 195 | + | func (_mock *MergeGit) DeleteBranch(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID) error { | |
| 196 | + | ret := _mock.Called(ctx, repo, branch, old) | |
| 197 | + | ||
| 198 | + | if len(ret) == 0 { | |
| 199 | + | panic("no return value specified for DeleteBranch") | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | var r0 error | |
| 203 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string, domain.ObjectID) error); ok { | |
| 204 | + | r0 = returnFunc(ctx, repo, branch, old) | |
| 205 | + | } else { | |
| 206 | + | r0 = ret.Error(0) | |
| 207 | + | } | |
| 208 | + | return r0 | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | // MergeGit_DeleteBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteBranch' | |
| 212 | + | type MergeGit_DeleteBranch_Call struct { | |
| 213 | + | *mock.Call | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | // DeleteBranch is a helper method to define mock.On call | |
| 217 | + | // - ctx context.Context | |
| 218 | + | // - repo domain.RepoName | |
| 219 | + | // - branch string | |
| 220 | + | // - old domain.ObjectID | |
| 221 | + | func (_e *MergeGit_Expecter) DeleteBranch(ctx any, repo any, branch any, old any) *MergeGit_DeleteBranch_Call { | |
| 222 | + | return &MergeGit_DeleteBranch_Call{Call: _e.mock.On("DeleteBranch", ctx, repo, branch, old)} | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | func (_c *MergeGit_DeleteBranch_Call) Run(run func(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID)) *MergeGit_DeleteBranch_Call { | |
| 226 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 227 | + | var arg0 context.Context | |
| 228 | + | if args[0] != nil { | |
| 229 | + | arg0 = args[0].(context.Context) | |
| 230 | + | } | |
| 231 | + | var arg1 domain.RepoName | |
| 232 | + | if args[1] != nil { | |
| 233 | + | arg1 = args[1].(domain.RepoName) | |
| 234 | + | } | |
| 235 | + | var arg2 string | |
| 236 | + | if args[2] != nil { | |
| 237 | + | arg2 = args[2].(string) | |
| 238 | + | } | |
| 239 | + | var arg3 domain.ObjectID | |
| 240 | + | if args[3] != nil { | |
| 241 | + | arg3 = args[3].(domain.ObjectID) | |
| 242 | + | } | |
| 243 | + | run( | |
| 244 | + | arg0, | |
| 245 | + | arg1, | |
| 246 | + | arg2, | |
| 247 | + | arg3, | |
| 248 | + | ) | |
| 249 | + | }) | |
| 250 | + | return _c | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | func (_c *MergeGit_DeleteBranch_Call) Return(err error) *MergeGit_DeleteBranch_Call { | |
| 254 | + | _c.Call.Return(err) | |
| 255 | + | return _c | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | func (_c *MergeGit_DeleteBranch_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID) error) *MergeGit_DeleteBranch_Call { | |
| 259 | + | _c.Call.Return(run) | |
| 260 | + | return _c | |
| 261 | + | } | |
| 262 | + | ||
| 194 | 263 | // DiffCommits provides a mock function for the type MergeGit | |
| 195 | 264 | func (_mock *MergeGit) DiffCommits(ctx context.Context, repo domain.RepoName, base domain.ObjectID, head domain.ObjectID, limits domain.DiffLimits) (domain.DiffStats, domain.Diff, error) { | |
| 196 | 265 | ret := _mock.Called(ctx, repo, base, head, limits) | |
| @@ -191,6 +191,75 @@ func (_c *MergeGit_CommitTree_Call) RunAndReturn(run func(ctx context.Context, r | |||||
| 191 | return _c | 191 | return _c | ||
| 192 | } | 192 | } | ||
| 193 | 193 | ||||
| 194 | + | // DeleteBranch provides a mock function for the type MergeGit | |||
| 195 | + | func (_mock *MergeGit) DeleteBranch(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID) error { | |||
| 196 | + | ret := _mock.Called(ctx, repo, branch, old) | |||
| 197 | + | ||||
| 198 | + | if len(ret) == 0 { | |||
| 199 | + | panic("no return value specified for DeleteBranch") | |||
| 200 | + | } | |||
| 201 | + | ||||
| 202 | + | var r0 error | |||
| 203 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string, domain.ObjectID) error); ok { | |||
| 204 | + | r0 = returnFunc(ctx, repo, branch, old) | |||
| 205 | + | } else { | |||
| 206 | + | r0 = ret.Error(0) | |||
| 207 | + | } | |||
| 208 | + | return r0 | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | // MergeGit_DeleteBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteBranch' | |||
| 212 | + | type MergeGit_DeleteBranch_Call struct { | |||
| 213 | + | *mock.Call | |||
| 214 | + | } | |||
| 215 | + | ||||
| 216 | + | // DeleteBranch is a helper method to define mock.On call | |||
| 217 | + | // - ctx context.Context | |||
| 218 | + | // - repo domain.RepoName | |||
| 219 | + | // - branch string | |||
| 220 | + | // - old domain.ObjectID | |||
| 221 | + | func (_e *MergeGit_Expecter) DeleteBranch(ctx any, repo any, branch any, old any) *MergeGit_DeleteBranch_Call { | |||
| 222 | + | return &MergeGit_DeleteBranch_Call{Call: _e.mock.On("DeleteBranch", ctx, repo, branch, old)} | |||
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | func (_c *MergeGit_DeleteBranch_Call) Run(run func(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID)) *MergeGit_DeleteBranch_Call { | |||
| 226 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 227 | + | var arg0 context.Context | |||
| 228 | + | if args[0] != nil { | |||
| 229 | + | arg0 = args[0].(context.Context) | |||
| 230 | + | } | |||
| 231 | + | var arg1 domain.RepoName | |||
| 232 | + | if args[1] != nil { | |||
| 233 | + | arg1 = args[1].(domain.RepoName) | |||
| 234 | + | } | |||
| 235 | + | var arg2 string | |||
| 236 | + | if args[2] != nil { | |||
| 237 | + | arg2 = args[2].(string) | |||
| 238 | + | } | |||
| 239 | + | var arg3 domain.ObjectID | |||
| 240 | + | if args[3] != nil { | |||
| 241 | + | arg3 = args[3].(domain.ObjectID) | |||
| 242 | + | } | |||
| 243 | + | run( | |||
| 244 | + | arg0, | |||
| 245 | + | arg1, | |||
| 246 | + | arg2, | |||
| 247 | + | arg3, | |||
| 248 | + | ) | |||
| 249 | + | }) | |||
| 250 | + | return _c | |||
| 251 | + | } | |||
| 252 | + | ||||
| 253 | + | func (_c *MergeGit_DeleteBranch_Call) Return(err error) *MergeGit_DeleteBranch_Call { | |||
| 254 | + | _c.Call.Return(err) | |||
| 255 | + | return _c | |||
| 256 | + | } | |||
| 257 | + | ||||
| 258 | + | func (_c *MergeGit_DeleteBranch_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID) error) *MergeGit_DeleteBranch_Call { | |||
| 259 | + | _c.Call.Return(run) | |||
| 260 | + | return _c | |||
| 261 | + | } | |||
| 262 | + | ||||
| 194 | // DiffCommits provides a mock function for the type MergeGit | 263 | // DiffCommits provides a mock function for the type MergeGit | ||
| 195 | func (_mock *MergeGit) DiffCommits(ctx context.Context, repo domain.RepoName, base domain.ObjectID, head domain.ObjectID, limits domain.DiffLimits) (domain.DiffStats, domain.Diff, error) { | 264 | func (_mock *MergeGit) DiffCommits(ctx context.Context, repo domain.RepoName, base domain.ObjectID, head domain.ObjectID, limits domain.DiffLimits) (domain.DiffStats, domain.Diff, error) { | ||
| 196 | ret := _mock.Called(ctx, repo, base, head, limits) | 265 | ret := _mock.Called(ctx, repo, base, head, limits) | ||
M internal/ports/ports.go +3 -0
| @@ -277,6 +277,9 @@ type MergeGit interface { | |||
| 277 | 277 | // UpdateBranch moves refs/heads/<branch> from old to new. It returns | |
| 278 | 278 | // domain.ErrConflict when the branch no longer points to old. | |
| 279 | 279 | UpdateBranch(ctx context.Context, repo domain.RepoName, branch string, old, new domain.ObjectID) error | |
| 280 | + | // DeleteBranch deletes refs/heads/<branch> if it still points to old. It | |
| 281 | + | // returns domain.ErrConflict when the branch moved or is gone. | |
| 282 | + | DeleteBranch(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID) error | |
| 280 | 283 | } | |
| 281 | 284 | ||
| 282 | 285 | // CommitSigner signs the commits ermite creates (ADR 0022). The private key | |
| @@ -277,6 +277,9 @@ type MergeGit interface { | |||||
| 277 | // UpdateBranch moves refs/heads/<branch> from old to new. It returns | 277 | // UpdateBranch moves refs/heads/<branch> from old to new. It returns | ||
| 278 | // domain.ErrConflict when the branch no longer points to old. | 278 | // domain.ErrConflict when the branch no longer points to old. | ||
| 279 | UpdateBranch(ctx context.Context, repo domain.RepoName, branch string, old, new domain.ObjectID) error | 279 | UpdateBranch(ctx context.Context, repo domain.RepoName, branch string, old, new domain.ObjectID) error | ||
| 280 | + | // DeleteBranch deletes refs/heads/<branch> if it still points to old. It | |||
| 281 | + | // returns domain.ErrConflict when the branch moved or is gone. | |||
| 282 | + | DeleteBranch(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID) error | |||
| 280 | } | 283 | } | ||
| 281 | 284 | ||||
| 282 | // CommitSigner signs the commits ermite creates (ADR 0022). The private key | 285 | // CommitSigner signs the commits ermite creates (ADR 0022). The private key | ||
M skills/ermite/CLI.md +1 -1
| @@ -55,7 +55,7 @@ A token can never grant more than the token that creates it. `--repo` (repeatabl | |||
| 55 | 55 | | `mr show <repo> <n> [--diff] [--since-last-read] [--mark-read] [--json]` | `mr:write` for `--mark-read` | commits, changed files and comments; `--diff` adds hunks with line numbers | | |
| 56 | 56 | | `mr comment <repo> <n> -m TEXT\|- [--path P --line N --side old\|new]` | `mr:write` | the line must be in the current diff; a deleted file uses its old path | | |
| 57 | 57 | | `mr resolve <repo> <n> <comment-id> [--undo]` | `mr:write` | doing it twice is not an error | | |
| 58 | - | | `mr merge <repo> <n> --head <full-id> [--squash]` | `mr:merge` | refused if the branch moved or the target is not an ancestor of the head | | |
| 58 | + | | `mr merge <repo> <n> --head <full-id> [--squash] [--delete-branch]` | `mr:merge` | refused if the branch moved or the target is not an ancestor of the head; `--delete-branch` deletes the source after the merge (refused for the default branch or a branch another open request uses; JSON adds `source_branch_deleted`) | | |
| 59 | 59 | | `mr close <repo> <n>` | `mr:write` | a closed merge request cannot be reopened | | |
| 60 | 60 | ||
| 61 | 61 | Merge request JSON: `repo`, `number`, `title`, `description`, `source_branch`, `target_branch`, `state`, `author` (token label), `created_at`, `updated_at`, `head_id`, `merged_commit_id`, `merge_mode`. `mr show --json` adds `commits`, `diff_base_id`, `stats`, `files` (with `hunks` under `--diff`: `kind`, `old_number`, `new_number`, `text`), `comments`, `last_read_id`, `last_read_lost`. | |
| @@ -55,7 +55,7 @@ A token can never grant more than the token that creates it. `--repo` (repeatabl | |||||
| 55 | | `mr show <repo> <n> [--diff] [--since-last-read] [--mark-read] [--json]` | `mr:write` for `--mark-read` | commits, changed files and comments; `--diff` adds hunks with line numbers | | 55 | | `mr show <repo> <n> [--diff] [--since-last-read] [--mark-read] [--json]` | `mr:write` for `--mark-read` | commits, changed files and comments; `--diff` adds hunks with line numbers | | ||
| 56 | | `mr comment <repo> <n> -m TEXT\|- [--path P --line N --side old\|new]` | `mr:write` | the line must be in the current diff; a deleted file uses its old path | | 56 | | `mr comment <repo> <n> -m TEXT\|- [--path P --line N --side old\|new]` | `mr:write` | the line must be in the current diff; a deleted file uses its old path | | ||
| 57 | | `mr resolve <repo> <n> <comment-id> [--undo]` | `mr:write` | doing it twice is not an error | | 57 | | `mr resolve <repo> <n> <comment-id> [--undo]` | `mr:write` | doing it twice is not an error | | ||
| 58 | - | | `mr merge <repo> <n> --head <full-id> [--squash]` | `mr:merge` | refused if the branch moved or the target is not an ancestor of the head | | 58 | + | | `mr merge <repo> <n> --head <full-id> [--squash] [--delete-branch]` | `mr:merge` | refused if the branch moved or the target is not an ancestor of the head; `--delete-branch` deletes the source after the merge (refused for the default branch or a branch another open request uses; JSON adds `source_branch_deleted`) | |
| 59 | | `mr close <repo> <n>` | `mr:write` | a closed merge request cannot be reopened | | 59 | | `mr close <repo> <n>` | `mr:write` | a closed merge request cannot be reopened | | ||
| 60 | 60 | ||||
| 61 | Merge request JSON: `repo`, `number`, `title`, `description`, `source_branch`, `target_branch`, `state`, `author` (token label), `created_at`, `updated_at`, `head_id`, `merged_commit_id`, `merge_mode`. `mr show --json` adds `commits`, `diff_base_id`, `stats`, `files` (with `hunks` under `--diff`: `kind`, `old_number`, `new_number`, `text`), `comments`, `last_read_id`, `last_read_lost`. | 61 | Merge request JSON: `repo`, `number`, `title`, `description`, `source_branch`, `target_branch`, `state`, `author` (token label), `created_at`, `updated_at`, `head_id`, `merged_commit_id`, `merge_mode`. `mr show --json` adds `commits`, `diff_base_id`, `stats`, `files` (with `hunks` under `--diff`: `kind`, `old_number`, `new_number`, `text`), `comments`, `last_read_id`, `last_read_lost`. | ||
M skills/ermite/SKILL.md +1 -1
| @@ -34,7 +34,7 @@ git push https://git.example.com/<repo>.git main # password = token, | |||
| 34 | 34 | 1. List: `ermite mr list <repo> --json`. | |
| 35 | 35 | 2. Read: `ermite mr show <repo> <n> --diff --json`. Keep `head_id`; take line numbers from the hunks. | |
| 36 | 36 | 3. Comment on a line: `ermite mr comment <repo> <n> -m "text" --path <file> --line <n> --side new`. Without `--path` the comment is general. The line must appear in the diff. | |
| 37 | - | 4. Merge only when asked: `ermite mr merge <repo> <n> --head <head_id>`, with `--squash` for one commit signed by the server. | |
| 37 | + | 4. Merge only when asked: `ermite mr merge <repo> <n> --head <head_id>`, with `--squash` for one commit signed by the server and `--delete-branch` to delete the source branch afterwards. | |
| 38 | 38 | ||
| 39 | 39 | The merge is refused when the branch moved since `head_id` (review again) or when the target is not an ancestor of the head (rebase and push again). The repository may allow only one merge mode. | |
| 40 | 40 | ||
| @@ -34,7 +34,7 @@ git push https://git.example.com/<repo>.git main # password = token, | |||||
| 34 | 1. List: `ermite mr list <repo> --json`. | 34 | 1. List: `ermite mr list <repo> --json`. | ||
| 35 | 2. Read: `ermite mr show <repo> <n> --diff --json`. Keep `head_id`; take line numbers from the hunks. | 35 | 2. Read: `ermite mr show <repo> <n> --diff --json`. Keep `head_id`; take line numbers from the hunks. | ||
| 36 | 3. Comment on a line: `ermite mr comment <repo> <n> -m "text" --path <file> --line <n> --side new`. Without `--path` the comment is general. The line must appear in the diff. | 36 | 3. Comment on a line: `ermite mr comment <repo> <n> -m "text" --path <file> --line <n> --side new`. Without `--path` the comment is general. The line must appear in the diff. | ||
| 37 | - | 4. Merge only when asked: `ermite mr merge <repo> <n> --head <head_id>`, with `--squash` for one commit signed by the server. | 37 | + | 4. Merge only when asked: `ermite mr merge <repo> <n> --head <head_id>`, with `--squash` for one commit signed by the server and `--delete-branch` to delete the source branch afterwards. |
| 38 | 38 | ||||
| 39 | The merge is refused when the branch moved since `head_id` (review again) or when the target is not an ancestor of the head (rebase and push again). The repository may allow only one merge mode. | 39 | The merge is refused when the branch moved since `head_id` (review again) or when the target is not an ancestor of the head (rebase and push again). The repository may allow only one merge mode. | ||
| 40 | 40 | ||||