Files
jeffryandClaude Opus 5 d7b169b87e a 404 at adoption has a third reading, and the tool can resolve it
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
2026-09-08 12:02:44 -04:00

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")
}