-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlink.go
More file actions
117 lines (107 loc) · 3.25 KB
/
Copy pathlink.go
File metadata and controls
117 lines (107 loc) · 3.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package specsync
import (
"fmt"
"sort"
"strings"
)
// LinkOptions configures a link run: establishing cross-references between two
// or more local specs so their GitHub issues reference each other.
type LinkOptions struct {
OpenSpecDir string // path to the openspec/ directory
Slugs []string // at least 2 slugs to link together
DryRun bool
}
// LinkedPair describes one spec after linking — the caller uses Repo to
// construct the right provider and sync the updated body to GitHub.
type LinkedPair struct {
Slug string
Dir string
Repo string // "owner/name" from the ref key, or "" for auto-detect
Ref Ref // the issue ref to update
}
// Link records cross-references between specs in their links.md files (at the
// change-dir root, replacing the former .specsync/links.json) and returns the
// pairs so the caller can sync each with the correct provider. It never writes to
// GitHub itself — syncing is left to the caller so provider selection (repo
// override, dry-runner) stays in one place.
func Link(opts LinkOptions) ([]LinkedPair, error) {
if len(opts.Slugs) < 2 {
return nil, fmt.Errorf("link: at least 2 slugs required")
}
type entry struct {
change Change
refKey string
ref Ref
}
entries := make([]entry, len(opts.Slugs))
for i, slug := range opts.Slugs {
c, err := LoadChangeBySlug(opts.OpenSpecDir, slug)
if err != nil {
return nil, err
}
refs, err := loadRefs(c.Dir)
if err != nil {
return nil, err
}
if len(refs) == 0 {
return nil, fmt.Errorf("change %q has no synced ref; run specsync -change %s first", slug, slug)
}
// Prefer the plain "github" key (same-repo); fall back to first available.
key, ref := firstRef(refs)
entries[i] = entry{change: *c, refKey: key, ref: ref}
}
pairs := make([]LinkedPair, len(entries))
for i, e := range entries {
// Collect the refs of all other slugs as links for this one.
links := make([]Ref, 0, len(entries)-1)
for j, other := range entries {
if j != i {
links = append(links, other.ref)
}
}
if !opts.DryRun {
if err := saveLinksToMD(e.change.Dir, links); err != nil {
return nil, fmt.Errorf("save links for %s: %w", e.change.Slug, err)
}
}
pairs[i] = LinkedPair{
Slug: e.change.Slug,
Dir: e.change.Dir,
Repo: repoFromKey(e.refKey),
Ref: e.ref,
}
}
return pairs, nil
}
// firstRef picks the ref to link against: canonical "github:owner/repo" keys
// first (the bare "github" key is a pre-migration relic and may be stale),
// then the bare key, then anything else. Keys are scanned in sorted order so
// the pick is deterministic when several remain.
func firstRef(refs map[string]Ref) (string, Ref) {
keys := make([]string, 0, len(refs))
for k := range refs {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if strings.HasPrefix(k, "github:") {
return k, refs[k]
}
}
if r, ok := refs["github"]; ok {
return "github", r
}
for _, k := range keys {
return k, refs[k]
}
return "", Ref{}
}
// repoFromKey extracts "owner/name" from "github:owner/name", returning ""
// for the plain "github" key (meaning auto-detect from git remote).
func repoFromKey(key string) string {
const prefix = "github:"
if strings.HasPrefix(key, prefix) {
return key[len(prefix):]
}
return ""
}