forge

package module
v0.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 25, 2026 License: MIT Imports: 10 Imported by: 0

README

forge

Parse repository references - URLs or shorthands - into their coordinates: host, owner, name, branch, tag, commit, pull request, file path. Pure and offline; no network calls anywhere.

Knows the URL shapes of GitHub, GitLab, Sourcehut, Codeberg/Gitea, Bitbucket, and Azure DevOps, and falls back to a generic owner/repo reading for self-hosted forges.

Install

go get github.com/gechr/forge

Usage

ref, err := forge.Parse("https://github.com/gechr/forge/pull/42")
// Ref{Host: "github.com", Owner: "gechr", Name: "forge", PullRequest: "42",
//     Forge: "github", Scheme: "https", CloneURL: "https://github.com/gechr/forge.git", ...}

ref, err = forge.Parse("forge#42", forge.WithDefaultOwner("gechr"))
// Ref{Owner: "gechr", Name: "forge", PullRequest: "42"}

ref, err = forge.Parse("gechr/forge@v1.2.3")
// Ref{Owner: "gechr", Name: "forge", Rev: "v1.2.3"} - branch or tag; the
// consumer resolves Rev against the remote. A 40-hex rev lands in Commit.

ref, ok := forge.ParseURL("git@gitlab.com:group/subgroup/repo.git")
// Ref{Owner: "group/subgroup", Name: "repo"} - GitLab nests projects in groups
// arbitrarily deep, so every segment but the last is the owner.
// ParseURL is also a probe: ok reports whether the input was a URL at all.

ref.Slug()     // "owner/name"
ref.WebURL()   // "https://<host>/owner/name"
ref.SSHURL()   // "git@<host>:owner/name.git"
ref.HTTPSURL() // "https://<host>/owner/name.git"
Accepted forms
  • URL (per git-clone(1)): https://, ssh://, git://, scp-like git@host:path, bare host github.com/owner/repo
  • Shorthand: repo, owner/repo, owner/repo#42, owner/repo@rev

API

Symbol Description
Parse(input, opts...) Full grammar: URL or shorthand
ParseURL(raw) URL forms only; (Ref, bool) probe
ParseShorthand(s, opts...) repo | owner/repo | owner/repo#42 | owner/repo@rev
WithDefaultOwner(owner) Owner assumed when shorthand omits one
WithDefaultHost(host) Host recorded on shorthand results
Resolve(value) Forge by name (github, ...) or hostname; "" defaults to GitHub
Register(forge) Add a self-hosted forge with its own parser
ExpandPRList(spec) "1,2,5-7"[1 2 5 6 7]
IsValidRepoName(name) Plausible repository name check
DetectVCS(dir) Alias for vcs.Detect, below

Checkout inspection - forge/vcs

Inspects an existing checkout from marker files alone - no subprocess, no repository opened. Driving a VCS is deliberately out of scope.

which := vcs.Detect(dir)   // "jj" | "git" | "" - what drives this checkout

r := vcs.NewResolver()
root := r.Root("src/main.go")     // repository the file belongs to, or ""
root, which = r.RootVCS(dir)      // both at once, for a nested path

rel, err := filepath.Rel(r.Root(p), r.Abs(p))  // path within its repository
Symbol Description
Detect(dir) "jj" | "git" | "" from dir's own markers (jj wins if colocated)
NewResolver() Resolver caching each directory it walks; safe for concurrent use
(*Resolver).Root(p) Repository containing the file p, or ""
(*Resolver).RootDir(d) Repository containing d, searching d itself first
(*Resolver).RootVCS(d) (root, vcs) for d - the safe pairing of RootDir and Detect
(*Resolver).Abs(p) p absolute and symlink-resolved, as the resolver keys on it
Markers() .jj, .git, .hg, .svn - build walk-pruning sets from this
Behaviour
  • Two questions, two marker sets. Detect reports what drives a checkout - git and jj only, so .hg and .svn yield "" rather than a name no consumer can act on. Resolver reports where a repository begins, and all four markers delimit one.
  • Markers are Lstated. A .git file (submodule, linked worktree) counts, as does a dangling symlink.
  • Paths are canonicalised before walking, so one repository has one root however the caller spells the way to it.
  • Pair Root with Abs, not filepath.Abs - the latter leaves symlinks in place, so the path no longer sits under the root and filepath.Rel escapes it.
  • Prefer RootVCS to Detect(r.RootDir(d)) - RootDir yields "" outside a repository, and Detect("") would inspect the process working directory.

