package 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@:handle/repo-name(.git)? // Matches tangled.sh and any knot host (knot.*, *.example.com, etc.) sshPattern = regexp.MustCompile(`^git@([^:]+):([^/]+)/([^/]+?)(?:\.git)?$`) // https:///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 }