just playing with tangled
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}