diff --git a/cmd/pkgdashcli/main.go b/cmd/pkgdashcli/main.go index 20295a3..7ca7486 100644 --- a/cmd/pkgdashcli/main.go +++ b/cmd/pkgdashcli/main.go @@ -265,7 +265,7 @@ func Execute(ctx context.Context, log logger.Logger, gitSource source.SourceCont case "checkupdate": js, err := json.Marshal(mvs) fmt.Println(fmt.Sprintf(`Modules get update: %s, %s`, js, err)) - case "update": + case "open": if cliCfg.Path != "" { // update one dep path = cliCfg.Path if mod, ok = mvs[path]; !ok { @@ -294,6 +294,35 @@ func Execute(ctx context.Context, log logger.Logger, gitSource source.SourceCont log.Debug(ctx, fmt.Sprintf("Update successful for %s", path)) } } + case "update": + if cliCfg.Path != "" { // update one dep + path = cliCfg.Path + if mod, ok = mvs[path]; !ok { + log.Fatal(ctx, fmt.Sprintf("For %s update not exist", path)) + } + log.Debug(ctx, fmt.Sprintf("Start update %s from %s to %s", path, mod.Module.Version, mod.Version)) + for _, branch := range cfg.Branches { + if err := gitSource.RequestUpdate(ctx, branch, path, mod); err != nil { + log.Fatal(ctx, fmt.Sprintf("failed to create pr: %v", err)) + } + } + log.Debug(ctx, fmt.Sprintf("Update successful for %s", path)) + return + } + for _, branch := range cfg.Branches { // update all dep + for path, mod = range mvs { + log.Debug(ctx, fmt.Sprintf("Start update %s from %s to %s", path, mod.Module.Version, mod.Version)) + err := gitSource.RequestUpdate(ctx, branch, path, mod) + if err != nil { + if strings.Contains(err.Error(), "already exists") { + log.Debug(ctx, fmt.Sprintf("skip %s, branch already exists", path)) + continue + } + log.Fatal(ctx, fmt.Sprintf("failed to create pr: %v", err)) + } + log.Debug(ctx, fmt.Sprintf("Update successful for %s", path)) + } + } case "close": if cliCfg.Path != "" { // close one dep path = cliCfg.Path diff --git a/internal/source/gitea/gitea.go b/internal/source/gitea/gitea.go index b2ebe97..374dc26 100644 --- a/internal/source/gitea/gitea.go +++ b/internal/source/gitea/gitea.go @@ -22,7 +22,7 @@ import ( "go.unistack.org/micro/v3/logger" "go.unistack.org/pkgdash/internal/configcli" "go.unistack.org/pkgdash/internal/modules" - gogit "go.unistack.org/pkgdash/internal/source/git" + //gogit "go.unistack.org/pkgdash/internal/source/git" ) var ErrPRNotExist = errors.New("pull request does not exist") @@ -104,134 +104,24 @@ func (g *Gitea) RequestOpen(ctx context.Context, branch string, path string, mod } // открытие гит репозитория с опцией обхода репозитория для нахождения .git - repo, err := gogit.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true}) + 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.Error(ctx, "failed to fetch repo", err) - return err - } // обновляем репозиторий - var headRef *plumbing.Reference - - branches, err := repo.Branches() + wtree, headRef, err := g.fetchCheckout(ctx, repo, branch, path, mod) if err != nil { - g.logger.Error(ctx, "cant get repo branches", err) - return err + g.logger.Error(ctx, "failed to checkout", err) } - for _, ref := range branches { - if ref.Name().Short() == branch { - - headRef = plumbing.NewHashReference(ref.Name(), ref.Hash()) - g.logger.Info(ctx, "headRef set to "+headRef.String()) - break - } - } - - if headRef == nil { - g.logger.Error(ctx, "failed to get repo branch head") - return err - } // Не получили нужную ветку - - g.logger.Info(ctx, "repo head "+headRef.String()) - - wtree, err := repo.Worktree() // todo вроде рабочее дерево не нужно - if err != nil { - g.logger.Error(ctx, "failed to get worktree", err) - return err - } - - g.pulls, err = g.GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Password) - if err != nil && err != ErrPRNotExist { - g.logger.Error(ctx, "GetPulls error", err) - return err - } - - for _, pull := range g.pulls { - if strings.Contains(pull.Title, path) && strings.Contains(pull.Base.Ref, branch) { - 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)) - - wstatus, err := wtree.Status() - if err != nil { - g.logger.Error(ctx, "failed to get worktree status", err) - return err - } - - g.logger.Info(ctx, "worktree status "+wstatus.String()) - /* - g.logger.Info(ctx, "try to reset worktree to "+headRef.Hash().String()) - if err = wtree.Reset(&git.ResetOptions{Commit: headRef.Hash(), Mode: git.HardReset}); err != nil { - g.logger.Error(ctx, "failed to reset repo branch to "+headRef.Hash().String(), err) - return 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 { - g.logger.Error(ctx, fmt.Sprintf("failed to pull repo: %v", err)) // подтягиваем изменения с удаленого репозитория - return err - } - - g.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 && err != git.ErrBranchExists { - g.logger.Error(ctx, fmt.Sprintf("failed to checkout tree: %v", err)) - return err - } // создаем новую ветку - defer func() { _ = g.checkout(wtree, headRef) }() - 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.Error(ctx, fmt.Sprintf("failed to run go mod edit: %s err: %v", out, err)) + if err = g.scopeUpdateDep(ctx, path, mod); err != nil { return 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.Error(ctx, fmt.Sprintf("failed to run go mod edit: %s err: %v", out, err)) - return err - } // пытаемся выполнить команду go mod edit с новой версией модуля - - cmd = exec.CommandContext(ctx, epath, "mod", "tidy") - if out, err = cmd.CombinedOutput(); err != nil { - g.logger.Error(ctx, fmt.Sprintf("failed to run go mod tidy: %s err: %v", out, err)) - return err - } // пытаемся выполнить команду go mod tidy пытаемся подтянуть новую версию модуля - g.logger.Info(ctx, "worktree add go.mod") if err = wtree.AddWithOptions(&git.AddOptions{Path: "go.mod"}); err != nil { g.logger.Error(ctx, fmt.Sprintf("failed to add file: %v", err)) @@ -271,40 +161,12 @@ func (g *Gitea) RequestOpen(ctx context.Context, branch string, path string, mod return err } // пытаемся за пушить изменения - body := map[string]string{ - "base": branch, - "body": wBody.String(), - "head": fmt.Sprintf("pkgdash/go_modules/%s-%s", path, mod.Version), - "title": wTitle.String(), - } - g.logger.Info(ctx, fmt.Sprintf("raw body: %#+v", body)) - - buf, err = json.Marshal(body) + rsp, err := g.postPullRequest(ctx, wBody, wTitle, branch, path, mod) if err != nil { - g.logger.Error(ctx, "failed to marshal", err) return err } - g.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 { - g.logger.Error(ctx, "http request error", err) - 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 { - g.logger.Error(ctx, "failed to call http request", err) - return err - } // Вроде создаем новый реквест на создание пулл реквеста + // Вроде создаем новый реквест на создание пулл реквеста if rsp.StatusCode != http.StatusCreated { buf, _ = io.ReadAll(rsp.Body) return fmt.Errorf("unknown error: %s", buf) @@ -365,27 +227,20 @@ func (g *Gitea) RequestUpdate(ctx context.Context, branch string, path string, m } prExist := false + var pullId int64 + var targetBranch plumbing.ReferenceName + for _, pull := range g.pulls { if strings.Contains(pull.Title, path) && pull.Base.Ref == branch { g.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 := g.DeleteBranch(ctx, g.URL, g.Owner, g.Repository, pull.Head.Ref, 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.Head.Ref)) - } else { + if !modules.IsNewerVersion(tVersion, mod.Version, false) { g.logger.Debug(ctx, "The existing PR is relevant") return nil } prExist = true + pullId = pull.ID + targetBranch = plumbing.ReferenceName(pull.Head.Ref) } } if !prExist { @@ -393,7 +248,103 @@ func (g *Gitea) RequestUpdate(ctx context.Context, branch string, path string, m return ErrPRNotExist } - return g.RequestOpen(ctx, branch, path, mod) + // создания шаблона названия для пулл реквеста + 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.Error(ctx, "failed to execute template", err) + return err + } + if err = tplBody.Execute(wBody, data); err != nil { + g.logger.Error(ctx, "failed to execute template", err) + return 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)) + } + + wtree, headRef, err := g.fetchCheckout(ctx, repo, targetBranch.Short(), path, mod) + if err != nil { + g.logger.Error(ctx, "failed to checkout", err) + return err + } + + defer func() { + _ = g.checkout(wtree, headRef) + }() + + if err = g.scopeUpdateDep(ctx, path, mod); err != nil { + return err + } + + g.logger.Info(ctx, "worktree add go.mod") + if err = wtree.AddWithOptions(&git.AddOptions{Path: "go.mod"}); err != nil { + g.logger.Error(ctx, fmt.Sprintf("failed to add file: %v", err)) + return err + } + + g.logger.Info(ctx, "worktree add go.sum") + if err = wtree.AddWithOptions(&git.AddOptions{Path: "go.sum"}); err != nil { + g.logger.Error(ctx, fmt.Sprintf("failed to add file: %v", err)) + return 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.Error(ctx, fmt.Sprintf("failed to commit: %v", err)) + return 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.Error(ctx, "failed to push repo branch", err) + return err + } // пытаемся за пушить изменения + + err = g.patchPullRequest(ctx, wBody, wTitle, pullId) + if err != nil { + return err + } + + g.logger.Info(ctx, fmt.Sprintf("PR update for %s-%s", path, mod.Version)) + + return nil } func (g *Gitea) RequestList(ctx context.Context, branch string) (map[string]string, error) { @@ -488,9 +439,9 @@ func (g *Gitea) GetPulls(ctx context.Context, url, owner, repo, password string) return pullsAll, nil } -func (g *Gitea) checkout(w gogit.Worktree, ref *plumbing.Reference) error { +func (g *Gitea) checkout(w *git.Worktree, ref *plumbing.Reference) error { ctx := context.Background() - g.logger.Debug(ctx, "checkout: "+ref.Name().Short()) + g.logger.Debug(ctx, "checkout: "+ref.String()) if err := w.Checkout(&git.CheckoutOptions{ Branch: ref.Name(), @@ -503,3 +454,223 @@ func (g *Gitea) checkout(w gogit.Worktree, ref *plumbing.Reference) error { } return nil } + +func (g Gitea) fetchCheckout(ctx context.Context, repo *git.Repository, branch, path string, mod modules.Update) (*git.Worktree, *plumbing.Reference, error) { + // обновляем ветки + 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.Error(ctx, "failed to fetch repo", err) + return nil, nil, err + } + + var headRef *plumbing.Reference + + branches, err := repo.Branches() + if err != nil { + g.logger.Error(ctx, "cant get repo branch", err) + return nil, nil, err + } + + for { + ref, err := branches.Next() + if err != nil { + return nil, nil, err + } + if ref.Name().Short() == branch { + //Получаем ссылку на нужную ветку + headRef = ref + g.logger.Info(ctx, "headRef set to "+headRef.String()) + break + } + } + + if headRef == nil { + g.logger.Error(ctx, "failed to get repo branch head") + return nil, nil, err + } // Не получили нужную ветку + + g.logger.Info(ctx, "repo head "+headRef.String()) + + wtree, err := repo.Worktree() + if err != nil { + g.logger.Error(ctx, "failed to get worktree", err) + return nil, nil, err + } + + if err = wtree.Reset(&git.ResetOptions{ + Mode: git.HardReset, + }); err != nil { + g.logger.Error(ctx, "reset work_tree error: ", err) + return nil, nil, err + } + + g.pulls, err = g.GetPulls(ctx, g.URL, g.Owner, g.Repository, g.Password) + if err != nil && err != ErrPRNotExist { + g.logger.Error(ctx, "GetPulls error", err) + return nil, nil, err + } + + var pullExist bool + for _, pull := range g.pulls { + if strings.Contains(pull.Title, path) && (strings.Contains(pull.Base.Ref, branch) || strings.Contains(pull.Head.Ref, branch)) { + pullExist = true + } // хотим проверить есть ли пулл реквест для этой ветки, если есть то выходим + } + + g.logger.Info(ctx, fmt.Sprintf("update %s from %s to %s", path, mod.Module.Version, mod.Version)) + + wstatus, err := wtree.Status() + if err != nil { + g.logger.Error(ctx, "failed to get worktree status", err) + return nil, nil, err + } + + g.logger.Info(ctx, "worktree status "+wstatus.String()) + + if err = wtree.PullContext(ctx, &git.PullOptions{ + Auth: &httpauth.BasicAuth{Username: g.Username, Password: g.Password}, + // Depth: 1, + // RemoteURL : + ReferenceName: headRef.Name(), + Force: true, + RemoteName: "origin", + }); err != nil && err != git.NoErrAlreadyUpToDate { + g.logger.Error(ctx, fmt.Sprintf("failed to pull repo: %v", err)) // подтягиваем изменения с удаленого репозитория + return nil, nil, err + } + + g.logger.Info(ctx, fmt.Sprintf("checkout ref %s", headRef)) + + if pullExist { + if err = wtree.Checkout(&git.CheckoutOptions{ + Branch: headRef.Name(), + Create: false, + Force: true, + }); err != nil && err != git.ErrBranchExists { + g.logger.Error(ctx, fmt.Sprintf("failed to checkout tree: %v", err)) + return nil, nil, err + } //переходим на существующею + } else { + 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 && err != git.ErrBranchExists { + g.logger.Error(ctx, fmt.Sprintf("failed to checkout tree: %v", err)) + return nil, nil, err + } // создаем новую ветку + } + return wtree, headRef, nil +} + +func (g *Gitea) postPullRequest(ctx context.Context, wBody, wTitle *bytes.Buffer, branch, path string, mod modules.Update) (*http.Response, error) { + body := map[string]string{ + "base": branch, + "body": wBody.String(), + "head": fmt.Sprintf("pkgdash/go_modules/%s-%s", path, mod.Version), + "title": wTitle.String(), + } + g.logger.Info(ctx, fmt.Sprintf("raw body: %#+v", body)) + + buf, err := json.Marshal(body) + if err != nil { + g.logger.Error(ctx, "failed to marshal", err) + return nil, err + } + + g.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 { + g.logger.Error(ctx, "http request error", err) + return nil, 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 { + g.logger.Error(ctx, "failed to call http request", err) + return rsp, err + } + + return rsp, nil +} + +func (g *Gitea) patchPullRequest(ctx context.Context, wBody, wTitle *bytes.Buffer, indexPR int64) error { + body := map[string]string{ + "body": wBody.String(), + "title": wTitle.String(), + } + g.logger.Info(ctx, fmt.Sprintf("raw body: %#+v", body)) + + buf, err := json.Marshal(body) + if err != nil { + g.logger.Error(ctx, "failed to marshal", err) + return err + } + + g.logger.Info(ctx, fmt.Sprintf("marshal body: %s", buf)) + req, err := http.NewRequestWithContext( + ctx, + http.MethodPatch, + fmt.Sprintf("https://%s/api/v1/repos/%s/%s/pulls/%d", g.URL, g.Owner, g.Repository, indexPR), + bytes.NewReader(buf), + ) + if err != nil { + g.logger.Error(ctx, "http request error", err) + return err + } + req.Header.Add("Accept", "application/json") + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", "Bearer "+g.Password) + + _, err = http.DefaultClient.Do(req) + if err != nil { + g.logger.Error(ctx, "failed to call http request", err) + return err + } + + return nil +} + +func (g *Gitea) scopeUpdateDep(ctx context.Context, path string, mod modules.Update) error { + 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.Error(ctx, fmt.Sprintf("failed to run go mod edit: %s err: %v", out, err)) + return 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.Error(ctx, fmt.Sprintf("failed to run go mod edit: %s err: %v", out, err)) + return err + } // пытаемся выполнить команду go mod edit с новой версией модуля + + cmd = exec.CommandContext(ctx, epath, "mod", "tidy") + if out, err = cmd.CombinedOutput(); err != nil { + g.logger.Error(ctx, fmt.Sprintf("failed to run go mod tidy: %s err: %v", out, err)) + return err + } // пытаемся выполнить команду go mod tidy пытаемся подтянуть новую версию модуля + + return nil +}