bridge/gitlab: support self-hosted GitLab instance

amine created

Change summary

bridge/core/bridge.go              |  1 +
bridge/github/config.go            |  4 ++++
bridge/gitlab/config.go            | 18 +++++++++++++++---
bridge/gitlab/export.go            | 14 +++++++++++++-
bridge/gitlab/export_test.go       | 20 +++++++++++++++-----
bridge/gitlab/gitlab.go            | 17 +++++++++++++----
bridge/gitlab/import.go            |  6 +++++-
bridge/gitlab/import_test.go       |  3 ++-
bridge/launchpad/config.go         |  3 +++
commands/bridge_configure.go       |  1 +
doc/man/git-bug-bridge-configure.1 |  4 ++++
doc/md/git-bug_bridge_configure.md |  1 +
misc/bash_completion/git-bug       |  4 ++++
misc/powershell_completion/git-bug |  2 ++
misc/zsh_completion/git-bug        |  1 +
15 files changed, 84 insertions(+), 15 deletions(-)

Detailed changes

bridge/core/bridge.go 🔗

@@ -35,6 +35,7 @@ type BridgeParams struct {
 	Owner      string
 	Project    string
 	URL        string
+	BaseURL    string
 	CredPrefix string
 	TokenRaw   string
 }

bridge/github/config.go 🔗

@@ -44,6 +44,10 @@ var (
 )
 
 func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (core.Configuration, error) {
+	if params.BaseURL != "" {
+		fmt.Println("warning: --base-url is ineffective for a Github bridge")
+	}
+
 	conf := make(core.Configuration)
 	var err error
 

bridge/gitlab/config.go 🔗

@@ -42,6 +42,10 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 		return nil, fmt.Errorf("you must provide a project URL to configure this bridge with a token")
 	}
 
+	if params.URL == "" {
+		params.URL = defaultBaseURL
+	}
+
 	var url string
 
 	// get project url
@@ -56,6 +60,10 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 		}
 	}
 
+	if !strings.HasPrefix(url, params.BaseURL) {
+		return nil, fmt.Errorf("base URL (%s) doesn't match the project URL (%s)", params.BaseURL, url)
+	}
+
 	user, err := repo.GetUserIdentity()
 	if err != nil {
 		return nil, err
@@ -87,13 +95,14 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 	}
 
 	// validate project url and get its ID
-	id, err := validateProjectURL(url, token)
+	id, err := validateProjectURL(params.BaseURL, url, token)
 	if err != nil {
 		return nil, errors.Wrap(err, "project validation")
 	}
 
 	conf[core.ConfigKeyTarget] = target
 	conf[keyProjectID] = strconv.Itoa(id)
