1use std::{
2 collections::BTreeSet,
3 fmt::{Display, Formatter},
4 path::{Path, PathBuf},
5 sync::{Arc, LazyLock},
6};
7
8use fs::Fs;
9use futures::StreamExt as _;
10use gpui::{App, AppContext as _, Entity, Subscription, Task};
11use itertools::Itertools;
12use postage::watch;
13use project::Worktree;
14use regex::Regex;
15use strum::VariantArray;
16use util::ResultExt as _;
17use worktree::ChildEntriesOptions;
18
19/// Matches the most common license locations, with US and UK English spelling.
20static LICENSE_FILE_NAME_REGEX: LazyLock<regex::bytes::Regex> = LazyLock::new(|| {
21 regex::bytes::RegexBuilder::new(
22 "^ \
23 (?: license | licence)? \
24 (?: [\\-._]? \
25 (?: apache (?: [\\-._] (?: 2.0 | 2 ))? | \
26 0? bsd (?: [\\-._] [0123])? (?: [\\-._] clause)? | \
27 isc | \
28 mit | \
29 upl | \
30 zlib))? \
31 (?: [\\-._]? (?: license | licence))? \
32 (?: \\.txt | \\.md)? \
33 $",
34 )
35 .ignore_whitespace(true)
36 .case_insensitive(true)
37 .build()
38 .unwrap()
39});
40
41#[derive(Debug, Clone, Copy, Eq, Ord, PartialOrd, PartialEq, VariantArray)]
42pub enum OpenSourceLicense {
43 Apache2_0,
44 BSDZero,
45 BSD,
46 ISC,
47 MIT,
48 UPL1_0,
49 Zlib,
50}
51
52impl Display for OpenSourceLicense {
53 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
54 write!(f, "{}", self.spdx_identifier())
55 }
56}
57
58impl OpenSourceLicense {
59 /// These are SPDX identifiers for the licenses, except for BSD, where the variants are not
60 /// distinguished.
61 pub fn spdx_identifier(&self) -> &'static str {
62 match self {
63 OpenSourceLicense::Apache2_0 => "apache-2.0",
64 OpenSourceLicense::BSDZero => "0bsd",
65 OpenSourceLicense::BSD => "bsd",
66 OpenSourceLicense::ISC => "isc",
67 OpenSourceLicense::MIT => "mit",
68 OpenSourceLicense::UPL1_0 => "upl-1.0",
69 OpenSourceLicense::Zlib => "zlib",
70 }
71 }
72
73 /// Regexes to match the license text. These regexes are expected to match the entire file. Also
74 /// note that `canonicalize_license_text` removes everything but alphanumeric ascii characters.
75 pub fn regex(&self) -> &'static str {
76 match self {
77 OpenSourceLicense::Apache2_0 => include_str!("../license_regexes/apache-2.0.regex"),
78 OpenSourceLicense::BSDZero => include_str!("../license_regexes/0bsd.regex"),
79 OpenSourceLicense::BSD => include_str!("../license_regexes/bsd.regex"),
80 OpenSourceLicense::ISC => include_str!("../license_regexes/isc.regex"),
81 OpenSourceLicense::MIT => include_str!("../license_regexes/mit.regex"),
82 OpenSourceLicense::UPL1_0 => include_str!("../license_regexes/upl-1.0.regex"),
83 OpenSourceLicense::Zlib => include_str!("../license_regexes/zlib.regex"),
84 }
85 }
86}
87
88fn detect_license(license: &str) -> Option<OpenSourceLicense> {
89 static LICENSE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
90 let mut regex_string = String::new();
91 let mut is_first = true;
92 for license in OpenSourceLicense::VARIANTS {
93 if is_first {
94 regex_string.push_str("^(?:(");
95 is_first = false;
96 } else {
97 regex_string.push_str(")|(");
98 }
99 regex_string.push_str(&canonicalize_license_regex(license.regex()));
100 }
101 regex_string.push_str("))$");
102 let regex = Regex::new(®ex_string).unwrap();
103 assert_eq!(regex.captures_len(), OpenSourceLicense::VARIANTS.len() + 1);
104 regex
105 });
106
107 LICENSE_REGEX
108 .captures(&canonicalize_license_text(license))
109 .and_then(|captures| {
110 let license = OpenSourceLicense::VARIANTS
111 .iter()
112 .enumerate()
113 .find(|(index, _)| captures.get(index + 1).is_some())
114 .map(|(_, license)| *license);
115 if license.is_none() {
116 log::error!("bug: open source license regex matched without any capture groups");
117 }
118 license
119 })
120}
121
122/// Canonicalizes the whitespace of license text.
123fn canonicalize_license_regex(license: &str) -> String {
124 license
125 .split_ascii_whitespace()
126 .join(" ")
127 .to_ascii_lowercase()
128}
129
130/// Canonicalizes the whitespace of license text.
131fn canonicalize_license_text(license: &str) -> String {
132 license
133 .chars()
134 .filter(|c| c.is_ascii_alphanumeric() || c.is_ascii_whitespace())
135 .map(|c| c.to_ascii_lowercase())
136 .collect::<String>()
137 .split_ascii_whitespace()
138 .join(" ")
139}
140
141pub enum LicenseDetectionWatcher {
142 Local {
143 is_open_source_rx: watch::Receiver<bool>,
144 _is_open_source_task: Task<()>,
145 _worktree_subscription: Subscription,
146 },
147 SingleFile,
148 Remote,
149}
150
151impl LicenseDetectionWatcher {
152 pub fn new(worktree: &Entity<Worktree>, cx: &mut App) -> Self {
153 let worktree_ref = worktree.read(cx);
154 if worktree_ref.is_single_file() {
155 return Self::SingleFile;
156 }
157
158 let (files_to_check_tx, mut files_to_check_rx) = futures::channel::mpsc::unbounded();
159
160 let Worktree::Local(local_worktree) = worktree_ref else {
161 return Self::Remote;
162 };
163 let fs = local_worktree.fs().clone();
164 let worktree_abs_path = local_worktree.abs_path().clone();
165
166 let options = ChildEntriesOptions {
167 include_files: true,
168 include_dirs: false,
169 include_ignored: true,
170 };
171 for top_file in local_worktree.child_entries_with_options(Path::new(""), options) {
172 let path_bytes = top_file.path.as_os_str().as_encoded_bytes();
173 if top_file.is_created() && LICENSE_FILE_NAME_REGEX.is_match(path_bytes) {
174 let rel_path = top_file.path.clone();
175 files_to_check_tx.unbounded_send(rel_path).ok();
176 }
177 }
178
179 let _worktree_subscription =
180 cx.subscribe(worktree, move |_worktree, event, _cx| match event {
181 worktree::Event::UpdatedEntries(updated_entries) => {
182 for updated_entry in updated_entries.iter() {
183 let rel_path = &updated_entry.0;
184 let path_bytes = rel_path.as_os_str().as_encoded_bytes();
185 if LICENSE_FILE_NAME_REGEX.is_match(path_bytes) {
186 files_to_check_tx.unbounded_send(rel_path.clone()).ok();
187 }
188 }
189 }
190 worktree::Event::DeletedEntry(_) | worktree::Event::UpdatedGitRepositories(_) => {}
191 });
192
193 let (mut is_open_source_tx, is_open_source_rx) = watch::channel_with::<bool>(false);
194
195 let _is_open_source_task = cx.background_spawn(async move {
196 let mut eligible_licenses = BTreeSet::new();
197 while let Some(rel_path) = files_to_check_rx.next().await {
198 let abs_path = worktree_abs_path.join(&rel_path);
199 let was_open_source = !eligible_licenses.is_empty();
200 if Self::is_path_eligible(&fs, abs_path).await.unwrap_or(false) {
201 eligible_licenses.insert(rel_path);
202 } else {
203 eligible_licenses.remove(&rel_path);
204 }
205 let is_open_source = !eligible_licenses.is_empty();
206 if is_open_source != was_open_source {
207 *is_open_source_tx.borrow_mut() = is_open_source;
208 }
209 }
210 });
211
212 Self::Local {
213 is_open_source_rx,
214 _is_open_source_task,
215 _worktree_subscription,
216 }
217 }
218
219 async fn is_path_eligible(fs: &Arc<dyn Fs>, abs_path: PathBuf) -> Option<bool> {
220 log::debug!("checking if `{abs_path:?}` is an open source license");
221 // Resolve symlinks so that the file size from metadata is correct.
222 let Some(abs_path) = fs.canonicalize(&abs_path).await.ok() else {
223 log::debug!(
224 "`{abs_path:?}` license file probably deleted (error canonicalizing the path)"
225 );
226 return None;
227 };
228 let metadata = fs.metadata(&abs_path).await.log_err()??;
229 // If the license file is >32kb it's unlikely to legitimately match any eligible license.
230 if metadata.len > 32768 {
231 return None;
232 }
233 let text = fs.load(&abs_path).await.log_err()?;
234 let is_eligible = detect_license(&text).is_some();
235 if is_eligible {
236 log::debug!(
237 "`{abs_path:?}` matches a license that is eligible for data collection (if enabled)"
238 );
239 } else {
240 log::debug!(
241 "`{abs_path:?}` does not match a license that is eligible for data collection"
242 );
243 }
244 Some(is_eligible)
245 }
246
247 /// Answers false until we find out it's open source
248 pub fn is_project_open_source(&self) -> bool {
249 match self {
250 Self::Local {
251 is_open_source_rx, ..
252 } => *is_open_source_rx.borrow(),
253 Self::SingleFile | Self::Remote => false,
254 }
255 }
256}
257
258#[cfg(test)]
259mod tests {
260
261 use fs::FakeFs;
262 use gpui::TestAppContext;
263 use serde_json::json;
264 use settings::{Settings as _, SettingsStore};
265 use unindent::unindent;
266 use worktree::WorktreeSettings;
267
268 use super::*;
269
270 const APACHE_2_0_TXT: &str = include_str!("../license_examples/apache-2.0-ex0.txt");
271 const ISC_TXT: &str = include_str!("../license_examples/isc.txt");
272 const MIT_TXT: &str = include_str!("../license_examples/mit-ex0.txt");
273 const UPL_1_0_TXT: &str = include_str!("../license_examples/upl-1.0.txt");
274 const BSD_0_TXT: &str = include_str!("../license_examples/0bsd.txt");
275
276 #[track_caller]
277 fn assert_matches_license(text: &str, license: OpenSourceLicense) {
278 if detect_license(text) != Some(license) {
279 let license_regex_text = canonicalize_license_regex(license.regex());
280 let license_regex = Regex::new(&format!("^{}$", license_regex_text)).unwrap();
281 let text = canonicalize_license_text(text);
282 let matched_regex = license_regex.is_match(&text);
283 if matched_regex {
284 panic!(
285 "The following text matches the individual regex for {}, \
286 but not the combined one:\n```license-text\n{}\n```\n",
287 license, text
288 );
289 } else {
290 panic!(
291 "The following text doesn't match the regex for {}:\n\
292 ```license-text\n{}\n```\n\n```regex\n{}\n```\n",
293 license, text, license_regex_text
294 );
295 }
296 }
297 }
298
299 /*
300 // Uncomment this and run with `cargo test -p zeta -- --no-capture &> licenses-output` to
301 // traverse your entire home directory and run license detection on every file that has a
302 // license-like name.
303 #[test]
304 fn test_check_all_licenses_in_home_dir() {
305 let mut detected = Vec::new();
306 let mut unrecognized = Vec::new();
307 let mut walked_entries = 0;
308 let homedir = std::env::home_dir().unwrap();
309 for entry in walkdir::WalkDir::new(&homedir) {
310 walked_entries += 1;
311 if walked_entries % 10000 == 0 {
312 println!(
313 "So far visited {} files in {}",
314 walked_entries,
315 homedir.display()
316 );
317 }
318 let Ok(entry) = entry else {
319 continue;
320 };
321 if !LICENSE_FILE_NAME_REGEX.is_match(entry.file_name().as_encoded_bytes()) {
322 continue;
323 }
324 let Ok(contents) = std::fs::read_to_string(entry.path()) else {
325 continue;
326 };
327 let path_string = entry.path().to_string_lossy().to_string();
328 match detect_license(&contents) {
329 Some(license) => detected.push((license, path_string)),
330 None => unrecognized.push(path_string),
331 }
332 }
333 println!("\nDetected licenses:\n");
334 detected.sort();
335 for (license, path) in &detected {
336 println!("{}: {}", license.spdx_identifier(), path);
337 }
338 println!("\nUnrecognized licenses:\n");
339 for path in &unrecognized {
340 println!("{}", path);
341 }
342 panic!(
343 "{} licenses detected, {} unrecognized",
344 detected.len(),
345 unrecognized.len()
346 );
347 println!("This line has a warning to make sure this test is always commented out");
348 }
349 */
350
351 #[test]
352 fn test_no_unicode_in_regexes() {
353 for license in OpenSourceLicense::VARIANTS {
354 assert!(
355 !license.regex().contains(|c: char| !c.is_ascii()),
356 "{}.regex contains unicode",
357 license.spdx_identifier()
358 );
359 }
360 }
361
362 #[test]
363 fn test_apache_positive_detection() {
364 assert_matches_license(APACHE_2_0_TXT, OpenSourceLicense::Apache2_0);
365
366 let license_with_appendix = format!(
367 r#"{APACHE_2_0_TXT}
368
369 END OF TERMS AND CONDITIONS
370
371 APPENDIX: How to apply the Apache License to your work.
372
373 To apply the Apache License to your work, attach the following
374 boilerplate notice, with the fields enclosed by brackets "[]"
375 replaced with your own identifying information. (Don't include
376 the brackets!) The text should be enclosed in the appropriate
377 comment syntax for the file format. We also recommend that a
378 file or class name and description of purpose be included on the
379 same "printed page" as the copyright notice for easier
380 identification within third-party archives.
381
382 Copyright [yyyy] [name of copyright owner]
383
384 Licensed under the Apache License, Version 2.0 (the "License");
385 you may not use this file except in compliance with the License.
386 You may obtain a copy of the License at
387
388 http://www.apache.org/licenses/LICENSE-2.0
389
390 Unless required by applicable law or agreed to in writing, software
391 distributed under the License is distributed on an "AS IS" BASIS,
392 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
393 See the License for the specific language governing permissions and
394 limitations under the License."#
395 );
396 assert_matches_license(&license_with_appendix, OpenSourceLicense::Apache2_0);
397
398 // Sometimes people fill in the appendix with copyright info.
399 let license_with_copyright = license_with_appendix.replace(
400 "Copyright [yyyy] [name of copyright owner]",
401 "Copyright 2025 John Doe",
402 );
403 assert!(license_with_copyright != license_with_appendix);
404 assert_matches_license(&license_with_copyright, OpenSourceLicense::Apache2_0);
405
406 assert_matches_license(
407 include_str!("../../../LICENSE-APACHE"),
408 OpenSourceLicense::Apache2_0,
409 );
410
411 assert_matches_license(
412 include_str!("../license_examples/apache-2.0-ex1.txt"),
413 OpenSourceLicense::Apache2_0,
414 );
415 assert_matches_license(
416 include_str!("../license_examples/apache-2.0-ex2.txt"),
417 OpenSourceLicense::Apache2_0,
418 );
419 assert_matches_license(
420 include_str!("../license_examples/apache-2.0-ex3.txt"),
421 OpenSourceLicense::Apache2_0,
422 );
423 }
424
425 #[test]
426 fn test_apache_negative_detection() {
427 assert!(
428 detect_license(&format!(
429 "{APACHE_2_0_TXT}\n\nThe terms in this license are void if P=NP."
430 ))
431 .is_none()
432 );
433 }
434
435 #[test]
436 fn test_bsd_1_clause_positive_detection() {
437 assert_matches_license(
438 include_str!("../license_examples/bsd-1-clause.txt"),
439 OpenSourceLicense::BSD,
440 );
441 }
442
443 #[test]
444 fn test_bsd_2_clause_positive_detection() {
445 assert_matches_license(
446 include_str!("../license_examples/bsd-2-clause-ex0.txt"),
447 OpenSourceLicense::BSD,
448 );
449 }
450
451 #[test]
452 fn test_bsd_3_clause_positive_detection() {
453 assert_matches_license(
454 include_str!("../license_examples/bsd-3-clause-ex0.txt"),
455 OpenSourceLicense::BSD,
456 );
457 assert_matches_license(
458 include_str!("../license_examples/bsd-3-clause-ex1.txt"),
459 OpenSourceLicense::BSD,
460 );
461 assert_matches_license(
462 include_str!("../license_examples/bsd-3-clause-ex2.txt"),
463 OpenSourceLicense::BSD,
464 );
465 assert_matches_license(
466 include_str!("../license_examples/bsd-3-clause-ex3.txt"),
467 OpenSourceLicense::BSD,
468 );
469 assert_matches_license(
470 include_str!("../license_examples/bsd-3-clause-ex4.txt"),
471 OpenSourceLicense::BSD,
472 );
473 }
474
475 #[test]
476 fn test_bsd_0_positive_detection() {
477 assert_matches_license(BSD_0_TXT, OpenSourceLicense::BSDZero);
478 }
479
480 #[test]
481 fn test_isc_positive_detection() {
482 assert_matches_license(ISC_TXT, OpenSourceLicense::ISC);
483 }
484
485 #[test]
486 fn test_isc_negative_detection() {
487 let license_text = format!(
488 r#"{ISC_TXT}
489
490 This project is dual licensed under the ISC License and the MIT License."#
491 );
492
493 assert!(detect_license(&license_text).is_none());
494 }
495
496 #[test]
497 fn test_mit_positive_detection() {
498 assert_matches_license(MIT_TXT, OpenSourceLicense::MIT);
499 assert_matches_license(
500 include_str!("../license_examples/mit-ex1.txt"),
501 OpenSourceLicense::MIT,
502 );
503 assert_matches_license(
504 include_str!("../license_examples/mit-ex2.txt"),
505 OpenSourceLicense::MIT,
506 );
507 assert_matches_license(
508 include_str!("../license_examples/mit-ex3.txt"),
509 OpenSourceLicense::MIT,
510 );
511 }
512
513 #[test]
514 fn test_mit_negative_detection() {
515 let license_text = format!(
516 r#"{MIT_TXT}
517
518 This project is dual licensed under the MIT License and the Apache License, Version 2.0."#
519 );
520 assert!(detect_license(&license_text).is_none());
521 }
522
523 #[test]
524 fn test_upl_positive_detection() {
525 assert_matches_license(UPL_1_0_TXT, OpenSourceLicense::UPL1_0);
526 }
527
528 #[test]
529 fn test_upl_negative_detection() {
530 let license_text = format!(
531 r#"{UPL_1_0_TXT}
532
533 This project is dual licensed under the UPL License and the MIT License."#
534 );
535
536 assert!(detect_license(&license_text).is_none());
537 }
538
539 #[test]
540 fn test_zlib_positive_detection() {
541 assert_matches_license(
542 include_str!("../license_examples/zlib-ex0.txt"),
543 OpenSourceLicense::Zlib,
544 );
545 }
546
547 #[test]
548 fn test_license_file_name_regex() {
549 // Test basic license file names
550 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE"));
551 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE"));
552 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"license"));
553 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"licence"));
554
555 // Test with extensions
556 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.txt"));
557 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.md"));
558 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE.txt"));
559 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE.md"));
560
561 // Test with specific license types
562 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-APACHE"));
563 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-MIT"));
564 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.MIT"));
565 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE_MIT"));
566 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-ISC"));
567 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-UPL"));
568
569 // Test with "license" coming after
570 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"APACHE-LICENSE"));
571
572 // Test version numbers
573 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"APACHE-2"));
574 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"APACHE-2.0"));
575 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"BSD-1"));
576 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"BSD-2"));
577 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"BSD-3"));
578 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"BSD-3-CLAUSE"));
579
580 // Test combinations
581 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-MIT.txt"));
582 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE.ISC.md"));
583 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"license_upl"));
584 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.APACHE.2.0"));
585
586 // Test case insensitive
587 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"License"));
588 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"license-mit.TXT"));
589 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE_isc.MD"));
590
591 // Test edge cases that should match
592 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"license.mit"));
593 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"licence-upl.txt"));
594
595 // Test non-matching patterns
596 assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"COPYING"));
597 assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.html"));
598 assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"MYLICENSE"));
599 assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"src/LICENSE"));
600 assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.old"));
601 assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-GPL"));
602 assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSEABC"));
603 }
604
605 #[test]
606 fn test_canonicalize_license_text() {
607 let input = " Paragraph 1\nwith multiple lines\n\n\n\nParagraph 2\nwith more lines\n ";
608 let expected = "paragraph 1 with multiple lines paragraph 2 with more lines";
609 assert_eq!(canonicalize_license_text(input), expected);
610
611 // Test tabs and mixed whitespace
612 let input = "Word1\t\tWord2\n\n Word3\r\n\r\n\r\nWord4 ";
613 let expected = "word1 word2 word3 word4";
614 assert_eq!(canonicalize_license_text(input), expected);
615 }
616
617 #[test]
618 fn test_license_detection_canonicalizes_whitespace() {
619 let mit_with_weird_spacing = unindent(
620 r#"
621 MIT License
622
623
624 Copyright (c) 2024 John Doe
625
626
627 Permission is hereby granted, free of charge, to any person obtaining a copy
628 of this software and associated documentation files (the "Software"), to deal
629 in the Software without restriction, including without limitation the rights
630 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
631 copies of the Software, and to permit persons to whom the Software is
632 furnished to do so, subject to the following conditions:
633
634
635
636 The above copyright notice and this permission notice shall be included in all
637 copies or substantial portions of the Software.
638
639
640
641 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
642 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
643 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
644 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
645 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
646 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
647 SOFTWARE.
648 "#
649 .trim(),
650 );
651
652 assert_matches_license(&mit_with_weird_spacing, OpenSourceLicense::MIT);
653 }
654
655 fn init_test(cx: &mut TestAppContext) {
656 cx.update(|cx| {
657 let settings_store = SettingsStore::test(cx);
658 cx.set_global(settings_store);
659 WorktreeSettings::register(cx);
660 });
661 }
662
663 #[gpui::test]
664 async fn test_watcher_single_file(cx: &mut TestAppContext) {
665 init_test(cx);
666
667 let fs = FakeFs::new(cx.background_executor.clone());
668 fs.insert_tree("/root", json!({ "main.rs": "fn main() {}" }))
669 .await;
670
671 let worktree = Worktree::local(
672 Path::new("/root/main.rs"),
673 true,
674 fs.clone(),
675 Default::default(),
676 &mut cx.to_async(),
677 )
678 .await
679 .unwrap();
680
681 let watcher = cx.update(|cx| LicenseDetectionWatcher::new(&worktree, cx));
682 assert!(matches!(watcher, LicenseDetectionWatcher::SingleFile));
683 assert!(!watcher.is_project_open_source());
684 }
685
686 #[gpui::test]
687 async fn test_watcher_updates_on_changes(cx: &mut TestAppContext) {
688 init_test(cx);
689
690 let fs = FakeFs::new(cx.background_executor.clone());
691 fs.insert_tree("/root", json!({ "main.rs": "fn main() {}" }))
692 .await;
693
694 let worktree = Worktree::local(
695 Path::new("/root"),
696 true,
697 fs.clone(),
698 Default::default(),
699 &mut cx.to_async(),
700 )
701 .await
702 .unwrap();
703
704 let watcher = cx.update(|cx| LicenseDetectionWatcher::new(&worktree, cx));
705 assert!(matches!(watcher, LicenseDetectionWatcher::Local { .. }));
706 assert!(!watcher.is_project_open_source());
707
708 fs.write(Path::new("/root/LICENSE-MIT"), MIT_TXT.as_bytes())
709 .await
710 .unwrap();
711
712 cx.background_executor.run_until_parked();
713 assert!(watcher.is_project_open_source());
714
715 fs.write(Path::new("/root/LICENSE-APACHE"), APACHE_2_0_TXT.as_bytes())
716 .await
717 .unwrap();
718
719 cx.background_executor.run_until_parked();
720 assert!(watcher.is_project_open_source());
721
722 fs.write(Path::new("/root/LICENSE-MIT"), "Nevermind".as_bytes())
723 .await
724 .unwrap();
725
726 // Still considered open source as LICENSE-APACHE is present
727 cx.background_executor.run_until_parked();
728 assert!(watcher.is_project_open_source());
729
730 fs.write(
731 Path::new("/root/LICENSE-APACHE"),
732 "Also nevermind".as_bytes(),
733 )
734 .await
735 .unwrap();
736
737 cx.background_executor.run_until_parked();
738 assert!(!watcher.is_project_open_source());
739 }
740
741 #[gpui::test]
742 async fn test_watcher_initially_opensource_and_then_deleted(cx: &mut TestAppContext) {
743 init_test(cx);
744
745 let fs = FakeFs::new(cx.background_executor.clone());
746 fs.insert_tree(
747 "/root",
748 json!({ "main.rs": "fn main() {}", "LICENSE-MIT": MIT_TXT }),
749 )
750 .await;
751
752 let worktree = Worktree::local(
753 Path::new("/root"),
754 true,
755 fs.clone(),
756 Default::default(),
757 &mut cx.to_async(),
758 )
759 .await
760 .unwrap();
761
762 let watcher = cx.update(|cx| LicenseDetectionWatcher::new(&worktree, cx));
763 assert!(matches!(watcher, LicenseDetectionWatcher::Local { .. }));
764
765 cx.background_executor.run_until_parked();
766 assert!(watcher.is_project_open_source());
767
768 fs.remove_file(
769 Path::new("/root/LICENSE-MIT"),
770 fs::RemoveOptions {
771 recursive: false,
772 ignore_if_not_exists: false,
773 },
774 )
775 .await
776 .unwrap();
777
778 cx.background_executor.run_until_parked();
779 assert!(!watcher.is_project_open_source());
780 }
781}