pkgdash/internal/source/gitea/gitea.go
2024-04-03 00:15:33 +03:00

480 lines
15 KiB
Go
Raw 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 gitea
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os/exec"
"regexp"
"strings"
"text/template"
"time"
"git.unistack.org/unistack-org/pkgdash/internal/configcli"
"git.unistack.org/unistack-org/pkgdash/internal/modules"
"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/v4/logger"
)
var ErrPRNotExist = errors.New("pull request does not exist")
type Gitea struct {
URL string
Token string
PRTitle string
PRBody string
Repository string
Owner string
pulls []*giteaPull
}
func NewGitea(cfg configcli.Config) *Gitea {
return &Gitea{
URL: cfg.Source.APIURL,
Token: cfg.Source.Token,
PRTitle: cfg.PullRequestTitle,
PRBody: cfg.PullRequestBody,
Repository: cfg.Source.Repository,
Owner: cfg.Source.Owner,
}
}
type giteaPull struct {
URL string `json:"url"`
Title string `json:"title"`
Base struct {
Ref string `json:"ref"`
} `json:"base"`
Head struct {
Ref string `json:"ref"`
} `json:"head"`
ID int64 `json:"id"`
}
func (g *Gitea) Name() string {
return "gitea"
}
func (g *Gitea) RequestOpen(ctx context.Context, branch string, path string, mod modules.Update) error {
logger.Debugf(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 {
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 {
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 {
logger.Fatal(ctx, fmt.Sprintf("failed to execute template: %v", err))
}
if err = tplBody.Execute(wBody, data); err != nil {
logger.Fatal(ctx, fmt.Sprintf("failed to execute template: %v", err))
}
// открытие гит репозитория с опцией обхода репозитория для нахождения .git
repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
logger.Fatal(ctx, fmt.Sprintf("failed to open repo: %v", err))
}
//извлекаем ссылки с объектами из удаленного объекта??
if err = repo.FetchContext(ctx, &git.FetchOptions{
Auth: &httpauth.BasicAuth{Username: g.Token, Password: g.Token},
Force: true,
}); err != nil && err != git.NoErrAlreadyUpToDate {
logger.Fatal(ctx, fmt.Sprintf("failed to fetch repo: %v", err))
} //обновляем репозиторий
var headRef *plumbing.Reference // вроде ссылка на гит
refIter, err := repo.Branches() //получение веток
if err != nil {
logger.Fatal(ctx, fmt.Sprintf("failed to get branches: %v", err))
return 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 {
logger.Fatal(ctx, "failed to get repo branch head")
return err
} // Не получили нужную ветку
logger.Info(ctx, fmt.Sprintf("repo head %s", headRef))
wtree, err := repo.Worktree() //todo вроде рабочее дерево не нужно
if err != nil {
logger.Fatal(ctx, fmt.Sprintf("failed to get worktree: %v", err))
}
g.pulls, err = GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Token)
if err != nil && err != ErrPRNotExist {
logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err))
return err
}
for _, pull := range g.pulls {
if strings.Contains(pull.Title, path) && strings.Contains(pull.Base.Ref, branch) {
logger.Info(ctx, fmt.Sprintf("PR for %s exists %s, call RequestUpdate", path, pull.URL))
return g.RequestUpdate(ctx, branch, path, mod)
} // хотим проверить есть ли пулл реквест для этой ветки, если есть то выходим
}
logger.Info(ctx, fmt.Sprintf("update %s from %s to %s", path, mod.Module.Version, mod.Version))
logger.Info(ctx, "reset worktree")
if err = wtree.Reset(&git.ResetOptions{Mode: git.HardReset}); err != nil {
logger.Error(ctx, fmt.Sprintf("failed to reset repo branch: %v", err))
} //вроде меняем ветку todo вроде можно удалить
if err = wtree.PullContext(ctx, &git.PullOptions{
Auth: &httpauth.BasicAuth{Username: g.Token, Password: g.Token},
Depth: 1,
// RemoteURL :
Force: true,
RemoteName: "origin",
}); err != nil && err != git.NoErrAlreadyUpToDate {
logger.Error(ctx, fmt.Sprintf("failed to pull repo: %v", err)) //подтягиваем изменения с удаленого репозитория
}
logger.Info(ctx, fmt.Sprintf("checkout ref %s", headRef))
if err = wtree.Checkout(&git.CheckoutOptions{
Hash: headRef.Hash(),
Branch: plumbing.NewBranchReferenceName(fmt.Sprintf("pkgdash/go_modules/%s-%s", path, mod.Version)),
Create: true,
Force: true,
}); err != nil {
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 {
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 {
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 {
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 {
logger.Fatal(ctx, fmt.Sprintf("failed to run go mod tidy: %s err: %v", out, err))
} // пытаемся выполнить команду go mod tidy пытаемся подтянуть новую версию модуля
logger.Info(ctx, "worktree add go.mod")
if _, err = wtree.Add("go.mod"); err != nil {
logger.Fatal(ctx, fmt.Sprintf("failed to add file: %v", err))
}
logger.Info(ctx, "worktree add go.sum")
if _, err = wtree.Add("go.sum"); err != nil {
logger.Fatal(ctx, fmt.Sprintf("failed to add file: %v", err))
}
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 {
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 как будто нужно переделать
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.Token, Password: g.Token},
Force: true,
}); err != nil {
logger.Fatal(ctx, fmt.Sprintf("failed to push repo branch: %v", err))
} // пытаемся за пушить изменения
body := map[string]string{
"base": branch,
"body": wBody.String(),
"head": fmt.Sprintf("pkgdash/go_modules/%s-%s", path, mod.Version),
"title": wTitle.String(),
}
logger.Info(ctx, fmt.Sprintf("raw body: %#+v", body))
buf, err = json.Marshal(body)
if err != nil {
return err
}
logger.Info(ctx, fmt.Sprintf("marshal body: %s", buf))
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
fmt.Sprintf("https://%s/api/v1/repos/%s/%s/pulls?token=%s", g.URL, g.Owner, g.Repository, g.Token),
bytes.NewReader(buf),
)
if err != nil {
return err
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
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)
}
if err = wtree.Checkout(&git.CheckoutOptions{
Hash: headRef.Hash(),
Branch: headRef.Name(),
Create: false,
Force: true,
}); err != nil {
logger.Error(ctx, fmt.Sprintf("failed to checkout tree: %v", err))
return err
}
err = repo.DeleteBranch(fmt.Sprintf("pkgdash/go_modules/%s-%s", path, mod.Version))
if err != nil {
logger.Error(ctx, fmt.Sprintf("Delete local branch error: %s", err))
}
logger.Info(ctx, fmt.Sprintf("PR create for %s-%s", path, mod.Version))
repo, err = git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
logger.Fatal(ctx, fmt.Sprintf("failed to open repo: %v", err))
}
return nil
}
func (g *Gitea) RequestClose(ctx context.Context, branch string, path string) error {
logger.Debugf(ctx, fmt.Sprintf("RequestClose start, mod title: %s", path))
pulls, err := GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Token)
if err != nil {
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 pulls {
if strings.Contains(pull.Title, path) && pull.Base.Ref == branch {
logger.Info(ctx, fmt.Sprintf("PR for %s exists: %s", path, pull.URL))
prExist = true
b = pull.Head.Ref
}
}
if !prExist {
logger.Error(ctx, fmt.Sprintf("skip %s since pr does not exist", path))
return ErrPRNotExist
}
req, err := DeleteBranch(ctx, g.URL, g.Owner, g.Repository, b, g.Token)
if err != nil {
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 {
logger.Error(ctx, fmt.Sprintf("failed to do request for delete the branch: %s, err: %s, code: %v", branch, err, rsp.StatusCode))
return err
}
logger.Info(ctx, fmt.Sprintf("Delete branch for %s successful", path))
return nil
}
func (g *Gitea) RequestUpdate(ctx context.Context, branch string, path string, mod modules.Update) error {
logger.Debugf(ctx, fmt.Sprintf("RequestUpdate start, mod title: %s", path))
var err error
if len(g.pulls) == 0 {
g.pulls, err = GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Token)
if err != nil {
logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err))
return err
}
}
prExist := false
for _, pull := range g.pulls {
if strings.Contains(pull.Title, path) && pull.Base.Ref == branch {
logger.Info(ctx, fmt.Sprintf("don't skip %s since pr exist %s", path, pull.URL)) //todo
tVersion := getVersions(pull.Head.Ref) //Надо взять просто из названия ветки последнюю версию
if modules.IsNewerVersion(tVersion, mod.Version, false) {
reqDel, err := DeleteBranch(ctx, g.URL, g.Owner, g.Repository, pull.Head.Ref, g.Token)
if err != nil {
logger.Error(ctx, fmt.Sprintf("Error with create request for branch: %s, err: %s", branch, err))
continue
}
rsp, err := http.DefaultClient.Do(reqDel)
if err != nil {
logger.Error(ctx, fmt.Sprintf("Error with do request for branch: %s, err: %s, code: %v", branch, err, rsp.StatusCode))
continue //думаю что если не можем удалить ветку не стоит заканчивать работу, а перейти к следующей итерации
}
logger.Info(ctx, fmt.Sprintf("Old pr %s successful delete", pull.Head.Ref))
} else {
logger.Debugf(ctx, "The existing PR is relevant")
return nil
}
prExist = true
}
}
if !prExist {
logger.Error(ctx, fmt.Sprintf("skip %s since pr does not exist", path))
return ErrPRNotExist
}
logger.Info(ctx, fmt.Sprintf("update %s from %s to %s", path, mod.Module.Version, mod.Version))
return g.RequestOpen(ctx, branch, path, mod)
}
func (g *Gitea) RequestList(ctx context.Context, branch string) (map[string]string, error) {
logger.Debugf(ctx, fmt.Sprintf("RequestList for %s", branch))
gPulls, err := GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Token)
if err != nil {
return nil, err
}
var path string
rMap := make(map[string]string)
for _, pull := range gPulls {
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 DeleteBranch(ctx context.Context, url, owner, repo, branch, token string) (*http.Request, error) {
var buf []byte
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, fmt.Sprintf("https://%s/api/v1/repos/%s/%s/branches/%s?token=%s", url, owner, repo, branch, token), bytes.NewReader(buf))
if err != nil {
return nil, err
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
return req, err
}
func GetPulls(ctx context.Context, url, owner, repo, token string) ([]*giteaPull, error) {
var pullsAll, pulls []*giteaPull
page := 1
for {
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
fmt.Sprintf("https://%s/api/v1//repos/%s/%s/pulls?state=open&page=%v&token=%s", url, owner, repo, page, token),
nil)
if err != nil {
return nil, err
} //вроде запроса к репозиторию
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
rsp, err := http.DefaultClient.Do(req) // выполнение запроса
if err != nil {
return nil, err
}
buf, _ := io.ReadAll(rsp.Body)
if buf == nil {
break
}
switch rsp.StatusCode {
case http.StatusOK:
if err = json.Unmarshal(buf, &pulls); err != nil {
logger.Error(ctx, fmt.Sprintf("failed to decode response %s err: %v", buf, err))
return nil, err
}
pullsAll = append(pullsAll, pulls...)
page++
case http.StatusNotFound:
logger.Info(ctx, "PL is not exist for %s", repo)
return nil, ErrPRNotExist
default:
return nil, fmt.Errorf("unknown error: %s", buf)
}
if len(pulls) == 0 {
break
}
}
return pullsAll, nil
}