forge

package module
v0.0.2 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

A pure, offline Go library that parses repository references - full URLs or shorthands - into their coordinates: host, owner, name, branch, tag, commit, pull request, and file path. It 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. No network calls anywhere.

Installation

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")
// ParseURL is 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 URL forms (per git-clone(1)): https://, ssh://, git://, scp-like (git@host:path), and bare host (github.com/owner/repo). Shorthand forms: 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

github.com/gechr/forge/vcs answers questions about an existing checkout from marker files alone - no subprocess, no repository opened - so it is as pure and offline as the parsing above. 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

Detect and Resolver answer different questions, so they consider different markers. Detect reports what a caller should drive the checkout with, and only git and jj qualify - a Mercurial or Subversion checkout yields "" rather than a name no consumer has a driver for. Resolver reports where a repository begins, and all four markers delimit one.

Markers are tested with Lstat, so a .git file (submodule, linked worktree) counts, as does a dangling symlink: the directory was set up as a checkout either way.

A Resolver canonicalises paths before walking them, so one repository has one root however a caller spells the way to it - relative or absolute, through a symlink or not. Two consequences worth knowing:

  • Pair Root with Abs, not filepath.Abs. The latter does not resolve symlinks, so a path reaching a repository through one does not sit under the root it resolved to, and filepath.Rel yields a "../"-escaping result instead of a path within the repository.
  • Prefer RootVCS over Detect(r.RootDir(d)). RootDir yields "" outside a repository, and joining a marker onto "" would inspect the process working directory - reporting whatever VCS governs wherever the program was started.

Extraction rules

  • Branch is only set 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's src/<ref> does not name its ref type: a 40-hex value is classified as Commit, anything else lands in Rev.
  • Gitea's src/branch|tag|commit/<value> names its ref type explicitly and is extracted as such.
  • Bare-host parsing requires a dot in the host, so owner/repo is shorthand, not a URL - and dotless hosts like localhost need an explicit scheme.

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. Like SSHURL, it assumes host/owner/name clone paths, which does not hold for Azure DevOps - prefer CloneURL when the Ref came from ParseURL.

func (Ref) SSHURL

func (r Ref) SSHURL() string

SSHURL returns the SSH clone URL for the repository.

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. 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