just playing with tangled
at globpattern 290 lines 9.3 kB view raw
1// Copyright 2024 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::collections::HashMap; 16use std::collections::HashSet; 17use std::path::Path; 18use std::process::Command; 19use std::sync::Arc; 20use std::time::Duration; 21use std::time::SystemTime; 22 23use futures::executor::block_on_stream; 24use jj_lib::backend::CommitId; 25use jj_lib::backend::CopyRecord; 26use jj_lib::commit::Commit; 27use jj_lib::git_backend::GitBackend; 28use jj_lib::repo::ReadonlyRepo; 29use jj_lib::repo::Repo as _; 30use jj_lib::repo_path::RepoPath; 31use jj_lib::repo_path::RepoPathBuf; 32use jj_lib::store::Store; 33use jj_lib::transaction::Transaction; 34use maplit::hashset; 35use testutils::create_random_commit; 36use testutils::create_tree; 37use testutils::CommitGraphBuilder; 38use testutils::TestRepo; 39use testutils::TestRepoBackend; 40 41fn get_git_backend(repo: &Arc<ReadonlyRepo>) -> &GitBackend { 42 repo.store() 43 .backend_impl() 44 .downcast_ref::<GitBackend>() 45 .unwrap() 46} 47 48fn collect_no_gc_refs(git_repo_path: &Path) -> HashSet<CommitId> { 49 // Load fresh git repo to isolate from false caching issue. Here we want to 50 // ensure that the underlying data is correct. We could test the in-memory 51 // data as well, but we don't have any special handling in our code. 52 let git_repo = gix::open(git_repo_path).unwrap(); 53 let git_refs = git_repo.references().unwrap(); 54 let no_gc_refs_iter = git_refs.prefixed("refs/jj/keep/").unwrap(); 55 no_gc_refs_iter 56 .map(|git_ref| CommitId::from_bytes(git_ref.unwrap().id().as_bytes())) 57 .collect() 58} 59 60fn get_copy_records( 61 store: &Store, 62 paths: Option<&[RepoPathBuf]>, 63 a: &Commit, 64 b: &Commit, 65) -> HashMap<String, String> { 66 let stream = store.get_copy_records(paths, a.id(), b.id()).unwrap(); 67 let mut res: HashMap<String, String> = HashMap::new(); 68 for CopyRecord { target, source, .. } in block_on_stream(stream).filter_map(|r| r.ok()) { 69 res.insert( 70 target.as_internal_file_string().into(), 71 source.as_internal_file_string().into(), 72 ); 73 } 74 res 75} 76 77fn make_commit( 78 tx: &mut Transaction, 79 parents: Vec<CommitId>, 80 content: &[(&RepoPath, &str)], 81) -> Commit { 82 let tree = create_tree(tx.base_repo(), content); 83 tx.repo_mut() 84 .new_commit(parents, tree.id()) 85 .write() 86 .unwrap() 87} 88 89#[test] 90fn test_gc() { 91 // TODO: Better way to disable the test if git command couldn't be executed 92 if Command::new("git").arg("--version").status().is_err() { 93 eprintln!("Skipping because git command might fail to run"); 94 return; 95 } 96 97 let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git); 98 let repo = test_repo.repo; 99 let git_repo_path = get_git_backend(&repo).git_repo_path(); 100 let base_index = repo.readonly_index(); 101 102 // Set up commits: 103 // 104 // H (predecessor: D) 105 // G | 106 // |\| 107 // | F 108 // E | 109 // D | | 110 // C |/ 111 // |/ 112 // B 113 // A 114 let mut tx = repo.start_transaction(); 115 let mut graph_builder = CommitGraphBuilder::new(tx.repo_mut()); 116 let commit_a = graph_builder.initial_commit(); 117 let commit_b = graph_builder.commit_with_parents(&[&commit_a]); 118 let commit_c = graph_builder.commit_with_parents(&[&commit_b]); 119 let commit_d = graph_builder.commit_with_parents(&[&commit_c]); 120 let commit_e = graph_builder.commit_with_parents(&[&commit_b]); 121 let commit_f = graph_builder.commit_with_parents(&[&commit_b]); 122 let commit_g = graph_builder.commit_with_parents(&[&commit_e, &commit_f]); 123 let commit_h = create_random_commit(tx.repo_mut()) 124 .set_parents(vec![commit_f.id().clone()]) 125 .set_predecessors(vec![commit_d.id().clone()]) 126 .write() 127 .unwrap(); 128 let repo = tx.commit("test").unwrap(); 129 assert_eq!( 130 *repo.view().heads(), 131 hashset! { 132 commit_d.id().clone(), 133 commit_g.id().clone(), 134 commit_h.id().clone(), 135 }, 136 ); 137 138 // At first, all commits have no-gc refs 139 assert_eq!( 140 collect_no_gc_refs(git_repo_path), 141 hashset! { 142 commit_a.id().clone(), 143 commit_b.id().clone(), 144 commit_c.id().clone(), 145 commit_d.id().clone(), 146 commit_e.id().clone(), 147 commit_f.id().clone(), 148 commit_g.id().clone(), 149 commit_h.id().clone(), 150 }, 151 ); 152 153 // Empty index, but all kept by file modification time 154 // (Beware that this invokes "git gc" and refs will be packed.) 155 repo.store() 156 .gc(base_index.as_index(), SystemTime::UNIX_EPOCH) 157 .unwrap(); 158 assert_eq!( 159 collect_no_gc_refs(git_repo_path), 160 hashset! { 161 commit_a.id().clone(), 162 commit_b.id().clone(), 163 commit_c.id().clone(), 164 commit_d.id().clone(), 165 commit_e.id().clone(), 166 commit_f.id().clone(), 167 commit_g.id().clone(), 168 commit_h.id().clone(), 169 }, 170 ); 171 172 // Don't rely on the exact system time because file modification time might 173 // have lower precision for example. 174 let now = || SystemTime::now() + Duration::from_secs(1); 175 176 // All reachable: redundant no-gc refs will be removed 177 repo.store().gc(repo.index(), now()).unwrap(); 178 assert_eq!( 179 collect_no_gc_refs(git_repo_path), 180 hashset! { 181 commit_d.id().clone(), 182 commit_g.id().clone(), 183 commit_h.id().clone(), 184 }, 185 ); 186 187 // G is no longer reachable 188 let mut mut_index = base_index.start_modification(); 189 mut_index.add_commit(&commit_a); 190 mut_index.add_commit(&commit_b); 191 mut_index.add_commit(&commit_c); 192 mut_index.add_commit(&commit_d); 193 mut_index.add_commit(&commit_e); 194 mut_index.add_commit(&commit_f); 195 mut_index.add_commit(&commit_h); 196 repo.store().gc(mut_index.as_index(), now()).unwrap(); 197 assert_eq!( 198 collect_no_gc_refs(git_repo_path), 199 hashset! { 200 commit_d.id().clone(), 201 commit_e.id().clone(), 202 commit_h.id().clone(), 203 }, 204 ); 205 206 // D|E|H are no longer reachable 207 let mut mut_index = base_index.start_modification(); 208 mut_index.add_commit(&commit_a); 209 mut_index.add_commit(&commit_b); 210 mut_index.add_commit(&commit_c); 211 mut_index.add_commit(&commit_f); 212 repo.store().gc(mut_index.as_index(), now()).unwrap(); 213 assert_eq!( 214 collect_no_gc_refs(git_repo_path), 215 hashset! { 216 commit_c.id().clone(), 217 commit_f.id().clone(), 218 }, 219 ); 220 221 // B|C|F are no longer reachable 222 let mut mut_index = base_index.start_modification(); 223 mut_index.add_commit(&commit_a); 224 repo.store().gc(mut_index.as_index(), now()).unwrap(); 225 assert_eq!( 226 collect_no_gc_refs(git_repo_path), 227 hashset! { 228 commit_a.id().clone(), 229 }, 230 ); 231 232 // All unreachable 233 repo.store().gc(base_index.as_index(), now()).unwrap(); 234 assert_eq!(collect_no_gc_refs(git_repo_path), hashset! {}); 235} 236 237#[test] 238fn test_copy_detection() { 239 let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git); 240 let repo = &test_repo.repo; 241 242 let paths = &[ 243 RepoPathBuf::from_internal_string("file0"), 244 RepoPathBuf::from_internal_string("file1"), 245 RepoPathBuf::from_internal_string("file2"), 246 ]; 247 248 let mut tx = repo.start_transaction(); 249 let commit_a = make_commit( 250 &mut tx, 251 vec![repo.store().root_commit_id().clone()], 252 &[(&paths[0], "content")], 253 ); 254 let commit_b = make_commit( 255 &mut tx, 256 vec![commit_a.id().clone()], 257 &[(&paths[1], "content")], 258 ); 259 let commit_c = make_commit( 260 &mut tx, 261 vec![commit_b.id().clone()], 262 &[(&paths[2], "content")], 263 ); 264 265 let store = repo.store(); 266 assert_eq!( 267 get_copy_records(store, Some(paths), &commit_a, &commit_b), 268 HashMap::from([("file1".to_string(), "file0".to_string())]) 269 ); 270 assert_eq!( 271 get_copy_records(store, Some(paths), &commit_b, &commit_c), 272 HashMap::from([("file2".to_string(), "file1".to_string())]) 273 ); 274 assert_eq!( 275 get_copy_records(store, Some(paths), &commit_a, &commit_c), 276 HashMap::from([("file2".to_string(), "file0".to_string())]) 277 ); 278 assert_eq!( 279 get_copy_records(store, None, &commit_a, &commit_c), 280 HashMap::from([("file2".to_string(), "file0".to_string())]) 281 ); 282 assert_eq!( 283 get_copy_records(store, Some(&[paths[1].clone()]), &commit_a, &commit_c), 284 HashMap::default(), 285 ); 286 assert_eq!( 287 get_copy_records(store, Some(paths), &commit_c, &commit_c), 288 HashMap::default(), 289 ); 290}