main 1fb2421

license.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6	"context"
7	"encoding/json"
8	"strings"
9
10	"github.com/bnema/zerowrap"
11
12	"bnema.dev/ermite/internal/domain"
13)
14
15// LicenseBadge names the root license file and its display identifier.
16type LicenseBadge struct {
17	ID, Href string
18}
19
20var licenseNames = []string{"license", "license.md", "license.txt", "licence", "licence.md", "licence.txt", "copying", "copying.md", "copying.txt"}
21
22// repositoryLicense caches both absence and detection by immutable head. The
23// caller has already authorized repository browsing. Failures are not cached.
24// A separate instance of the bounded immutable-result cache coalesces misses.
25func (s *Server) repositoryLicense(ctx context.Context, repo domain.RepoName, head string) *LicenseBadge {
26	key := repo.String() + "\x00" + head
27	body, err := s.licenses.load(ctx, repo.String(), key, func() ([]byte, error) {
28		badge, err := s.detectLicense(ctx, repo, head)
29		if err != nil {
30			return nil, err
31		}
32		return json.Marshal(badge)
33	})
34	if err != nil {
35		log := zerowrap.FromCtx(ctx)
36		log.Warn().Err(err).Str("repo", repo.String()).Msg("read the repository license")
37		return nil
38	}
39	var badge *LicenseBadge
40	if err := json.Unmarshal(body, &badge); err != nil {
41		return nil
42	}
43	return badge
44}
45
46//hexcheck:ignore suspicious-business-logic-in-adapter picks a root license file for display only
47func (s *Server) detectLicense(ctx context.Context, repo domain.RepoName, head string) (*LicenseBadge, error) {
48	tree, err := s.getTree(ctx, repo, head, "", false)
49	if err != nil {
50		return nil, err
51	}
52	for _, name := range licenseNames {
53		for _, entry := range tree.Entries {
54			if (entry.Kind != domain.EntryFile && entry.Kind != domain.EntryExecutable) || !strings.EqualFold(name, entry.Name) {
55				continue
56			}
57			body, binary, err := s.readBlob(ctx, repo, head, entry.Path, 64<<10)
58			if err != nil {
59				return nil, err
60			}
61			id := "Other"
62			if !binary {
63				id = licenseID(string(body))
64			}
65			return &LicenseBadge{ID: id, Href: repoHref(repo.String(), "blob", head, entry.Path)}, nil
66		}
67	}
68	return nil, nil
69}
70
71// licenseID deliberately recognizes titles and characteristic grants, not
72// incidental mentions of other licenses in their full text.
73func licenseID(body string) string {
74	for _, line := range strings.Split(body, "\n") {
75		if i := strings.Index(strings.ToLower(line), "spdx-license-identifier:"); i >= 0 {
76			id := strings.TrimSpace(line[i+len("spdx-license-identifier:"):])
77			id = strings.TrimSpace(strings.TrimSuffix(strings.TrimSuffix(id, "-->"), "*/"))
78			if id != "" && len(id) <= 160 {
79				return id
80			}
81		}
82	}
83	text := strings.ToLower(strings.Join(strings.Fields(body), " "))
84	title := text[:min(len(text), 600)]
85	switch {
86	case strings.Contains(title, "gnu affero general public license") && strings.Contains(title, "version 3"):
87		return "AGPL-3.0"
88	case strings.Contains(title, "gnu lesser general public license") && strings.Contains(title, "version 3"):
89		return "LGPL-3.0"
90	case strings.Contains(title, "gnu lesser general public license") && strings.Contains(title, "version 2.1"):
91		return "LGPL-2.1"
92	case strings.Contains(title, "gnu general public license") && strings.Contains(title, "version 3"):
93		return "GPL-3.0"
94	case strings.Contains(title, "gnu general public license") && strings.Contains(title, "version 2"):
95		return "GPL-2.0"
96	case strings.Contains(title, "apache license") && strings.Contains(title, "version 2.0"):
97		return "Apache-2.0"
98	case strings.Contains(title, "mozilla public license") && strings.Contains(title, "2.0"):
99		return "MPL-2.0"
100	case strings.Contains(text, "permission is hereby granted, free of charge") && strings.Contains(text, "the software is provided \"as is\""):
101		if strings.Contains(text, "the above copyright notice and this permission notice shall be included in all copies or substantial portions of the software") {
102			return "MIT"
103		}
104		return "MIT-0"
105	case strings.Contains(text, "all advertising materials"):
106		return "Other"
107	case strings.Contains(text, "redistribution and use in source and binary forms") && strings.Contains(text, "neither the name"):
108		return "BSD-3-Clause"
109	case strings.Contains(text, "redistribution and use in source and binary forms") && strings.Contains(text, "this list of conditions"):
110		return "BSD-2-Clause"
111	case strings.Contains(text, "permission to use, copy, modify, and/or distribute this software for any purpose with or without fee"):
112		return "ISC"
113	}
114	return "Other"
115}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close