From 700ba16470cfbbd507b47706feb6d1e81cc55b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=20=D0=93=D0=BE=D1=80?= =?UTF-8?q?=D0=B1=D1=83=D0=BD=D0=BE=D0=B2?= Date: Sat, 6 Apr 2024 15:18:43 +0300 Subject: [PATCH] =?UTF-8?q?#8=20=D0=9F=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20project=20id=20=D0=B4=D0=BB=D1=8F=20gitlab=20(#1?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vasiliy Tolstov Co-authored-by: Gorbunov Kirill Andreevich Reviewed-on: https://git.unistack.org/unistack-org/pkgdash/pulls/15 Co-authored-by: Кирилл Горбунов Co-committed-by: Кирилл Горбунов --- internal/source/gitea/gitea.go | 54 ++-- internal/source/github/github.go | 320 +++++++++++++++++++- internal/source/gitlab/gitlab.go | 496 ++++++++++++++++++++++++++++++- 3 files changed, 833 insertions(+), 37 deletions(-) diff --git a/internal/source/gitea/gitea.go b/internal/source/gitea/gitea.go index 1b84fd3..b318fd6 100644 --- a/internal/source/gitea/gitea.go +++ b/internal/source/gitea/gitea.go @@ -28,8 +28,8 @@ var ErrPRNotExist = errors.New("pull request does not exist") type Gitea struct { URL string - Username string - Password string + Username string + Password string PRTitle string PRBody string Repository string @@ -40,8 +40,8 @@ type Gitea struct { func NewGitea(cfg configcli.Config) *Gitea { return &Gitea{ URL: cfg.Source.APIURL, - Username: cfg.Source.Username, - Password: cfg.Source.Password, + Username: cfg.Source.Username, + Password: cfg.Source.Password, PRTitle: cfg.PullRequestTitle, PRBody: cfg.PullRequestBody, Repository: cfg.Source.Repository, @@ -105,10 +105,10 @@ func (g *Gitea) RequestOpen(ctx context.Context, branch string, path string, mod } //извлекаем ссылки с объектами из удаленного объекта?? if err = repo.FetchContext(ctx, &git.FetchOptions{ - // Auth: &httpauth.BasicAuth{Username: g.Username, Password: g.Password}, + // Auth: &httpauth.BasicAuth{Username: g.Username, Password: g.Password}, Force: true, }); err != nil && err != git.NoErrAlreadyUpToDate { - logger.Fatal(ctx, fmt.Sprintf("failed to fetch repo : %v",err)) + logger.Fatal(ctx, fmt.Sprintf("failed to fetch repo : %v", err)) } //обновляем репозиторий var headRef *plumbing.Reference // вроде ссылка на гит @@ -140,8 +140,9 @@ func (g *Gitea) RequestOpen(ctx context.Context, branch string, path string, mod if err != nil { logger.Fatal(ctx, fmt.Sprintf("failed to get worktree: %v", err)) } + defer checkout(wtree, *headRef) - g.pulls, err = GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Username, g.Password) + g.pulls, err = GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Password) if err != nil && err != ErrPRNotExist { logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err)) return err @@ -157,7 +158,7 @@ func (g *Gitea) RequestOpen(ctx context.Context, branch string, path string, 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{Commit: headRef.Hash(), Mode: git.HardReset}); err != nil { + if err = wtree.Reset(&git.ResetOptions{Commit: headRef.Hash(), Mode: git.HardReset}); err != nil { logger.Error(ctx, fmt.Sprintf("failed to reset repo branch: %v", err)) } //вроде меняем ветку todo вроде можно удалить @@ -261,7 +262,7 @@ func (g *Gitea) RequestOpen(ctx context.Context, branch string, path string, mod req, err := http.NewRequestWithContext( ctx, http.MethodPost, - fmt.Sprintf("https://%s/api/v1/repos/%s/%s/pulls", g.URL, g.Owner, g.Repository, g.Password), + fmt.Sprintf("https://%s/api/v1/repos/%s/%s/pulls", g.URL, g.Owner, g.Repository), bytes.NewReader(buf), ) if err != nil { @@ -269,7 +270,7 @@ func (g *Gitea) RequestOpen(ctx context.Context, branch string, path string, mod } req.Header.Add("Accept", "application/json") req.Header.Add("Content-Type", "application/json") -req.Header.Add("Authorization", g.Password) + req.Header.Add("Authorization", g.Password) rsp, err := http.DefaultClient.Do(req) if err != nil { @@ -293,7 +294,7 @@ req.Header.Add("Authorization", g.Password) func (g *Gitea) RequestClose(ctx context.Context, branch string, path string) error { logger.Debug(ctx, fmt.Sprintf("RequestClose start, mod title: %s", path)) - pulls, err := GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Username, g.Password) + pulls, err := GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Password) if err != nil { logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err)) return err @@ -313,7 +314,7 @@ func (g *Gitea) RequestClose(ctx context.Context, branch string, path string) er return ErrPRNotExist } - req, err := DeleteBranch(ctx, g.URL, g.Owner, g.Repository, b, g.Username, g.Password) + req, err := DeleteBranch(ctx, g.URL, g.Owner, g.Repository, b, g.Password) if err != nil { logger.Error(ctx, fmt.Sprintf("failed to create request for delete the branch: %s, err: %s", branch, err)) return err @@ -333,7 +334,7 @@ func (g *Gitea) RequestUpdate(ctx context.Context, branch string, path string, m var err error if len(g.pulls) == 0 { - g.pulls, err = GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Username, g.Password) + g.pulls, err = GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Password) if err != nil { logger.Error(ctx, fmt.Sprintf("GetPulls error: %s", err)) return err @@ -346,15 +347,15 @@ func (g *Gitea) RequestUpdate(ctx context.Context, branch string, path string, m 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.Username, g.Password) + reqDel, err := DeleteBranch(ctx, g.URL, g.Owner, g.Repository, pull.Head.Ref, g.Password) if err != nil { logger.Error(ctx, fmt.Sprintf("Error with create request for branch: %s, err: %s", branch, err)) - continue + return err } 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 //думаю что если не можем удалить ветку не стоит заканчивать работу, а перейти к следующей итерации + return err } logger.Info(ctx, fmt.Sprintf("Old pr %s successful delete", pull.Head.Ref)) } else { @@ -369,8 +370,6 @@ func (g *Gitea) RequestUpdate(ctx context.Context, branch string, path string, m 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) } @@ -378,7 +377,7 @@ func (g *Gitea) RequestList(ctx context.Context, branch string) (map[string]stri logger.Debug(ctx, fmt.Sprintf("RequestList for %s", branch)) var err error - g.pulls, err = GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Username, g.Password) + g.pulls, err = GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Password) if err != nil { return nil, err } @@ -387,7 +386,7 @@ func (g *Gitea) RequestList(ctx context.Context, branch string) (map[string]stri rMap := make(map[string]string) for _, pull := range g.pulls { - if !strings.HasPrefix(pull.Title, "Bump ") { //добавляем только реквесты бота по обновлению модулей + if !strings.HasPrefix(pull.Title, "Bump ") || pull.Base.Ref != branch { //добавляем только реквесты бота по обновлению модулей continue } path = strings.Split(pull.Title, " ")[1] //todo Работет только для дефолтного шаблона @@ -404,7 +403,7 @@ func getVersions(s string) string { return version } -func DeleteBranch(ctx context.Context, url, owner, repo, branch, username, password string) (*http.Request, error) { +func DeleteBranch(ctx context.Context, url, owner, repo, branch, password 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", url, owner, repo, branch), bytes.NewReader(buf)) if err != nil { @@ -416,7 +415,7 @@ func DeleteBranch(ctx context.Context, url, owner, repo, branch, username, passw return req, err } -func GetPulls(ctx context.Context, url, owner, repo, username, password string) ([]*giteaPull, error) { +func GetPulls(ctx context.Context, url, owner, repo, password string) ([]*giteaPull, error) { var pullsAll []*giteaPull page := 1 @@ -425,7 +424,7 @@ func GetPulls(ctx context.Context, url, owner, repo, username, password string) req, err := http.NewRequestWithContext( ctx, http.MethodGet, - fmt.Sprintf("https://%s/api/v1/repos/%s/%s/pulls?state=open&page=%v", url, owner, repo, page), + fmt.Sprintf("https://%s/api/v1/repos/%s/%s/pulls?state=open&page=%v", url, owner, repo, page), nil) if err != nil { return nil, err @@ -433,7 +432,7 @@ func GetPulls(ctx context.Context, url, owner, repo, username, password string) req.Header.Add("Accept", "application/json") req.Header.Add("Content-Type", "application/json") -req.Header.Add("Authorization", password) + req.Header.Add("Authorization", password) rsp, err := http.DefaultClient.Do(req) // выполнение запроса if err != nil { @@ -464,3 +463,10 @@ req.Header.Add("Authorization", password) return pullsAll, nil } + +func checkout(w *git.Worktree, ref plumbing.Reference) { + ctx := context.Background() + if err := w.Reset(&git.ResetOptions{Commit: ref.Hash(), Mode: git.HardReset}); err != nil { + logger.Fatal(ctx, fmt.Sprintf("failed to reset: %v", err)) + } +} diff --git a/internal/source/github/github.go b/internal/source/github/github.go index 92c0254..22b355b 100644 --- a/internal/source/github/github.go +++ b/internal/source/github/github.go @@ -1,31 +1,292 @@ package github import ( + "bytes" "context" + "encoding/json" + "errors" "fmt" + "io" + "net/http" + "os/exec" + "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 Github struct { - Username string - Password string + URL string + Username string + Password string + PRTitle string + PRBody string + Repository string + Owner string + pulls []*githubPull } func NewGithub(cfg configcli.Config) *Github { return &Github{ - Username: cfg.Source.Username, - Password: cfg.Source.Password, + 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 githubPull 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 *Github) Name() string { return "github" } func (g *Github) RequestOpen(ctx context.Context, branch string, path string, mod modules.Update) error { - return fmt.Errorf("implement me") + 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 { + 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.Username, Password: g.Password}, + 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.Password) + 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{Commit: headRef.Hash(), 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.Username, Password: g.Password}, + 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.Username, Password: g.Password}, + 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", g.URL, g.Owner, g.Repository), + 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", 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) + } + + 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 *Github) RequestClose(ctx context.Context, branch string, path string) error { return fmt.Errorf("implement me") @@ -36,3 +297,52 @@ func (g *Github) RequestUpdate(ctx context.Context, branch string, path string, func (g *Github) RequestList(ctx context.Context, branch string) (map[string]string, error) { return nil, fmt.Errorf("implement me") } + +func GetPulls(ctx context.Context, url, owner, repo, password string) ([]*githubPull, error) { + var pullsAll []*githubPull + page := 1 + + for { + pulls := make([]*githubPull, 0, 10) + req, err := http.NewRequestWithContext( + ctx, + http.MethodGet, + fmt.Sprintf("https://%s/api/v1/repos/%s/%s/pulls?state=open&page=%v", url, owner, repo, page), + nil) + if err != nil { + return nil, err + } //вроде запроса к репозиторию + + 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 nil, err + } + + buf, _ := io.ReadAll(rsp.Body) + + 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, fmt.Sprintf("pull-request 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 +} diff --git a/internal/source/gitlab/gitlab.go b/internal/source/gitlab/gitlab.go index 3528b13..a096eeb 100644 --- a/internal/source/gitlab/gitlab.go +++ b/internal/source/gitlab/gitlab.go @@ -1,38 +1,518 @@ package gitlab import ( + "bytes" "context" + "encoding/json" + "errors" "fmt" + "io" + "net/http" + "os/exec" + "regexp" + "strconv" + "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 Gitlab struct { - Username string - Password string + URL string + Username string + Password string + PRTitle string + PRBody string + Repository string + RepositoryId string + Owner string + pulls []*gitlabPull } func NewGitlab(cfg configcli.Config) *Gitlab { return &Gitlab{ - Username: cfg.Source.Username, - Password: cfg.Source.Password, + 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 { - return fmt.Errorf("implement me") + 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 { + 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.Username, Password: g.Password}, + 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)) + } + defer checkout(wtree, *headRef) + + g.RepositoryId, err = 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 = GetPulls(ctx, g.URL, g.RepositoryId, branch, g.Password) + 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) { + 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)) + + sourceBranch := fmt.Sprintf("pkgdash/go_modules/%s-%s", path, mod.Version) + + logger.Info(ctx, "reset worktree") + if err = wtree.Reset(&git.ResetOptions{Commit: headRef.Hash(), Mode: git.HardReset}); err != nil { + 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 { + 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(sourceBranch), + 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.Username, Password: g.Password}, + Force: true, + }); err != nil { + 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(), + } + 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/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", 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) + } + + 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 { - return fmt.Errorf("implement me") + logger.Debug(ctx, fmt.Sprintf("RequestClose start, mod title: %s", path)) + + var err error + + g.RepositoryId, err = GetRepoID(ctx, g.URL, g.Owner, g.Repository, g.Password) + if err != nil || g.RepositoryId == "" { + return fmt.Errorf("project id is empty") + } + + pulls, err := GetPulls(ctx, g.URL, g.RepositoryId, branch, g.Password) + 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) { + logger.Info(ctx, fmt.Sprintf("PR for %s exists: %s", path, pull.URL)) + prExist = true + b = pull.Source + } + } + if !prExist { + logger.Error(ctx, fmt.Sprintf("skip %s since pr does not exist", path)) + return ErrPRNotExist + } + + req, err := DeleteBranch(ctx, g.URL, g.RepositoryId, b, g.Password) + 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 *Gitlab) RequestUpdate(ctx context.Context, branch string, path string, mod modules.Update) error { - return fmt.Errorf("implement me") + logger.Debug(ctx, fmt.Sprintf("RequestUpdate start, mod title: %s", path)) + var err error + + g.RepositoryId, err = GetRepoID(ctx, g.URL, g.Owner, g.Repository, g.Password) + if err != nil || g.RepositoryId == "" { + return fmt.Errorf("project id is empty") + } + + if len(g.pulls) == 0 { + g.pulls, err = GetPulls(ctx, g.URL, g.RepositoryId, branch, g.Password) + 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) { + 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 := DeleteBranch(ctx, g.URL, g.RepositoryId, pull.Source, g.Password) + if err != nil { + 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 { + logger.Error(ctx, fmt.Sprintf("Error with do request for branch: %s, err: %s, code: %v", branch, err, rsp.StatusCode)) + return err + } + logger.Info(ctx, fmt.Sprintf("Old pr %s successful delete", pull.Source)) + } else { + logger.Debug(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 + } + + return g.RequestOpen(ctx, branch, path, mod) // todo это мне не нравится } + func (g *Gitlab) RequestList(ctx context.Context, branch string) (map[string]string, error) { - return nil, fmt.Errorf("implement me") + logger.Debug(ctx, fmt.Sprintf("RequestList for %s", branch)) + var err error + + g.RepositoryId, err = 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 = GetPulls(ctx, g.URL, g.RepositoryId, branch, g.Password) + if err != nil { + 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 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", password) + return req, err +} + +func 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", 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 { + logger.Error(ctx, fmt.Sprintf("failed to decode response %s err: %v", buf, err)) + return nil, err + } + return pulls, nil + case http.StatusNotFound: + 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 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 { + 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 checkout(w *git.Worktree, ref plumbing.Reference) { + ctx := context.Background() + if err := w.Reset(&git.ResetOptions{Commit: ref.Hash(), Mode: git.HardReset}); err != nil { + logger.Fatal(ctx, fmt.Sprintf("failed to reset: %v", err)) + } }