gitlink-cli/internal/web/url_builder.go

204 lines
7.8 KiB
Go

// Package web constructs GitLink web page URLs and opens browsers.
//
// It is the single source of truth for the "terminal ↔ web" mapping used by
// the `show`, `browse`, and `--web` features. Every CLI command that needs a
// GitLink web URL must go through Builder instead of hand-rolling string
// concatenation, so the URL pattern table lives in exactly one place.
package web
import (
"fmt"
"net/url"
"strings"
)
// DefaultBaseURL is the canonical GitLink front-end host.
const DefaultBaseURL = "https://gitlink.org.cn"
// ResourceURL represents a resolvable GitLink web resource.
//
// The JSON tags deliberately match the keys the demo script and AI Agents
// expect (html_url / resource / show_command), so a Builder result can be
// wrapped straight into an output envelope.
type ResourceURL struct {
URL string `json:"html_url"`
Resource string `json:"resource"`
Identifier string `json:"identifier,omitempty"`
CLICommand string `json:"show_command,omitempty"`
}
// Builder turns (resource type, identifiers) into GitLink web URLs.
//
// Builder never resolves owner/repo itself — callers pass them in, normally
// obtained from internal/context.ResolveOwnerRepo. This keeps Builder a pure
// formatter with no filesystem or git dependency.
type Builder struct {
BaseURL string
}
// NewBuilder returns a Builder pointed at DefaultBaseURL.
func NewBuilder() *Builder { return &Builder{BaseURL: DefaultBaseURL} }
// join concatenates the base URL with a path, tolerating missing/extra slashes.
func (b *Builder) join(path string) string {
base := strings.TrimRight(b.BaseURL, "/")
if path == "" {
return base
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return base + path
}
// make assembles a ResourceURL from its parts.
func (b *Builder) make(resource, path, identifier, cli string) *ResourceURL {
return &ResourceURL{
URL: b.join(path),
Resource: resource,
Identifier: identifier,
CLICommand: cli,
}
}
// repoBase returns the "/owner/repo" prefix shared by most URLs.
func repoBase(owner, repo string) string { return fmt.Sprintf("/%s/%s", owner, repo) }
// --- Repository-scoped resources -------------------------------------------
// RepoURL is the repository home page.
func (b *Builder) RepoURL(owner, repo string) *ResourceURL {
return b.make("repo", repoBase(owner, repo), "",
fmt.Sprintf("gitlink-cli show repo --owner %s --repo %s", owner, repo))
}
// IssueURL is a specific issue page (number > 0) or the issue list.
func (b *Builder) IssueURL(owner, repo string, number int) *ResourceURL {
if number <= 0 {
return b.make("issue", repoBase(owner, repo)+"/issues", "",
fmt.Sprintf("gitlink-cli show repo --owner %s --repo %s --tab issues", owner, repo))
}
return b.make("issue",
fmt.Sprintf("%s/issues/%d", repoBase(owner, repo), number),
fmt.Sprintf("#%d", number),
fmt.Sprintf("gitlink-cli show issue --owner %s --repo %s --number %d", owner, repo, number))
}
// PRURL is a specific pull request page (number > 0) or the PR list.
func (b *Builder) PRURL(owner, repo string, number int) *ResourceURL {
if number <= 0 {
return b.make("pr", repoBase(owner, repo)+"/pulls", "",
fmt.Sprintf("gitlink-cli show repo --owner %s --repo %s --tab pulls", owner, repo))
}
return b.make("pr",
fmt.Sprintf("%s/pulls/%d", repoBase(owner, repo), number),
fmt.Sprintf("#%d", number),
fmt.Sprintf("gitlink-cli show pr --owner %s --repo %s --number %d", owner, repo, number))
}
// WikiURL is a specific wiki page (page != "") or the wiki index.
func (b *Builder) WikiURL(owner, repo, page string) *ResourceURL {
if page == "" {
return b.make("wiki", repoBase(owner, repo)+"/wiki", "",
fmt.Sprintf("gitlink-cli show wiki --owner %s --repo %s", owner, repo))
}
encoded := url.PathEscape(page)
return b.make("wiki",
fmt.Sprintf("%s/wiki/%s", repoBase(owner, repo), encoded),
page,
fmt.Sprintf("gitlink-cli show wiki --owner %s --repo %s --page %s", owner, repo, page))
}
// MemberURL is the repository collaborator settings page.
func (b *Builder) MemberURL(owner, repo string) *ResourceURL {
return b.make("member", repoBase(owner, repo)+"/settings/collaboration", "",
fmt.Sprintf("gitlink-cli show member --owner %s --repo %s", owner, repo))
}
// WebhookURL is the repository webhook settings page.
func (b *Builder) WebhookURL(owner, repo string) *ResourceURL {
return b.make("webhook", repoBase(owner, repo)+"/settings/hooks", "",
fmt.Sprintf("gitlink-cli show webhook --owner %s --repo %s", owner, repo))
}
// LabelURL is the repository issue-labels management page.
func (b *Builder) LabelURL(owner, repo string) *ResourceURL {
return b.make("label", repoBase(owner, repo)+"/issues/labels", "",
fmt.Sprintf("gitlink-cli show label --owner %s --repo %s", owner, repo))
}
// MilestoneURL is the repository milestones page.
func (b *Builder) MilestoneURL(owner, repo string) *ResourceURL {
return b.make("milestone", repoBase(owner, repo)+"/milestones", "",
fmt.Sprintf("gitlink-cli show milestone --owner %s --repo %s", owner, repo))
}
// BranchURL is the branches list, or a specific branch page when branch != "".
func (b *Builder) BranchURL(owner, repo, branch string) *ResourceURL {
if branch == "" {
return b.make("branch", repoBase(owner, repo)+"/branches", "",
fmt.Sprintf("gitlink-cli show branch --owner %s --repo %s", owner, repo))
}
return b.make("branch",
fmt.Sprintf("%s/branches/%s", repoBase(owner, repo), url.PathEscape(branch)),
branch,
fmt.Sprintf("gitlink-cli show branch --owner %s --repo %s --branch %s", owner, repo, branch))
}
// ReleaseURL is the releases list, or a specific release when tag != "".
func (b *Builder) ReleaseURL(owner, repo, tag string) *ResourceURL {
if tag == "" {
return b.make("release", repoBase(owner, repo)+"/releases", "",
fmt.Sprintf("gitlink-cli show release --owner %s --repo %s", owner, repo))
}
return b.make("release",
fmt.Sprintf("%s/releases/%s", repoBase(owner, repo), url.PathEscape(tag)),
tag,
fmt.Sprintf("gitlink-cli show release --owner %s --repo %s --tag %s", owner, repo, tag))
}
// CommitURL is a specific commit page.
func (b *Builder) CommitURL(owner, repo, sha string) *ResourceURL {
if sha == "" {
return b.make("commit", repoBase(owner, repo)+"/commits", "",
fmt.Sprintf("gitlink-cli show repo --owner %s --repo %s --tab commits", owner, repo))
}
return b.make("commit",
fmt.Sprintf("%s/commits/%s", repoBase(owner, repo), url.PathEscape(sha)),
sha,
fmt.Sprintf("gitlink-cli show commit --owner %s --repo %s --sha %s", owner, repo, sha))
}
// CIURL is the repository CI/Actions page.
func (b *Builder) CIURL(owner, repo string) *ResourceURL {
return b.make("ci", repoBase(owner, repo)+"/actions", "",
fmt.Sprintf("gitlink-cli show ci --owner %s --repo %s", owner, repo))
}
// CompareURL is the branch/tag/commit comparison page.
func (b *Builder) CompareURL(owner, repo, base, head string) *ResourceURL {
path := fmt.Sprintf("%s/compare/%s...%s", repoBase(owner, repo), url.PathEscape(base), url.PathEscape(head))
return b.make("compare", path,
fmt.Sprintf("%s...%s", base, head),
fmt.Sprintf("gitlink-cli show compare --owner %s --repo %s --base %s --head %s", owner, repo, base, head))
}
// --- Global resources (no owner/repo) --------------------------------------
// NotificationURL is the platform notification center.
func (b *Builder) NotificationURL() *ResourceURL {
return b.make("notification", "/notifications", "", "gitlink-cli show notification")
}
// OrgURL is an organization page.
func (b *Builder) OrgURL(org string) *ResourceURL {
return b.make("org", fmt.Sprintf("/%s", org), org,
fmt.Sprintf("gitlink-cli show org --name %s", org))
}
// UserURL is a user profile page.
func (b *Builder) UserURL(login string) *ResourceURL {
return b.make("user", fmt.Sprintf("/%s", login), login,
fmt.Sprintf("gitlink-cli show user --login %s", login))
}