+	conf[keyGitlabBaseUrl] = params.BaseURL
 
 	err = g.ValidateConfig(conf)
 	if err != nil {
@@ -302,13 +311,16 @@ func getValidGitlabRemoteURLs(remotes map[string]string) []string {
 	return urls
 }
 
-func validateProjectURL(url string, token *auth.Token) (int, error) {
+func validateProjectURL(baseURL, url string, token *auth.Token) (int, error) {
 	projectPath, err := getProjectPath(url)
 	if err != nil {
 		return 0, err
 	}
 
-	client := buildClient(token)
+	client, err := buildClient(baseURL, token)
+	if err != nil {
+		return 0, err
+	}
 
 	project, _, err := client.Projects.GetProject(projectPath, &gitlab.GetProjectOptions{})
 	if err != nil {

bridge/gitlab/export.go 🔗

@@ -62,7 +62,11 @@ func (ge *gitlabExporter) cacheAllClient(repo repository.RepoConfig) error {
 
 	for _, cred := range creds {
 		if _, ok := ge.identityClient[cred.UserId()]; !ok {
-			client := buildClient(creds[0].(*auth.Token))
+			client, err := buildClient(ge.conf[keyGitlabBaseUrl], creds[0].(*auth.Token))
+			if err != nil {
+				return err
+			}
+
 			ge.identityClient[cred.UserId()] = client
 		}
 	}
@@ -133,6 +137,7 @@ func (ge *gitlabExporter) exportBug(ctx context.Context, b *cache.BugCache, sinc
 	var err error
 	var bugGitlabID int
 	var bugGitlabIDString string
+	var GitlabBaseUrl string
 	var bugCreationId string
 
 	// Special case:
@@ -153,6 +158,12 @@ func (ge *gitlabExporter) exportBug(ctx context.Context, b *cache.BugCache, sinc
 	// get gitlab bug ID
 	gitlabID, ok := snapshot.GetCreateMetadata(metaKeyGitlabId)
 	if ok {
+		gitlabBaseUrl, ok := snapshot.GetCreateMetadata(metaKeyGitlabBaseUrl)
+		if ok && gitlabBaseUrl != ge.conf[gitlabBaseUrl] {
+			out <- core.NewExportNothing(b.Id(), "skipping issue imported from another Gitlab instance")
+			return
+		}
+
 		projectID, ok := snapshot.GetCreateMetadata(metaKeyGitlabProject)
 		if !ok {
 			err := fmt.Errorf("expected to find gitlab project id")
@@ -199,6 +210,7 @@ func (ge *gitlabExporter) exportBug(ctx context.Context, b *cache.BugCache, sinc
 				metaKeyGitlabId:      idString,
 				metaKeyGitlabUrl:     url,
 				metaKeyGitlabProject: ge.repositoryID,
+				metaKeyGitlabBaseUrl: GitlabBaseUrl,
 			},
 		)
 		if err != nil {

bridge/gitlab/export_test.go 🔗

@@ -188,7 +188,8 @@ func TestPushPull(t *testing.T) {
 	// initialize exporter
 	exporter := &gitlabExporter{}
 	err = exporter.Init(backend, core.Configuration{
-		keyProjectID: strconv.Itoa(projectID),
+		keyProjectID:     strconv.Itoa(projectID),
+		keyGitlabBaseUrl: "https://gitlab.com/",
 	})
 	require.NoError(t, err)
 
@@ -215,7 +216,8 @@ func TestPushPull(t *testing.T) {
 
 	importer := &gitlabImporter{}
 	err = importer.Init(backend, core.Configuration{
-		keyProjectID: strconv.Itoa(projectID),
+		keyProjectID:     strconv.Itoa(projectID),
+		keyGitlabBaseUrl: "https://gitlab.com/",
 	})
 	require.NoError(t, err)
 
@@ -280,7 +282,11 @@ func generateRepoName() string {
 
 // create repository need a token with scope 'repo'
 func createRepository(ctx context.Context, name string, token *auth.Token) (int, error) {
-	client := buildClient(token)
+	client, err := buildClient("https://gitlab.com/", token)
+	if err != nil {
+		return 0, err
+	}
+
 	project, _, err := client.Projects.CreateProject(
 		&gitlab.CreateProjectOptions{
 			Name: gitlab.String(name),
@@ -296,7 +302,11 @@ func createRepository(ctx context.Context, name string, token *auth.Token) (int,
 
 // delete repository need a token with scope 'delete_repo'
 func deleteRepository(ctx context.Context, project int, token *auth.Token) error {
-	client := buildClient(token)
-	_, err := client.Projects.DeleteProject(project, gitlab.WithContext(ctx))
+	client, err := buildClient("https://gitlab.com/", token)
+	if err != nil {
+		return err
+	}
+
+	_, err = client.Projects.DeleteProject(project, gitlab.WithContext(ctx))
 	return err
 }

bridge/gitlab/gitlab.go 🔗

@@ -17,9 +17,12 @@ const (
 	metaKeyGitlabUrl     = "gitlab-url"
 	metaKeyGitlabLogin   = "gitlab-login"
 	metaKeyGitlabProject = "gitlab-project-id"
+	metaKeyGitlabBaseUrl = "gitlab-base-url"
 
-	keyProjectID = "project-id"
+	keyProjectID     = "project-id"
+	keyGitlabBaseUrl = "base-url"
 
+	defaultBaseURL = "https://gitlab.com/"
 	defaultTimeout = 60 * time.Second
 )
 
@@ -37,10 +40,16 @@ func (*Gitlab) NewExporter() core.Exporter {
 	return &gitlabExporter{}
 }
 
-func buildClient(token *auth.Token) *gitlab.Client {
-	client := &http.Client{
+func buildClient(baseURL string, token *auth.Token) (*gitlab.Client, error) {
+	httpClient := &http.Client{
 		Timeout: defaultTimeout,
 	}
 
-	return gitlab.NewClient(client, token.Value)
+	gitlabClient := gitlab.NewClient(httpClient, token.Value)
+	err := gitlabClient.SetBaseURL(baseURL)
+	if err != nil {
+		return nil, err
+	}
+
+	return gitlabClient, nil
 }

bridge/gitlab/import.go 🔗

@@ -52,7 +52,10 @@ func (gi *gitlabImporter) Init(repo *cache.RepoCache, conf core.Configuration) e
 		return ErrMissingIdentityToken
 	}
 
-	gi.client = buildClient(creds[0].(*auth.Token))
+	gi.client, err = buildClient(conf[keyGitlabBaseUrl], creds[0].(*auth.Token))
+	if err != nil {
+		return err
+	}
 
 	return nil
 }
@@ -151,6 +154,7 @@ func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue
 			metaKeyGitlabId:      parseID(issue.IID),
 			metaKeyGitlabUrl:     issue.WebURL,
 			metaKeyGitlabProject: gi.conf[keyProjectID],
+			metaKeyGitlabBaseUrl: gi.conf[keyGitlabBaseUrl],
 		},
 	)
 

bridge/gitlab/import_test.go 🔗

@@ -103,7 +103,8 @@ func TestImport(t *testing.T) {
 
 	importer := &gitlabImporter{}
 	err = importer.Init(backend, core.Configuration{
-		keyProjectID: projectID,
+		keyProjectID:     projectID,
+		keyGitlabBaseUrl: "https://gitlab.com",
 	})
 	require.NoError(t, err)
 

bridge/launchpad/config.go 🔗

@@ -29,6 +29,9 @@ func (l *Launchpad) Configure(repo *cache.RepoCache, params core.BridgeParams) (
 	if params.Owner != "" {
 		fmt.Println("warning: --owner is ineffective for a Launchpad bridge")
 	}
+	if params.BaseURL != "" {
+		fmt.Println("warning: --base-url is ineffective for a Launchpad bridge")
+	}
 
 	conf := make(core.Configuration)
 	var err error

commands/bridge_configure.go 🔗

@@ -216,6 +216,7 @@ func init() {
 	bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureTarget, "target", "t", "",
 		fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(bridge.Targets(), ",")))
 	bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.URL, "url", "u", "", "The URL of the target repository")
+	bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.BaseURL, "base-url", "b", "", "The base URL of your issue tracker service")
 	bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.Owner, "owner", "o", "", "The owner of the target repository")
 	bridgeConfigureCmd.Flags().StringVarP(&bridgeConfigureParams.CredPrefix, "credential", "c", "", "The identifier or prefix of an already known credential for the API (see \"git-bug bridge auth\")")
 	bridgeConfigureCmd.Flags().StringVar(&bridgeConfigureToken, "token", "", "A raw authentication token for the API")

doc/man/git-bug-bridge-configure.1 🔗

@@ -39,6 +39,10 @@ Token configuration can be directly passed with the \-\-token flag or in the ter
 \fB\-u\fP, \fB\-\-url\fP=""
     The URL of the target repository
 
+.PP
+\fB\-b\fP, \fB\-\-base\-url\fP=""
+    The base URL of your issue tracker service
+
 .PP
 \fB\-o\fP, \fB\-\-owner\fP=""
     The owner of the target repository

doc/md/git-bug_bridge_configure.md 🔗

@@ -73,6 +73,7 @@ git bug bridge configure \
   -n, --name string         A distinctive name to identify the bridge
   -t, --target string       The target of the bridge. Valid values are [github,gitlab,launchpad-preview]
   -u, --url string          The URL of the target repository
+  -b, --base-url string     The base URL of your issue tracker service
   -o, --owner string        The owner of the target repository
   -c, --credential string   The identifier or prefix of an already known credential for the API (see "git-bug bridge auth")
       --token string        A raw authentication token for the API

misc/bash_completion/git-bug 🔗

@@ -400,6 +400,10 @@ _git-bug_bridge_configure()
     two_word_flags+=("--url")
     two_word_flags+=("-u")
     local_nonpersistent_flags+=("--url=")
+    flags+=("--base-url=")
+    two_word_flags+=("--base-url")
+    two_word_flags+=("-b")
+    local_nonpersistent_flags+=("--base-url=")
     flags+=("--owner=")
     two_word_flags+=("--owner")
     two_word_flags+=("-o")

misc/powershell_completion/git-bug 🔗

@@ -79,6 +79,8 @@ Register-ArgumentCompleter -Native -CommandName 'git-bug' -ScriptBlock {
             [CompletionResult]::new('--target', 'target', [CompletionResultType]::ParameterName, 'The target of the bridge. Valid values are [github,gitlab,launchpad-preview]')
             [CompletionResult]::new('-u', 'u', [CompletionResultType]::ParameterName, 'The URL of the target repository')
             [CompletionResult]::new('--url', 'url', [CompletionResultType]::ParameterName, 'The URL of the target repository')
+            [CompletionResult]::new('-b', 'b', [CompletionResultType]::ParameterName, 'The base URL of your issue tracker service')
+            [CompletionResult]::new('--base-url', 'base-url', [CompletionResultType]::ParameterName, 'The base URL of your issue tracker service')
             [CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'The owner of the target repository')
             [CompletionResult]::new('--owner', 'owner', [CompletionResultType]::ParameterName, 'The owner of the target repository')
             [CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, 'The identifier or prefix of an already known credential for the API (see "git-bug bridge auth")')

misc/zsh_completion/git-bug 🔗

@@ -193,6 +193,7 @@ function _git-bug_bridge_configure {
     '(-n --name)'{-n,--name}'[A distinctive name to identify the bridge]:' \
     '(-t --target)'{-t,--target}'[The target of the bridge. Valid values are [github,gitlab,launchpad-preview]]:' \
     '(-u --url)'{-u,--url}'[The URL of the target repository]:' \
+    '(-b --base-url)'{-b,--base-url}'[The base URL of your issue tracker service]:' \
     '(-o --owner)'{-o,--owner}'[The owner of the target repository]:' \
     '(-c --credential)'{-c,--credential}'[The identifier or prefix of an already known credential for the API (see "git-bug bridge auth")]:' \
     '--token[A raw authentication token for the API]:' \