Skip to content

Prototype Support for Azure DevOps Git Repos #97

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"ideas",
"bug"
]
}
},
],
"commitType": "docs",
"commitConvention": "angular",
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.DS_Store
dist/
git-sync*
.vscode/
3 changes: 3 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/AkashRajpurohit/git-sync/pkg/github"
"github.com/AkashRajpurohit/git-sync/pkg/gitlab"
"github.com/AkashRajpurohit/git-sync/pkg/logger"
"github.com/AkashRajpurohit/git-sync/pkg/msdevops"
"github.com/AkashRajpurohit/git-sync/pkg/raw"
ch "github.com/robfig/cron/v3"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -95,6 +96,8 @@ var rootCmd = &cobra.Command{
case "forgejo", "gitea":
// Forgejo and Gitea have same API, so we can use the same client
platformClient = forgejo.NewForgejoClient(cfg.Server, cfg.Tokens)
case "msdevops":
platformClient = msdevops.NewMSDevOpsClient(cfg.Server, cfg.Tokens)
default:
if !hasRawURLs {
logger.Fatalf("Platform %s not supported", cfg.Platform)
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
Expand All @@ -38,7 +40,7 @@ require (
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.5.0 // indirect
Expand Down
8 changes: 7 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ github.com/google/go-github/v68 v68.0.0 h1:ZW57zeNZiXTdQ16qrDiZ0k6XucrxZ2CGmoTvc
github.com/google/go-github/v68 v68.0.0/go.mod h1:K9HAUBovM2sLwM408A18h+wd9vqdLOEqTUCbnRIcx68=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
Expand Down Expand Up @@ -59,6 +62,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0 h1:mmJCWLe63QvybxhW1iBmQWEaCKdc4SKgALfTNZ+OphU=
github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0/go.mod h1:mDunUZ1IUJdJIRHvFb+LPBUtxe3AYB5MI6BMXNg8194=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
Expand Down Expand Up @@ -128,8 +133,9 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
Expand Down
5 changes: 5 additions & 0 deletions pkg/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ func SetSensibleDefaults(cfg *Config) {
cfg.Server.Domain = "gitea.com"
cfg.Server.Protocol = "https"
}

if cfg.Platform == "msdevops" {
cfg.Server.Domain = "dev.azure.com"
cfg.Server.Protocol = "https"
}
}

// TODO: Remove these before v1.0.0 release
Expand Down
4 changes: 2 additions & 2 deletions pkg/config/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ func ValidateConfig(cfg Config) error {
return fmt.Errorf("at least one token must be provided when no raw git URLs are provided. See here: https://github.com/AkashRajpurohit/git-sync/wiki/Configuration")
}

if cfg.Platform != "github" && cfg.Platform != "gitlab" && cfg.Platform != "bitbucket" && cfg.Platform != "forgejo" && cfg.Platform != "gitea" {
return fmt.Errorf("platform can only be `github`, `gitlab`, `bitbucket`, `forgejo` or `gitea` when no raw git URLs are provided")
if cfg.Platform != "github" && cfg.Platform != "gitlab" && cfg.Platform != "bitbucket" && cfg.Platform != "forgejo" && cfg.Platform != "gitea" && cfg.Platform != "msdevops" {
return fmt.Errorf("platform can only be `github`, `gitlab`, `bitbucket`, `forgejo`, `gitea`, or `msdevops` when no raw git URLs are provided")
}

// Server configuration is required for platform-specific sync
Expand Down
108 changes: 108 additions & 0 deletions pkg/msdevops/msdevops.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package msdevops

import (
"context"
"fmt"

"github.com/AkashRajpurohit/git-sync/pkg/config"
"github.com/AkashRajpurohit/git-sync/pkg/logger"
gitSync "github.com/AkashRajpurohit/git-sync/pkg/sync"
"github.com/AkashRajpurohit/git-sync/pkg/token"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/git"
)

type MSDevOpsClient struct {
tokenManager *token.Manager
serverConfig config.Server
}

func NewMSDevOpsClient(serverConfig config.Server, tokens []string) *MSDevOpsClient {
return &MSDevOpsClient{
tokenManager: token.NewManager(tokens),
serverConfig: serverConfig,
}
}

func (c *MSDevOpsClient) GetTokenManager() *token.Manager {
return c.tokenManager
}

// createClient initializes and returns a new Azure DevOps Git client using the provided token manager and server configuration.
func (c *MSDevOpsClient) createClient() (git.Client, error) {

organizationURL := fmt.Sprintf("%s://%s", c.serverConfig.Protocol, c.serverConfig.Domain)
logger.Debugf("organizationURL: %s", organizationURL)
ctx := context.Background()
token := c.tokenManager.GetNextToken()
if token == "" {
return nil, fmt.Errorf("a valid token was not available")
}
connection := azuredevops.NewPatConnection(organizationURL, token)

client, err := git.NewClient(ctx, connection)
if err != nil {
return nil, err
}
return client, nil
}

// derefString returns the value of a string pointer or an empty string if the pointer is nil.
func derefString(ref *string) string {
if ref == nil {
return ""
}
return *ref
}

func (c *MSDevOpsClient) Sync(cfg config.Config) error {
repos, err := c.getUserRepos(cfg)
if err != nil {
return err
}

gitSync.LogRepoCount(len(repos), cfg.Platform)

gitSync.SyncReposWithConcurrency(cfg, repos, func(repo git.GitRepository) {
repoOwner := derefString(repo.Project.Name)
repoName := derefString(repo.Name)
repoURL := derefString(repo.WebUrl)
protoLen := len(cfg.Server.Protocol + "://")

// Need to manually construct the repo URL by inserting the user token into the URL
repoAuthURL := repoURL[:protoLen] + c.tokenManager.GetNextToken() + "@" + repoURL[protoLen:]
logger.Debugf("repoAuthURL: %s", repoAuthURL)

if *repo.IsDisabled {
logger.Warnf("Skipping repo %s as it is disabled", repoName)
} else {
gitSync.CloneOrUpdateRawRepo(repoOwner, repoName, repoAuthURL, cfg)
}

})
gitSync.LogSyncSummary()
return nil
}

func (c *MSDevOpsClient) getUserRepos(cfg config.Config) ([]git.GitRepository, error) {
logger.Debug("Fetching list of repositories ⏳")
client, err := c.createClient()
if err != nil {
return nil, err
}

ctx := context.Background()
allRepos, err := client.GetRepositories(ctx, git.GetRepositoriesArgs{
Project: &cfg.Workspace,
})
Comment on lines +94 to +96
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it require a workspace as well? can you share the sample config file (without sensitive data) as example for running this?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also is this not a paginated API, if it is then we will be fetching only certain limit of records in this current setup.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging has been removed

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it require a workspace as well? can you share the sample config file (without sensitive data) as example for running this?

I will need to double check this. I suspect it may not be required. Azure DevOps has an odd URL for the git repo which I couldn't easily assemble from the info in the config file - but if you ask it for the list of repos, a usable URL is available. I think I updated the code to disassemble that and insert the PAT@ at the start to ensure PAT based auth.

I will review it again when I get time.

if err != nil {
return nil, err
}

for _, repo := range *allRepos {
logger.Debugf("Found repo: %s", derefString(repo.Name))
logger.Debugf("Repo WebURL: %s", derefString(repo.WebUrl))
}

return *allRepos, nil
}