package azure import ( "context" "encoding/base64" "errors" "fmt" "freeleaps.com/gitops/initializer/internal/repo" "github.com/microsoft/azure-devops-go-api/azuredevops" "github.com/microsoft/azure-devops-go-api/azuredevops/git" ) type AzureDevOpsRepoManager struct { options *repo.Options client git.Client } var _ repo.RepoManager = &AzureDevOpsRepoManager{} func init() { repo.RegisterPlugin("azure-devops", func() repo.RepoManager { return &AzureDevOpsRepoManager{} }) } // Initialize implements repo.RepoManager. func (a *AzureDevOpsRepoManager) Initialize(context context.Context, opts ...repo.Option) error { options := &repo.Options{} for _, opt := range opts { opt(options) } if options.Project == "" { return errors.New("missing project name") } if options.Name == "" { return errors.New("missing repository name") } if options.Branch == "" { options.Branch = "master" } if options.AuthorName == "" { options.AuthorName = repo.DefaultAuthorName } if options.AuthorEmail == "" { options.AuthorEmail = repo.DefaultAuthorEmail } if options.PluginConfig == nil { return errors.New("missing plugin config") } config := options.PluginConfig pat, err := config.Get("pat") if err != nil { return errors.New("missing PAT") } organization, err := config.Get("organizationUrl") if err != nil { return errors.New("missing organization URL") } conn := azuredevops.NewPatConnection(organization.(string), pat.(string)) a.client, err = git.NewClient(context, conn) if err != nil { return fmt.Errorf("failed to create git client: %w", err) } a.options = options return nil } func (a *AzureDevOpsRepoManager) getBranchHeadCommit() (string, error) { filter := "heads/" filterContains := a.options.Branch refs, err := a.client.GetRefs(context.Background(), git.GetRefsArgs{ RepositoryId: &a.options.Name, Project: &a.options.Project, Filter: &filter, FilterContains: &filterContains, }) if err != nil { return "", fmt.Errorf("failed to get branch ref: %v", err) } if len(refs.Value) == 0 || (refs.Value)[0].ObjectId == nil { return "", fmt.Errorf("branch '%s' not found", a.options.Branch) } return *refs.Value[0].ObjectId, nil } // ApplyChanges implements repo.RepoManager. func (a *AzureDevOpsRepoManager) ApplyChanges(context context.Context, changes []repo.ChangeRequest, message string) error { targetRepo, err := a.client.GetRepository(context, git.GetRepositoryArgs{ RepositoryId: &a.options.Name, Project: &a.options.Project, }) if err != nil { return fmt.Errorf("failed to get repository: %w", err) } // convert changes to git changes gitChanges := make([]interface{}, 0, len(changes)) for _, change := range changes { gitChange := git.GitChange{ ChangeType: toAzureGitChangeType(change.Operation), Item: &git.GitItem{ Path: &change.Path, }, } if change.Operation != repo.OpDelete { contentBase64 := base64.StdEncoding.EncodeToString(change.Content) gitChange.NewContent = &git.ItemContent{ Content: &contentBase64, ContentType: &git.ItemContentTypeValues.Base64Encoded, } } gitChanges = append(gitChanges, gitChange) } // get head commit id baseCommitId, err := a.getBranchHeadCommit() if err != nil { return err } // generate ref name refName := "refs/heads/" + a.options.Branch // create push _, err = a.client.CreatePush(context, git.CreatePushArgs{ RepositoryId: &a.options.Name, Project: targetRepo.Project.Name, Push: &git.GitPush{ RefUpdates: &[]git.GitRefUpdate{ { Name: &refName, OldObjectId: &baseCommitId, }, }, Commits: &[]git.GitCommitRef{ { Comment: &message, Changes: &gitChanges, Author: &git.GitUserDate{ Name: &a.options.AuthorName, Email: &a.options.AuthorEmail, }, }, }, }, }) return err } func toAzureGitChangeType(op repo.FileOperation) *git.VersionControlChangeType { switch op { case repo.OpCreate: return &git.VersionControlChangeTypeValues.Add case repo.OpDelete: return &git.VersionControlChangeTypeValues.Delete case repo.OpUpdate: return &git.VersionControlChangeTypeValues.Edit default: panic("unreachable") } }