Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions cmd/bw/cross_repo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package main

import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/jallum/beadwork/internal/config"
"github.com/jallum/beadwork/internal/registry"
"github.com/jallum/beadwork/internal/repo"
)

// resolveCFlag expands the value of -C: tries the registry first
// (prefix), falls through to a filesystem path if no match.
func resolveCFlag(cfg *config.Config, arg string) string {
paths := registry.ResolveAll(cfg, arg)
if len(paths) > 1 {
fatal(fmt.Sprintf("-C %s: prefix registered for %d repositories; use -C <path> to disambiguate:\n %s",
arg, len(paths), strings.Join(paths, "\n ")))
}
if len(paths) == 1 {
return paths[0]
}
abs, err := filepath.Abs(arg)
if err != nil {
fatal(fmt.Sprintf("-C %s: %s", arg, err))
}
return abs
}

// resolveCrossRepo rewrites repoDir if the first positional argument
// references an issue in a different beadwork-enabled repository, using
// the host-local registry as a prefix → path lookup.
//
// If the prefix is unknown, we do nothing (let the downstream command
// fail with its usual "not found" error). If multiple positionals
// reference conflicting prefixes, the call fails loudly.
//
// The resolver runs AFTER -C extraction and BEFORE command dispatch, so
// an explicit -C wins.
func resolveCrossRepo(cfg *config.Config, args []string) {
if repoDir != "" {
return
}

localPrefix := ""
if r, err := repo.FindRepoAt(""); err == nil && r.IsInitialized() {
localPrefix = r.Prefix
}

known := knownPrefixes(cfg)
if localPrefix != "" {
known[localPrefix] = true
}

prefixes := extractPrefixCandidates(args, known)
if len(prefixes) == 0 {
return
}

var foreign string
localSeen := false
for _, p := range prefixes {
if p == localPrefix {
localSeen = true
continue
}
if foreign == "" {
foreign = p
continue
}
if foreign != p {
fatal(fmt.Sprintf("cross-repo references mixing prefixes %q and %q are not supported", foreign, p))
}
}
if foreign == "" {
return
}
if localSeen {
fatal(fmt.Sprintf("cross-repo references mixing local prefix %q with %q are not supported", localPrefix, foreign))
}

paths := registry.ResolveAll(cfg, foreign)
if len(paths) > 1 {
fatal(fmt.Sprintf("prefix %q registered for %d repositories; use -C <path> to disambiguate:\n %s",
foreign, len(paths), strings.Join(paths, "\n ")))
}
if len(paths) == 0 {
return
}
repoDir = paths[0]
if os.Getenv("BW_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "[cross-repo] %q → %s\n", foreign, paths[0])
}
}

// knownPrefixes returns the set of live prefixes from registered repos.
func knownPrefixes(cfg *config.Config) map[string]bool {
known := map[string]bool{}
for _, r := range registry.Repos(cfg) {
if r.Prefix != "" {
known[r.Prefix] = true
}
}
return known
}

// extractPrefixCandidates returns the prefixes of ID-shaped positional
// arguments that are also in the `known` set.
func extractPrefixCandidates(args []string, known map[string]bool) []string {
var prefixes []string
seen := map[string]bool{}

skipNext := false
for _, a := range args {
if skipNext {
skipNext = false
continue
}
if strings.HasPrefix(a, "-") {
if !strings.Contains(a, "=") {
skipNext = true
}
continue
}
idx := strings.IndexByte(a, '-')
if idx <= 0 {
continue
}
p := a[:idx]
if !known[p] {
continue
}
if !seen[p] {
seen[p] = true
prefixes = append(prefixes, p)
}
}
return prefixes
}
28 changes: 14 additions & 14 deletions cmd/bw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package main
import (
"fmt"
"os"
"path/filepath"
"time"

"github.com/jallum/beadwork/internal/config"
Expand Down Expand Up @@ -55,8 +54,13 @@ func main() {
w = PlainWriter(os.Stdout)
}

cfg, err := config.Load(config.DefaultPath())
if err != nil {
fatal(err.Error())
}

allArgs := os.Args[1:]
allArgs = extractDirFlag(allArgs)
allArgs = extractDirFlag(cfg, allArgs)

if len(allArgs) < 1 {
printUsage(w)
Expand Down Expand Up @@ -100,6 +104,11 @@ func main() {
return
}

// Cross-repo routing: if the first ID-shaped positional references a
// different registered prefix, rewrite repoDir so the command targets
// that repo. Never overrides an explicit -C.
resolveCrossRepo(cfg, args)

var store *issue.Store
if c.NeedsStore {
var err error
Expand All @@ -111,11 +120,6 @@ func main() {
maybeCheckForUpgrade(store, w)
}

cfg, err := config.Load(config.DefaultPath())
if err != nil {
fatal(err.Error())
}

originalCfg := cfg

newCfg, err := c.Run(store, args, w, cfg)
Expand Down Expand Up @@ -151,19 +155,15 @@ func bwNow() time.Time {
}

// extractDirFlag removes all -C <dir> pairs from args and sets repoDir.
func extractDirFlag(args []string) []string {
func extractDirFlag(cfg *config.Config, args []string) []string {
out := make([]string, 0, len(args))
for i := 0; i < len(args); i++ {
if args[i] == "-C" {
if i+1 >= len(args) {
fatal("-C requires an argument")
}
abs, err := filepath.Abs(args[i+1])
if err != nil {
fatal(fmt.Sprintf("-C %s: %s", args[i+1], err))
}
repoDir = abs
i++ // skip value
repoDir = resolveCFlag(cfg, args[i+1])
i++
} else {
out = append(out, args[i])
}
Expand Down
11 changes: 11 additions & 0 deletions internal/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ func Resolve(cfg *config.Config, prefix string) (string, bool) {
return "", false
}

// ResolveAll returns all repo paths that share the given prefix.
func ResolveAll(cfg *config.Config, prefix string) []string {
var paths []string
for _, r := range Repos(cfg) {
if r.Prefix == prefix {
paths = append(paths, r.Path)
}
}
return paths
}

// Register returns a new config with path added to the registry. If the
// path is already registered, returns cfg unchanged (same pointer).
func Register(cfg *config.Config, path string) *config.Config {
Expand Down
Loading
Loading