license_detection.rs

  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}