package forge import ( "context" "fmt" "net/url" "strings" "code.gitea.io/sdk/gitea" ) // Gitea is a Provider backed by a Gitea/Forgejo instance. It also decides which // repos it serves: only those whose remote host matches its base URL. type Gitea struct { host string client *gitea.Client } // NewGitea builds a Gitea provider from a base URL and token. It returns // (nil, nil) when not configured (either value empty) so forge features simply // stay absent (§8.4 graceful degradation). func NewGitea(baseURL, token string) (*Gitea, error) { baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/") token = strings.TrimSpace(token) if baseURL == "" || token == "" { return nil, nil } u, err := url.Parse(baseURL) if err != nil || u.Hostname() == "" { return nil, fmt.Errorf("invalid GITEA_URL %q", baseURL) } c, err := gitea.NewClient(baseURL, gitea.SetToken(token)) if err != nil { return nil, err } return &Gitea{host: u.Hostname(), client: c}, nil } // Handles reports whether a remote host is this Gitea instance. func (g *Gitea) Handles(host string) bool { return strings.EqualFold(host, g.host) } // ListPullRequests lists the open PRs of owner/repo. func (g *Gitea) ListPullRequests(_ context.Context, owner, repo string) ([]PullRequest, error) { prs, _, err := g.client.ListRepoPullRequests(owner, repo, gitea.ListPullRequestsOptions{State: gitea.StateOpen}) if err != nil { return nil, err } out := make([]PullRequest, 0, len(prs)) for _, p := range prs { out = append(out, toPR(p)) } return out, nil } // MergeAndCleanup merges the PR and deletes its head branch (when the head is in // the same repo — never a fork's branch). Callers MUST have confirmed with the // user first (§1.4). func (g *Gitea) MergeAndCleanup(_ context.Context, owner, repo string, number int64, method MergeMethod) (MergeResult, error) { pr, _, err := g.client.GetPullRequest(owner, repo, number) if err != nil { return MergeResult{}, err } branch := "" if pr.Head != nil { branch = pr.Head.Ref } sameRepo := pr.Head != nil && pr.Base != nil && pr.Head.RepoID == pr.Base.RepoID del := sameRepo && branch != "" merged, _, err := g.client.MergePullRequest(owner, repo, number, gitea.MergePullRequestOption{ Style: toStyle(method), DeleteBranchAfterMerge: &del, }) if err != nil { return MergeResult{}, err } res := MergeResult{Number: number, Merged: merged, Branch: branch, BranchDeleted: del && merged} // Best-effort fallback in case the merge option didn't delete the branch // (older Gitea). Ignore the error — the branch may already be gone. if merged && del { _, _, _ = g.client.DeleteRepoBranch(owner, repo, branch) } return res, nil } func toPR(p *gitea.PullRequest) PullRequest { pr := PullRequest{ Number: p.Index, Title: p.Title, Draft: p.Draft, Mergeable: p.Mergeable, URL: p.HTMLURL, } if p.Poster != nil { pr.Author = p.Poster.UserName } if p.Head != nil { pr.Head = p.Head.Ref } if p.Base != nil { pr.Base = p.Base.Ref } if p.Head != nil && p.Base != nil { pr.SameRepo = p.Head.RepoID == p.Base.RepoID } return pr } func toStyle(m MergeMethod) gitea.MergeStyle { switch m { case MergeMerge: return gitea.MergeStyleMerge case MergeRebase: return gitea.MergeStyleRebase default: return gitea.MergeStyleSquash } }