pkgdash/internal/source/gitea/gitea.go
2024-02-21 18:52:55 +03:00

673 lines
23 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"
"os/exec"
"regexp"
"strings"
"text/template"
"time"
"git.unistack.org/unistack-org/pkgdash/internal/configcli"
"git.unistack.org/unistack-org/pkgdash/internal/modules"
"git.unistack.org/unistack-org/pkgdash/internal/source"
"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"
)
type Gitea struct {
Token string
}
func NewGitea(t string) *Gitea {
return &Gitea{
Token: t,
}
}
type giteaPull struct {
URL string `json:"url"`
Title string `json:"title"`
Base struct {
Ref string `json:"ref"`
} `json:"base"`
ID int64 `json:"id"`
}
func (g *Gitea) RequestOpen(ctx context.Context, cfg *configcli.Config, branch string, path string, mod modules.Update) error {
logger.Debugf(ctx, "RequestOpen start, mod title: %s", path)
if cfg.Source == nil {
cfg.Source = &configcli.Source{
TypeGit: "gitea",
Token: os.Getenv("GITHUB_TOKEN"),
APIURL: os.Getenv("GITHUB_API_URL"),
Repository: os.Getenv("GITHUB_REPOSITORY"),
}
}
var buf []byte
var err error
// создания шаблона названия для пулл реквеста
tplTitle, err := template.New("pull_request_title").Parse(cfg.PullRequestTitle)
if err != nil {
logger.Fatalf(ctx, "failed to parse template: %v", err)
}
wTitle := bytes.NewBuffer(nil)
// создания шаблона тела для пулл реквеста
tplBody, err := template.New("pull_request_body").Parse(cfg.PullRequestBody)
if err != nil {
logger.Fatalf(ctx, "failed to parse template: %v", err)
}
wBody := bytes.NewBuffer(nil)
// открытие гит репозитория с опцией обхода репозитория для нахождения .git
repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
logger.Fatalf(ctx, "failed to open repo: %v", err)
}
//извлекаем ссылки с объектами из удаленного объекта??
if err = repo.FetchContext(ctx, &git.FetchOptions{
Auth: &httpauth.BasicAuth{Username: cfg.Source.Token, Password: cfg.Source.Token},
Force: true,
}); err != nil && err != git.NoErrAlreadyUpToDate {
logger.Fatalf(ctx, "failed to fetch repo: %v", err)
} //обновляем репозиторий
var headRef *plumbing.Reference // вроде ссылка на гит
refIter, err := repo.Branches() //получение веток
if err != nil {
logger.Fatalf(ctx, "failed to get branches: %v", err)
}
for {
ref, err := refIter.Next()
if err != nil {
break
}
if ref.Name().String() == branch { //todo вот тут возможно нужно переделать
headRef = ref
break
}
} //перебираем получение ветки и когда находим нужную выходим из цикла записав ветку в headRef
refIter.Close()
if headRef == nil {
logger.Fatalf(ctx, "failed to get repo branch head")
} // Не получили нужную ветку
logger.Infof(ctx, "repo head %s", headRef)
wtree, err := repo.Worktree() //todo вроде рабочее дерево не нужно
if err != nil {
logger.Fatalf(ctx, "failed to get worktree: %v", err)
}
var pulls []*giteaPull
req, err := http.NewRequestWithContext(ctx, http.MethodGet, cfg.Source.APIURL+"/repos/"+cfg.Source.Repository+"/pulls?state=open&token="+cfg.Source.Token, nil)
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
}
buf, _ = io.ReadAll(rsp.Body)
if rsp.StatusCode != http.StatusOK {
return fmt.Errorf("unknown error: %s", buf)
}
if err = json.Unmarshal(buf, &pulls); err != nil {
logger.Fatalf(ctx, "failed to decode response %s err: %v", buf, err)
} // записываем ответ от гита по пулл реквестам, видимо существующим
// перебираем наши модификации и если они уже есть в гите удаляем их из mods
for _, pull := range pulls {
if strings.Contains(pull.Title, path) && pull.Base.Ref == branch {
logger.Infof(ctx, "skip %s as pr already exists %s", path, pull.URL)
return source.ErrPRExist
} // хотим проверить есть ли пулл реквест для этой ветки, если есть то выходим
}
logger.Infof(ctx, "update %s from %s to %s", path, mod.Module.Version, mod.Version)
logger.Infof(ctx, "reset worktree")
if err = wtree.Reset(&git.ResetOptions{Mode: git.HardReset}); err != nil {
logger.Fatalf(ctx, "failed to reset repo branch: %v", err)
} //вроде меняем ветку todo вроде можно удалить
if err = wtree.PullContext(ctx, &git.PullOptions{
Auth: &httpauth.BasicAuth{Username: cfg.Source.Token, Password: cfg.Source.Token},
Depth: 1,
// RemoteURL :
Force: true,
RemoteName: "origin",
}); err != nil && err != git.NoErrAlreadyUpToDate {
logger.Fatalf(ctx, "failed to pull repo: %v", err) //подтягиваем изменения с удаленого репозитория
}
logger.Infof(ctx, "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.Fatalf(ctx, "failed to checkout tree: %v", err)
} //создаем новую ветку
epath, err := exec.LookPath("go")
if errors.Is(err, exec.ErrDot) {
err = nil
}
if err != nil {
logger.Fatalf(ctx, "failed to find go command: %v", err)
} // ищем go файл
var cmd *exec.Cmd
var out []byte
cmd = exec.CommandContext(ctx, epath, "mod", "edit", fmt.Sprintf("-require=%s@%s", path, mod.Version))
if out, err = cmd.CombinedOutput(); err != nil {
logger.Fatalf(ctx, "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.Fatalf(ctx, "failed to run go mod tidy: %s err: %v", out, err)
} // пытаемся выполнить команду go mod tidy пытаемся подтянуть новую версию модуля
logger.Infof(ctx, "worktree add go.mod")
if _, err = wtree.Add("go.mod"); err != nil {
logger.Fatalf(ctx, "failed to add file: %v", err)
}
logger.Infof(ctx, "worktree add go.sum")
if _, err = wtree.Add("go.sum"); err != nil {
logger.Fatalf(ctx, "failed to add file: %v", err)
}
logger.Infof(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.Fatalf(ctx, "failed to commit: %v", err)
}
// newref := plumbing.NewHashReference(plumbing.ReferenceName(fmt.Sprintf("refs/heads/pkgdash-1/go_modules/%s-%s", path, mod.Version)), headRef.Hash())
/*
if err = repo.Storer.SetReference(newref); err != nil {
logger.Fatalf(ctx, "failed to create repo branch: %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.Module.Version)) //todo как будто нужно переделать
logger.Infof(ctx, "try to push refspec %s", refspec)
if err = repo.PushContext(ctx, &git.PushOptions{
RefSpecs: []gitconfig.RefSpec{refspec},
Auth: &httpauth.BasicAuth{Username: cfg.Source.Token, Password: cfg.Source.Token},
Force: true,
}); err != nil {
logger.Fatalf(ctx, "failed to push repo branch: %v", err)
} // пытаемся за пушить изменения
data := map[string]string{
"Name": path,
"VersionOld": mod.Module.Version,
"VersionNew": mod.Version,
}
if err = tplTitle.Execute(wTitle, data); err != nil {
logger.Fatalf(ctx, "failed to execute template: %v", err)
}
if err = tplBody.Execute(wBody, data); err != nil {
logger.Fatalf(ctx, "failed to execute template: %v", err)
}
body := map[string]string{
"base": branch,
"body": wBody.String(),
"head": fmt.Sprintf("pkgdash-1/go_modules/%s-%s", path, mod.Version),
"title": wTitle.String(),
}
logger.Infof(ctx, "raw body: %#+v", body)
buf, err = json.Marshal(body)
if err != nil {
return err
}
logger.Infof(ctx, "marshal body: %s", buf)
req, err = http.NewRequestWithContext(ctx, http.MethodPost, cfg.Source.APIURL+"/repos/"+cfg.Source.Repository+"/pulls?token="+cfg.Source.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)
}
return nil
}
func (g *Gitea) RequestClose(ctx context.Context, cfg *configcli.Config, branch string, path string) error {
logger.Debugf(ctx, "RequestOpen start, mod title: %s", path)
if cfg.Source == nil {
cfg.Source = &configcli.Source{
TypeGit: "gitea",
Token: os.Getenv("GITHUB_TOKEN"),
APIURL: os.Getenv("GITHUB_API_URL"),
Repository: os.Getenv("GITHUB_REPOSITORY"),
}
}
var buf []byte
var err error
repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
logger.Fatalf(ctx, "failed to open repo: %v", err)
}
//извлекаем ссылки с объектами из удаленного объекта??
if err = repo.FetchContext(ctx, &git.FetchOptions{
Auth: &httpauth.BasicAuth{Username: cfg.Source.Token, Password: cfg.Source.Token},
Force: true,
}); err != nil && err != git.NoErrAlreadyUpToDate {
logger.Fatalf(ctx, "failed to fetch repo: %v", err)
}
var headRef *plumbing.Reference // вроде ссылка на гит
refIter, err := repo.Branches() //получение веток
if err != nil {
logger.Fatalf(ctx, "failed to get branches: %v", err)
}
for {
ref, err := refIter.Next()
if err != nil {
break
}
if ref.Name().String() == branch {
headRef = ref
break
}
} //перебираем получение ветки и когда находим нужную выходим из цикла записав ветку в headRef
refIter.Close()
if headRef == nil {
logger.Fatalf(ctx, "failed to get repo branch head")
} // Не получили нужную ветку
logger.Infof(ctx, "repo head %s", headRef)
var pulls []*giteaPull
req, err := http.NewRequestWithContext(ctx, http.MethodGet, cfg.Source.APIURL+"/repos/"+cfg.Source.Repository+"/pulls?state=open&token="+cfg.Source.Token, nil)
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
}
buf, _ = io.ReadAll(rsp.Body)
if rsp.StatusCode != http.StatusOK {
return fmt.Errorf("unknown error: %s", buf)
}
if err = json.Unmarshal(buf, &pulls); err != nil {
logger.Fatalf(ctx, "failed to decode response %s err: %v", buf, err)
} // записываем ответ от гита по пулл реквестам, видимо существующим
// перебираем наши модификации и если они уже есть в гите удаляем их из mods
prExist := false
for _, pull := range pulls {
if strings.Contains(pull.Title, path) && pull.Base.Ref == branch {
logger.Infof(ctx, "skip %s since pr does not exist %s", path, pull.URL)
prExist = true
}
}
if !prExist {
logger.Errorf(ctx, " skip %s since pr does not exist", path)
return source.ErrPRNotExist
}
req, err = DeleteBranch(ctx, cfg, branch)
if err != nil {
logger.Errorf(ctx, "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.Errorf(ctx, "failed to do request for delete the branch: %s, err: %s, code: %s", branch, err, rsp.StatusCode)
return err
}
logger.Infof(ctx, "Delete branch %s successful", branch)
return nil
}
func (g *Gitea) RequestUpdate(ctx context.Context, cfg *configcli.Config, branch string, path string, mod modules.Update) error {
logger.Debugf(ctx, "RequestOpen start, mod title: %s", path)
if cfg.Source == nil {
cfg.Source = &configcli.Source{
TypeGit: "gitea",
Token: os.Getenv("GITHUB_TOKEN"),
APIURL: os.Getenv("GITHUB_API_URL"),
Repository: os.Getenv("GITHUB_REPOSITORY"),
}
}
var buf []byte
var err error
// создания шаблона названия для пулл реквеста
tplTitle, err := template.New("pull_request_title").Parse(cfg.PullRequestTitle)
if err != nil {
logger.Fatalf(ctx, "failed to parse template: %v", err)
}
wTitle := bytes.NewBuffer(nil)
// создания шаблона тела для пулл реквеста
tplBody, err := template.New("pull_request_body").Parse(cfg.PullRequestBody)
if err != nil {
logger.Fatalf(ctx, "failed to parse template: %v", err)
}
wBody := bytes.NewBuffer(nil)
// открытие гит репозитория с опцией обхода репозитория для нахождения .git
repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
logger.Fatalf(ctx, "failed to open repo: %v", err)
}
//извлекаем ссылки с объектами из удаленного объекта??
if err = repo.FetchContext(ctx, &git.FetchOptions{
Auth: &httpauth.BasicAuth{Username: cfg.Source.Token, Password: cfg.Source.Token},
Force: true,
}); err != nil && err != git.NoErrAlreadyUpToDate {
logger.Fatalf(ctx, "failed to fetch repo: %v", err)
}
var headRef *plumbing.Reference // вроде ссылка на гит
refIter, err := repo.Branches() //получение веток
if err != nil {
logger.Fatalf(ctx, "failed to get branches: %v", err)
}
for {
ref, err := refIter.Next()
if err != nil {
break
}
if ref.Name().String() == branch {
headRef = ref
break
}
} //перебираем получение ветки и когда находим нужную выходим из цикла записав ветку в headRef
refIter.Close()
if headRef == nil {
logger.Fatalf(ctx, "failed to get repo branch head")
} // Не получили нужную ветку
logger.Infof(ctx, "repo head %s", headRef)
wtree, err := repo.Worktree()
if err != nil {
logger.Fatalf(ctx, "failed to get worktree: %v", err)
}
var pulls []*giteaPull
req, err := http.NewRequestWithContext(ctx, http.MethodGet, cfg.Source.APIURL+"/repos/"+cfg.Source.Repository+"/pulls?state=open&token="+cfg.Source.Token, nil)
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
}
buf, _ = io.ReadAll(rsp.Body)
if rsp.StatusCode != http.StatusOK {
return fmt.Errorf("unknown error: %s", buf)
}
if err = json.Unmarshal(buf, &pulls); err != nil {
logger.Fatalf(ctx, "failed to decode response %s err: %v", buf, err)
} // записываем ответ от гита по пулл реквестам, видимо существующим
// перебираем наши модификации и если они уже есть в гите удаляем их из mods
prExist := false
for _, pull := range pulls {
if strings.Contains(pull.Title, path) && pull.Base.Ref != branch {
logger.Infof(ctx, "skip %s since pr does not exist %s", path, pull.URL) //todo
titleVersions := getVersions(pull.Title)
if modules.IsNewerVersion(titleVersions.NewV, mod.Version, false) {
reqDel, err := DeleteBranch(ctx, cfg, branch)
if err != nil {
logger.Errorf(ctx, "Error with create request for branch: %s, err: %s", branch, err)
continue
}
rsp, err := http.DefaultClient.Do(reqDel)
if err != nil {
logger.Errorf(ctx, "Error with do request for branch: %s, err: %s, code: %v", branch, err, rsp.StatusCode)
continue //думаю что если не можем удалить ветку не стоит заканчивать работу, а перейти к следующей итерации
}
}
prExist = true
}
}
if !prExist {
logger.Errorf(ctx, " skip %s since pr does not exist", path)
return source.ErrPRNotExist
}
logger.Infof(ctx, "update %s from %s to %s", path, mod.Module.Version, mod.Version)
logger.Infof(ctx, "reset worktree")
if err = wtree.Reset(&git.ResetOptions{Mode: git.HardReset}); err != nil {
logger.Fatalf(ctx, "failed to reset repo branch: %v", err)
} //вроде меняем ветку
if err = wtree.PullContext(ctx, &git.PullOptions{
Auth: &httpauth.BasicAuth{Username: cfg.Source.Token, Password: cfg.Source.Token},
Depth: 1,
// RemoteURL :
Force: true,
RemoteName: "origin",
}); err != nil && err != git.NoErrAlreadyUpToDate {
logger.Fatalf(ctx, "failed to pull repo: %v", err)
}
logger.Infof(ctx, "checkout ref %s", headRef)
if err = wtree.Checkout(&git.CheckoutOptions{
Hash: headRef.Hash(),
Branch: headRef.Name(),
Create: false,
Force: true,
}); err != nil {
logger.Fatalf(ctx, "failed to checkout tree: %v", err)
} //вроде как переходим на другую ветку
epath, err := exec.LookPath("go")
if errors.Is(err, exec.ErrDot) {
err = nil
}
if err != nil {
logger.Fatalf(ctx, "failed to find go command: %v", err)
} // ищем go файл
var cmd *exec.Cmd
var out []byte
cmd = exec.CommandContext(ctx, epath, "mod", "edit", fmt.Sprintf("-require=%s@%s", path, mod.Version))
if out, err = cmd.CombinedOutput(); err != nil {
logger.Fatalf(ctx, "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.Fatalf(ctx, "failed to run go mod tidy: %s err: %v", out, err)
} // пытаемся выполнить команду go mod tidy пытаемся подтянуть новую версию модуля
logger.Infof(ctx, "worktree add go.mod")
if _, err = wtree.Add("go.mod"); err != nil {
logger.Fatalf(ctx, "failed to add file: %v", err)
}
logger.Infof(ctx, "worktree add go.sum")
if _, err = wtree.Add("go.sum"); err != nil {
logger.Fatalf(ctx, "failed to add file: %v", err)
}
logger.Infof(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.Fatalf(ctx, "failed to commit: %v", err)
}
// newref := plumbing.NewHashReference(plumbing.ReferenceName(fmt.Sprintf("refs/heads/pkgdash-1/go_modules/%s-%s", path, mod.Version)), headRef.Hash())
/*
if err = repo.Storer.SetReference(newref); err != nil {
logger.Fatalf(ctx, "failed to create repo branch: %v", err)
}
*/
refspec := gitconfig.RefSpec(fmt.Sprintf("+refs/heads/pkgdash-1/go_modules/%s-%s:refs/heads/pkgdash-1/go_modules/%s-%s", path, mod.Version, path, mod.Version))
logger.Infof(ctx, "try to push refspec %s", refspec)
if err = repo.PushContext(ctx, &git.PushOptions{
RefSpecs: []gitconfig.RefSpec{refspec},
Auth: &httpauth.BasicAuth{Username: cfg.Source.Token, Password: cfg.Source.Token},
Force: true,
}); err != nil {
logger.Fatalf(ctx, "failed to push repo branch: %v", err)
} // пытаемся за пушить изменения
data := map[string]string{
"Name": path,
"VersionOld": mod.Module.Version,
"VersionNew": mod.Version,
}
if err = tplTitle.Execute(wTitle, data); err != nil {
logger.Fatalf(ctx, "failed to execute template: %v", err)
}
if err = tplBody.Execute(wBody, data); err != nil {
logger.Fatalf(ctx, "failed to execute template: %v", err)
}
body := map[string]string{
"base": branch,
"body": wBody.String(),
"head": fmt.Sprintf("pkgdash-1/go_modules/%s-%s", path, mod.Version),
"title": wTitle.String(),
}
logger.Infof(ctx, "raw body: %#+v", body)
buf, err = json.Marshal(body)
if err != nil {
return err
}
logger.Infof(ctx, "marshal body: %s", buf)
req, err = http.NewRequestWithContext(ctx, http.MethodPost, cfg.Source.APIURL+"/repos/"+cfg.Source.Repository+"/pulls?token="+cfg.Source.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)
}
return nil
}
func getVersions(s string) *Tmod {
rsp := new(Tmod)
re := regexp.MustCompile("[0-9]+\\.[0-9]+\\.[0-9]+")
versions := re.FindAllString(s, -1)
if len(versions) < 2 {
return nil
}
if modules.IsNewerVersion(versions[0], versions[1], false) {
rsp.OldV = versions[0]
rsp.NewV = versions[1]
return rsp
}
rsp.OldV = versions[1]
rsp.NewV = versions[0]
return rsp
}
type Tmod struct {
OldV string
NewV string
}
func DeleteBranch(ctx context.Context, cfg *configcli.Config, branch string) (*http.Request, error) {
var buf []byte
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, cfg.Source.APIURL+"/repos/"+cfg.Source.Repository+"/branches/"+branch+"?token="+cfg.Source.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
}