Monorepo for Tangled tangled.org
854
fork

Configure Feed

Select the types of activity you want to include in your feed.

appview,knotserver: show repo license as metadata #301

open opened by kevinyap.ca targeting master from kevinyap.ca/core: license-meta

This PR introduces licenses as a new piece of metadata associated with a repo. This was modeled to function similarly to how repo language detection already works. On ref updates to the default branch of a repo, run licensecheck across files in the root of the directory that match a license-like filename (LICENSE, unlicense.txt, etc.). If there are any matches with a confidence level of 75% or greater, the best match is stored as the license for this repository.

Repos with a populated license entry have a pill with a scale icon shown in the "metadata" header, after the URL entry (if present) and before any labels. Clicking the pill will link to /org/repo/blob/HEAD/<path> to navigate to the license file directly.

Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:kewhcp362af66czceva4dhrl/sh.tangled.repo.pull/3mkuy2tpmw322
+571 -2
Diff #0
+251 -1
api/tangled/cbor_gen.go
··· 2116 2116 2117 2117 return nil 2118 2118 } 2119 - func (t *GitRefUpdate_Meta) MarshalCBOR(w io.Writer) error { 2119 + func (t *GitRefUpdate_LicenseInfo) MarshalCBOR(w io.Writer) error { 2120 2120 if t == nil { 2121 2121 _, err := w.Write(cbg.CborNull) 2122 2122 return err ··· 2125 2125 cw := cbg.NewCborWriter(w) 2126 2126 fieldCount := 3 2127 2127 2128 + if t.Confidence == nil { 2129 + fieldCount-- 2130 + } 2131 + 2132 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 2133 + return err 2134 + } 2135 + 2136 + // t.Confidence (int64) (int64) 2137 + if t.Confidence != nil { 2138 + 2139 + if len("confidence") > 1000000 { 2140 + return xerrors.Errorf("Value in field \"confidence\" was too long") 2141 + } 2142 + 2143 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("confidence"))); err != nil { 2144 + return err 2145 + } 2146 + if _, err := cw.WriteString(string("confidence")); err != nil { 2147 + return err 2148 + } 2149 + 2150 + if t.Confidence == nil { 2151 + if _, err := cw.Write(cbg.CborNull); err != nil { 2152 + return err 2153 + } 2154 + } else { 2155 + if *t.Confidence >= 0 { 2156 + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.Confidence)); err != nil { 2157 + return err 2158 + } 2159 + } else { 2160 + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-*t.Confidence-1)); err != nil { 2161 + return err 2162 + } 2163 + } 2164 + } 2165 + 2166 + } 2167 + 2168 + // t.Path (string) (string) 2169 + if len("path") > 1000000 { 2170 + return xerrors.Errorf("Value in field \"path\" was too long") 2171 + } 2172 + 2173 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("path"))); err != nil { 2174 + return err 2175 + } 2176 + if _, err := cw.WriteString(string("path")); err != nil { 2177 + return err 2178 + } 2179 + 2180 + if len(t.Path) > 1000000 { 2181 + return xerrors.Errorf("Value in field t.Path was too long") 2182 + } 2183 + 2184 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Path))); err != nil { 2185 + return err 2186 + } 2187 + if _, err := cw.WriteString(string(t.Path)); err != nil { 2188 + return err 2189 + } 2190 + 2191 + // t.SpdxId (string) (string) 2192 + if len("spdxId") > 1000000 { 2193 + return xerrors.Errorf("Value in field \"spdxId\" was too long") 2194 + } 2195 + 2196 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("spdxId"))); err != nil { 2197 + return err 2198 + } 2199 + if _, err := cw.WriteString(string("spdxId")); err != nil { 2200 + return err 2201 + } 2202 + 2203 + if len(t.SpdxId) > 1000000 { 2204 + return xerrors.Errorf("Value in field t.SpdxId was too long") 2205 + } 2206 + 2207 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.SpdxId))); err != nil { 2208 + return err 2209 + } 2210 + if _, err := cw.WriteString(string(t.SpdxId)); err != nil { 2211 + return err 2212 + } 2213 + return nil 2214 + } 2215 + 2216 + func (t *GitRefUpdate_LicenseInfo) UnmarshalCBOR(r io.Reader) (err error) { 2217 + *t = GitRefUpdate_LicenseInfo{} 2218 + 2219 + cr := cbg.NewCborReader(r) 2220 + 2221 + maj, extra, err := cr.ReadHeader() 2222 + if err != nil { 2223 + return err 2224 + } 2225 + defer func() { 2226 + if err == io.EOF { 2227 + err = io.ErrUnexpectedEOF 2228 + } 2229 + }() 2230 + 2231 + if maj != cbg.MajMap { 2232 + return fmt.Errorf("cbor input should be of type map") 2233 + } 2234 + 2235 + if extra > cbg.MaxLength { 2236 + return fmt.Errorf("GitRefUpdate_LicenseInfo: map struct too large (%d)", extra) 2237 + } 2238 + 2239 + n := extra 2240 + 2241 + nameBuf := make([]byte, 10) 2242 + for i := uint64(0); i < n; i++ { 2243 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 2244 + if err != nil { 2245 + return err 2246 + } 2247 + 2248 + if !ok { 2249 + // Field doesn't exist on this type, so ignore it 2250 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 2251 + return err 2252 + } 2253 + continue 2254 + } 2255 + 2256 + switch string(nameBuf[:nameLen]) { 2257 + // t.Confidence (int64) (int64) 2258 + case "confidence": 2259 + { 2260 + 2261 + b, err := cr.ReadByte() 2262 + if err != nil { 2263 + return err 2264 + } 2265 + if b != cbg.CborNull[0] { 2266 + if err := cr.UnreadByte(); err != nil { 2267 + return err 2268 + } 2269 + maj, extra, err := cr.ReadHeader() 2270 + if err != nil { 2271 + return err 2272 + } 2273 + var extraI int64 2274 + switch maj { 2275 + case cbg.MajUnsignedInt: 2276 + extraI = int64(extra) 2277 + if extraI < 0 { 2278 + return fmt.Errorf("int64 positive overflow") 2279 + } 2280 + case cbg.MajNegativeInt: 2281 + extraI = int64(extra) 2282 + if extraI < 0 { 2283 + return fmt.Errorf("int64 negative overflow") 2284 + } 2285 + extraI = -1 - extraI 2286 + default: 2287 + return fmt.Errorf("wrong type for int64 field: %d", maj) 2288 + } 2289 + 2290 + t.Confidence = (*int64)(&extraI) 2291 + } 2292 + } 2293 + // t.Path (string) (string) 2294 + case "path": 2295 + 2296 + { 2297 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2298 + if err != nil { 2299 + return err 2300 + } 2301 + 2302 + t.Path = string(sval) 2303 + } 2304 + // t.SpdxId (string) (string) 2305 + case "spdxId": 2306 + 2307 + { 2308 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2309 + if err != nil { 2310 + return err 2311 + } 2312 + 2313 + t.SpdxId = string(sval) 2314 + } 2315 + 2316 + default: 2317 + // Field doesn't exist on this type, so ignore it 2318 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 2319 + return err 2320 + } 2321 + } 2322 + } 2323 + 2324 + return nil 2325 + } 2326 + func (t *GitRefUpdate_Meta) MarshalCBOR(w io.Writer) error { 2327 + if t == nil { 2328 + _, err := w.Write(cbg.CborNull) 2329 + return err 2330 + } 2331 + 2332 + cw := cbg.NewCborWriter(w) 2333 + fieldCount := 4 2334 + 2128 2335 if t.LangBreakdown == nil { 2129 2336 fieldCount-- 2130 2337 } 2131 2338 2339 + if t.LicenseInfo == nil { 2340 + fieldCount-- 2341 + } 2342 + 2132 2343 if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 2133 2344 return err 2134 2345 } ··· 2183 2394 return err 2184 2395 } 2185 2396 } 2397 + 2398 + // t.LicenseInfo (tangled.GitRefUpdate_LicenseInfo) (struct) 2399 + if t.LicenseInfo != nil { 2400 + 2401 + if len("licenseInfo") > 1000000 { 2402 + return xerrors.Errorf("Value in field \"licenseInfo\" was too long") 2403 + } 2404 + 2405 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("licenseInfo"))); err != nil { 2406 + return err 2407 + } 2408 + if _, err := cw.WriteString(string("licenseInfo")); err != nil { 2409 + return err 2410 + } 2411 + 2412 + if err := t.LicenseInfo.MarshalCBOR(cw); err != nil { 2413 + return err 2414 + } 2415 + } 2186 2416 return nil 2187 2417 } 2188 2418 ··· 2285 2515 } 2286 2516 2287 2517 } 2518 + // t.LicenseInfo (tangled.GitRefUpdate_LicenseInfo) (struct) 2519 + case "licenseInfo": 2520 + 2521 + { 2522 + 2523 + b, err := cr.ReadByte() 2524 + if err != nil { 2525 + return err 2526 + } 2527 + if b != cbg.CborNull[0] { 2528 + if err := cr.UnreadByte(); err != nil { 2529 + return err 2530 + } 2531 + t.LicenseInfo = new(GitRefUpdate_LicenseInfo) 2532 + if err := t.LicenseInfo.UnmarshalCBOR(cr); err != nil { 2533 + return xerrors.Errorf("unmarshaling t.LicenseInfo pointer: %w", err) 2534 + } 2535 + } 2536 + 2537 + } 2288 2538 2289 2539 default: 2290 2540 // Field doesn't exist on this type, so ignore it
+11
api/tangled/gitrefUpdate.go
··· 57 57 Inputs []*GitRefUpdate_IndividualLanguageSize `json:"inputs,omitempty" cborgen:"inputs,omitempty"` 58 58 } 59 59 60 + // GitRefUpdate_LicenseInfo is a "licenseInfo" in the sh.tangled.git.refUpdate schema. 61 + type GitRefUpdate_LicenseInfo struct { 62 + // confidence: Match confidence as a percentage (0-100) 63 + Confidence *int64 `json:"confidence,omitempty" cborgen:"confidence,omitempty"` 64 + // path: Path to the license file in the repo, relative to root 65 + Path string `json:"path" cborgen:"path"` 66 + // spdxId: SPDX identifier of the detected license (e.g. 'MIT', 'Apache-2.0') 67 + SpdxId string `json:"spdxId" cborgen:"spdxId"` 68 + } 69 + 60 70 // GitRefUpdate_Meta is a "meta" in the sh.tangled.git.refUpdate schema. 61 71 type GitRefUpdate_Meta struct { 62 72 CommitCount *GitRefUpdate_CommitCountBreakdown `json:"commitCount" cborgen:"commitCount"` 63 73 IsDefaultRef bool `json:"isDefaultRef" cborgen:"isDefaultRef"` 64 74 LangBreakdown *GitRefUpdate_LangBreakdown `json:"langBreakdown,omitempty" cborgen:"langBreakdown,omitempty"` 75 + LicenseInfo *GitRefUpdate_LicenseInfo `json:"licenseInfo,omitempty" cborgen:"licenseInfo,omitempty"` 65 76 }
+10
appview/db/db.go
··· 458 458 unique(repo_at, ref, language) 459 459 ); 460 460 461 + create table if not exists repo_license ( 462 + -- repo identifiers 463 + repo_at text primary key, 464 + 465 + -- license information 466 + spdx_id text not null, 467 + path text not null, 468 + confidence integer not null default 0 469 + ); 470 + 461 471 create table if not exists signups_inflight ( 462 472 id integer primary key autoincrement, 463 473 email text not null unique,
+37
appview/db/license.go
··· 1 + package db 2 + 3 + import ( 4 + "database/sql" 5 + "errors" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + "tangled.org/core/appview/models" 9 + ) 10 + 11 + func GetRepoLicense(e Execer, repoAt syntax.ATURI) (*models.RepoLicense, error) { 12 + var l models.RepoLicense 13 + row := e.QueryRow( 14 + `select repo_at, spdx_id, path, confidence from repo_license where repo_at = ?`, 15 + repoAt, 16 + ) 17 + if err := row.Scan(&l.RepoAt, &l.SpdxID, &l.Path, &l.Confidence); err != nil { 18 + if errors.Is(err, sql.ErrNoRows) { 19 + return nil, nil 20 + } 21 + return nil, err 22 + } 23 + return &l, nil 24 + } 25 + 26 + func UpsertRepoLicense(e Execer, l models.RepoLicense) error { 27 + _, err := e.Exec( 28 + `insert or replace into repo_license (repo_at, spdx_id, path, confidence) values (?, ?, ?, ?)`, 29 + l.RepoAt, l.SpdxID, l.Path, l.Confidence, 30 + ) 31 + return err 32 + } 33 + 34 + func DeleteRepoLicense(e Execer, repoAt syntax.ATURI) error { 35 + _, err := e.Exec(`delete from repo_license where repo_at = ?`, repoAt) 36 + return err 37 + }
+12
appview/models/license.go
··· 1 + package models 2 + 3 + import ( 4 + "github.com/bluesky-social/indigo/atproto/syntax" 5 + ) 6 + 7 + type RepoLicense struct { 8 + RepoAt syntax.ATURI 9 + SpdxID string 10 + Path string 11 + Confidence int64 12 + }
+1
appview/pages/repoinfo/repoinfo.go
··· 70 70 Source *models.Repo 71 71 Ref string 72 72 CurrentDir string 73 + License *models.RepoLicense 73 74 } 74 75 75 76 // each tab on a repo could have some metadata:
+7
appview/pages/templates/layouts/repobase.html
··· 100 100 </span> 101 101 {{ end }} 102 102 103 + {{ with .RepoInfo.License }} 104 + <span class="flex items-center gap-1"> 105 + <span class="flex-shrink-0">{{ i "scale" "size-4" }}</span> 106 + <a href="/{{ $.RepoInfo.FullName }}/blob/HEAD/{{ .Path }}" title="Open {{ .Path }}">{{ .SpdxID }}</a> 107 + </span> 108 + {{ end }} 109 + 103 110 {{ if .RepoInfo.Topics }} 104 111 <div class="flex items-center gap-1"> 105 112 {{ range .RepoInfo.Topics }}
+7
appview/reporesolver/resolver.go
··· 123 123 ownerHandle = h 124 124 } 125 125 126 + license, licenseErr := db.GetRepoLicense(rr.execer, repoAt) 127 + if licenseErr != nil { 128 + log.Println("failed to get license for ", repoAt, licenseErr) 129 + } 130 + 126 131 repoInfo := repoinfo.RepoInfo{ 127 132 // this is basically a models.Repo 128 133 OwnerDid: ownerId.DID.String(), ··· 146 151 // info related to the session 147 152 IsStarred: isStarred, 148 153 Roles: roles, 154 + 155 + License: license, 149 156 } 150 157 151 158 return repoInfo
+37 -1
appview/state/knotstream.go
··· 139 139 140 140 errPunchcard := populatePunchcard(d, record) 141 141 errLanguages := updateRepoLanguages(d, record) 142 + errLicense := updateRepoLicense(d, record) 142 143 143 144 var errPosthog error 144 145 if !dev && record.CommitterDid != "" { ··· 153 154 go triggerSitesDeployIfNeeded(ctx, d, cfClient, c, record, source) 154 155 } 155 156 156 - return errors.Join(errPunchcard, errLanguages, errPosthog) 157 + return errors.Join(errPunchcard, errLanguages, errLicense, errPosthog) 157 158 } 158 159 159 160 // triggerSitesDeployIfNeeded checks whether the pushed ref matches the sites ··· 247 248 return db.AddPunch(d, punch) 248 249 } 249 250 251 + func updateRepoLicense(d *db.DB, record tangled.GitRefUpdate) error { 252 + if record.Meta == nil || !record.Meta.IsDefaultRef { 253 + return nil 254 + } 255 + 256 + ownerDid := "" 257 + if record.OwnerDid != nil { 258 + ownerDid = *record.OwnerDid 259 + } 260 + 261 + r, lookupErr := resolveRepo(d, record.RepoDid, ownerDid, record.RepoName) 262 + if lookupErr != nil { 263 + return fmt.Errorf("failed to look up repo: %w", lookupErr) 264 + } 265 + 266 + info := record.Meta.LicenseInfo 267 + // Treat a missing or empty record as "no license" — clear any stale entry 268 + // so the header stops showing a removed license. 269 + if info == nil || info.SpdxId == "" || info.Path == "" { 270 + return db.DeleteRepoLicense(d, r.RepoAt()) 271 + } 272 + 273 + var confidence int64 274 + if info.Confidence != nil { 275 + confidence = *info.Confidence 276 + } 277 + 278 + return db.UpsertRepoLicense(d, models.RepoLicense{ 279 + RepoAt: r.RepoAt(), 280 + SpdxID: info.SpdxId, 281 + Path: info.Path, 282 + Confidence: confidence, 283 + }) 284 + } 285 + 250 286 func updateRepoLanguages(d *db.DB, record tangled.GitRefUpdate) error { 251 287 if record.Meta == nil || record.Meta.LangBreakdown == nil || record.Meta.LangBreakdown.Inputs == nil { 252 288 return fmt.Errorf("empty language data for repo: %v/%s", record.OwnerDid, record.RepoName)
+1
cmd/cborgen/cborgen.go
··· 22 22 tangled.GitRefUpdate_IndividualEmailCommitCount{}, 23 23 tangled.GitRefUpdate_IndividualLanguageSize{}, 24 24 tangled.GitRefUpdate_LangBreakdown{}, 25 + tangled.GitRefUpdate_LicenseInfo{}, 25 26 tangled.GitRefUpdate_Meta{}, 26 27 tangled.GraphFollow{}, 27 28 tangled.GraphVouch{},
+20
cmd/genkey/main.go
··· 1 + // Tiny replacement for `goat key generate -t P-256` so non-Nix devs can 2 + // produce a multibase-encoded P-256 private key for TANGLED_OAUTH_CLIENT_SECRET. 3 + // Prints the multibase secret on stdout, nothing else. 4 + package main 5 + 6 + import ( 7 + "fmt" 8 + "os" 9 + 10 + "github.com/bluesky-social/indigo/atproto/atcrypto" 11 + ) 12 + 13 + func main() { 14 + priv, err := atcrypto.GeneratePrivateKeyP256() 15 + if err != nil { 16 + fmt.Fprintln(os.Stderr, "generate:", err) 17 + os.Exit(1) 18 + } 19 + fmt.Println(priv.Multibase()) 20 + }
+1
go.mod
··· 147 147 github.com/golang/protobuf v1.5.4 // indirect 148 148 github.com/golang/snappy v0.0.4 // indirect 149 149 github.com/google/go-querystring v1.1.0 // indirect 150 + github.com/google/licensecheck v0.3.1 // indirect 150 151 github.com/gorilla/css v1.0.1 // indirect 151 152 github.com/gorilla/securecookie v1.1.2 // indirect 152 153 github.com/hashicorp/errwrap v1.1.0 // indirect
+2
go.sum
··· 288 288 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 289 289 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 290 290 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 291 + github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs= 292 + github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY= 291 293 github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 292 294 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 293 295 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+129
knotserver/git/license.go
··· 1 + package git 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "io" 7 + "strings" 8 + 9 + "github.com/go-git/go-git/v5/plumbing/object" 10 + "github.com/google/licensecheck" 11 + ) 12 + 13 + type LicenseInfo struct { 14 + SpdxID string 15 + Path string 16 + Confidence int64 // percent, 0-100 17 + } 18 + 19 + // Filenames (case-insensitive) that the license check will run on. 20 + var licenseFileBaseNames = []string{ 21 + "license", 22 + "licence", 23 + "copying", 24 + "unlicense", 25 + } 26 + 27 + // Ignore matches below a 75% confidence threshold. 28 + const minLicenseConfidence = 75 29 + 30 + // Walk the repo root and run licensecheck against matching files. Returns nil 31 + // with no error when nothing matches, since license detection is best-effort. 32 + func (g *GitRepo) DetectLicense(ctx context.Context) (*LicenseInfo, error) { 33 + commit, err := g.r.CommitObject(g.h) 34 + if err != nil { 35 + return nil, fmt.Errorf("commit object: %w", err) 36 + } 37 + 38 + tree, err := commit.Tree() 39 + if err != nil { 40 + return nil, fmt.Errorf("tree: %w", err) 41 + } 42 + 43 + var best *LicenseInfo 44 + for _, entry := range tree.Entries { 45 + select { 46 + case <-ctx.Done(): 47 + return best, ctx.Err() 48 + default: 49 + } 50 + 51 + if !entry.Mode.IsFile() { 52 + continue 53 + } 54 + if !isLicenseFileName(entry.Name) { 55 + continue 56 + } 57 + 58 + content, err := readEntry(tree, entry) 59 + if err != nil { 60 + continue 61 + } 62 + 63 + coverage := licensecheck.Scan(content) 64 + if len(coverage.Match) == 0 { 65 + continue 66 + } 67 + 68 + topID, _ := bestMatch(coverage) 69 + if topID == "" { 70 + continue 71 + } 72 + conf := int64(coverage.Percent + 0.5) 73 + if conf < minLicenseConfidence { 74 + continue 75 + } 76 + if best != nil && conf <= best.Confidence { 77 + continue 78 + } 79 + 80 + best = &LicenseInfo{ 81 + SpdxID: topID, 82 + Path: entry.Name, 83 + Confidence: conf, 84 + } 85 + } 86 + 87 + return best, nil 88 + } 89 + 90 + func isLicenseFileName(name string) bool { 91 + lower := strings.ToLower(name) 92 + base := lower 93 + if dot := strings.IndexByte(lower, '.'); dot != -1 { 94 + base = lower[:dot] 95 + } 96 + for _, candidate := range licenseFileBaseNames { 97 + if base == candidate { 98 + return true 99 + } 100 + } 101 + return false 102 + } 103 + 104 + func readEntry(tree *object.Tree, entry object.TreeEntry) ([]byte, error) { 105 + file, err := tree.TreeEntryFile(&entry) 106 + if err != nil { 107 + return nil, err 108 + } 109 + reader, err := file.Reader() 110 + if err != nil { 111 + return nil, err 112 + } 113 + defer reader.Close() 114 + return io.ReadAll(reader) 115 + } 116 + 117 + // bestMatch returns the SPDX ID of the longest match. 118 + func bestMatch(coverage licensecheck.Coverage) (string, licensecheck.Match) { 119 + var winner licensecheck.Match 120 + winnerLen := -1 121 + for _, m := range coverage.Match { 122 + size := m.End - m.Start 123 + if size > winnerLen { 124 + winner = m 125 + winnerLen = size 126 + } 127 + } 128 + return winner.ID, winner 129 + }
+23
knotserver/git/post_receive.go
··· 52 52 CommitCount CommitCount 53 53 IsDefaultRef bool 54 54 LangBreakdown LangBreakdown 55 + License *LicenseInfo 55 56 } 56 57 57 58 type CommitCount struct { ··· 72 73 breakdown, err := g.AnalyzeLanguages(ctx) 73 74 errors.Join(errs, err) 74 75 76 + // License only displays for the default branch, so skip the scan on 77 + // pushes to other refs to keep the post-receive hook fast. 78 + var license *LicenseInfo 79 + if isDefaultRef { 80 + licenseCtx, cancelLicense := context.WithTimeout(context.Background(), time.Second*2) 81 + defer cancelLicense() 82 + license, err = g.DetectLicense(licenseCtx) 83 + errors.Join(errs, err) 84 + } 85 + 75 86 return RefUpdateMeta{ 76 87 CommitCount: commitCount, 77 88 IsDefaultRef: isDefaultRef, 78 89 LangBreakdown: breakdown, 90 + License: license, 79 91 }, errs 80 92 } 81 93 ··· 161 173 }) 162 174 } 163 175 176 + var license *tangled.GitRefUpdate_LicenseInfo 177 + if m.License != nil { 178 + conf := m.License.Confidence 179 + license = &tangled.GitRefUpdate_LicenseInfo{ 180 + SpdxId: m.License.SpdxID, 181 + Path: m.License.Path, 182 + Confidence: &conf, 183 + } 184 + } 185 + 164 186 return tangled.GitRefUpdate_Meta{ 165 187 CommitCount: &tangled.GitRefUpdate_CommitCountBreakdown{ 166 188 ByEmail: byEmail, ··· 169 191 LangBreakdown: &tangled.GitRefUpdate_LangBreakdown{ 170 192 Inputs: langs, 171 193 }, 194 + LicenseInfo: license, 172 195 } 173 196 }
+22
lexicons/git/refUpdate.json
··· 76 76 "commitCount": { 77 77 "type": "ref", 78 78 "ref": "#commitCountBreakdown" 79 + }, 80 + "licenseInfo": { 81 + "type": "ref", 82 + "ref": "#licenseInfo" 83 + } 84 + } 85 + }, 86 + "licenseInfo": { 87 + "type": "object", 88 + "required": ["spdxId", "path"], 89 + "properties": { 90 + "spdxId": { 91 + "type": "string", 92 + "description": "SPDX identifier of the detected license (e.g. 'MIT', 'Apache-2.0')" 93 + }, 94 + "path": { 95 + "type": "string", 96 + "description": "Path to the license file in the repo, relative to root" 97 + }, 98 + "confidence": { 99 + "type": "integer", 100 + "description": "Match confidence as a percentage (0-100)" 79 101 } 80 102 } 81 103 },

History

1 round 0 comments
sign up or login to add to the discussion
kevinyap.ca submitted #0
1 commit
expand
appview,knotserver: show repo license as metadata
merge conflicts detected
expand
  • appview/state/knotstream.go:247
  • go.mod:147
expand 0 comments