just playing with tangled
at tmp-tutorial 369 lines 12 kB view raw
1// Copyright 2020 The Jujutsu Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15use std::fs::{self, OpenOptions}; 16use std::io::{Read, Write}; 17use std::path::{Path, PathBuf}; 18use std::sync::{Arc, Once}; 19 20use itertools::Itertools; 21use jj_lib::backend::{Backend, BackendInitError, FileId, ObjectId, TreeId, TreeValue}; 22use jj_lib::commit::Commit; 23use jj_lib::commit_builder::CommitBuilder; 24use jj_lib::git_backend::GitBackend; 25use jj_lib::local_backend::LocalBackend; 26use jj_lib::repo::{MutableRepo, ReadonlyRepo, Repo, RepoLoader, StoreFactories}; 27use jj_lib::repo_path::RepoPath; 28use jj_lib::rewrite::RebasedDescendant; 29use jj_lib::settings::UserSettings; 30use jj_lib::store::Store; 31use jj_lib::tree::Tree; 32use jj_lib::tree_builder::TreeBuilder; 33use jj_lib::working_copy::SnapshotOptions; 34use jj_lib::workspace::Workspace; 35use tempfile::TempDir; 36 37pub fn hermetic_libgit2() { 38 // libgit2 respects init.defaultBranch (and possibly other config 39 // variables) in the user's config files. Disable access to them to make 40 // our tests hermetic. 41 // 42 // set_search_path is unsafe because it cannot guarantee thread safety (as 43 // its documentation states). For the same reason, we wrap these invocations 44 // in `call_once`. 45 static CONFIGURE_GIT2: Once = Once::new(); 46 CONFIGURE_GIT2.call_once(|| unsafe { 47 git2::opts::set_search_path(git2::ConfigLevel::System, "").unwrap(); 48 git2::opts::set_search_path(git2::ConfigLevel::Global, "").unwrap(); 49 git2::opts::set_search_path(git2::ConfigLevel::XDG, "").unwrap(); 50 git2::opts::set_search_path(git2::ConfigLevel::ProgramData, "").unwrap(); 51 }); 52} 53 54pub fn new_temp_dir() -> TempDir { 55 hermetic_libgit2(); 56 tempfile::Builder::new() 57 .prefix("jj-test-") 58 .tempdir() 59 .unwrap() 60} 61 62pub fn user_settings() -> UserSettings { 63 let config = config::Config::builder() 64 .add_source(config::File::from_str( 65 r#" 66 user.name = "Test User" 67 user.email = "test.user@example.com" 68 operation.username = "test-username" 69 operation.hostname = "host.example.com" 70 debug.randomness-seed = "42" 71 "#, 72 config::FileFormat::Toml, 73 )) 74 .build() 75 .unwrap(); 76 UserSettings::from_config(config) 77} 78 79pub struct TestRepo { 80 _temp_dir: TempDir, 81 pub repo: Arc<ReadonlyRepo>, 82} 83 84impl TestRepo { 85 pub fn init(use_git: bool) -> Self { 86 let settings = user_settings(); 87 let temp_dir = new_temp_dir(); 88 89 let repo_dir = temp_dir.path().join("repo"); 90 fs::create_dir(&repo_dir).unwrap(); 91 92 let repo = if use_git { 93 let git_path = temp_dir.path().join("git-repo"); 94 git2::Repository::init(&git_path).unwrap(); 95 ReadonlyRepo::init( 96 &settings, 97 &repo_dir, 98 |store_path| -> Result<Box<dyn Backend>, BackendInitError> { 99 Ok(Box::new(GitBackend::init_external(store_path, &git_path)?)) 100 }, 101 ReadonlyRepo::default_op_store_factory(), 102 ReadonlyRepo::default_op_heads_store_factory(), 103 ReadonlyRepo::default_index_store_factory(), 104 ReadonlyRepo::default_submodule_store_factory(), 105 ) 106 .unwrap() 107 } else { 108 ReadonlyRepo::init( 109 &settings, 110 &repo_dir, 111 |store_path| -> Result<Box<dyn Backend>, BackendInitError> { 112 Ok(Box::new(LocalBackend::init(store_path))) 113 }, 114 ReadonlyRepo::default_op_store_factory(), 115 ReadonlyRepo::default_op_heads_store_factory(), 116 ReadonlyRepo::default_index_store_factory(), 117 ReadonlyRepo::default_submodule_store_factory(), 118 ) 119 .unwrap() 120 }; 121 122 Self { 123 _temp_dir: temp_dir, 124 repo, 125 } 126 } 127} 128 129pub struct TestWorkspace { 130 temp_dir: TempDir, 131 pub workspace: Workspace, 132 pub repo: Arc<ReadonlyRepo>, 133} 134 135impl TestWorkspace { 136 pub fn init(settings: &UserSettings, use_git: bool) -> Self { 137 let temp_dir = new_temp_dir(); 138 139 let workspace_root = temp_dir.path().join("repo"); 140 fs::create_dir(&workspace_root).unwrap(); 141 142 let (workspace, repo) = if use_git { 143 let git_path = temp_dir.path().join("git-repo"); 144 git2::Repository::init(&git_path).unwrap(); 145 Workspace::init_external_git(settings, &workspace_root, &git_path).unwrap() 146 } else { 147 Workspace::init_local(settings, &workspace_root).unwrap() 148 }; 149 150 Self { 151 temp_dir, 152 workspace, 153 repo, 154 } 155 } 156 157 pub fn root_dir(&self) -> PathBuf { 158 self.temp_dir.path().join("repo").join("..") 159 } 160 161 /// Snapshots the working copy and returns the tree. Updates the working 162 /// copy state on disk, but does not update the working-copy commit (no 163 /// new operation). 164 pub fn snapshot(&mut self) -> Tree { 165 let mut locked_wc = self.workspace.working_copy_mut().start_mutation().unwrap(); 166 let tree_id = locked_wc 167 .snapshot(SnapshotOptions::empty_for_test()) 168 .unwrap(); 169 // arbitrary operation id 170 locked_wc.finish(self.repo.op_id().clone()).unwrap(); 171 return self 172 .repo 173 .store() 174 .get_tree(&RepoPath::root(), &tree_id) 175 .unwrap(); 176 } 177} 178 179pub fn load_repo_at_head(settings: &UserSettings, repo_path: &Path) -> Arc<ReadonlyRepo> { 180 RepoLoader::init(settings, repo_path, &StoreFactories::default()) 181 .unwrap() 182 .load_at_head(settings) 183 .unwrap() 184} 185 186pub fn read_file(store: &Store, path: &RepoPath, id: &FileId) -> Vec<u8> { 187 let mut reader = store.read_file(path, id).unwrap(); 188 let mut content = vec![]; 189 reader.read_to_end(&mut content).unwrap(); 190 content 191} 192 193pub fn write_file(store: &Store, path: &RepoPath, contents: &str) -> FileId { 194 store.write_file(path, &mut contents.as_bytes()).unwrap() 195} 196 197pub fn write_normal_file( 198 tree_builder: &mut TreeBuilder, 199 path: &RepoPath, 200 contents: &str, 201) -> FileId { 202 let id = write_file(tree_builder.store(), path, contents); 203 tree_builder.set( 204 path.clone(), 205 TreeValue::File { 206 id: id.clone(), 207 executable: false, 208 }, 209 ); 210 id 211} 212 213pub fn write_executable_file(tree_builder: &mut TreeBuilder, path: &RepoPath, contents: &str) { 214 let id = write_file(tree_builder.store(), path, contents); 215 tree_builder.set( 216 path.clone(), 217 TreeValue::File { 218 id, 219 executable: true, 220 }, 221 ); 222} 223 224pub fn write_symlink(tree_builder: &mut TreeBuilder, path: &RepoPath, target: &str) { 225 let id = tree_builder.store().write_symlink(path, target).unwrap(); 226 tree_builder.set(path.clone(), TreeValue::Symlink(id)); 227} 228 229pub fn create_tree(repo: &Arc<ReadonlyRepo>, path_contents: &[(&RepoPath, &str)]) -> Tree { 230 let store = repo.store(); 231 let mut tree_builder = store.tree_builder(store.empty_tree_id().clone()); 232 for (path, contents) in path_contents { 233 write_normal_file(&mut tree_builder, path, contents); 234 } 235 let id = tree_builder.write_tree(); 236 store.get_tree(&RepoPath::root(), &id).unwrap() 237} 238 239#[must_use] 240pub fn create_random_tree(repo: &Arc<ReadonlyRepo>) -> TreeId { 241 let mut tree_builder = repo 242 .store() 243 .tree_builder(repo.store().empty_tree_id().clone()); 244 let number = rand::random::<u32>(); 245 let path = RepoPath::from_internal_string(format!("file{number}").as_str()); 246 write_normal_file(&mut tree_builder, &path, "contents"); 247 tree_builder.write_tree() 248} 249 250pub fn create_random_commit<'repo>( 251 mut_repo: &'repo mut MutableRepo, 252 settings: &UserSettings, 253) -> CommitBuilder<'repo> { 254 let tree_id = create_random_tree(mut_repo.base_repo()); 255 let number = rand::random::<u32>(); 256 mut_repo 257 .new_commit( 258 settings, 259 vec![mut_repo.store().root_commit_id().clone()], 260 tree_id, 261 ) 262 .set_description(format!("random commit {number}")) 263} 264 265pub fn dump_tree(store: &Arc<Store>, tree_id: &TreeId) -> String { 266 use std::fmt::Write; 267 let mut buf = String::new(); 268 writeln!(&mut buf, "tree {}", tree_id.hex()).unwrap(); 269 let tree = store.get_tree(&RepoPath::root(), tree_id).unwrap(); 270 for (path, value) in tree.entries() { 271 match value { 272 TreeValue::File { id, executable: _ } => { 273 let file_buf = read_file(store, &path, &id); 274 let file_contents = String::from_utf8_lossy(&file_buf); 275 writeln!( 276 &mut buf, 277 " file {path:?} ({}): {file_contents:?}", 278 id.hex() 279 ) 280 .unwrap(); 281 } 282 TreeValue::Symlink(id) => { 283 writeln!(&mut buf, " symlink {path:?} ({})", id.hex()).unwrap(); 284 } 285 TreeValue::Conflict(id) => { 286 writeln!(&mut buf, " conflict {path:?} ({})", id.hex()).unwrap(); 287 } 288 TreeValue::GitSubmodule(id) => { 289 writeln!(&mut buf, " submodule {path:?} ({})", id.hex()).unwrap(); 290 } 291 entry => { 292 unimplemented!("dumping tree entry {entry:?}"); 293 } 294 } 295 } 296 buf 297} 298 299pub fn write_random_commit(mut_repo: &mut MutableRepo, settings: &UserSettings) -> Commit { 300 create_random_commit(mut_repo, settings).write().unwrap() 301} 302 303pub fn write_working_copy_file(workspace_root: &Path, path: &RepoPath, contents: &str) { 304 let path = path.to_fs_path(workspace_root); 305 if let Some(parent) = path.parent() { 306 fs::create_dir_all(parent).unwrap(); 307 } 308 let mut file = OpenOptions::new() 309 .write(true) 310 .create(true) 311 .truncate(true) 312 .open(path) 313 .unwrap(); 314 file.write_all(contents.as_bytes()).unwrap(); 315} 316 317pub struct CommitGraphBuilder<'settings, 'repo> { 318 settings: &'settings UserSettings, 319 mut_repo: &'repo mut MutableRepo, 320} 321 322impl<'settings, 'repo> CommitGraphBuilder<'settings, 'repo> { 323 pub fn new( 324 settings: &'settings UserSettings, 325 mut_repo: &'repo mut MutableRepo, 326 ) -> CommitGraphBuilder<'settings, 'repo> { 327 CommitGraphBuilder { settings, mut_repo } 328 } 329 330 pub fn initial_commit(&mut self) -> Commit { 331 write_random_commit(self.mut_repo, self.settings) 332 } 333 334 pub fn commit_with_parents(&mut self, parents: &[&Commit]) -> Commit { 335 let parent_ids = parents 336 .iter() 337 .map(|commit| commit.id().clone()) 338 .collect_vec(); 339 create_random_commit(self.mut_repo, self.settings) 340 .set_parents(parent_ids) 341 .write() 342 .unwrap() 343 } 344} 345 346pub fn assert_rebased( 347 rebased: Option<RebasedDescendant>, 348 expected_old_commit: &Commit, 349 expected_new_parents: &[&Commit], 350) -> Commit { 351 if let Some(RebasedDescendant { 352 old_commit, 353 new_commit, 354 }) = rebased 355 { 356 assert_eq!(old_commit, *expected_old_commit); 357 assert_eq!(new_commit.change_id(), expected_old_commit.change_id()); 358 assert_eq!( 359 new_commit.parent_ids(), 360 expected_new_parents 361 .iter() 362 .map(|commit| commit.id().clone()) 363 .collect_vec() 364 ); 365 new_commit 366 } else { 367 panic!("expected rebased commit: {rebased:?}"); 368 } 369}