Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
assets/base.css +71 −0
| @@ -595,6 +595,77 @@ body.drawer-open .drawer-backdrop { | |||
| 595 | 595 | flex: none; | |
| 596 | 596 | } | |
| 597 | 597 | ||
| 598 | + | /* License summary banner shown above a LICENSE file. */ | |
| 599 | + | .license-banner { | |
| 600 | + | margin-bottom: 1rem; | |
| 601 | + | } | |
| 602 | + | .license-head { | |
| 603 | + | display: flex; | |
| 604 | + | align-items: center; | |
| 605 | + | gap: 0.75rem; | |
| 606 | + | } | |
| 607 | + | .license-head > .icon { | |
| 608 | + | width: 2rem; | |
| 609 | + | height: 2rem; | |
| 610 | + | flex: none; | |
| 611 | + | color: var(--fb-fg-muted); | |
| 612 | + | } | |
| 613 | + | .license-head h3 { | |
| 614 | + | margin: 0; | |
| 615 | + | } | |
| 616 | + | .license-eyebrow { | |
| 617 | + | margin: 0; | |
| 618 | + | font-size: 0.85em; | |
| 619 | + | } | |
| 620 | + | .license-desc { | |
| 621 | + | margin: 0.75rem 0; | |
| 622 | + | } | |
| 623 | + | .license-cols { | |
| 624 | + | display: grid; | |
| 625 | + | grid-template-columns: repeat(3, 1fr); | |
| 626 | + | gap: 1rem; | |
| 627 | + | padding-top: 0.75rem; | |
| 628 | + | border-top: 1px solid var(--fb-border); | |
| 629 | + | } | |
| 630 | + | .license-col h4 { | |
| 631 | + | margin: 0 0 0.5rem; | |
| 632 | + | font-size: 0.9rem; | |
| 633 | + | } | |
| 634 | + | .license-col ul { | |
| 635 | + | list-style: none; | |
| 636 | + | display: flex; | |
| 637 | + | flex-direction: column; | |
| 638 | + | gap: 0.3rem; | |
| 639 | + | } | |
| 640 | + | .license-col li { | |
| 641 | + | display: flex; | |
| 642 | + | align-items: center; | |
| 643 | + | gap: 0.4rem; | |
| 644 | + | } | |
| 645 | + | .license-col li .icon { | |
| 646 | + | width: 0.95rem; | |
| 647 | + | height: 0.95rem; | |
| 648 | + | flex: none; | |
| 649 | + | } | |
| 650 | + | .license-col li.ok .icon { | |
| 651 | + | color: var(--fb-success); | |
| 652 | + | } | |
| 653 | + | .license-col li.no .icon { | |
| 654 | + | color: var(--fb-danger); | |
| 655 | + | } | |
| 656 | + | .license-col li.cond .icon { | |
| 657 | + | color: var(--fb-accent); | |
| 658 | + | } | |
| 659 | + | .license-note { | |
| 660 | + | margin: 0.75rem 0 0; | |
| 661 | + | font-size: 0.82em; | |
| 662 | + | } | |
| 663 | + | @media (max-width: 40rem) { | |
| 664 | + | .license-cols { | |
| 665 | + | grid-template-columns: 1fr; | |
| 666 | + | } | |
| 667 | + | } | |
| 668 | + | ||
| 598 | 669 | /* Pagination controls for long lists. */ | |
| 599 | 670 | .pagination { | |
| 600 | 671 | display: flex; | |
crates/web/src/icons.rs +8 −0
| @@ -34,6 +34,9 @@ pub enum Icon { | |||
| 34 | 34 | Compass, | |
| 35 | 35 | MapPin, | |
| 36 | 36 | Mail, | |
| 37 | + | Check, | |
| 38 | + | X, | |
| 39 | + | Scale, | |
| 37 | 40 | Globe, | |
| 38 | 41 | Download, | |
| 39 | 42 | Github, | |
| @@ -104,6 +107,11 @@ impl Icon { | |||
| 104 | 107 | Icon::Mail => { | |
| 105 | 108 | r#"<rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>"# | |
| 106 | 109 | } | |
| 110 | + | Icon::Check => r#"<path d="M20 6 9 17l-5-5"/>"#, | |
| 111 | + | Icon::X => r#"<path d="M18 6 6 18"/><path d="m6 6 12 12"/>"#, | |
| 112 | + | Icon::Scale => { | |
| 113 | + | r#"<path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/><path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/><path d="M7 21h10"/><path d="M12 3v18"/><path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"/>"# | |
| 114 | + | } | |
| 107 | 115 | Icon::Globe => { | |
| 108 | 116 | r#"<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>"# | |
| 109 | 117 | } | |
crates/web/src/lib.rs +1 −0
| @@ -19,6 +19,7 @@ mod git_http; | |||
| 19 | 19 | mod icons; | |
| 20 | 20 | mod issues; | |
| 21 | 21 | mod layout; | |
| 22 | + | mod license; | |
| 22 | 23 | mod markdown; | |
| 23 | 24 | mod pages; | |
| 24 | 25 | mod pulls; | |
crates/web/src/license.rs +261 −0
| @@ -0,0 +1,261 @@ | |||
| 1 | + | // This Source Code Form is subject to the terms of the Mozilla Public | |
| 2 | + | // License, v. 2.0. If a copy of the MPL was not distributed with this | |
| 3 | + | // file, You can obtain one at https://mozilla.org/MPL/2.0/. | |
| 4 | + | ||
| 5 | + | //! Lightweight license detection for the file view. | |
| 6 | + | //! | |
| 7 | + | //! This is not a full SPDX matcher (à la GitHub's licensee): it recognizes a | |
| 8 | + | //! curated set of common licenses by distinctive marker phrases and maps each to | |
| 9 | + | //! a small table of permissions / conditions / limitations sourced from | |
| 10 | + | //! [choosealicense.com](https://choosealicense.com). It is deliberately | |
| 11 | + | //! conservative — an unrecognized or heavily modified license simply shows no | |
| 12 | + | //! banner. The rights summary is informational, never legal advice. | |
| 13 | + | ||
| 14 | + | /// A recognized license and its rights summary. | |
| 15 | + | #[derive(Debug, Clone, Copy)] | |
| 16 | + | pub(crate) struct LicenseInfo { | |
| 17 | + | /// Human-readable name, e.g. "MIT License". | |
| 18 | + | pub name: &'static str, | |
| 19 | + | /// SPDX identifier, e.g. "MIT". | |
| 20 | + | pub spdx: &'static str, | |
| 21 | + | /// One-line plain description. | |
| 22 | + | pub description: &'static str, | |
| 23 | + | /// What the license permits. | |
| 24 | + | pub permissions: &'static [&'static str], | |
| 25 | + | /// What it requires. | |
| 26 | + | pub conditions: &'static [&'static str], | |
| 27 | + | /// What it limits. | |
| 28 | + | pub limitations: &'static [&'static str], | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | /// Whether `path`'s filename looks like a license file (`LICENSE`, `LICENCE`, | |
| 32 | + | /// `COPYING`, optionally with an extension, case-insensitive). | |
| 33 | + | pub(crate) fn is_license_path(path: &str) -> bool { | |
| 34 | + | let name = path.rsplit('/').next().unwrap_or(path).to_ascii_lowercase(); | |
| 35 | + | let stem = name.split('.').next().unwrap_or(&name); | |
| 36 | + | matches!( | |
| 37 | + | stem, | |
| 38 | + | "license" | "licence" | "copying" | "copyright" | "unlicense" | |
| 39 | + | ) | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | /// Detect the license from a file's text, or `None` if it is not one of the | |
| 43 | + | /// recognized licenses. Matching is order-sensitive (most specific first). | |
| 44 | + | pub(crate) fn detect(content: &str) -> Option<&'static LicenseInfo> { | |
| 45 | + | // Collapse whitespace and lowercase so wrapping/indentation doesn't matter. | |
| 46 | + | let normalized: String = content | |
| 47 | + | .to_ascii_lowercase() | |
| 48 | + | .split_whitespace() | |
| 49 | + | .collect::<Vec<_>>() | |
| 50 | + | .join(" "); | |
| 51 | + | let has = |needle: &str| normalized.contains(needle); | |
| 52 | + | ||
| 53 | + | // Order matters: check more specific markers before generic ones. | |
| 54 | + | if has("gnu affero general public license") { | |
| 55 | + | return Some(&AGPL_3_0); | |
| 56 | + | } | |
| 57 | + | if has("gnu lesser general public license") { | |
| 58 | + | return Some(&LGPL_3_0); | |
| 59 | + | } | |
| 60 | + | if has("gnu general public license") { | |
| 61 | + | if has("version 2") { | |
| 62 | + | return Some(&GPL_2_0); | |
| 63 | + | } | |
| 64 | + | return Some(&GPL_3_0); | |
| 65 | + | } | |
| 66 | + | if has("mozilla public license version 2.0") { | |
| 67 | + | return Some(&MPL_2_0); | |
| 68 | + | } | |
| 69 | + | if has("apache license") && has("version 2.0") { | |
| 70 | + | return Some(&APACHE_2_0); | |
| 71 | + | } | |
| 72 | + | if has("this is free and unencumbered software released into the public domain") { | |
| 73 | + | return Some(&UNLICENSE); | |
| 74 | + | } | |
| 75 | + | if has("permission to use, copy, modify, and/or distribute this software") { | |
| 76 | + | // 0BSD drops ISC's "provided that the above copyright" retention clause. | |
| 77 | + | if has("provided that the above copyright") { | |
| 78 | + | return Some(&ISC); | |
| 79 | + | } | |
| 80 | + | return Some(&BSD_0); | |
| 81 | + | } | |
| 82 | + | if has("redistribution and use in source and binary forms") { | |
| 83 | + | if has("neither the name") { | |
| 84 | + | return Some(&BSD_3_CLAUSE); | |
| 85 | + | } | |
| 86 | + | return Some(&BSD_2_CLAUSE); | |
| 87 | + | } | |
| 88 | + | if has("permission is hereby granted, free of charge") { | |
| 89 | + | return Some(&MIT); | |
| 90 | + | } | |
| 91 | + | None | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // ---- Rights vocabulary (choosealicense.com labels) ---- | |
| 95 | + | const COMMERCIAL: &str = "Commercial use"; | |
| 96 | + | const MODIFY: &str = "Modification"; | |
| 97 | + | const DISTRIBUTE: &str = "Distribution"; | |
| 98 | + | const PRIVATE: &str = "Private use"; | |
| 99 | + | const PATENT: &str = "Patent use"; | |
| 100 | + | const LIABILITY: &str = "Liability"; | |
| 101 | + | const WARRANTY: &str = "Warranty"; | |
| 102 | + | const TRADEMARK: &str = "Trademark use"; | |
| 103 | + | const KEEP_NOTICE: &str = "License and copyright notice"; | |
| 104 | + | const STATE_CHANGES: &str = "State changes"; | |
| 105 | + | const DISCLOSE_SOURCE: &str = "Disclose source"; | |
| 106 | + | const SAME_LICENSE: &str = "Same license"; | |
| 107 | + | const NETWORK_DISCLOSE: &str = "Network use is distribution"; | |
| 108 | + | ||
| 109 | + | const PERMISSIVE: &[&str] = &[COMMERCIAL, MODIFY, DISTRIBUTE, PRIVATE]; | |
| 110 | + | const PERMISSIVE_PATENT: &[&str] = &[COMMERCIAL, MODIFY, DISTRIBUTE, PATENT, PRIVATE]; | |
| 111 | + | const NO_WARRANTY: &[&str] = &[LIABILITY, WARRANTY]; | |
| 112 | + | ||
| 113 | + | const MIT: LicenseInfo = LicenseInfo { | |
| 114 | + | name: "MIT License", | |
| 115 | + | spdx: "MIT", | |
| 116 | + | description: "A short, permissive license: do almost anything, provided the copyright and license notice are preserved.", | |
| 117 | + | permissions: PERMISSIVE, | |
| 118 | + | conditions: &[KEEP_NOTICE], | |
| 119 | + | limitations: NO_WARRANTY, | |
| 120 | + | }; | |
| 121 | + | ||
| 122 | + | const APACHE_2_0: LicenseInfo = LicenseInfo { | |
| 123 | + | name: "Apache License 2.0", | |
| 124 | + | spdx: "Apache-2.0", | |
| 125 | + | description: "A permissive license with an express grant of patent rights and requirements to note changes.", | |
| 126 | + | permissions: PERMISSIVE_PATENT, | |
| 127 | + | conditions: &[KEEP_NOTICE, STATE_CHANGES], | |
| 128 | + | limitations: &[TRADEMARK, LIABILITY, WARRANTY], | |
| 129 | + | }; | |
| 130 | + | ||
| 131 | + | const BSD_2_CLAUSE: LicenseInfo = LicenseInfo { | |
| 132 | + | name: "BSD 2-Clause License", | |
| 133 | + | spdx: "BSD-2-Clause", | |
| 134 | + | description: "A permissive license that requires preservation of the copyright and license notices.", | |
| 135 | + | permissions: PERMISSIVE, | |
| 136 | + | conditions: &[KEEP_NOTICE], | |
| 137 | + | limitations: NO_WARRANTY, | |
| 138 | + | }; | |
| 139 | + | ||
| 140 | + | const BSD_3_CLAUSE: LicenseInfo = LicenseInfo { | |
| 141 | + | name: "BSD 3-Clause License", | |
| 142 | + | spdx: "BSD-3-Clause", | |
| 143 | + | description: "A permissive license like BSD 2-Clause, also forbidding use of contributors' names to endorse derivatives.", | |
| 144 | + | permissions: PERMISSIVE, | |
| 145 | + | conditions: &[KEEP_NOTICE], | |
| 146 | + | limitations: NO_WARRANTY, | |
| 147 | + | }; | |
| 148 | + | ||
| 149 | + | const BSD_0: LicenseInfo = LicenseInfo { | |
| 150 | + | name: "BSD Zero Clause License", | |
| 151 | + | spdx: "0BSD", | |
| 152 | + | description: "A public-domain-equivalent license with no attribution requirement whatsoever.", | |
| 153 | + | permissions: PERMISSIVE, | |
| 154 | + | conditions: &[], | |
| 155 | + | limitations: NO_WARRANTY, | |
| 156 | + | }; | |
| 157 | + | ||
| 158 | + | const ISC: LicenseInfo = LicenseInfo { | |
| 159 | + | name: "ISC License", | |
| 160 | + | spdx: "ISC", | |
| 161 | + | description: "A permissive license functionally equivalent to MIT/BSD 2-Clause, with simpler language.", | |
| 162 | + | permissions: PERMISSIVE, | |
| 163 | + | conditions: &[KEEP_NOTICE], | |
| 164 | + | limitations: NO_WARRANTY, | |
| 165 | + | }; | |
| 166 | + | ||
| 167 | + | const MPL_2_0: LicenseInfo = LicenseInfo { | |
| 168 | + | name: "Mozilla Public License 2.0", | |
| 169 | + | spdx: "MPL-2.0", | |
| 170 | + | description: "A file-level copyleft license: modified files must stay open, but they can be combined with proprietary code.", | |
| 171 | + | permissions: PERMISSIVE_PATENT, | |
| 172 | + | conditions: &[DISCLOSE_SOURCE, KEEP_NOTICE, SAME_LICENSE], | |
| 173 | + | limitations: &[TRADEMARK, LIABILITY, WARRANTY], | |
| 174 | + | }; | |
| 175 | + | ||
| 176 | + | const GPL_2_0: LicenseInfo = LicenseInfo { | |
| 177 | + | name: "GNU General Public License v2.0", | |
| 178 | + | spdx: "GPL-2.0", | |
| 179 | + | description: "A strong copyleft license: derivatives must be released under the same terms with source available.", | |
| 180 | + | permissions: PERMISSIVE, | |
| 181 | + | conditions: &[DISCLOSE_SOURCE, KEEP_NOTICE, SAME_LICENSE, STATE_CHANGES], | |
| 182 | + | limitations: NO_WARRANTY, | |
| 183 | + | }; | |
| 184 | + | ||
| 185 | + | const GPL_3_0: LicenseInfo = LicenseInfo { | |
| 186 | + | name: "GNU General Public License v3.0", | |
| 187 | + | spdx: "GPL-3.0", | |
| 188 | + | description: "A strong copyleft license with a patent grant: derivatives must stay open under the same terms.", | |
| 189 | + | permissions: PERMISSIVE_PATENT, | |
| 190 | + | conditions: &[DISCLOSE_SOURCE, KEEP_NOTICE, SAME_LICENSE, STATE_CHANGES], | |
| 191 | + | limitations: NO_WARRANTY, | |
| 192 | + | }; | |
| 193 | + | ||
| 194 | + | const LGPL_3_0: LicenseInfo = LicenseInfo { | |
| 195 | + | name: "GNU Lesser General Public License v3.0", | |
| 196 | + | spdx: "LGPL-3.0", | |
| 197 | + | description: "A weaker copyleft: the library stays open under the same terms, but may be linked from other software.", | |
| 198 | + | permissions: PERMISSIVE_PATENT, | |
| 199 | + | conditions: &[DISCLOSE_SOURCE, KEEP_NOTICE, SAME_LICENSE, STATE_CHANGES], | |
| 200 | + | limitations: NO_WARRANTY, | |
| 201 | + | }; | |
| 202 | + | ||
| 203 | + | const AGPL_3_0: LicenseInfo = LicenseInfo { | |
| 204 | + | name: "GNU Affero General Public License v3.0", | |
| 205 | + | spdx: "AGPL-3.0", | |
| 206 | + | description: "Like GPLv3, but network use counts as distribution — users interacting over a network must get the source.", | |
| 207 | + | permissions: PERMISSIVE_PATENT, | |
| 208 | + | conditions: &[ | |
| 209 | + | DISCLOSE_SOURCE, | |
| 210 | + | KEEP_NOTICE, | |
| 211 | + | NETWORK_DISCLOSE, | |
| 212 | + | SAME_LICENSE, | |
| 213 | + | STATE_CHANGES, | |
| 214 | + | ], | |
| 215 | + | limitations: NO_WARRANTY, | |
| 216 | + | }; | |
| 217 | + | ||
| 218 | + | const UNLICENSE: LicenseInfo = LicenseInfo { | |
| 219 | + | name: "The Unlicense", | |
| 220 | + | spdx: "Unlicense", | |
| 221 | + | description: "A public-domain dedication: do anything, with no conditions at all.", | |
| 222 | + | permissions: PERMISSIVE, | |
| 223 | + | conditions: &[], | |
| 224 | + | limitations: NO_WARRANTY, | |
| 225 | + | }; | |
| 226 | + | ||
| 227 | + | #[cfg(test)] | |
| 228 | + | mod tests { | |
| 229 | + | use super::*; | |
| 230 | + | ||
| 231 | + | #[test] | |
| 232 | + | fn recognizes_license_filenames() { | |
| 233 | + | assert!(is_license_path("LICENSE")); | |
| 234 | + | assert!(is_license_path("license.md")); | |
| 235 | + | assert!(is_license_path("path/to/COPYING")); | |
| 236 | + | assert!(is_license_path("LICENCE.txt")); | |
| 237 | + | assert!(!is_license_path("src/main.rs")); | |
| 238 | + | assert!(!is_license_path("readme.md")); | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | #[test] | |
| 242 | + | fn detects_common_licenses() { | |
| 243 | + | assert_eq!( | |
| 244 | + | detect("Permission is hereby granted, free of charge, to any person").map(|l| l.spdx), | |
| 245 | + | Some("MIT") | |
| 246 | + | ); | |
| 247 | + | assert_eq!( | |
| 248 | + | detect("Apache License\nVersion 2.0, January 2004").map(|l| l.spdx), | |
| 249 | + | Some("Apache-2.0") | |
| 250 | + | ); | |
| 251 | + | assert_eq!( | |
| 252 | + | detect("GNU GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007").map(|l| l.spdx), | |
| 253 | + | Some("GPL-3.0") | |
| 254 | + | ); | |
| 255 | + | let zero_bsd = "BSD Zero Clause License\n\nPermission to use, copy, modify, \ | |
| 256 | + | and/or distribute this software for any purpose with or without fee is \ | |
| 257 | + | hereby granted."; | |
| 258 | + | assert_eq!(detect(zero_bsd).map(|l| l.spdx), Some("0BSD")); | |
| 259 | + | assert!(detect("just some random text file").is_none()); | |
| 260 | + | } | |
| 261 | + | } | |
crates/web/src/repo.rs +47 −0
| @@ -621,10 +621,17 @@ async fn blob_view( | |||
| 621 | 621 | attr_lang.as_deref(), | |
| 622 | 622 | source_view, | |
| 623 | 623 | ); | |
| 624 | + | // For a license file, detect it and show a rights summary above the content. | |
| 625 | + | let license = (!blob.is_binary && crate::license::is_license_path(&path)) | |
| 626 | + | .then(|| crate::license::detect(&String::from_utf8_lossy(&blob.content))) | |
| 627 | + | .flatten(); | |
| 624 | 628 | let branches = branch_names(state, &ctx.repo.id).await; | |
| 625 | 629 | let body = html! { | |
| 626 | 630 | (repo_header(state, &ctx, Tab::Code, &rev_name, &branches)) | |
| 627 | 631 | (breadcrumb(&ctx.owner.username, &ctx.repo.path, &rev_name, &path)) | |
| 632 | + | @if let Some(info) = license { | |
| 633 | + | (license_banner(info)) | |
| 634 | + | } | |
| 628 | 635 | (content_body) | |
| 629 | 636 | }; | |
| 630 | 637 | let (jar, chrome) = build_chrome(state, jar, viewer, uri.path().to_string()); | |
| @@ -632,6 +639,46 @@ async fn blob_view( | |||
| 632 | 639 | Ok((jar, page(&chrome, &title, body)).into_response()) | |
| 633 | 640 | } | |
| 634 | 641 | ||
| 642 | + | /// The GitHub-style license summary banner: name, description, and the | |
| 643 | + | /// permissions / conditions / limitations columns. Informational, not legal | |
| 644 | + | /// advice. | |
| 645 | + | fn license_banner(info: &crate::license::LicenseInfo) -> Markup { | |
| 646 | + | let column = |title: &str, items: &[&str], marker: Icon, class: &str| { | |
| 647 | + | html! { | |
| 648 | + | div class="license-col" { | |
| 649 | + | h4 { (title) } | |
| 650 | + | @if items.is_empty() { | |
| 651 | + | p class="muted license-none" { "None" } | |
| 652 | + | } @else { | |
| 653 | + | ul { | |
| 654 | + | @for item in items { | |
| 655 | + | li class=(class) { (icon(marker)) span { (item) } } | |
| 656 | + | } | |
| 657 | + | } | |
| 658 | + | } | |
| 659 | + | } | |
| 660 | + | } | |
| 661 | + | }; | |
| 662 | + | html! { | |
| 663 | + | div class="card license-banner" { | |
| 664 | + | div class="license-head" { | |
| 665 | + | (icon(Icon::Scale)) | |
| 666 | + | div { | |
| 667 | + | p class="muted license-eyebrow" { "This repository is licensed under the" } | |
| 668 | + | h3 { (info.name) " " span class="muted mono" { "(" (info.spdx) ")" } } | |
| 669 | + | } | |
| 670 | + | } | |
| 671 | + | p class="license-desc muted" { (info.description) } | |
| 672 | + | div class="license-cols" { | |
| 673 | + | (column("Permissions", info.permissions, Icon::Check, "ok")) | |
| 674 | + | (column("Conditions", info.conditions, Icon::Issue, "cond")) | |
| 675 | + | (column("Limitations", info.limitations, Icon::X, "no")) | |
| 676 | + | } | |
| 677 | + | p class="license-note muted" { "This is a summary, not legal advice." } | |
| 678 | + | } | |
| 679 | + | } | |
| 680 | + | } | |
| 681 | + | ||
| 635 | 682 | /// Render a blob: a rendered preview for markdown, highlighted code with line | |
| 636 | 683 | /// anchors, an inline image, or a binary notice. | |
| 637 | 684 | fn render_blob( | |
crates/web/src/tests.rs +36 −0
| @@ -980,6 +980,42 @@ async fn new_repo_can_use_sha256_object_format() { | |||
| 980 | 980 | } | |
| 981 | 981 | ||
| 982 | 982 | #[tokio::test] | |
| 983 | + | async fn license_file_shows_a_rights_banner() { | |
| 984 | + | let (state, app, _dir) = app_with_git().await; | |
| 985 | + | let ada = state.store.user_by_username("ada").await.unwrap().unwrap(); | |
| 986 | + | let repo = state | |
| 987 | + | .store | |
| 988 | + | .create_repo(NewRepo { | |
| 989 | + | owner_id: ada.id, | |
| 990 | + | group_id: None, | |
| 991 | + | name: "proj".to_string(), | |
| 992 | + | path: "proj".to_string(), | |
| 993 | + | description: None, | |
| 994 | + | visibility: model::Visibility::Public, | |
| 995 | + | default_branch: "main".to_string(), | |
| 996 | + | }) | |
| 997 | + | .await | |
| 998 | + | .unwrap(); | |
| 999 | + | let hook = state.config.storage.data_dir.join("fabrica"); | |
| 1000 | + | git::create_bare(&state.config.storage.repo_dir, &repo.id, "main", &hook).unwrap(); | |
| 1001 | + | let path = git::repo_path(&state.config.storage.repo_dir, &repo.id).unwrap(); | |
| 1002 | + | let raw = git2::Repository::open_bare(&path).unwrap(); | |
| 1003 | + | let mit = "MIT License\n\nPermission is hereby granted, free of charge, to any person \ | |
| 1004 | + | obtaining a copy of this software.\n"; | |
| 1005 | + | seed_commit(&raw, "main", &[], &[("LICENSE", mit)], "add license", 1); | |
| 1006 | + | ||
| 1007 | + | let res = app | |
| 1008 | + | .oneshot(get("/ada/proj/-/blob/main/LICENSE")) | |
| 1009 | + | .await | |
| 1010 | + | .unwrap(); | |
| 1011 | + | assert_eq!(res.status(), StatusCode::OK); | |
| 1012 | + | let html = body_string(res).await; | |
| 1013 | + | assert!(html.contains("MIT License"), "license name shown"); | |
| 1014 | + | assert!(html.contains("Permissions"), "rights columns shown"); | |
| 1015 | + | assert!(html.contains("license-banner"), "banner rendered"); | |
| 1016 | + | } | |
| 1017 | + | ||
| 1018 | + | #[tokio::test] | |
| 983 | 1019 | async fn pulls_404_when_disabled() { | |
| 984 | 1020 | let (state, app) = app().await; | |
| 985 | 1021 | let ada = state.store.user_by_username("ada").await.unwrap().unwrap(); | |