1434 lines · 45.2 KB · Rust
RawDownload
// Copyright 2021 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. use std::collections::HashMap;use std::collections::HashSet; use indexmap::IndexSet;use indoc::indoc;use jj_lib::backend::CommitId;use jj_lib::backend::FileId;use jj_lib::commit::Commit;use jj_lib::fix::FileFixer;use jj_lib::fix::FileToFix;use jj_lib::fix::FixError;use jj_lib::fix::LineRange;use jj_lib::fix::ParallelFileFixer;use jj_lib::fix::RegionsToFormat;use jj_lib::fix::compute_changed_ranges;use jj_lib::fix::fix_files;use jj_lib::fix::get_base_commit_map;use jj_lib::matchers::EverythingMatcher;use jj_lib::merged_tree::MergedTree;use jj_lib::repo::Repo as _;use jj_lib::repo_path::RepoPath;use jj_lib::repo_path::RepoPathBuf;use jj_lib::rewrite::merge_commit_trees;use jj_lib::store::Store;use jj_lib::transaction::Transaction;use pollster::FutureExt as _;use testutils::CommitBuilderExt as _;use testutils::TestRepo;use testutils::TestResult;use testutils::assert_tree_eq;use testutils::create_tree;use testutils::create_tree_with;use testutils::read_file;use testutils::repo_path;use thiserror::Error; type ReplacementKey = (RepoPathBuf, Option<Vec<u8>>, Vec<u8>); #[derive(Clone, Debug)]struct TestFileFixer {    replacements: HashMap<ReplacementKey, Vec<u8>>,} impl TestFileFixer {    fn new() -> Self {        Self {            replacements: HashMap::new(),        }    }     fn add_replacement(        &mut self,        repo_path: &RepoPath,        base_content: Option<&[u8]>,        old_content: impl AsRef<[u8]>,        new_content: impl AsRef<[u8]>,    ) {        self.replacements.insert(            (                repo_path.to_owned(),                base_content.map(|c| c.to_vec()),                old_content.as_ref().to_vec(),            ),            new_content.as_ref().to_vec(),        );    }     fn fix_file(&mut self, store: &Store, file_to_fix: &FileToFix) -> Result<FileId, FixError> {        let old_content = read_file(store, &file_to_fix.repo_path, &file_to_fix.file_id);        let base_content = file_to_fix            .base_file_id            .as_ref()            .map(|base_file_id| read_file(store, &file_to_fix.repo_path, base_file_id));         let key = (            file_to_fix.repo_path.clone(),            base_content,            old_content.clone(),        );        let Some(new_content) = self.replacements.remove(&key) else {            return Err(make_fix_content_error(&format!(                indoc! {"                    Unexpected fix request:                    path: {}                    old_content: {:?}                "},                file_to_fix.repo_path.as_internal_file_string(),                String::from_utf8_lossy(&old_content)            )));        };         let new_file_id = store            .write_file(&file_to_fix.repo_path, &mut new_content.as_slice())            .block_on()?;        Ok(new_file_id)    }} impl FileFixer for TestFileFixer {    fn fix_files<'a>(        &mut self,        store: &Store,        files_to_fix: &'a HashSet<FileToFix>,    ) -> Result<HashMap<&'a FileToFix, FileId>, FixError> {        let mut changed_files = HashMap::new();        for file_to_fix in files_to_fix {            let new_file_id = self.fix_file(store, file_to_fix)?;            changed_files.insert(file_to_fix, new_file_id);        }        assert!(self.replacements.is_empty());        Ok(changed_files)    }} #[derive(Error, Debug)]#[error("Forced failure: {0}")]struct MyFixerError(String); fn make_fix_content_error(message: &str) -> FixError {    FixError::FixContent(Box::new(MyFixerError(message.into())))} // Reads the file from store. If the file starts with "fixme", its contents are// changed to uppercase and the new file id is returned. If the file starts with// "error", an error is raised. Otherwise returns None.//// This is used for testing `ParallelFileFixer`.fn fix_file(store: &Store, file_to_fix: &FileToFix) -> Result<Option<FileId>, FixError> {    let old_content = read_file(store, &file_to_fix.repo_path, &file_to_fix.file_id);     if let Some(rest) = old_content.strip_prefix(b"fixme:") {        let new_content = rest.to_ascii_uppercase();        let new_file_id = store            .write_file(&file_to_fix.repo_path, &mut new_content.as_slice())            .block_on()?;        Ok(Some(new_file_id))    } else if let Some(rest) = old_content.strip_prefix(b"error:") {        Err(make_fix_content_error(str::from_utf8(rest).unwrap()))    } else {        Ok(None)    }} fn create_commit(tx: &mut Transaction, parents: Vec<CommitId>, tree: MergedTree) -> CommitId {    tx.repo_mut()        .new_commit(parents, tree)        .write_unwrap()        .id()        .clone()} fn line_range(first: usize, last: usize) -> LineRange {    LineRange::new(first, last)} #[test]fn test_fix_added_and_modified_files() -> TestResult {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     let mut tx = repo.start_transaction();    let path1 = repo_path("file1");     let tree1 = create_tree(repo, &[(path1, "unformatted")]);    let commit_a = create_commit(&mut tx, vec![repo.store().root_commit_id().clone()], tree1);    let tree2 = create_tree(repo, &[(path1, "modified")]);    let commit_b = create_commit(&mut tx, vec![commit_a.clone()], tree2);     let root_commits = vec![commit_a.clone()];    let mut file_fixer = TestFileFixer::new();    let include_unchanged_files = false;    file_fixer.add_replacement(path1, None, b"unformatted", b"Formatted");    file_fixer.add_replacement(path1, None, b"modified", b"Formatted");     let summary = fix_files(        root_commits,        &EverythingMatcher,        include_unchanged_files,        tx.repo_mut(),        &mut file_fixer,    )    .block_on()?;     assert_eq!(summary.rewrites.len(), 2);    assert!(summary.rewrites.contains_key(&commit_a));    assert!(summary.rewrites.contains_key(&commit_b));    assert_eq!(summary.num_checked_commits, 2);    assert_eq!(summary.num_fixed_commits, 2);     let expected_tree = create_tree(repo, &[(path1, "Formatted")]);    let new_commit_a = repo        .store()        .get_commit(summary.rewrites.get(&commit_a).unwrap())?;    assert_tree_eq!(new_commit_a.tree(), expected_tree);     let new_commit_b = repo        .store()        .get_commit(summary.rewrites.get(&commit_b).unwrap())?;    assert_tree_eq!(new_commit_b.tree(), expected_tree);    Ok(())} #[test]fn test_fixer_does_not_change_content() -> TestResult {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     let mut tx = repo.start_transaction();    let path1 = repo_path("file1");    let tree1 = create_tree(repo, &[(path1, "Formatted")]);    let commit_a = create_commit(&mut tx, vec![repo.store().root_commit_id().clone()], tree1);     let root_commits = vec![commit_a.clone()];    let mut file_fixer = TestFileFixer::new();    let include_unchanged_files = false;    file_fixer.add_replacement(path1, None, b"Formatted", b"Formatted");     let summary = fix_files(        root_commits,        &EverythingMatcher,        include_unchanged_files,        tx.repo_mut(),        &mut file_fixer,    )    .block_on()?;     assert!(summary.rewrites.is_empty());    assert_eq!(summary.num_checked_commits, 1);    assert_eq!(summary.num_fixed_commits, 0);    Ok(())} #[test]fn test_empty_commit() -> TestResult {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     let mut tx = repo.start_transaction();    let tree1 = create_tree(repo, &[]);    let commit_a = create_commit(&mut tx, vec![repo.store().root_commit_id().clone()], tree1);     let root_commits = vec![commit_a.clone()];    let mut file_fixer = TestFileFixer::new();    let include_unchanged_files = false;     let summary = fix_files(        root_commits,        &EverythingMatcher,        include_unchanged_files,        tx.repo_mut(),        &mut file_fixer,    )    .block_on()?;     assert!(summary.rewrites.is_empty());    assert_eq!(summary.num_checked_commits, 1);    assert_eq!(summary.num_fixed_commits, 0);    Ok(())} #[test]fn test_fix_empty_revset() -> TestResult {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;    let mut tx = repo.start_transaction();     let mut file_fixer = TestFileFixer::new();    let summary = fix_files(        vec![],        &EverythingMatcher,        false,        tx.repo_mut(),        &mut file_fixer,    )    .block_on()?;     assert!(summary.rewrites.is_empty());    Ok(())} #[test]fn test_fixer_fails() -> TestResult {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     let mut tx = repo.start_transaction();    let path1 = repo_path("file1");    let tree1 = create_tree(repo, &[(path1, "unformatted")]);    let commit_a = create_commit(&mut tx, vec![repo.store().root_commit_id().clone()], tree1);     let root_commits = vec![commit_a.clone()];    let mut file_fixer = TestFileFixer::new();    let include_unchanged_files = false;     let result = fix_files(        root_commits,        &EverythingMatcher,        include_unchanged_files,        tx.repo_mut(),        &mut file_fixer,    )    .block_on();     let error = result.err().unwrap();    assert_eq!(        error.to_string(),        indoc! {r#"            Forced failure: Unexpected fix request:            path: file1            old_content: "unformatted"        "#}    );    Ok(())} #[test]fn test_unchanged_file_is_not_fixed() -> TestResult {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     let mut tx = repo.start_transaction();    let path1 = repo_path("file1");     let tree1 = create_tree(repo, &[(path1, "unformatted")]);    let commit_a = create_commit(&mut tx, vec![repo.store().root_commit_id().clone()], tree1);    let tree2 = create_tree(repo, &[(path1, "unformatted")]);    let commit_b = create_commit(&mut tx, vec![commit_a.clone()], tree2);     let root_commits = vec![commit_b.clone()];    let mut file_fixer = TestFileFixer::new();    let include_unchanged_files = false;     let summary = fix_files(        root_commits,        &EverythingMatcher,        include_unchanged_files,        tx.repo_mut(),        &mut file_fixer,    )    .block_on()?;     assert!(summary.rewrites.is_empty());    assert_eq!(summary.num_checked_commits, 1);    assert_eq!(summary.num_fixed_commits, 0);    Ok(())} #[test]fn test_fix_include_unchanged_files() -> TestResult {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     let mut tx = repo.start_transaction();    let path_changed = repo_path("changed.txt");    let path_unchanged = repo_path("unchanged.txt");    let path_new = repo_path("newfile.txt");     // c1: changed.txt, unchanged.txt    let tree1 = create_tree(        repo,        &[(path_changed, "base"), (path_unchanged, "unformatted 2")],    );    let c1 = create_commit(&mut tx, vec![repo.store().root_commit_id().clone()], tree1);     // c2: changed.txt, unchanged.txt, newfile.txt    let tree2 = create_tree(        repo,        &[            (path_changed, "unformatted 1"),            (path_unchanged, "unformatted 2"),            (path_new, "unformatted 3"),        ],    );    let c2 = create_commit(&mut tx, vec![c1.clone()], tree2);     // Run fix on commit 2 with include_unchanged_files = true.    let root_commits = vec![c2.clone()];    let mut file_fixer = TestFileFixer::new();    file_fixer.add_replacement(        path_changed,        Some(b"base"),        b"unformatted 1",        b"Formatted 1",    );    file_fixer.add_replacement(        path_unchanged,        Some(b"unformatted 2"),        b"unformatted 2",        b"sorted includes\nunformatted 2",    );    file_fixer.add_replacement(path_new, None, b"unformatted 3", b"Formatted 3");     let include_unchanged_files = true;    let summary = fix_files(        root_commits,        &EverythingMatcher,        include_unchanged_files,        tx.repo_mut(),        &mut file_fixer,    )    .block_on()?;     assert_eq!(summary.rewrites.len(), 1);    assert!(summary.rewrites.contains_key(&c2));     let new_c2 = repo        .store()        .get_commit(summary.rewrites.get(&c2).unwrap())?;     let expected_tree = create_tree(        repo,        &[            (path_changed, "Formatted 1"),            (path_unchanged, "sorted includes\nunformatted 2"),            (path_new, "Formatted 3"),        ],    );    assert_tree_eq!(new_c2.tree(), expected_tree);    Ok(())} /// If a descendant is already correctly formatted, it should still be rewritten/// but its tree should be preserved.#[test]fn test_already_fixed_descendant() -> TestResult {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     let mut tx = repo.start_transaction();    let path1 = repo_path("file1");     let tree1 = create_tree(repo, &[(path1, "unformatted")]);    let commit_a = create_commit(&mut tx, vec![repo.store().root_commit_id().clone()], tree1);    let tree2 = create_tree(repo, &[(path1, "Formatted")]);    let commit_b = create_commit(&mut tx, vec![commit_a.clone()], tree2.clone());     let root_commits = vec![commit_a.clone()];    let mut file_fixer = TestFileFixer::new();    let include_unchanged_files = true;    file_fixer.add_replacement(path1, None, b"unformatted", b"Formatted");    file_fixer.add_replacement(path1, None, b"Formatted", b"Formatted");     let summary = fix_files(        root_commits,        &EverythingMatcher,        include_unchanged_files,        tx.repo_mut(),        &mut file_fixer,    )    .block_on()?;     assert_eq!(summary.rewrites.len(), 2);    assert!(summary.rewrites.contains_key(&commit_a));    assert!(summary.rewrites.contains_key(&commit_b));    assert_eq!(summary.num_checked_commits, 2);    assert_eq!(summary.num_fixed_commits, 1);     let new_commit_a = repo        .store()        .get_commit(summary.rewrites.get(&commit_a).unwrap())?;    assert_tree_eq!(new_commit_a.tree(), tree2);    let new_commit_b = repo        .store()        .get_commit(summary.rewrites.get(&commit_a).unwrap())?;    assert_tree_eq!(new_commit_b.tree(), tree2);    Ok(())} #[test]fn test_parallel_fixer_basic() -> TestResult {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     let mut tx = repo.start_transaction();    let path1 = repo_path("file1");    let tree1 = create_tree(repo, &[(path1, "fixme:content")]);    let commit_a = create_commit(&mut tx, vec![repo.store().root_commit_id().clone()], tree1);     let root_commits = vec![commit_a.clone()];    let include_unchanged_files = false;    let mut parallel_fixer = ParallelFileFixer::new(fix_file);     let summary = fix_files(        root_commits,        &EverythingMatcher,        include_unchanged_files,

Lines 1–500 of 1434.