Extraction rules

  • Branch is set only when unambiguous - a trailing file path could belong to a slash-containing branch name - so .../tree/main sets it and .../tree/main/src/foo.go does not.
  • Bitbucket src/<ref> does not name its ref type: 40-hex lands in Commit, anything else in Rev.
  • Gitea src/branch|tag|commit/<value> names its ref type explicitly, and is extracted as such.
  • Bare hosts need a dot, so owner/repo is shorthand rather than a URL, and localhost needs an explicit scheme.
  • GitLab groups nest arbitrarily deep, so the project path ends at the first action segment (-, tree, blob, merge_requests, ...) rather than at a fixed depth: group/subgroup/repo is all project path, while owner/repo/tree/main and the current /-/ form both stop at repo. An action word in the first two positions is a project named after one.
  • Azure DevOps keeps org/project in Owner, so the Ref is lossless. Its _git segment and separate SSH host cannot be expressed as host/owner/name, so WebURL, HTTPSURL, and SSHURL return "" rather than a URL that has never existed - use CloneURL.

Documentation

Overview

Package forge parses repository references into their coordinates. A reference can be a full URL in any of the forms git-clone(1) accepts (https, ssh, scp-like, git, bare hostname) or a shorthand (repo, owner/repo, owner/repo#42, owner/repo@rev), and parsing yields the host, owner, name, and any branch, tag, commit, pull request, or file path the reference carries. The URL shapes of GitHub, GitLab, Sourcehut, Codeberg, Bitbucket, and Azure DevOps are known; anything else falls back to a generic owner/repo reading, so self-hosted forges work out of the box. Everything here is pure and offline - no network calls; resolving what an ambiguous reference points at (e.g. Ref.Rev) is the caller's job.

Index

Constants

View Source
const (
	ForgeAzureDevOps = "azuredevops"
	ForgeBitbucket   = "bitbucket"
	ForgeCodeberg    = "codeberg"
	ForgeGitHub      = "github"
	ForgeGitLab      = "gitlab"
	ForgeSourcehut   = "sourcehut"
)

Named forges recognized by Resolve and reported in Ref.Forge. Azure DevOps is deliberately not a Resolve name - its clone URLs embed an org/project path that owner/name construction cannot express - but dev.azure.com URLs are still recognized by ParseURL.

View Source
const (
	HostAzureDevOps = "dev.azure.com"
	HostBitbucket   = "bitbucket.org"
	HostCodeberg    = "codeberg.org"
	HostGitHub      = "github.com"
	HostGitLab      = "gitlab.com"
	HostSourcehut   = "git.sr.ht"
)

Canonical hostnames of the built-in forges.

View Source
const (
	SchemeGit   = "git"
	SchemeHTTPS = "https"
	SchemeSSH   = "ssh"
)

Transports reported in Ref.Scheme. HTTP inputs are normalized to HTTPS.

View Source
const (
	VCSGit = vcs.Git
	VCSJJ  = vcs.JJ
)

VCS names returned by DetectVCS, aliasing the vcs package's own so the two spellings cannot drift.

Variables

This section is empty.

Functions

func DetectVCS

func DetectVCS(dir string) string

DetectVCS inspects an existing checkout to decide which VCS drives it, returning VCSJJ, VCSGit, or "" when neither marker is present. A `.jj` directory takes precedence over `.git`, which covers both jj layouts: colocated repos have a top-level `.git` alongside `.jj` and jj should own the working copy, while non-colocated repos have no top-level `.git` at all (their git store lives inside `.jj/repo/store/git`).

It delegates to vcs.Detect; reach for the vcs package directly when you also need vcs.Resolver to find the root a nested path belongs to.

func ExpandPRList

func ExpandPRList(spec string) ([]int, error)

ExpandPRList expands a PR list like "1,2,5-7" into []int{1, 2, 5, 6, 7}. Ranges are inclusive on both ends; duplicates are dropped; negative or zero numbers are rejected.

func IsValidRepoName

func IsValidRepoName(name string) bool

IsValidRepoName reports whether name is a plausible repository name: ASCII letters, digits, '-', '_', and '.', at most 255 bytes (the common filesystem NAME_MAX for a single path component), and not "." or "..".

func Register

func Register(f Forge)

Register adds a forge to the registry consulted by Resolve and ParseURL, replacing any existing entry with the same name or host. It lets consumers wire up self-hosted forges (e.g. an internal Gitea) whose URL shapes differ from the generic owner/repo fallback.

Types

type Forge

type Forge struct {
	// GitSuffix reports whether the forge's clone URLs end in ".git".
	// Sourcehut is the only built-in forge whose URLs do not.
	GitSuffix bool
	// Host is the forge's canonical hostname.
	Host string
	// Name is the identifier Resolve accepts and Ref.Forge reports.
	Name string
	// Parse extracts a Ref from a URL path already split from its scheme
	// and host. It is a plain field rather than an interface method so that
	// registering a self-hosted forge stays a one-liner.
	Parse func(path, scheme, host string) (Ref, bool)
}

Forge describes how to recognize and construct URLs for a single forge.

func Resolve

func Resolve(value string) (Forge, error)

Resolve resolves a forge identifier - a registered name or a hostname, case-insensitively and with surrounding whitespace ignored - to its Forge. An empty value defaults to GitHub, and any unregistered value containing a dot is accepted as a self-hosted host with generic owner/repo parsing.

type Option

type Option func(*options)

Option configures Parse and ParseShorthand.

func WithDefaultHost

func WithDefaultHost(host string) Option

WithDefaultHost sets the Host recorded on shorthand results, so a GitLab-first consumer can flip the default away from GitHub. It does not affect URL parsing, and when unset shorthand results carry an empty Host.

func WithDefaultOwner

func WithDefaultOwner(owner string) Option

WithDefaultOwner sets the owner assumed when a shorthand reference omits one ("repo" rather than "owner/repo").

type Ref

type Ref struct {
	// Branch is the branch name from URLs like .../tree/<branch> (GitHub,
	// GitLab, Sourcehut) or .../src/branch/<branch> (Gitea). It is only
	// set when the branch is unambiguous: with a trailing file path a
	// slash-containing branch name cannot be told apart from the path, so
	// forms like .../tree/<branch>/<path> leave Branch empty.
	Branch string
	// CloneURL is the clone URL derived from the reference, preserving the
	// transport the reference used. Empty for shorthand references.
	CloneURL string
	// Commit is a full 40-hex commit hash from URLs like .../commit/<sha>
	// or .../src/commit/<sha>, or from @<sha> shorthand.
	Commit string
	// ExplicitOwner reports whether the reference named its owner, as
	// opposed to inheriting WithDefaultOwner.
	ExplicitOwner bool
	// FilePath is the sub-path from URLs like .../blob/<ref>/<path>,
	// .../tree/<ref>/<path>, or .../src/<ref>/<path>, assuming a
	// single-segment ref.
	FilePath string
	// Forge is the forge that recognized the reference: a registered name
	// (github, gitlab, ...) or the bare hostname for unrecognized hosts.
	// Empty for shorthand references.
	Forge string
	// Host is the canonical hostname, lowercased with any "www." prefix
	// stripped. Empty for shorthand references unless WithDefaultHost is
	// given.
	Host string
	// Name is the repository name with any ".git" suffix stripped.
	Name string
	// Owner is the user, organization, or (for GitLab) nested group path
	// that owns the repository.
	Owner string
	// PullRequest is the pull/merge request number from URLs like
	// .../pull/<n> or the #<n> shorthand. It is kept as a string because
	// parsing does not validate it - consumers decide how strict to be.
	PullRequest string
	// Rev is an ambiguous revision from @<rev> shorthand or from URLs like
	// Bitbucket's .../src/<ref>: it may name a branch or a tag, and only
	// the remote can tell, so Branch and Tag are left empty. A 40-hex rev
	// is classified as Commit instead.
	Rev string
	// Scheme is the transport the reference used: https, ssh, or git.
	// Empty for shorthand references.
	Scheme string
	// Tag is the tag name from URLs like .../releases/tag/<tag> or
	// .../src/tag/<tag>.
	Tag string
}

Ref is the resolved coordinates of a repository reference.

func Parse

func Parse(input string, opts ...Option) (Ref, error)

Parse resolves a repository reference: a full URL in any form ParseURL accepts, or failing that a shorthand in any form ParseShorthand accepts. Destination suffixes (e.g. clone's "=<dir>") are not part of the grammar - strip them before calling.

func ParseShorthand

func ParseShorthand(input string, opts ...Option) (Ref, error)

ParseShorthand parses a shorthand repository reference:

repo
owner/repo
owner/repo#42          (pull request)
owner/repo@main        (branch or tag - see Ref.Rev)
owner/repo@v1.2.3
owner/repo@<sha>       (40-hex commit)

func ParseURL

func ParseURL(raw string) (Ref, bool)

ParseURL attempts to parse a full repository URL. It is a probe rather than a validator - callers legitimately try it and fall through to shorthand parsing - so failure is reported as ok=false, not an error.

Supported URL forms (per git-clone(1)):

ssh://[user@]host[:port]/path
[user@]host:path              (scp-like)
http[s]://host[:port]/path
git://host[:port]/path
host/path                     (bare hostname)

func (Ref) HTTPSURL

func (r Ref) HTTPSURL() string

HTTPSURL returns the HTTPS clone URL for the repository, or "" when it cannot be built - see [Ref.constructable]. Prefer CloneURL when the Ref came from ParseURL: it preserves the exact path the reference carried.

func (Ref) SSHURL

func (r Ref) SSHURL() string

SSHURL returns the SSH clone URL for the repository, or "" when it cannot be built - see [Ref.constructable].

func (Ref) Slug

func (r Ref) Slug() string

Slug returns "owner/name".

func (Ref) WebURL

func (r Ref) WebURL() string

WebURL returns the browsable web URL for the repository, or "" when the forge's URLs cannot be built from host/owner/name - see [Ref.constructable]. An empty Host defaults to GitHub.

Directories

Path Synopsis
Package vcs inspects an existing checkout: which version control system drives it, and where the repository containing a given path begins.
Package vcs inspects an existing checkout: which version control system drives it, and where the repository containing a given path begins.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL