1use std::{
2 collections::BTreeSet,
3 path::{Path, PathBuf},
4 sync::{Arc, LazyLock},
5};
6
7use fs::Fs;
8use futures::StreamExt as _;
9use gpui::{App, AppContext as _, Entity, Subscription, Task};
10use postage::watch;
11use project::Worktree;
12use regex::Regex;
13use util::ResultExt as _;
14use worktree::ChildEntriesOptions;
15
16/// Matches the most common license locations, with US and UK English spelling.
17static LICENSE_FILE_NAME_REGEX: LazyLock<regex::bytes::Regex> = LazyLock::new(|| {
18 regex::bytes::RegexBuilder::new(
19 "^ \
20 (?: license | licence) \
21 (?: [\\-._] (?: apache | isc | mit | upl))? \
22 (?: \\.txt | \\.md)? \
23 $",
24 )
25 .ignore_whitespace(true)
26 .case_insensitive(true)
27 .build()
28 .unwrap()
29});
30
31fn is_license_eligible_for_data_collection(license: &str) -> bool {
32 static LICENSE_REGEXES: LazyLock<Vec<Regex>> = LazyLock::new(|| {
33 [
34 include_str!("license_detection/apache.regex"),
35 include_str!("license_detection/isc.regex"),
36 include_str!("license_detection/mit.regex"),
37 include_str!("license_detection/upl.regex"),
38 ]
39 .into_iter()
40 .map(|pattern| Regex::new(&canonicalize_license_text(pattern)).unwrap())
41 .collect()
42 });
43
44 let license = canonicalize_license_text(license);
45 LICENSE_REGEXES.iter().any(|regex| regex.is_match(&license))
46}
47
48/// Canonicalizes the whitespace of license text and license regexes.
49fn canonicalize_license_text(license: &str) -> String {
50 static PARAGRAPH_SEPARATOR_REGEX: LazyLock<Regex> =
51 LazyLock::new(|| Regex::new(r"\s*\n\s*\n\s*").unwrap());
52
53 PARAGRAPH_SEPARATOR_REGEX
54 .split(license)
55 .filter(|paragraph| !paragraph.trim().is_empty())
56 .map(|paragraph| {
57 paragraph
58 .trim()
59 .split_whitespace()
60 .collect::<Vec<_>>()
61 .join(" ")
62 })
63 .collect::<Vec<_>>()
64 .join("\n\n")
65}
66
67pub enum LicenseDetectionWatcher {
68 Local {
69 is_open_source_rx: watch::Receiver<bool>,
70 _is_open_source_task: Task<()>,
71 _worktree_subscription: Subscription,
72 },
73 SingleFile,
74 Remote,
75}
76
77impl LicenseDetectionWatcher {
78 pub fn new(worktree: &Entity<Worktree>, cx: &mut App) -> Self {
79 let worktree_ref = worktree.read(cx);
80 if worktree_ref.is_single_file() {
81 return Self::SingleFile;
82 }
83
84 let (files_to_check_tx, mut files_to_check_rx) = futures::channel::mpsc::unbounded();
85
86 let Worktree::Local(local_worktree) = worktree_ref else {
87 return Self::Remote;
88 };
89 let fs = local_worktree.fs().clone();
90 let worktree_abs_path = local_worktree.abs_path().clone();
91
92 let options = ChildEntriesOptions {
93 include_files: true,
94 include_dirs: false,
95 include_ignored: true,
96 };
97 for top_file in local_worktree.child_entries_with_options(Path::new(""), options) {
98 let path_bytes = top_file.path.as_os_str().as_encoded_bytes();
99 if top_file.is_created() && LICENSE_FILE_NAME_REGEX.is_match(path_bytes) {
100 let rel_path = top_file.path.clone();
101 files_to_check_tx.unbounded_send(rel_path).ok();
102 }
103 }
104
105 let _worktree_subscription =
106 cx.subscribe(worktree, move |_worktree, event, _cx| match event {
107 worktree::Event::UpdatedEntries(updated_entries) => {
108 for updated_entry in updated_entries.iter() {
109 let rel_path = &updated_entry.0;
110 let path_bytes = rel_path.as_os_str().as_encoded_bytes();
111 if LICENSE_FILE_NAME_REGEX.is_match(path_bytes) {
112 files_to_check_tx.unbounded_send(rel_path.clone()).ok();
113 }
114 }
115 }
116 worktree::Event::DeletedEntry(_) | worktree::Event::UpdatedGitRepositories(_) => {}
117 });
118
119 let (mut is_open_source_tx, is_open_source_rx) = watch::channel_with::<bool>(false);
120
121 let _is_open_source_task = cx.background_spawn(async move {
122 let mut eligible_licenses = BTreeSet::new();
123 while let Some(rel_path) = files_to_check_rx.next().await {
124 let abs_path = worktree_abs_path.join(&rel_path);
125 let was_open_source = !eligible_licenses.is_empty();
126 if Self::is_path_eligible(&fs, abs_path).await.unwrap_or(false) {
127 eligible_licenses.insert(rel_path);
128 } else {
129 eligible_licenses.remove(&rel_path);
130 }
131 let is_open_source = !eligible_licenses.is_empty();
132 if is_open_source != was_open_source {
133 *is_open_source_tx.borrow_mut() = is_open_source;
134 }
135 }
136 });
137
138 Self::Local {
139 is_open_source_rx,
140 _is_open_source_task,
141 _worktree_subscription,
142 }
143 }
144
145 async fn is_path_eligible(fs: &Arc<dyn Fs>, abs_path: PathBuf) -> Option<bool> {
146 log::debug!("checking if `{abs_path:?}` is an open source license");
147 // Resolve symlinks so that the file size from metadata is correct.
148 let Some(abs_path) = fs.canonicalize(&abs_path).await.ok() else {
149 log::debug!(
150 "`{abs_path:?}` license file probably deleted (error canonicalizing the path)"
151 );
152 return None;
153 };
154 let metadata = fs.metadata(&abs_path).await.log_err()??;
155 // If the license file is >32kb it's unlikely to legitimately match any eligible license.
156 if metadata.len > 32768 {
157 return None;
158 }
159 let text = fs.load(&abs_path).await.log_err()?;
160 let is_eligible = is_license_eligible_for_data_collection(&text);
161 if is_eligible {
162 log::debug!(
163 "`{abs_path:?}` matches a license that is eligible for data collection (if enabled)"
164 );
165 } else {
166 log::debug!(
167 "`{abs_path:?}` does not match a license that is eligible for data collection"
168 );
169 }
170 Some(is_eligible)
171 }
172
173 /// Answers false until we find out it's open source
174 pub fn is_project_open_source(&self) -> bool {
175 match self {
176 Self::Local {
177 is_open_source_rx, ..
178 } => *is_open_source_rx.borrow(),
179 Self::SingleFile | Self::Remote => false,
180 }
181 }
182}
183
184#[cfg(test)]
185mod tests {
186
187 use fs::FakeFs;
188 use gpui::TestAppContext;
189 use serde_json::json;
190 use settings::{Settings as _, SettingsStore};
191 use unindent::unindent;
192 use worktree::WorktreeSettings;
193
194 use super::*;
195
196 const MIT_LICENSE: &str = include_str!("license_detection/mit-text");
197 const APACHE_LICENSE: &str = include_str!("license_detection/apache-text");
198
199 #[test]
200 fn test_mit_positive_detection() {
201 assert!(is_license_eligible_for_data_collection(MIT_LICENSE));
202 }
203
204 #[test]
205 fn test_mit_negative_detection() {
206 let example_license = format!(
207 r#"{MIT_LICENSE}
208
209 This project is dual licensed under the MIT License and the Apache License, Version 2.0."#
210 );
211 assert!(!is_license_eligible_for_data_collection(&example_license));
212 }
213
214 #[test]
215 fn test_isc_positive_detection() {
216 let example_license = unindent(
217 r#"
218 ISC License
219
220 Copyright (c) 2024, John Doe
221
222 Permission to use, copy, modify, and/or distribute this software for any
223 purpose with or without fee is hereby granted, provided that the above
224 copyright notice and this permission notice appear in all copies.
225
226 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
227 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
228 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
229 ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
230 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
231 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
232 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
233 "#
234 .trim(),
235 );
236
237 assert!(is_license_eligible_for_data_collection(&example_license));
238 }
239
240 #[test]
241 fn test_isc_negative_detection() {
242 let example_license = unindent(
243 r#"
244 ISC License
245
246 Copyright (c) 2024, John Doe
247
248 Permission to use, copy, modify, and/or distribute this software for any
249 purpose with or without fee is hereby granted, provided that the above
250 copyright notice and this permission notice appear in all copies.
251
252 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
253 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
254 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
255 ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
256 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
257 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
258 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
259
260 This project is dual licensed under the ISC License and the MIT License.
261 "#
262 .trim(),
263 );
264
265 assert!(!is_license_eligible_for_data_collection(&example_license));
266 }
267
268 #[test]
269 fn test_upl_positive_detection() {
270 let example_license = unindent(
271 r#"
272 Copyright (c) 2025, John Doe
273
274 The Universal Permissive License (UPL), Version 1.0
275
276 Subject to the condition set forth below, permission is hereby granted to any person
277 obtaining a copy of this software, associated documentation and/or data (collectively
278 the "Software"), free of charge and under any and all copyright rights in the
279 Software, and any and all patent rights owned or freely licensable by each licensor
280 hereunder covering either (i) the unmodified Software as contributed to or provided
281 by such licensor, or (ii) the Larger Works (as defined below), to deal in both
282
283 (a) the Software, and
284
285 (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if one is
286 included with the Software (each a "Larger Work" to which the Software is
287 contributed by such licensors),
288
289 without restriction, including without limitation the rights to copy, create
290 derivative works of, display, perform, and distribute the Software and make, use,
291 sell, offer for sale, import, export, have made, and have sold the Software and the
292 Larger Work(s), and to sublicense the foregoing rights on either these or other
293 terms.
294
295 This license is subject to the following condition:
296
297 The above copyright notice and either this complete permission notice or at a minimum
298 a reference to the UPL must be included in all copies or substantial portions of the
299 Software.
300
301 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
302 INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
303 PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
304 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
305 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
306 OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
307 "#
308 .trim(),
309 );
310
311 assert!(is_license_eligible_for_data_collection(&example_license));
312 }
313
314 #[test]
315 fn test_upl_negative_detection() {
316 let example_license = unindent(
317 r#"
318 UPL License
319
320 Copyright (c) 2024, John Doe
321
322 The Universal Permissive License (UPL), Version 1.0
323
324 Subject to the condition set forth below, permission is hereby granted to any person
325 obtaining a copy of this software, associated documentation and/or data (collectively
326 the "Software"), free of charge and under any and all copyright rights in the
327 Software, and any and all patent rights owned or freely licensable by each licensor
328 hereunder covering either (i) the unmodified Software as contributed to or provided
329 by such licensor, or (ii) the Larger Works (as defined below), to deal in both
330
331 (a) the Software, and
332
333 (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if one is
334 included with the Software (each a "Larger Work" to which the Software is
335 contributed by such licensors),
336
337 without restriction, including without limitation the rights to copy, create
338 derivative works of, display, perform, and distribute the Software and make, use,
339 sell, offer for sale, import, export, have made, and have sold the Software and the
340 Larger Work(s), and to sublicense the foregoing rights on either these or other
341 terms.
342
343 This license is subject to the following condition:
344
345 The above copyright notice and either this complete permission notice or at a minimum
346 a reference to the UPL must be included in all copies or substantial portions of the
347 Software.
348
349 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
350 INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
351 PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
352 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
353 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
354 OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
355
356 This project is dual licensed under the ISC License and the MIT License.
357 "#
358 .trim(),
359 );
360
361 assert!(!is_license_eligible_for_data_collection(&example_license));
362 }
363
364 #[test]
365 fn test_apache_positive_detection() {
366 assert!(is_license_eligible_for_data_collection(APACHE_LICENSE));
367
368 let license_with_appendix = format!(
369 r#"{APACHE_LICENSE}
370
371 END OF TERMS AND CONDITIONS
372
373 APPENDIX: How to apply the Apache License to your work.
374
375 To apply the Apache License to your work, attach the following
376 boilerplate notice, with the fields enclosed by brackets "[]"
377 replaced with your own identifying information. (Don't include
378 the brackets!) The text should be enclosed in the appropriate
379 comment syntax for the file format. We also recommend that a
380 file or class name and description of purpose be included on the
381 same "printed page" as the copyright notice for easier
382 identification within third-party archives.
383
384 Copyright [yyyy] [name of copyright owner]
385
386 Licensed under the Apache License, Version 2.0 (the "License");
387 you may not use this file except in compliance with the License.
388 You may obtain a copy of the License at
389
390 http://www.apache.org/licenses/LICENSE-2.0
391
392 Unless required by applicable law or agreed to in writing, software
393 distributed under the License is distributed on an "AS IS" BASIS,
394 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
395 See the License for the specific language governing permissions and
396 limitations under the License."#
397 );
398 assert!(is_license_eligible_for_data_collection(
399 &license_with_appendix
400 ));
401
402 // Sometimes people fill in the appendix with copyright info.
403 let license_with_copyright = license_with_appendix.replace(
404 "Copyright [yyyy] [name of copyright owner]",
405 "Copyright 2025 John Doe",
406 );
407 assert!(license_with_copyright != license_with_appendix);
408 assert!(is_license_eligible_for_data_collection(
409 &license_with_copyright
410 ));
411 }
412
413 #[test]
414 fn test_apache_negative_detection() {
415 assert!(!is_license_eligible_for_data_collection(&format!(
416 "{APACHE_LICENSE}\n\nThe terms in this license are void if P=NP."
417 )));
418 }
419
420 #[test]
421 fn test_license_file_name_regex() {
422 // Test basic license file names
423 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE"));
424 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE"));
425 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"license"));
426 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"licence"));
427
428 // Test with extensions
429 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.txt"));
430 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.md"));
431 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE.txt"));
432 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE.md"));
433
434 // Test with specific license types
435 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-APACHE"));
436 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-MIT"));
437 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.MIT"));
438 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE_MIT"));
439 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-ISC"));
440 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-UPL"));
441
442 // Test combinations
443 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-MIT.txt"));
444 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE.ISC.md"));
445 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"license_upl"));
446
447 // Test case insensitive
448 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"License"));
449 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"license-mit.TXT"));
450 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE_isc.MD"));
451
452 // Test edge cases that should match
453 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"license.mit"));
454 assert!(LICENSE_FILE_NAME_REGEX.is_match(b"licence-upl.txt"));
455
456 // Test non-matching patterns
457 assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"COPYING"));
458 assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.html"));
459 assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"MYLICENSE"));
460 assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"src/LICENSE"));
461 assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.old"));
462 assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-GPL"));
463 assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSEABC"));
464 assert!(!LICENSE_FILE_NAME_REGEX.is_match(b""));
465 }
466
467 #[test]
468 fn test_canonicalize_license_text() {
469 // Test basic whitespace normalization
470 let input = "Line 1\n Line 2 \n\n\n Line 3 ";
471 let expected = "Line 1 Line 2\n\nLine 3";
472 assert_eq!(canonicalize_license_text(input), expected);
473
474 // Test paragraph separation
475 let input = "Paragraph 1\nwith multiple lines\n\n\n\nParagraph 2\nwith more lines";
476 let expected = "Paragraph 1 with multiple lines\n\nParagraph 2 with more lines";
477 assert_eq!(canonicalize_license_text(input), expected);
478
479 // Test empty paragraphs are filtered out
480 let input = "\n\n\nParagraph 1\n\n\n \n\n\nParagraph 2\n\n\n";
481 let expected = "Paragraph 1\n\nParagraph 2";
482 assert_eq!(canonicalize_license_text(input), expected);
483
484 // Test single line
485 let input = " Single line with spaces ";
486 let expected = "Single line with spaces";
487 assert_eq!(canonicalize_license_text(input), expected);
488
489 // Test multiple consecutive spaces within lines
490 let input = "Word1 Word2\n\nWord3 Word4";
491 let expected = "Word1 Word2\n\nWord3 Word4";
492 assert_eq!(canonicalize_license_text(input), expected);
493
494 // Test tabs and mixed whitespace
495 let input = "Word1\t\tWord2\n\n Word3\r\n\r\n\r\nWord4 ";
496 let expected = "Word1 Word2\n\nWord3\n\nWord4";
497 assert_eq!(canonicalize_license_text(input), expected);
498 }
499
500 #[test]
501 fn test_license_detection_canonicalizes_whitespace() {
502 let mit_with_weird_spacing = unindent(
503 r#"
504 MIT License
505
506
507 Copyright (c) 2024 John Doe
508
509
510 Permission is hereby granted, free of charge, to any person obtaining a copy
511 of this software and associated documentation files (the "Software"), to deal
512 in the Software without restriction, including without limitation the rights
513 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
514 copies of the Software, and to permit persons to whom the Software is
515 furnished to do so, subject to the following conditions:
516
517
518
519 The above copyright notice and this permission notice shall be included in all
520 copies or substantial portions of the Software.
521
522
523
524 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
525 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
526 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
527 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
528 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
529 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
530 SOFTWARE.
531 "#
532 .trim(),
533 );
534
535 assert!(is_license_eligible_for_data_collection(
536 &mit_with_weird_spacing
537 ));
538 }
539
540 fn init_test(cx: &mut TestAppContext) {
541 cx.update(|cx| {
542 let settings_store = SettingsStore::test(cx);
543 cx.set_global(settings_store);
544 WorktreeSettings::register(cx);
545 });
546 }
547
548 #[gpui::test]
549 async fn test_watcher_single_file(cx: &mut TestAppContext) {
550 init_test(cx);
551
552 let fs = FakeFs::new(cx.background_executor.clone());
553 fs.insert_tree("/root", json!({ "main.rs": "fn main() {}" }))
554 .await;
555
556 let worktree = Worktree::local(
557 Path::new("/root/main.rs"),
558 true,
559 fs.clone(),
560 Default::default(),
561 &mut cx.to_async(),
562 )
563 .await
564 .unwrap();
565
566 let watcher = cx.update(|cx| LicenseDetectionWatcher::new(&worktree, cx));
567 assert!(matches!(watcher, LicenseDetectionWatcher::SingleFile));
568 assert!(!watcher.is_project_open_source());
569 }
570
571 #[gpui::test]
572 async fn test_watcher_updates_on_changes(cx: &mut TestAppContext) {
573 init_test(cx);
574
575 let fs = FakeFs::new(cx.background_executor.clone());
576 fs.insert_tree("/root", json!({ "main.rs": "fn main() {}" }))
577 .await;
578
579 let worktree = Worktree::local(
580 Path::new("/root"),
581 true,
582 fs.clone(),
583 Default::default(),
584 &mut cx.to_async(),
585 )
586 .await
587 .unwrap();
588
589 let watcher = cx.update(|cx| LicenseDetectionWatcher::new(&worktree, cx));
590 assert!(matches!(watcher, LicenseDetectionWatcher::Local { .. }));
591 assert!(!watcher.is_project_open_source());
592
593 fs.write(Path::new("/root/LICENSE-MIT"), MIT_LICENSE.as_bytes())
594 .await
595 .unwrap();
596
597 cx.background_executor.run_until_parked();
598 assert!(watcher.is_project_open_source());
599
600 fs.write(Path::new("/root/LICENSE-APACHE"), APACHE_LICENSE.as_bytes())
601 .await
602 .unwrap();
603
604 cx.background_executor.run_until_parked();
605 assert!(watcher.is_project_open_source());
606
607 fs.write(Path::new("/root/LICENSE-MIT"), "Nevermind".as_bytes())
608 .await
609 .unwrap();
610
611 // Still considered open source as LICENSE-APACHE is present
612 cx.background_executor.run_until_parked();
613 assert!(watcher.is_project_open_source());
614
615 fs.write(
616 Path::new("/root/LICENSE-APACHE"),
617 "Also nevermind".as_bytes(),
618 )
619 .await
620 .unwrap();
621
622 cx.background_executor.run_until_parked();
623 assert!(!watcher.is_project_open_source());
624 }
625
626 #[gpui::test]
627 async fn test_watcher_initially_opensource_and_then_deleted(cx: &mut TestAppContext) {
628 init_test(cx);
629
630 let fs = FakeFs::new(cx.background_executor.clone());
631 fs.insert_tree(
632 "/root",
633 json!({ "main.rs": "fn main() {}", "LICENSE-MIT": MIT_LICENSE }),
634 )
635 .await;
636
637 let worktree = Worktree::local(
638 Path::new("/root"),
639 true,
640 fs.clone(),
641 Default::default(),
642 &mut cx.to_async(),
643 )
644 .await
645 .unwrap();
646
647 let watcher = cx.update(|cx| LicenseDetectionWatcher::new(&worktree, cx));
648 assert!(matches!(watcher, LicenseDetectionWatcher::Local { .. }));
649
650 cx.background_executor.run_until_parked();
651 assert!(watcher.is_project_open_source());
652
653 fs.remove_file(
654 Path::new("/root/LICENSE-MIT"),
655 fs::RemoveOptions {
656 recursive: false,
657 ignore_if_not_exists: false,
658 },
659 )
660 .await
661 .unwrap();
662
663 cx.background_executor.run_until_parked();
664 assert!(!watcher.is_project_open_source());
665 }
666}