149 lines
4.2 KiB
Go
149 lines
4.2 KiB
Go
package initwizard
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/KofTwentyTwo/license-tool/internal/config"
|
|
"github.com/KofTwentyTwo/license-tool/internal/detect"
|
|
"github.com/KofTwentyTwo/license-tool/internal/enumerate"
|
|
"github.com/KofTwentyTwo/license-tool/internal/spdx"
|
|
)
|
|
|
|
// Detected marks which seeded Answers fields came from repo detection, so the form
|
|
// can badge them and the user sees what was inferred versus defaulted.
|
|
type Detected struct {
|
|
License bool
|
|
Holder bool
|
|
Manage bool
|
|
}
|
|
|
|
// SeedDeps holds injectable seams for Seed. WHY only the git author is a seam: the
|
|
// header scan is deterministic against a temp directory (the project's test style),
|
|
// but the configured git author depends on machine/CI git config, so it is injected.
|
|
type SeedDeps struct {
|
|
// GitAuthor returns the configured author name for root, or "" when unknown.
|
|
GitAuthor func(root string) string
|
|
}
|
|
|
|
// seedReadFile is a seam over os.ReadFile so the per-file read-error path is testable.
|
|
var seedReadFile = os.ReadFile
|
|
|
|
// Seed pre-fills base from repository detection and reports which fields were
|
|
// inferred. It never overrides a non-empty base field (e.g. one set from a flag).
|
|
// Detection reuses the same enumerate + detect + spdx machinery as audit, so it
|
|
// agrees with the rest of the tool, and it writes nothing: these are editable
|
|
// defaults the user reviews against the live preview.
|
|
func Seed(root string, base Answers, deps SeedDeps) (Answers, Detected) {
|
|
if deps.GitAuthor == nil {
|
|
deps.GitAuthor = gitAuthor
|
|
}
|
|
var detected Detected
|
|
|
|
licenseID, holder := scanHeaders(root)
|
|
|
|
if base.License.SPDXID == "" && licenseID != "" {
|
|
// Only seed a license the tool can actually render; an existing header in an
|
|
// id we cannot emit is left for the user to choose explicitly.
|
|
if _, ok := spdx.Lookup(licenseID); ok {
|
|
base.License.SPDXID = licenseID
|
|
detected.License = true
|
|
}
|
|
}
|
|
|
|
if base.Identity.Holder == "" {
|
|
switch {
|
|
case holder != "":
|
|
base.Identity.Holder = holder
|
|
detected.Holder = true
|
|
default:
|
|
if name := deps.GitAuthor(root); name != "" {
|
|
base.Identity.Holder = name
|
|
detected.Holder = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if licenseFileExists(root) {
|
|
detected.Manage = true
|
|
}
|
|
|
|
return base, detected
|
|
}
|
|
|
|
// scanHeaders enumerates source files and returns the most common detected license
|
|
// id and holder among recognized headers; empty strings when none are found.
|
|
func scanHeaders(root string) (licenseID, holder string) {
|
|
classify := config.ContentLookupFunc(config.Defaults())
|
|
entries, err := enumerate.WithContent(root, enumerate.Options{}, classify)
|
|
if err != nil {
|
|
return "", ""
|
|
}
|
|
|
|
licenseCounts := map[string]int{}
|
|
holderCounts := map[string]int{}
|
|
for _, e := range entries {
|
|
if e.Skip {
|
|
continue
|
|
}
|
|
content, rerr := seedReadFile(e.AbsPath)
|
|
if rerr != nil {
|
|
continue
|
|
}
|
|
dh, derr := detect.Detect(content, e.FileType)
|
|
if derr != nil || !dh.Present {
|
|
continue
|
|
}
|
|
if dh.SPDXID != "" {
|
|
licenseCounts[dh.SPDXID]++
|
|
}
|
|
if h := strings.TrimSpace(dh.Holder); h != "" {
|
|
holderCounts[h]++
|
|
}
|
|
}
|
|
return mostCommon(licenseCounts), mostCommon(holderCounts)
|
|
}
|
|
|
|
// mostCommon returns the highest-count key, breaking ties by lexical order for
|
|
// determinism; empty when the map is empty.
|
|
func mostCommon(counts map[string]int) string {
|
|
keys := make([]string, 0, len(counts))
|
|
for k := range counts {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
best, bestCount := "", 0
|
|
for _, k := range keys {
|
|
if counts[k] > bestCount {
|
|
best, bestCount = k, counts[k]
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
|
|
// licenseFileExists reports whether a top-level LICENSE file or LICENSES/ directory
|
|
// already exists, which the form uses to badge the manage-license-files choice.
|
|
func licenseFileExists(root string) bool {
|
|
if _, err := os.Stat(filepath.Join(root, "LICENSE")); err == nil {
|
|
return true
|
|
}
|
|
if info, err := os.Stat(filepath.Join(root, "LICENSES")); err == nil && info.IsDir() {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// gitAuthor returns the configured git author name for root, or "" when git is
|
|
// absent or the value is unset.
|
|
func gitAuthor(root string) string {
|
|
out, err := exec.Command("git", "-C", root, "config", "user.name").Output()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
}
|