Skip to content

Commit 90939a7

Browse files
authored
feat: new SCM: gerrit (#565)
1 parent 749a4e4 commit 90939a7

File tree

16 files changed

+1139
-11
lines changed

16 files changed

+1139
-11
lines changed

cmd/other.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ func getToken(flag *flag.FlagSet) (string, error) {
3737
token = ght
3838
} else if ght := os.Getenv("BITBUCKET_CLOUD_WORKSPACE_TOKEN"); ght != "" {
3939
token = ght
40+
} else if ght := os.Getenv("GERRIT_TOKEN"); ght != "" {
41+
token = ght
4042
}
4143
}
4244

4345
if token == "" {
44-
return "", errors.New("either the --token flag or the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN/BITBUCKET_CLOUD_APP_PASSWORD/BITBUCKET_CLOUD_WORKSPACE_TOKEN environment variable has to be set")
46+
return "", errors.New("either the --token flag or the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN/BITBUCKET_CLOUD_APP_PASSWORD/BITBUCKET_CLOUD_WORKSPACE_TOKEN/GERRIT_TOKEN environment variable has to be set")
4547
}
4648

4749
return token, nil

cmd/platform.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/lindell/multi-gitter/internal/multigitter"
1111
"github.com/lindell/multi-gitter/internal/scm/bitbucketcloud"
1212
"github.com/lindell/multi-gitter/internal/scm/bitbucketserver"
13+
"github.com/lindell/multi-gitter/internal/scm/gerrit"
1314
"github.com/lindell/multi-gitter/internal/scm/gitea"
1415
"github.com/lindell/multi-gitter/internal/scm/github"
1516
"github.com/lindell/multi-gitter/internal/scm/gitlab"
@@ -22,10 +23,10 @@ import (
2223
func configurePlatform(cmd *cobra.Command) {
2324
flags := cmd.Flags()
2425

25-
flags.StringP("base-url", "g", "", "Base URL of the target platform, needs to be changed for GitHub enterprise, a self-hosted GitLab instance, Gitea or BitBucket.")
26+
flags.StringP("base-url", "g", "", "Base URL of the target platform, needs to be changed for GitHub enterprise, a self-hosted GitLab instance, Gitea or BitBucket, Gerrit.")
2627
flags.BoolP("insecure", "", false, "Insecure controls whether a client verifies the server certificate chain and host name. Used only for Bitbucket server.")
2728
flags.StringP("username", "u", "", "The Bitbucket server username.")
28-
flags.StringP("token", "T", "", "The personal access token for the targeting platform. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN/BITBUCKET_CLOUD_APP_PASSWORD/BITBUCKET_CLOUD_WORKSPACE_TOKEN environment variable.")
29+
flags.StringP("token", "T", "", "The personal access token for the targeting platform. Can also be set using the GITHUB_TOKEN/GITLAB_TOKEN/GITEA_TOKEN/BITBUCKET_SERVER_TOKEN/BITBUCKET_CLOUD_APP_PASSWORD/BITBUCKET_CLOUD_WORKSPACE_TOKEN/GERRIT_TOKEN environment variable.")
2930
flags.StringP("auth-type", "", "app-password", `The authentication type. Used only for Bitbucket cloud. Available values: app-password, workspace-token.`)
3031
_ = cmd.RegisterFlagCompletionFunc("auth-type", func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
3132
return []string{"app-password", "workspace-token"}, cobra.ShellCompDirectiveNoFileComp
@@ -43,7 +44,7 @@ func configurePlatform(cmd *cobra.Command) {
4344
flags.BoolP("ssh-auth", "", false, `Use SSH cloning URL instead of HTTPS + token. This requires that a setup with ssh keys that have access to all repos and that the server is already in known_hosts.`)
4445
flags.BoolP("skip-forks", "", false, `Skip repositories which are forks.`)
4546

46-
flags.StringP("platform", "p", "github", "The platform that is used. Available values: github, gitlab, gitea, bitbucket_server, bitbucket_cloud. Note: bitbucket_cloud is in Beta")
47+
flags.StringP("platform", "p", "github", "The platform that is used. Available values: github, gitlab, gitea, bitbucket_server, bitbucket_cloud, gerrit. Note: bitbucket_cloud is in Beta")
4748
_ = cmd.RegisterFlagCompletionFunc("platform", func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
4849
return []string{"github", "gitlab", "gitea", "bitbucket_server", "bitbucket_cloud"}, cobra.ShellCompDirectiveDefault
4950
})
@@ -123,6 +124,8 @@ func getVersionController(flag *flag.FlagSet, verifyFlags bool, readOnly bool) (
123124
return createBitbucketServerClient(flag, verifyFlags)
124125
case "bitbucket_cloud":
125126
return createBitbucketCloudClient(flag, verifyFlags)
127+
case "gerrit":
128+
return createGerritClient(flag, verifyFlags)
126129
default:
127130
return nil, fmt.Errorf("unknown platform: %s", platform)
128131
}
@@ -392,6 +395,28 @@ func createBitbucketServerClient(flag *flag.FlagSet, verifyFlags bool) (multigit
392395
return vc, nil
393396
}
394397

398+
func createGerritClient(flag *flag.FlagSet, _ bool) (multigitter.VersionController, error) {
399+
username, _ := flag.GetString("username")
400+
if username == "" {
401+
return nil, errors.New("no username set")
402+
}
403+
404+
gerritBaseURL, _ := flag.GetString("base-url")
405+
if gerritBaseURL == "" {
406+
return nil, errors.New("no base-url set")
407+
}
408+
409+
repoSearch, _ := flag.GetString("repo-search")
410+
411+
token, err := getToken(flag)
412+
if err != nil {
413+
return nil, err
414+
}
415+
416+
vc, err := gerrit.New(username, token, gerritBaseURL, repoSearch)
417+
return vc, err
418+
}
419+
395420
// versionControllerCompletion is a helper function to allow for easier implementation of Cobra autocompletions that depend on a version controller
396421
func versionControllerCompletion(cmd *cobra.Command, flagName string, fn func(vc multigitter.VersionController, toComplete string) ([]string, error)) {
397422
_ = cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.23.3
44

55
require (
66
code.gitea.io/sdk/gitea v0.21.0
7+
github.com/andygrunwald/go-gerrit v1.1.0
78
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
89
github.com/gfleury/go-bitbucket-v1 v0.0.0-20240917142304-df385efaac68
910
github.com/go-git/go-git/v5 v5.16.2

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
1010
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
1111
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
1212
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
13+
github.com/andygrunwald/go-gerrit v1.0.0 h1:TrRGbso70QjJcXPC4kkLiKQrAfCBoBV+cBs7NrJxeno=
14+
github.com/andygrunwald/go-gerrit v1.0.0/go.mod h1:SeP12EkHZxEVjuJ2HZET304NBtHGG2X6w2Gzd0QXAZw=
15+
github.com/andygrunwald/go-gerrit v1.1.0 h1:+svCkLj2kkrClYWZhynSCOizAoCjw8uZJhRs+x3nbAs=
16+
github.com/andygrunwald/go-gerrit v1.1.0/go.mod h1:SeP12EkHZxEVjuJ2HZET304NBtHGG2X6w2Gzd0QXAZw=
1317
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
1418
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
1519
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=

internal/git/cmdgit/git.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,16 @@ func (g *Git) BranchExist(remoteName, branchName string) (bool, error) {
140140
}
141141

142142
// Push the committed changes to the remote
143-
func (g *Git) Push(ctx context.Context, remoteName string, force bool) error {
143+
func (g *Git) Push(ctx context.Context, remoteName, remoteReference string, force bool) error {
144144
args := []string{"push", "--no-verify", remoteName}
145145
if force {
146146
args = append(args, "--force")
147147
}
148-
args = append(args, "HEAD")
148+
refSpec := "HEAD"
149+
if remoteReference != "" {
150+
refSpec = refSpec + ":" + remoteReference
151+
}
152+
args = append(args, refSpec)
149153

150154
cmd := exec.CommandContext(ctx, "git", args...)
151155
_, err := g.run(cmd)

internal/git/gogit/git.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,23 @@ func (g *Git) BranchExist(remoteName, branchName string) (bool, error) {
205205
}
206206

207207
// Push the committed changes to the remote
208-
func (g *Git) Push(ctx context.Context, remoteName string, force bool) error {
208+
func (g *Git) Push(ctx context.Context, remoteName, remoteReference string, force bool) error {
209+
var refSpecs []config.RefSpec
210+
211+
if remoteReference != "" {
212+
// go-git doesn't support refSpec like HEAD:<name>, so first we need to resolve SHA1 commit related to HEAD
213+
head, err := g.repo.Head()
214+
if err != nil {
215+
return errors.Wrap(err, "Unable to get HEAD")
216+
}
217+
refSpecs = []config.RefSpec{
218+
config.RefSpec(head.Hash().String() + ":" + remoteReference),
219+
}
220+
}
209221
return g.repo.PushContext(ctx, &git.PushOptions{
210222
RemoteName: remoteName,
211223
Force: force,
224+
RefSpecs: refSpecs,
212225
})
213226
}
214227

internal/multigitter/run.go

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ type VersionController interface {
3535
ForkRepository(ctx context.Context, repo scm.Repository, newOwner string) (scm.Repository, error)
3636
}
3737

38+
type VersionControllerEnhanceCommit interface {
39+
EnhanceCommit(ctx context.Context, repo scm.Repository, branchName string, commitMessage string) (string, error)
40+
}
41+
42+
type VersionControllerFeatureBranchExist interface {
43+
FeatureBranchExist(ctx context.Context, repo scm.Repository, branchName string) (bool, error)
44+
}
45+
46+
type VersionControllerRemoteReference interface {
47+
RemoteReference(baseBranch string, featureBranch string, skipPullRequest bool, pushOnly bool) string
48+
}
49+
3850
// Runner contains fields to be able to do the run
3951
type Runner struct {
4052
VersionController VersionController
@@ -285,7 +297,8 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
285297
return nil, errNoChange
286298
}
287299

288-
err = sourceController.Commit(r.CommitAuthor, r.CommitMessage)
300+
commitMessage := r.enhanceCommitMessage(ctx, repo)
301+
err = sourceController.Commit(r.CommitAuthor, commitMessage)
289302
if err != nil {
290303
return nil, err
291304
}
@@ -324,7 +337,7 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
324337
// Determine if a branch already exists and (depending on the conflict strategy) skip making changes
325338
featureBranchExist := false
326339
if !r.SkipPullRequest && !r.PushOnly {
327-
featureBranchExist, err = sourceController.BranchExist(remoteName, r.FeatureBranch)
340+
featureBranchExist, err = r.featureBranchExist(ctx, repo, remoteName, sourceController)
328341
if err != nil {
329342
return nil, errors.Wrap(err, "could not verify if branch already exists")
330343
} else if featureBranchExist && r.ConflictStrategy == ConflictStrategySkip {
@@ -341,7 +354,8 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
341354
forcePush := featureBranchExist && r.ConflictStrategy == ConflictStrategyReplace
342355

343356
if !r.APIPush {
344-
err = sourceController.Push(ctx, remoteName, forcePush)
357+
remoteReference := r.remoteReference(baseBranch, r.FeatureBranch)
358+
err = sourceController.Push(ctx, remoteName, remoteReference, forcePush)
345359
if err != nil {
346360
return nil, errors.Wrap(err, "could not push changes")
347361
}
@@ -372,6 +386,31 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
372386
return r.ensurePullRequestExists(ctx, log, repo, prRepo, baseBranch, featureBranchExist)
373387
}
374388

389+
func (r *Runner) enhanceCommitMessage(ctx context.Context, repo scm.Repository) string {
390+
vcs, ok := r.VersionController.(VersionControllerEnhanceCommit)
391+
if ok {
392+
commitMessage, _ := vcs.EnhanceCommit(ctx, repo, r.FeatureBranch, r.CommitMessage)
393+
return commitMessage
394+
}
395+
return r.CommitMessage
396+
}
397+
398+
func (r *Runner) featureBranchExist(ctx context.Context, repo scm.Repository, remoteName string, sourceController Git) (bool, error) {
399+
vcs, ok := r.VersionController.(VersionControllerFeatureBranchExist)
400+
if ok {
401+
return vcs.FeatureBranchExist(ctx, repo, r.FeatureBranch)
402+
}
403+
return sourceController.BranchExist(remoteName, r.FeatureBranch)
404+
}
405+
406+
func (r *Runner) remoteReference(baseBranch string, featureBranch string) string {
407+
vcs, ok := r.VersionController.(VersionControllerRemoteReference)
408+
if ok {
409+
return vcs.RemoteReference(baseBranch, featureBranch, r.SkipPullRequest, r.PushOnly)
410+
}
411+
return ""
412+
}
413+
375414
func (r *Runner) ensurePullRequestExists(ctx context.Context, log log.FieldLogger, repo scm.Repository, prRepo scm.Repository, baseBranch string, featureBranchExist bool) (scm.PullRequest, error) {
376415
if r.SkipPullRequest {
377416
return nil, nil

internal/multigitter/shared.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type Git interface {
3232
Changes() (bool, error)
3333
Commit(commitAuthor *git.CommitAuthor, commitMessage string) error
3434
BranchExist(remoteName, branchName string) (bool, error)
35-
Push(ctx context.Context, remoteName string, force bool) error
35+
Push(ctx context.Context, remoteName, remoteReference string, force bool) error
3636
AddRemote(name, url string) error
3737
}
3838

internal/scm/gerrit/change.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package gerrit
2+
3+
import (
4+
"fmt"
5+
"github.com/lindell/multi-gitter/internal/scm"
6+
7+
gogerrit "github.com/andygrunwald/go-gerrit"
8+
)
9+
10+
type change struct {
11+
id string
12+
project string
13+
branch string
14+
number int
15+
changeID string
16+
status scm.PullRequestStatus
17+
webURL string
18+
}
19+
20+
func (r change) String() string {
21+
return fmt.Sprintf("%d: %s", r.number, r.project)
22+
}
23+
24+
func (r change) Status() scm.PullRequestStatus {
25+
return r.status
26+
}
27+
28+
func (r change) URL() string {
29+
return r.webURL
30+
}
31+
32+
func convertChange(changeInfo gogerrit.ChangeInfo, baseURL string) scm.PullRequest {
33+
status := scm.PullRequestStatusUnknown
34+
35+
if changeInfo.Submittable {
36+
status = scm.PullRequestStatusSuccess
37+
} else {
38+
switch changeInfo.Status {
39+
case "NEW":
40+
status = scm.PullRequestStatusPending
41+
case "MERGED":
42+
status = scm.PullRequestStatusMerged
43+
case "ABANDONED":
44+
status = scm.PullRequestStatusClosed
45+
}
46+
}
47+
48+
return change{
49+
id: changeInfo.ID,
50+
project: changeInfo.Project,
51+
branch: changeInfo.Branch,
52+
number: changeInfo.Number,
53+
changeID: changeInfo.ChangeID,
54+
status: status,
55+
webURL: fmt.Sprintf("%s/c/%s/+/%d", baseURL, changeInfo.Project, changeInfo.Number),
56+
}
57+
}

0 commit comments

Comments
 (0)