Vasiliy Tolstov c5f3fa325e update
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-12-07 02:35:30 +03:00

523 lines
17 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package gitlab
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os/exec"
"regexp"
"strconv"
"strings"
"text/template"
"time"
"github.com/go-git/go-git/v5"
gitconfig "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
httpauth "github.com/go-git/go-git/v5/plumbing/transport/http"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/pkgdash/internal/configcli"
"go.unistack.org/pkgdash/internal/modules"
)
var ErrPRNotExist = errors.New("pull request does not exist")
type Gitlab struct {
logger logger.Logger
URL string
Username string
Password string
PRTitle string
PRBody string
Repository string
RepositoryId string
Owner string
pulls []*gitlabPull
baseRef *plumbing.Reference
}
func NewGitlab(cfg configcli.Config, log logger.Logger) *Gitlab {
return &Gitlab{
logger: log,
URL: cfg.Source.APIURL,
Username: cfg.Source.Username,
Password: cfg.Source.Password,
PRTitle: cfg.PullRequestTitle,
PRBody: cfg.PullRequestBody,
Repository: cfg.Source.Repository,
Owner: cfg.Source.Owner,
}
}
type gitlabPull struct {
URL string `json:"web_url"`
Title string `json:"title"`
Target string `json:"target_branch"`
Source string `json:"source_branch"`
ID int64 `json:"id"`
}
type gitlabProject struct {
Id int64 `json:"id"`
Name string `json:"name"`
}
func (g *Gitlab) Name() string {
return "gitlab"
}
func (g *Gitlab) RequestOpen(ctx context.Context, branch string, path string, mod modules.Update) error {
g.logger.Debug(ctx, fmt.Sprintf("RequestOpen start, mod title: %s", path))
var buf []byte
var err error
// создания шаблона названия для пулл реквеста
tplTitle, err := template.New("pull_request_title").Parse(g.PRTitle)
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to parse template: %v", err))
}
wTitle := bytes.NewBuffer(nil)
// создания шаблона тела для пулл реквеста
tplBody, err := template.New("pull_request_body").Parse(g.PRTitle)
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to parse template: %v", err))
}
wBody := bytes.NewBuffer(nil)
data := map[string]string{
"Name": path,
"VersionOld": mod.Module.Version,
"VersionNew": mod.Version,
}
if err = tplTitle.Execute(wTitle, data); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to execute template: %v", err))
}
if err = tplBody.Execute(wBody, data); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to execute template: %v", err))
}
// открытие гит репозитория с опцией обхода репозитория для нахождения .git
repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to open repo: %v", err))
}
// извлекаем ссылки с объектами из удаленного объекта??
if err = repo.FetchContext(ctx, &git.FetchOptions{
// Auth: &httpauth.BasicAuth{Username: g.Username, Password: g.Password},
Force: true,
}); err != nil && err != git.NoErrAlreadyUpToDate {
g.logger.Fatal(ctx, fmt.Sprintf("failed to fetch repo : %v", err))
} // обновляем репозиторий
var headRef *plumbing.Reference // вроде ссылка на гит
if g.baseRef == nil {
g.baseRef, err = repo.Head()
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("Error head: %s", err))
}
}
refIter, err := repo.Branches() // получение веток
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to get branches: %v", err))
}
for {
ref, err := refIter.Next()
if err != nil {
break
}
if ref.Name().Short() == branch { // todo вот тут возможно нужно переделать
headRef = ref
break
}
} // перебираем получение ветки и когда находим нужную выходим из цикла записав ветку в headRef
refIter.Close()
if headRef == nil {
g.logger.Fatal(ctx, "failed to get repo branch head")
return err
} // Не получили нужную ветку
g.logger.Info(ctx, fmt.Sprintf("repo head %s", headRef))
wtree, err := repo.Worktree() // todo вроде рабочее дерево не нужно
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to get worktree: %v", err))
}
defer g.checkout(*wtree, *g.baseRef)
g.pulls, err = g.GetPulls(ctx, g.URL, g.RepositoryId, branch, g.Password)
if err != nil && err != ErrPRNotExist {
g.logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err))
return err
}
for _, pull := range g.pulls {
if strings.Contains(pull.Title, path) {
g.logger.Info(ctx, fmt.Sprintf("PR for %s exists %s, call RequestUpdate", path, pull.URL))
return g.RequestUpdate(ctx, branch, path, mod)
} // хотим проверить есть ли пулл реквест для этой ветки, если есть то выходим
}
g.logger.Info(ctx, fmt.Sprintf("update %s from %s to %s", path, mod.Module.Version, mod.Version))
sourceBranch := fmt.Sprintf("pkgdash/go_modules/%s-%s", path, mod.Version)
g.logger.Info(ctx, "reset worktree")
if err = wtree.Reset(&git.ResetOptions{Commit: headRef.Hash(), Mode: git.HardReset}); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to reset repo branch: %v", err))
}
if err = wtree.PullContext(ctx, &git.PullOptions{
Auth: &httpauth.BasicAuth{Username: g.Username, Password: g.Password},
Depth: 1,
// RemoteURL :
Force: true,
RemoteName: "origin",
}); err != nil && err != git.NoErrAlreadyUpToDate {
g.logger.Error(ctx, fmt.Sprintf("failed to pull repo: %v", err)) // подтягиваем изменения с удаленого репозитория
}
g.logger.Info(ctx, fmt.Sprintf("checkout ref %s", headRef))
if err = wtree.Checkout(&git.CheckoutOptions{
Hash: headRef.Hash(),
Branch: plumbing.NewBranchReferenceName(sourceBranch),
Create: true,
Force: true,
}); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to checkout tree: %v", err))
return err
} // создаем новую ветку
epath, err := exec.LookPath("go")
if errors.Is(err, exec.ErrDot) {
err = nil
}
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to find go command: %v", err))
} // ищем go файл
var cmd *exec.Cmd
var out []byte
cmd = exec.CommandContext(ctx, epath, "mod", "edit", fmt.Sprintf("-droprequire=%s", mod.Module.Path))
if out, err = cmd.CombinedOutput(); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to run go mod edit: %s err: %v", out, err))
}
cmd = exec.CommandContext(ctx, epath, "mod", "edit", fmt.Sprintf("-require=%s@%s", path, mod.Version))
if out, err = cmd.CombinedOutput(); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to run go mod edit: %s err: %v", out, err))
} // пытаемся выполнить команду go mod edit с новой версией модуля
cmd = exec.CommandContext(ctx, epath, "mod", "tidy")
if out, err = cmd.CombinedOutput(); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to run go mod tidy: %s err: %v", out, err))
} // пытаемся выполнить команду go mod tidy пытаемся подтянуть новую версию модуля
g.logger.Info(ctx, "worktree add go.mod")
if _, err = wtree.Add("go.mod"); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to add file: %v", err))
}
g.logger.Info(ctx, "worktree add go.sum")
if _, err = wtree.Add("go.sum"); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to add file: %v", err))
}
g.logger.Info(ctx, "worktree commit")
_, err = wtree.Commit(wTitle.String(), &git.CommitOptions{
Parents: []plumbing.Hash{headRef.Hash()},
Author: &object.Signature{
Name: "gitea-actions",
Email: "info@unistack.org",
When: time.Now(),
},
}) // хотим за коммитить изменения
if err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to commit: %v", err))
}
refspec := gitconfig.RefSpec(fmt.Sprintf("+refs/heads/pkgdash/go_modules/%s-%s:refs/heads/pkgdash/go_modules/%s-%s", path, mod.Version, path, mod.Version)) // todo как будто нужно переделать
g.logger.Info(ctx, fmt.Sprintf("try to push refspec %s", refspec))
if err = repo.PushContext(ctx, &git.PushOptions{
RefSpecs: []gitconfig.RefSpec{refspec},
Auth: &httpauth.BasicAuth{Username: g.Username, Password: g.Password},
Force: true,
}); err != nil {
g.logger.Fatal(ctx, fmt.Sprintf("failed to push repo branch: %v", err))
} // пытаемся за пушить изменения
body := map[string]string{
"id": g.RepositoryId,
"source_branch": sourceBranch,
"target_branch": branch,
"title": wTitle.String(),
"description": wBody.String(),
}
g.logger.Info(ctx, fmt.Sprintf("raw body: %#+v", body))
buf, err = json.Marshal(body)
if err != nil {
return err
}
g.logger.Info(ctx, fmt.Sprintf("marshal body: %s", buf))
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
fmt.Sprintf("https://%s/api/v4/projects/%s/merge_requests", g.URL, g.RepositoryId),
bytes.NewReader(buf),
)
if err != nil {
return err
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+g.Password)
rsp, err := http.DefaultClient.Do(req)
if err != nil {
return err
} // Вроде создаем новый реквест на создание пулл реквеста
if rsp.StatusCode != http.StatusCreated {
buf, _ = io.ReadAll(rsp.Body)
return fmt.Errorf("unknown error: %s", buf)
}
g.logger.Info(ctx, fmt.Sprintf("PR create for %s-%s", path, mod.Version))
return nil
}
func (g *Gitlab) RequestClose(ctx context.Context, branch string, path string) error {
g.logger.Debug(ctx, fmt.Sprintf("RequestClose start, mod title: %s", path))
var err error
g.pulls, err = g.GetPulls(ctx, g.URL, g.RepositoryId, branch, g.Password)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err))
return err
}
prExist := false
var b string // Name of the branch to be deleted
for _, pull := range g.pulls {
if strings.Contains(pull.Title, path) {
g.logger.Info(ctx, fmt.Sprintf("PR for %s exists: %s", path, pull.URL))
prExist = true
b = pull.Source
}
}
if !prExist {
g.logger.Error(ctx, fmt.Sprintf("skip %s since pr does not exist", path))
return ErrPRNotExist
}
req, err := g.DeleteBranch(ctx, g.URL, g.RepositoryId, b, g.Password)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to create request for delete the branch: %s, err: %s", branch, err))
return err
}
rsp, err := http.DefaultClient.Do(req)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to do request for delete the branch: %s, err: %s, code: %v", branch, err, rsp.StatusCode))
return err
}
g.logger.Info(ctx, fmt.Sprintf("Delete branch for %s successful", path))
return nil
}
func (g *Gitlab) RequestUpdate(ctx context.Context, branch string, path string, mod modules.Update) error {
g.logger.Debug(ctx, fmt.Sprintf("RequestUpdate start, mod title: %s", path))
var err error
g.RepositoryId, err = g.GetRepoID(ctx, g.URL, g.Owner, g.Repository, g.Password)
if err != nil || g.RepositoryId == "" {
return fmt.Errorf("project id is empty")
}
g.pulls, err = g.GetPulls(ctx, g.URL, g.RepositoryId, branch, g.Password)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err))
return err
}
prExist := false
for _, pull := range g.pulls {
if strings.Contains(pull.Title, path) {
g.logger.Info(ctx, fmt.Sprintf("don't skip %s since pr exist %s", path, pull.URL)) // todo
tVersion := getVersions(pull.Source) // Надо взять просто из названия ветки последнюю версию
if modules.IsNewerVersion(tVersion, mod.Version, false) {
reqDel, err := g.DeleteBranch(ctx, g.URL, g.RepositoryId, pull.Source, g.Password)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("Error with create request for branch: %s, err: %s", branch, err))
return err
}
rsp, err := http.DefaultClient.Do(reqDel)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("Error with do request for branch: %s, err: %s, code: %v", branch, err, rsp.StatusCode))
return err
}
g.logger.Info(ctx, fmt.Sprintf("Old pr %s successful delete", pull.Source))
} else {
g.logger.Debug(ctx, "The existing PR is relevant")
return nil
}
prExist = true
}
}
if !prExist {
g.logger.Error(ctx, fmt.Sprintf("skip %s since pr does not exist", path))
return ErrPRNotExist
}
return g.RequestOpen(ctx, branch, path, mod) // todo это мне не нравится
}
func (g *Gitlab) RequestList(ctx context.Context, branch string) (map[string]string, error) {
g.logger.Debug(ctx, fmt.Sprintf("RequestList for %s", branch))
var err error
g.RepositoryId, err = g.GetRepoID(ctx, g.URL, g.Owner, g.Repository, g.Password)
if err != nil || g.RepositoryId == "" {
return nil, fmt.Errorf("project id is empty")
}
g.pulls, err = g.GetPulls(ctx, g.URL, g.RepositoryId, branch, g.Password)
if err != nil {
g.logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err))
return nil, err
}
var path string
rMap := make(map[string]string)
for _, pull := range g.pulls {
if !strings.HasPrefix(pull.Title, "Bump ") { // добавляем только реквесты бота по обновлению модулей
continue
}
path = strings.Split(pull.Title, " ")[1] // todo Работет только для дефолтного шаблона
rMap[path] = pull.Title
}
return rMap, nil
}
func getVersions(s string) string {
re := regexp.MustCompile("[vV][0-9]+\\.[0-9]+\\.[0-9]+")
version := re.FindString(s)
return version
}
func (g *Gitlab) DeleteBranch(ctx context.Context, url, projectId, branch, password string) (*http.Request, error) {
var buf []byte
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, fmt.Sprintf("https://%s/api/v4/projects/%s/repository/branches/%s", url, projectId, branch), bytes.NewReader(buf))
if err != nil {
return nil, err
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+password)
return req, err
}
func (g *Gitlab) GetPulls(ctx context.Context, url, projectId, branch, password string) ([]*gitlabPull, error) {
pulls := make([]*gitlabPull, 0, 10)
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
fmt.Sprintf("https://%s/api/v4/projects/%s/merge_requests?state=opened&target_branch=%s", url, projectId, branch),
nil)
if err != nil {
return nil, err
} // вроде запроса к репозиторию
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+password)
rsp, err := http.DefaultClient.Do(req) // выполнение запроса
if err != nil {
return nil, err
}
buf, _ := io.ReadAll(rsp.Body)
switch rsp.StatusCode {
case http.StatusOK:
if err = json.Unmarshal(buf, &pulls); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to decode response %s err: %v", buf, err))
return nil, err
}
return pulls, nil
case http.StatusNotFound:
g.logger.Info(ctx, fmt.Sprintf("pull-request is not exist for %s", projectId))
return nil, ErrPRNotExist
default:
return nil, fmt.Errorf("unknown error: %s", buf)
}
}
func (g *Gitlab) GetRepoID(ctx context.Context, url, owner, repo, password string) (rId string, err error) {
var buf []byte
projects := make([]*gitlabProject, 0, 10)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s/api/v4/users/%s/projects?owned=true", url, owner), nil)
if err != nil {
return
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", password)
rsp, err := http.DefaultClient.Do(req)
if err != nil {
return
}
buf, _ = io.ReadAll(rsp.Body)
switch rsp.StatusCode {
case http.StatusOK:
if err = json.Unmarshal(buf, &projects); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to decode response %s err: %v", buf, err))
}
for _, p := range projects {
if p.Name == repo {
rId = strconv.Itoa(int(p.Id))
}
}
return
default:
return rId, fmt.Errorf("unknown error: %s", buf)
}
}
func (g *Gitlab) checkout(w git.Worktree, ref plumbing.Reference) {
ctx := context.Background()
g.logger.Debug(ctx, fmt.Sprintf("Checkout: %s", ref.Name().Short()))
if err := w.Checkout(&git.CheckoutOptions{
Branch: ref.Name(),
Create: false,
Force: true,
Keep: false,
}); err != nil {
g.logger.Error(ctx, fmt.Sprintf("failed to reset: %v", err))
}
}