package main import ( "context" "errors" "fmt" "net/url" "regexp" "strings" "gitea/internal/forgehttp" ) // The forge as this bundle reaches it: its own HTTP interface on the machine, as the forge's admin account // (internal/forgehttp, shared with the npm-registry bundle beside it). // Forge is the forge's HTTP interface. type Forge struct { forgehttp.Client } // ForgeFromEnv is the forge as the manifest hands it to this bundle. func ForgeFromEnv() (*Forge, error) { c, missing := forgehttp.FromEnv() if len(missing) > 0 { return nil, fmt.Errorf("the forge cannot be reached: %s not set by the manifest", strings.Join(missing, ", ")) } c.Timeout = deadline return &Forge{Client: c}, nil } // errNotFound is the forge answering 404. var errNotFound = forgehttp.ErrNotFound // maxBody is the largest JSON answer read from the forge; maxArchive the largest archive code search reads. const ( maxBody = forgehttp.MaxBody maxArchive = 64 << 20 ) // Refused is a refusal of policy or of an argument, in the words the caller sees. type Refused struct{ msg string } func (r *Refused) Error() string { return r.msg } func refuse(format string, a ...any) error { return &Refused{fmt.Sprintf(format, a...)} } var nameShape = regexp.MustCompile(`^[A-Za-z0-9_][A-Za-z0-9_.-]*$`) // repoPath is /api/v1/repos/{owner}/{repo} for names that cannot become another path. func repoPath(owner, repo string) (string, error) { for _, v := range [][2]string{{"owner", owner}, {"name", repo}} { if !nameShape.MatchString(v[1]) || v[1] == "." || v[1] == ".." { return "", refuse("%q is not a repository %s", v[1], v[0]) } } return "/api/v1/repos/" + owner + "/" + repo, nil } // segments encodes a value that is several path segments — a branch like fix/1-x, a file's path — refusing // any segment that could make the request another path: empty, `.` or `..`, or holding `?`, `#`, `%`, `\` // or a control character (the same rule as the TypeScript bundle's pathSegments, guard.ts). func segments(what, value string) (string, error) { parts := strings.Split(value, "/") for i, p := range parts { if p == "" || p == "." || p == ".." || strings.ContainsAny(p, "?#%\\") || strings.IndexFunc(p, func(r rune) bool { return r < 0x20 || r == 0x7f }) >= 0 { return "", refuse("%q is not a %s this tool puts in a path. Nothing was sent", value, what) } parts[i] = url.PathEscape(p) } return strings.Join(parts, "/"), nil } // Repo is what the tools read of a repository. type Repo struct { FullName string `json:"full_name"` DefaultBranch string `json:"default_branch"` Empty bool `json:"empty"` } // Protection is one branch protection rule, as far as covering a branch goes. type Protection struct { RuleName string `json:"rule_name"` BranchName string `json:"branch_name"` } func (p Protection) name() string { if p.RuleName != "" { return p.RuleName } return p.BranchName } // Branch is one branch with the commit it points at. type Branch struct { Name string `json:"name"` Commit struct { ID string `json:"id"` } `json:"commit"` } // branch is the named branch, nil when the forge has none of that name. func (f *Forge) branch(ctx context.Context, repo, name string) (*Branch, error) { seg, err := segments("branch", name) if err != nil { return nil, err } var b Branch if err := f.GetJSON(ctx, repo+"/branches/"+seg, &b); err != nil { if errors.Is(err, errNotFound) { return nil, nil } return nil, err } return &b, nil }