package gitea import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "os" "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" ) 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 { 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 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 nil } } wTitle.Reset() wBody.Reset() 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: plumbing.NewBranchReferenceName(fmt.Sprintf("pkgdash-1/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-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 (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 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) return nil } } if err = repo.DeleteBranch(path); err != nil { logger.Errorf(ctx, "failed to delete the branch: %s", path) return err } logger.Infof(ctx, "Delete branch %s successful", path) 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 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) return nil } } wTitle.Reset() wBody.Reset() 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 }