Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
assets/base.css +71 −0
| @@ -595,6 +595,77 @@ body.drawer-open .drawer-backdrop { | |||
| 595 | flex: none; | 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 | /* Pagination controls for long lists. */ | 669 | /* Pagination controls for long lists. */ |
| 599 | .pagination { | 670 | .pagination { |
| 600 | display: flex; | 671 | display: flex; |
crates/web/src/icons.rs +8 −0
| @@ -34,6 +34,9 @@ pub enum Icon { | |||
| 34 | Compass, | 34 | Compass, |
| 35 | MapPin, | 35 | MapPin, |
| 36 | Mail, | 36 | Mail, |
| 37 | Check, | ||
| 38 | X, | ||
| 39 | Scale, | ||
| 37 | Globe, | 40 | Globe, |
| 38 | Download, | 41 | Download, |
| 39 | Github, | 42 | Github, |
| @@ -104,6 +107,11 @@ impl Icon { | |||
| 104 | Icon::Mail => { | 107 | Icon::Mail => { |
| 105 | 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"/>"# | 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 | Icon::Globe => { | 115 | Icon::Globe => { |
| 108 | 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"/>"# | 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 | mod icons; | 19 | mod icons; |
| 20 | mod issues; | 20 | mod issues; |
| 21 | mod layout; | 21 | mod layout; |
| 22 | mod license; | ||
| 22 | mod markdown; | 23 | mod markdown; |
| 23 | mod pages; | 24 | mod pages; |
| 24 | mod pulls; | 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 | attr_lang.as_deref(), | 621 | attr_lang.as_deref(), |
| 622 | source_view, | 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 | let branches = branch_names(state, &ctx.repo.id).await; | 628 | let branches = branch_names(state, &ctx.repo.id).await; |
| 625 | let body = html! { | 629 | let body = html! { |
| 626 | (repo_header(state, &ctx, Tab::Code, &rev_name, &branches)) | 630 | (repo_header(state, &ctx, Tab::Code, &rev_name, &branches)) |
| 627 | (breadcrumb(&ctx.owner.username, &ctx.repo.path, &rev_name, &path)) | 631 | (breadcrumb(&ctx.owner.username, &ctx.repo.path, &rev_name, &path)) |
| 632 | @if let Some(info) = license { | ||
| 633 | (license_banner(info)) | ||
| 634 | } | ||
| 628 | (content_body) | 635 | (content_body) |
| 629 | }; | 636 | }; |
| 630 | let (jar, chrome) = build_chrome(state, jar, viewer, uri.path().to_string()); | 637 | let (jar, chrome) = build_chrome(state, jar, viewer, uri.path().to_string()); |
| @@ -632,6 +639,46 @@ async fn blob_view( | |||
| 632 | Ok((jar, page(&chrome, &title, body)).into_response()) | 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 | /// Render a blob: a rendered preview for markdown, highlighted code with line | 682 | /// Render a blob: a rendered preview for markdown, highlighted code with line |
| 636 | /// anchors, an inline image, or a binary notice. | 683 | /// anchors, an inline image, or a binary notice. |
| 637 | fn render_blob( | 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 | #[tokio::test] | 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 | async fn pulls_404_when_disabled() { | 1019 | async fn pulls_404_when_disabled() { |
| 984 | let (state, app) = app().await; | 1020 | let (state, app) = app().await; |
| 985 | let ada = state.store.user_by_username("ada").await.unwrap().unwrap(); | 1021 | let ada = state.store.user_by_username("ada").await.unwrap().unwrap(); |