mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-27 04:11:03 +00:00
f15b27aadf
at some point (~7years ago) the oauth2 implementation was copied into the code-base and never touched. We only use it for gitlab the rest is already back using std. This migrates to the std oauth2 implementation
678 lines
18 KiB
Go
678 lines
18 KiB
Go
// Copyright 2018 Drone.IO Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package gitlab
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
"github.com/xanzy/go-gitlab"
|
|
"golang.org/x/oauth2"
|
|
|
|
"github.com/woodpecker-ci/woodpecker/server"
|
|
"github.com/woodpecker-ci/woodpecker/server/model"
|
|
"github.com/woodpecker-ci/woodpecker/server/remote"
|
|
"github.com/woodpecker-ci/woodpecker/server/remote/common"
|
|
"github.com/woodpecker-ci/woodpecker/server/store"
|
|
"github.com/woodpecker-ci/woodpecker/shared/utils"
|
|
)
|
|
|
|
const (
|
|
defaultScope = "api"
|
|
perPage = 100
|
|
)
|
|
|
|
// Opts defines configuration options.
|
|
type Opts struct {
|
|
URL string // Gitlab server url.
|
|
ClientID string // Oauth2 client id.
|
|
ClientSecret string // Oauth2 client secret.
|
|
SkipVerify bool // Skip ssl verification.
|
|
}
|
|
|
|
// Gitlab implements "Remote" interface
|
|
type Gitlab struct {
|
|
URL string
|
|
ClientID string
|
|
ClientSecret string
|
|
SkipVerify bool
|
|
HideArchives bool
|
|
Search bool
|
|
}
|
|
|
|
// New returns a Remote implementation that integrates with Gitlab, an open
|
|
// source Git service. See https://gitlab.com
|
|
func New(opts Opts) (remote.Remote, error) {
|
|
return &Gitlab{
|
|
URL: opts.URL,
|
|
ClientID: opts.ClientID,
|
|
ClientSecret: opts.ClientSecret,
|
|
SkipVerify: opts.SkipVerify,
|
|
}, nil
|
|
}
|
|
|
|
// Name returns the string name of this driver
|
|
func (g *Gitlab) Name() string {
|
|
return "gitlab"
|
|
}
|
|
|
|
func (g *Gitlab) oauth2Config(ctx context.Context) (*oauth2.Config, context.Context) {
|
|
return &oauth2.Config{
|
|
ClientID: g.ClientID,
|
|
ClientSecret: g.ClientSecret,
|
|
Endpoint: oauth2.Endpoint{
|
|
AuthURL: fmt.Sprintf("%s/oauth/authorize", g.URL),
|
|
TokenURL: fmt.Sprintf("%s/oauth/token", g.URL),
|
|
},
|
|
Scopes: []string{defaultScope},
|
|
RedirectURL: fmt.Sprintf("%s/authorize", server.Config.Server.OAuthHost),
|
|
},
|
|
|
|
context.WithValue(ctx, oauth2.HTTPClient, &http.Client{Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: g.SkipVerify},
|
|
Proxy: http.ProxyFromEnvironment,
|
|
}})
|
|
}
|
|
|
|
// Login authenticates the session and returns the
|
|
// remote user details.
|
|
func (g *Gitlab) Login(ctx context.Context, res http.ResponseWriter, req *http.Request) (*model.User, error) {
|
|
config, oauth2Ctx := g.oauth2Config(ctx)
|
|
|
|
// get the OAuth errors
|
|
if err := req.FormValue("error"); err != "" {
|
|
return nil, &remote.AuthError{
|
|
Err: err,
|
|
Description: req.FormValue("error_description"),
|
|
URI: req.FormValue("error_uri"),
|
|
}
|
|
}
|
|
|
|
// get the OAuth code
|
|
code := req.FormValue("code")
|
|
if len(code) == 0 {
|
|
http.Redirect(res, req, config.AuthCodeURL("woodpecker"), http.StatusSeeOther)
|
|
return nil, nil
|
|
}
|
|
|
|
token, err := config.Exchange(oauth2Ctx, code)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error exchanging token. %s", err)
|
|
}
|
|
|
|
client, err := newClient(g.URL, token.AccessToken, g.SkipVerify)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
login, _, err := client.Users.CurrentUser(gitlab.WithContext(ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user := &model.User{
|
|
Login: login.Username,
|
|
Email: login.Email,
|
|
Avatar: login.AvatarURL,
|
|
Token: token.AccessToken,
|
|
Secret: token.RefreshToken,
|
|
}
|
|
if !strings.HasPrefix(user.Avatar, "http") {
|
|
user.Avatar = g.URL + "/" + login.AvatarURL
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// Refresh refreshes the Gitlab oauth2 access token. If the token is
|
|
// refreshed the user is updated and a true value is returned.
|
|
func (g *Gitlab) Refresh(ctx context.Context, user *model.User) (bool, error) {
|
|
config, oauth2Ctx := g.oauth2Config(ctx)
|
|
config.RedirectURL = ""
|
|
|
|
source := config.TokenSource(oauth2Ctx, &oauth2.Token{
|
|
AccessToken: user.Token,
|
|
RefreshToken: user.Secret,
|
|
Expiry: time.Unix(user.Expiry, 0),
|
|
})
|
|
|
|
token, err := source.Token()
|
|
if err != nil || len(token.AccessToken) == 0 {
|
|
return false, err
|
|
}
|
|
|
|
user.Token = token.AccessToken
|
|
user.Secret = token.RefreshToken
|
|
user.Expiry = token.Expiry.UTC().Unix()
|
|
return true, nil
|
|
}
|
|
|
|
// Auth authenticates the session and returns the remote user login for the given token
|
|
func (g *Gitlab) Auth(ctx context.Context, token, _ string) (string, error) {
|
|
client, err := newClient(g.URL, token, g.SkipVerify)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
login, _, err := client.Users.CurrentUser(gitlab.WithContext(ctx))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return login.Username, nil
|
|
}
|
|
|
|
// Teams fetches a list of team memberships from the remote system.
|
|
func (g *Gitlab) Teams(ctx context.Context, user *model.User) ([]*model.Team, error) {
|
|
client, err := newClient(g.URL, user.Token, g.SkipVerify)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
teams := make([]*model.Team, 0, perPage)
|
|
|
|
for i := 1; true; i++ {
|
|
batch, _, err := client.Groups.ListGroups(&gitlab.ListGroupsOptions{
|
|
ListOptions: gitlab.ListOptions{Page: i, PerPage: perPage},
|
|
AllAvailable: gitlab.Bool(false),
|
|
MinAccessLevel: gitlab.AccessLevel(gitlab.DeveloperPermissions), // TODO: check what's best here
|
|
}, gitlab.WithContext(ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i := range batch {
|
|
teams = append(teams, &model.Team{
|
|
Login: batch[i].Name,
|
|
Avatar: batch[i].AvatarURL,
|
|
},
|
|
)
|
|
}
|
|
|
|
if len(batch) < perPage {
|
|
break
|
|
}
|
|
}
|
|
|
|
return teams, nil
|
|
}
|
|
|
|
// getProject fetches the named repository from the remote system.
|
|
func (g *Gitlab) getProject(ctx context.Context, client *gitlab.Client, owner, name string) (*gitlab.Project, error) {
|
|
repo, _, err := client.Projects.GetProject(fmt.Sprintf("%s/%s", owner, name), nil, gitlab.WithContext(ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return repo, nil
|
|
}
|
|
|
|
// Repo fetches the named repository from the remote system.
|
|
func (g *Gitlab) Repo(ctx context.Context, user *model.User, owner, name string) (*model.Repo, error) {
|
|
client, err := newClient(g.URL, user.Token, g.SkipVerify)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_repo, err := g.getProject(ctx, client, owner, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return g.convertGitlabRepo(_repo)
|
|
}
|
|
|
|
// Repos fetches a list of repos from the remote system.
|
|
func (g *Gitlab) Repos(ctx context.Context, user *model.User) ([]*model.Repo, error) {
|
|
client, err := newClient(g.URL, user.Token, g.SkipVerify)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repos := make([]*model.Repo, 0, perPage)
|
|
opts := &gitlab.ListProjectsOptions{
|
|
ListOptions: gitlab.ListOptions{PerPage: perPage},
|
|
MinAccessLevel: gitlab.AccessLevel(gitlab.DeveloperPermissions), // TODO: check what's best here
|
|
}
|
|
if g.HideArchives {
|
|
opts.Archived = gitlab.Bool(false)
|
|
}
|
|
|
|
for i := 1; true; i++ {
|
|
opts.Page = i
|
|
batch, _, err := client.Projects.ListProjects(opts, gitlab.WithContext(ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i := range batch {
|
|
repo, err := g.convertGitlabRepo(batch[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO(648) remove when woodpecker understands nested repos
|
|
if strings.Count(repo.FullName, "/") > 1 {
|
|
log.Debug().Msgf("Skipping nested repository %s for user %s, because they are not supported, yet (see #648).", repo.FullName, user.Login)
|
|
continue
|
|
}
|
|
|
|
repos = append(repos, repo)
|
|
}
|
|
|
|
if len(batch) < perPage {
|
|
break
|
|
}
|
|
}
|
|
|
|
return repos, err
|
|
}
|
|
|
|
// Perm fetches the named repository from the remote system.
|
|
func (g *Gitlab) Perm(ctx context.Context, user *model.User, r *model.Repo) (*model.Perm, error) {
|
|
client, err := newClient(g.URL, user.Token, g.SkipVerify)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
repo, err := g.getProject(ctx, client, r.Owner, r.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// repo owner is granted full access
|
|
if repo.Owner != nil && repo.Owner.Username == user.Login {
|
|
return &model.Perm{Push: true, Pull: true, Admin: true}, nil
|
|
}
|
|
|
|
// return permission for current user
|
|
return &model.Perm{
|
|
Pull: isRead(repo),
|
|
Push: isWrite(repo),
|
|
Admin: isAdmin(repo),
|
|
}, nil
|
|
}
|
|
|
|
// File fetches a file from the remote repository and returns in string format.
|
|
func (g *Gitlab) File(ctx context.Context, user *model.User, repo *model.Repo, build *model.Build, fileName string) ([]byte, error) {
|
|
client, err := newClient(g.URL, user.Token, g.SkipVerify)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_repo, err := g.getProject(ctx, client, repo.Owner, repo.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
file, _, err := client.RepositoryFiles.GetRawFile(_repo.ID, fileName, &gitlab.GetRawFileOptions{Ref: &build.Commit}, gitlab.WithContext(ctx))
|
|
return file, err
|
|
}
|
|
|
|
// Dir fetches a folder from the remote repository
|
|
func (g *Gitlab) Dir(ctx context.Context, user *model.User, repo *model.Repo, build *model.Build, path string) ([]*remote.FileMeta, error) {
|
|
client, err := newClient(g.URL, user.Token, g.SkipVerify)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
files := make([]*remote.FileMeta, 0, perPage)
|
|
_repo, err := g.getProject(ctx, client, repo.Owner, repo.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts := &gitlab.ListTreeOptions{
|
|
ListOptions: gitlab.ListOptions{PerPage: perPage},
|
|
Path: &path,
|
|
Ref: &build.Commit,
|
|
Recursive: gitlab.Bool(false),
|
|
}
|
|
|
|
for i := 1; true; i++ {
|
|
opts.Page = 1
|
|
batch, _, err := client.Repositories.ListTree(_repo.ID, opts, gitlab.WithContext(ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i := range batch {
|
|
if batch[i].Type != "blob" { // no file
|
|
continue
|
|
}
|
|
data, err := g.File(ctx, user, repo, build, batch[i].Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
files = append(files, &remote.FileMeta{
|
|
Name: batch[i].Path,
|
|
Data: data,
|
|
})
|
|
}
|
|
|
|
if len(batch) < perPage {
|
|
break
|
|
}
|
|
}
|
|
|
|
return files, nil
|
|
}
|
|
|
|
// Status sends the commit status back to gitlab.
|
|
func (g *Gitlab) Status(ctx context.Context, user *model.User, repo *model.Repo, build *model.Build, proc *model.Proc) error {
|
|
client, err := newClient(g.URL, user.Token, g.SkipVerify)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_repo, err := g.getProject(ctx, client, repo.Owner, repo.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, _, err = client.Commits.SetCommitStatus(_repo.ID, build.Commit, &gitlab.SetCommitStatusOptions{
|
|
State: getStatus(proc.State),
|
|
Description: gitlab.String(common.GetBuildStatusDescription(proc.State)),
|
|
TargetURL: gitlab.String(common.GetBuildStatusLink(repo, build, proc)),
|
|
Context: gitlab.String(common.GetBuildStatusContext(repo, build, proc)),
|
|
}, gitlab.WithContext(ctx))
|
|
|
|
return err
|
|
}
|
|
|
|
// Netrc returns a netrc file capable of authenticating Gitlab requests and
|
|
// cloning Gitlab repositories. The netrc will use the global machine account
|
|
// when configured.
|
|
func (g *Gitlab) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
|
|
login := ""
|
|
token := ""
|
|
|
|
if u != nil {
|
|
login = "oauth2"
|
|
token = u.Token
|
|
}
|
|
|
|
host, err := common.ExtractHostFromCloneURL(r.Clone)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &model.Netrc{
|
|
Login: login,
|
|
Password: token,
|
|
Machine: host,
|
|
}, nil
|
|
}
|
|
|
|
func (g *Gitlab) getTokenAndWebURL(link string) (token, webURL string, err error) {
|
|
uri, err := url.Parse(link)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
token = uri.Query().Get("access_token")
|
|
webURL = fmt.Sprintf("%s://%s/api/hook", uri.Scheme, uri.Host)
|
|
return token, webURL, nil
|
|
}
|
|
|
|
// Activate activates a repository by adding a Post-commit hook and
|
|
// a Public Deploy key, if applicable.
|
|
func (g *Gitlab) Activate(ctx context.Context, user *model.User, repo *model.Repo, link string) error {
|
|
client, err := newClient(g.URL, user.Token, g.SkipVerify)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_repo, err := g.getProject(ctx, client, repo.Owner, repo.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
token, webURL, err := g.getTokenAndWebURL(link)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(token) == 0 {
|
|
return fmt.Errorf("no token found")
|
|
}
|
|
|
|
_, _, err = client.Projects.AddProjectHook(_repo.ID, &gitlab.AddProjectHookOptions{
|
|
URL: gitlab.String(webURL),
|
|
Token: gitlab.String(token),
|
|
PushEvents: gitlab.Bool(true),
|
|
TagPushEvents: gitlab.Bool(true),
|
|
MergeRequestsEvents: gitlab.Bool(true),
|
|
DeploymentEvents: gitlab.Bool(true),
|
|
EnableSSLVerification: gitlab.Bool(!g.SkipVerify),
|
|
}, gitlab.WithContext(ctx))
|
|
|
|
return err
|
|
}
|
|
|
|
// Deactivate removes a repository by removing all the post-commit hooks
|
|
// which are equal to link and removing the SSH deploy key.
|
|
func (g *Gitlab) Deactivate(ctx context.Context, user *model.User, repo *model.Repo, link string) error {
|
|
client, err := newClient(g.URL, user.Token, g.SkipVerify)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_repo, err := g.getProject(ctx, client, repo.Owner, repo.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, webURL, err := g.getTokenAndWebURL(link)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hookID := -1
|
|
listProjectHooksOptions := &gitlab.ListProjectHooksOptions{
|
|
PerPage: 10,
|
|
Page: 1,
|
|
}
|
|
for {
|
|
hooks, resp, err := client.Projects.ListProjectHooks(_repo.ID, listProjectHooksOptions, gitlab.WithContext(ctx))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, hook := range hooks {
|
|
if hook.URL == webURL {
|
|
hookID = hook.ID
|
|
break
|
|
}
|
|
}
|
|
|
|
// Exit the loop when we've seen all pages
|
|
if resp.CurrentPage >= resp.TotalPages {
|
|
break
|
|
}
|
|
|
|
// Update the page number to get the next page
|
|
listProjectHooksOptions.Page = resp.NextPage
|
|
}
|
|
|
|
if hookID == -1 {
|
|
return fmt.Errorf("could not find hook to delete")
|
|
}
|
|
|
|
_, err = client.Projects.DeleteProjectHook(_repo.ID, hookID, gitlab.WithContext(ctx))
|
|
|
|
return err
|
|
}
|
|
|
|
// Branches returns the names of all branches for the named repository.
|
|
func (g *Gitlab) Branches(ctx context.Context, user *model.User, repo *model.Repo) ([]string, error) {
|
|
token := ""
|
|
if user != nil {
|
|
token = user.Token
|
|
}
|
|
client, err := newClient(g.URL, token, g.SkipVerify)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_repo, err := g.getProject(ctx, client, repo.Owner, repo.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
gitlabBranches, _, err := client.Branches.ListBranches(_repo.ID, &gitlab.ListBranchesOptions{}, gitlab.WithContext(ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
branches := make([]string, 0)
|
|
for _, branch := range gitlabBranches {
|
|
branches = append(branches, branch.Name)
|
|
}
|
|
return branches, nil
|
|
}
|
|
|
|
// Hook parses the post-commit hook from the Request body
|
|
// and returns the required data in a standard format.
|
|
func (g *Gitlab) Hook(ctx context.Context, req *http.Request) (*model.Repo, *model.Build, error) {
|
|
defer req.Body.Close()
|
|
payload, err := io.ReadAll(req.Body)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
parsed, err := gitlab.ParseWebhook(gitlab.WebhookEventType(req), payload)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
switch event := parsed.(type) {
|
|
case *gitlab.MergeEvent:
|
|
mergeIID, repo, build, err := convertMergeRequestHook(event, req)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if build, err = g.loadChangedFilesFromMergeRequest(ctx, repo, build, mergeIID); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return repo, build, nil
|
|
case *gitlab.PushEvent:
|
|
return convertPushHook(event)
|
|
case *gitlab.TagEvent:
|
|
return convertTagHook(event)
|
|
default:
|
|
return nil, nil, nil
|
|
}
|
|
}
|
|
|
|
// OrgMembership returns if user is member of organization and if user
|
|
// is admin/owner in this organization.
|
|
func (g *Gitlab) OrgMembership(ctx context.Context, u *model.User, owner string) (*model.OrgPerm, error) {
|
|
client, err := newClient(g.URL, u.Token, g.SkipVerify)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
groups, _, err := client.Groups.ListGroups(&gitlab.ListGroupsOptions{
|
|
ListOptions: gitlab.ListOptions{
|
|
Page: 1,
|
|
PerPage: 100,
|
|
},
|
|
Search: gitlab.String(owner),
|
|
}, gitlab.WithContext(ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var gid int
|
|
for _, group := range groups {
|
|
if group.Name == owner {
|
|
gid = group.ID
|
|
break
|
|
}
|
|
}
|
|
if gid == 0 {
|
|
return &model.OrgPerm{}, nil
|
|
}
|
|
|
|
opts := &gitlab.ListGroupMembersOptions{
|
|
ListOptions: gitlab.ListOptions{
|
|
Page: 1,
|
|
PerPage: 100,
|
|
},
|
|
}
|
|
|
|
for i := 1; true; i++ {
|
|
opts.Page = i
|
|
members, _, err := client.Groups.ListAllGroupMembers(gid, opts, gitlab.WithContext(ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, member := range members {
|
|
if member.Username == u.Login {
|
|
return &model.OrgPerm{Member: true, Admin: member.AccessLevel >= gitlab.OwnerPermissions}, nil
|
|
}
|
|
}
|
|
|
|
if len(members) < opts.PerPage {
|
|
break
|
|
}
|
|
}
|
|
|
|
return &model.OrgPerm{}, nil
|
|
}
|
|
|
|
func (g *Gitlab) loadChangedFilesFromMergeRequest(ctx context.Context, tmpRepo *model.Repo, build *model.Build, mergeIID int) (*model.Build, error) {
|
|
_store, ok := store.TryFromContext(ctx)
|
|
if !ok {
|
|
log.Error().Msg("could not get store from context")
|
|
return build, nil
|
|
}
|
|
|
|
repo, err := _store.GetRepoName(tmpRepo.Owner + "/" + tmpRepo.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user, err := _store.GetUser(repo.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client, err := newClient(g.URL, user.Token, g.SkipVerify)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_repo, err := g.getProject(ctx, client, repo.Owner, repo.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
changes, _, err := client.MergeRequests.GetMergeRequestChanges(_repo.ID, mergeIID, &gitlab.GetMergeRequestChangesOptions{}, gitlab.WithContext(ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
files := make([]string, 0, len(changes.Changes)*2)
|
|
for _, file := range changes.Changes {
|
|
files = append(files, file.NewPath, file.OldPath)
|
|
}
|
|
build.ChangedFiles = utils.DedupStrings(files)
|
|
|
|
return build, nil
|
|
}
|