Something went wrong. Try again.
A command-line interface for tangled, the decentralized Git collaboration platform built on the AT Protocol. eric-wien.tngl.io/tng-cli
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122package git
import ( "regexp" "strings")
// Remote represents a git remote with its fetch and push URLs.type Remote struct { Name string FetchURL string PushURL string}
// TangledRepo identifies a repository on the Tangled platform.type TangledRepo struct { Owner string // AT Protocol handle (e.g., "alice.bsky.social") Name string // Repository name}
func (r TangledRepo) FullName() string { return r.Owner + "/" + r.Name}
var ( // git@<host>:handle/repo-name(.git)? // Matches tangled.sh and any knot host (knot.*, *.example.com, etc.) sshPattern = regexp.MustCompile(`^git@([^:]+):([^/]+)/([^/]+?)(?:\.git)?$`) // https://<host>/handle/repo-name(.git)? httpsPattern = regexp.MustCompile(`^https?://([^/]+)/([^/]+)/([^/]+?)(?:\.git)?$`))
// knownTangledHosts contains hostnames that are always recognized as Tangled.var knownTangledHosts = map[string]bool{ "tangled.sh": true,}
// ParseTangledURL extracts owner and repo name from a Tangled remote URL.// Recognizes tangled.sh and hosts following the "knot." naming convention// (see isTangledHost). Returns nil if the URL is not a recognized Tangled// remote; self-hosted knots on other hostnames need `git config// tng.default-repo` or TNG_REPO instead.func ParseTangledURL(rawURL string) *TangledRepo { if m := sshPattern.FindStringSubmatch(rawURL); m != nil { host, owner, name := m[1], m[2], m[3] if isTangledHost(host) { return &TangledRepo{Owner: owner, Name: name} } } if m := httpsPattern.FindStringSubmatch(rawURL); m != nil { host, owner, name := m[1], m[2], m[3] if isTangledHost(host) { return &TangledRepo{Owner: owner, Name: name} } } return nil}
// isTangledHost checks if a hostname belongs to the Tangled network.// A host is recognized if it's tangled.sh or starts with "knot.".func isTangledHost(host string) bool { if knownTangledHosts[host] { return true } // Knot hostnames conventionally start with "knot." if strings.HasPrefix(host, "knot.") { return true } return false}
// SSHCloneURL builds the SSH clone URL for a repo on a knot. Hosts with an// explicit port need the ssh:// form — git would otherwise parse// "git@host:port:owner/repo" with the port as part of the path.func SSHCloneURL(host, owner, name string) string { if strings.Contains(host, ":") { return "ssh://git@" + host + "/" + owner + "/" + name } return "git@" + host + ":" + owner + "/" + name}
// ParseRemotes parses `git remote -v` output into Remote structs.func ParseRemotes(output string) []Remote { byName := map[string]*Remote{} var order []string
for _, line := range strings.Split(output, "\n") { line = strings.TrimSpace(line) if line == "" { continue } // Format: "name\turl (fetch|push)" parts := strings.Fields(line) if len(parts) < 3 { continue } name := parts[0] url := parts[1] kind := strings.Trim(parts[2], "()")
r, ok := byName[name] if !ok { r = &Remote{Name: name} byName[name] = r order = append(order, name) }
switch kind { case "fetch": r.FetchURL = url case "push": r.PushURL = url } }
remotes := make([]Remote, 0, len(order)) for _, name := range order { remotes = append(remotes, *byName[name]) } return remotes}