externals names two readings of a 404 — withdrawn, or access lost — and those are the two a locked document can have. Adoption by name has a third: a document that was never there under that name. Found in use, where a missing s reported the ambiguity instead of the typo. The tool was reporting an ambiguity it had the means to resolve: the by-name form knows the repository, so on a 404 it now lists the published surface and says which names exist. It claims that only when the listing succeeds — if listing fails too, the repository is unreachable and the original ambiguity is the honest answer, which is the same discipline as recording public and not-public rather than private. Not filed as a gap against externals. The third reading cannot occur where that document is speaking, which is check against a lock; it exists only at adoption, which is ours. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UTxuSizozEA8yDitPuris
111 lines
3.4 KiB
Go
111 lines
3.4 KiB
Go
package external
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"git.hypertheory-labs.dev/loom/loom-cli/internal/config"
|
|
)
|
|
|
|
// AddByName adopts a document named the way `list` prints it, against the
|
|
// current context.
|
|
//
|
|
// loomctl external add loom/cart cart
|
|
//
|
|
// Nobody should have to type a host's raw-file route to adopt a document. The
|
|
// route belongs to the host, so it lives in the context; the published
|
|
// directory belongs to the convention, so it lives in the code; and what is
|
|
// left — which repository, which document — is the only part a person knows.
|
|
func AddByName(root, ownerRepo, name string, out io.Writer) error {
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx, err := cfg.Current()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.Count(ownerRepo, "/") != 1 {
|
|
return fmt.Errorf("want owner/repo, got %q", ownerRepo)
|
|
}
|
|
if !strings.HasSuffix(name, ".md") {
|
|
name += ".md"
|
|
}
|
|
|
|
raw, resolved := ctx.RawURL(ownerRepo, PublishedDir+"/"+name)
|
|
if raw == "" {
|
|
return fmt.Errorf("context %q has flavor %q; known flavors are %s",
|
|
cfg.CurrentContext, ctx.Flavor, config.Flavors())
|
|
}
|
|
if !resolved {
|
|
// The lock must record a resolved URL. A short form that stays short
|
|
// follows whatever the default branch is at the time you ask, so a
|
|
// branch rename would report as a change in the document.
|
|
branch, err := defaultBranch(ctx.CloneURL(ownerRepo))
|
|
if err != nil {
|
|
return fmt.Errorf("resolving the default branch of %s: %w", ownerRepo, err)
|
|
}
|
|
raw = fmt.Sprintf(raw, branch)
|
|
}
|
|
err = Add(root, raw, "", out)
|
|
|
|
// A 404 here has a reading the convention does not list, because it can only
|
|
// happen at adoption: the name is wrong. We know the repository, so rather
|
|
// than reporting an ambiguity we can resolve, look.
|
|
var nf *NotFoundError
|
|
if errors.As(err, &nf) {
|
|
var names strings.Builder
|
|
if lerr := List(ctx.CloneURL(ownerRepo), &names); lerr == nil {
|
|
return fmt.Errorf("%s publishes no %q. It publishes:\n%s",
|
|
ownerRepo, name, indent(names.String()))
|
|
}
|
|
// Listing failed too, so the repository itself is unreachable and the
|
|
// original ambiguity stands.
|
|
}
|
|
return err
|
|
}
|
|
|
|
func indent(s string) string {
|
|
var b strings.Builder
|
|
for _, line := range strings.Split(strings.TrimRight(s, "\n"), "\n") {
|
|
fmt.Fprintf(&b, " %s\n", line)
|
|
}
|
|
return strings.TrimRight(b.String(), "\n")
|
|
}
|
|
|
|
// ListByName enumerates a repository named against the current context.
|
|
func ListByName(ownerRepo string, out io.Writer) error {
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx, err := cfg.Current()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return List(ctx.CloneURL(ownerRepo), out)
|
|
}
|
|
|
|
// defaultBranch asks the remote which branch HEAD points at, without cloning.
|
|
//
|
|
// Only hosts whose raw URLs do not redirect need this. Gitea resolves its own
|
|
// short form, which is why adopting from gitea needs no git at all.
|
|
func defaultBranch(cloneURL string) (string, error) {
|
|
cmd := exec.Command("git", "ls-remote", "--symref", cloneURL, "HEAD")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout, cmd.Stderr = &stdout, &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return "", fmt.Errorf("%w: %s", err, strings.TrimSpace(stderr.String()))
|
|
}
|
|
for _, line := range strings.Split(stdout.String(), "\n") {
|
|
if after, ok := strings.CutPrefix(line, "ref: refs/heads/"); ok {
|
|
return strings.TrimSpace(strings.SplitN(after, "\t", 2)[0]), nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("no symref in git ls-remote output")
|
|
}
|