just playing with tangled
at globpattern 377 lines 13 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::fmt::Write as _; 16use std::rc::Rc; 17 18use jj_lib::annotate::get_annotation_for_file; 19use jj_lib::annotate::get_annotation_with_file_content; 20use jj_lib::annotate::FileAnnotation; 21use jj_lib::backend::CommitId; 22use jj_lib::backend::MergedTreeId; 23use jj_lib::backend::MillisSinceEpoch; 24use jj_lib::backend::Signature; 25use jj_lib::backend::Timestamp; 26use jj_lib::backend::TreeValue; 27use jj_lib::commit::Commit; 28use jj_lib::repo::MutableRepo; 29use jj_lib::repo::Repo; 30use jj_lib::repo_path::RepoPath; 31use jj_lib::revset::ResolvedRevsetExpression; 32use jj_lib::revset::RevsetExpression; 33use testutils::create_tree; 34use testutils::TestRepo; 35 36fn create_commit_fn( 37 mut_repo: &mut MutableRepo, 38) -> impl FnMut(&str, &[&CommitId], MergedTreeId) -> Commit + use<'_> { 39 // stabilize commit IDs for ease of debugging 40 let signature = Signature { 41 name: "Some One".to_owned(), 42 email: "some.one@example.com".to_owned(), 43 timestamp: Timestamp { 44 timestamp: MillisSinceEpoch(0), 45 tz_offset: 0, 46 }, 47 }; 48 move |description, parent_ids, tree_id| { 49 let parent_ids = parent_ids.iter().map(|&id| id.clone()).collect(); 50 mut_repo 51 .new_commit(parent_ids, tree_id) 52 .set_author(signature.clone()) 53 .set_committer(signature.clone()) 54 .set_description(description) 55 .write() 56 .unwrap() 57 } 58} 59 60fn annotate(repo: &dyn Repo, commit: &Commit, file_path: &RepoPath) -> String { 61 let domain = RevsetExpression::all(); 62 annotate_within(repo, commit, &domain, file_path) 63} 64 65fn annotate_within( 66 repo: &dyn Repo, 67 commit: &Commit, 68 domain: &Rc<ResolvedRevsetExpression>, 69 file_path: &RepoPath, 70) -> String { 71 let annotation = get_annotation_for_file(repo, commit, domain, file_path).unwrap(); 72 format_annotation(repo, &annotation) 73} 74 75fn annotate_parent_tree(repo: &dyn Repo, commit: &Commit, file_path: &RepoPath) -> String { 76 let tree = commit.parent_tree(repo).unwrap(); 77 let text = match tree.path_value(file_path).unwrap().into_resolved().unwrap() { 78 Some(TreeValue::File { id, .. }) => { 79 let mut reader = repo.store().read_file(file_path, &id).unwrap(); 80 let mut buf = Vec::new(); 81 reader.read_to_end(&mut buf).unwrap(); 82 buf 83 } 84 value => panic!("unexpected path value: {value:?}"), 85 }; 86 let domain = RevsetExpression::all(); 87 let annotation = 88 get_annotation_with_file_content(repo, commit.id(), &domain, file_path, text).unwrap(); 89 format_annotation(repo, &annotation) 90} 91 92fn format_annotation(repo: &dyn Repo, annotation: &FileAnnotation) -> String { 93 let mut output = String::new(); 94 for (commit_id, line) in annotation.lines() { 95 let id = commit_id.unwrap_or_else(|id| id); 96 let commit = repo.store().get_commit(id).unwrap(); 97 let desc = commit.description().trim_end(); 98 let sigil = if commit_id.is_err() { '*' } else { ' ' }; 99 write!(output, "{desc}{sigil}: {line}").unwrap(); 100 } 101 output 102} 103 104#[test] 105fn test_annotate_linear() { 106 let test_repo = TestRepo::init(); 107 let repo = &test_repo.repo; 108 109 let root_commit_id = repo.store().root_commit_id(); 110 let file_path = RepoPath::from_internal_string("file"); 111 112 let mut tx = repo.start_transaction(); 113 let mut create_commit = create_commit_fn(tx.repo_mut()); 114 let content1 = ""; 115 let content2 = "2a\n2b\n"; 116 let content3 = "2b\n3\n"; 117 let tree1 = create_tree(repo, &[(file_path, content1)]); 118 let tree2 = create_tree(repo, &[(file_path, content2)]); 119 let tree3 = create_tree(repo, &[(file_path, content3)]); 120 let commit1 = create_commit("commit1", &[root_commit_id], tree1.id()); 121 let commit2 = create_commit("commit2", &[commit1.id()], tree2.id()); 122 let commit3 = create_commit("commit3", &[commit2.id()], tree3.id()); 123 let commit4 = create_commit("commit4", &[commit3.id()], tree3.id()); // empty commit 124 drop(create_commit); 125 126 insta::assert_snapshot!(annotate(tx.repo(), &commit1, file_path), @""); 127 insta::assert_snapshot!(annotate(tx.repo(), &commit2, file_path), @r" 128 commit2 : 2a 129 commit2 : 2b 130 "); 131 insta::assert_snapshot!(annotate(tx.repo(), &commit3, file_path), @r" 132 commit2 : 2b 133 commit3 : 3 134 "); 135 insta::assert_snapshot!(annotate(tx.repo(), &commit4, file_path), @r" 136 commit2 : 2b 137 commit3 : 3 138 "); 139} 140 141#[test] 142fn test_annotate_merge_simple() { 143 let test_repo = TestRepo::init(); 144 let repo = &test_repo.repo; 145 146 let root_commit_id = repo.store().root_commit_id(); 147 let file_path = RepoPath::from_internal_string("file"); 148 149 // 4 "2 1 3" 150 // |\ 151 // | 3 "1 3" 152 // | | 153 // 2 | "2 1" 154 // |/ 155 // 1 "1" 156 let mut tx = repo.start_transaction(); 157 let mut create_commit = create_commit_fn(tx.repo_mut()); 158 let content1 = "1\n"; 159 let content2 = "2\n1\n"; 160 let content3 = "1\n3\n"; 161 let content4 = "2\n1\n3\n"; 162 let tree1 = create_tree(repo, &[(file_path, content1)]); 163 let tree2 = create_tree(repo, &[(file_path, content2)]); 164 let tree3 = create_tree(repo, &[(file_path, content3)]); 165 let tree4 = create_tree(repo, &[(file_path, content4)]); 166 let commit1 = create_commit("commit1", &[root_commit_id], tree1.id()); 167 let commit2 = create_commit("commit2", &[commit1.id()], tree2.id()); 168 let commit3 = create_commit("commit3", &[commit1.id()], tree3.id()); 169 let commit4 = create_commit("commit4", &[commit2.id(), commit3.id()], tree4.id()); 170 drop(create_commit); 171 172 insta::assert_snapshot!(annotate(tx.repo(), &commit4, file_path), @r" 173 commit2 : 2 174 commit1 : 1 175 commit3 : 3 176 "); 177 178 // Exclude the fork commit and its ancestors. 179 let domain = RevsetExpression::commit(commit1.id().clone()) 180 .ancestors() 181 .negated(); 182 insta::assert_snapshot!(annotate_within(tx.repo(), &commit4, &domain, file_path), @r" 183 commit2 : 2 184 commit2*: 1 185 commit3 : 3 186 "); 187 188 // Exclude one side of the merge and its ancestors. 189 let domain = RevsetExpression::commit(commit2.id().clone()) 190 .ancestors() 191 .negated(); 192 insta::assert_snapshot!(annotate_within(tx.repo(), &commit4, &domain, file_path), @r" 193 commit4*: 2 194 commit4*: 1 195 commit3 : 3 196 "); 197 198 // Exclude both sides of the merge and their ancestors. 199 let domain = RevsetExpression::commit(commit4.id().clone()); 200 insta::assert_snapshot!(annotate_within(tx.repo(), &commit4, &domain, file_path), @r" 201 commit4*: 2 202 commit4*: 1 203 commit4*: 3 204 "); 205 206 // Exclude intermediate commit, which is useless but works. 207 let domain = RevsetExpression::commit(commit3.id().clone()).negated(); 208 insta::assert_snapshot!(annotate_within(tx.repo(), &commit4, &domain, file_path), @r" 209 commit2 : 2 210 commit1 : 1 211 commit4 : 3 212 "); 213} 214 215#[test] 216fn test_annotate_merge_split() { 217 let test_repo = TestRepo::init(); 218 let repo = &test_repo.repo; 219 220 let root_commit_id = repo.store().root_commit_id(); 221 let file_path = RepoPath::from_internal_string("file"); 222 223 // 4 "2 1a 1b 3 4" 224 // |\ 225 // | 3 "1b 3" 226 // | | 227 // 2 | "2 1a" 228 // |/ 229 // 1 "1a 1b" 230 let mut tx = repo.start_transaction(); 231 let mut create_commit = create_commit_fn(tx.repo_mut()); 232 let content1 = "1a\n1b\n"; 233 let content2 = "2\n1a\n"; 234 let content3 = "1b\n3\n"; 235 let content4 = "2\n1a\n1b\n3\n4\n"; 236 let tree1 = create_tree(repo, &[(file_path, content1)]); 237 let tree2 = create_tree(repo, &[(file_path, content2)]); 238 let tree3 = create_tree(repo, &[(file_path, content3)]); 239 let tree4 = create_tree(repo, &[(file_path, content4)]); 240 let commit1 = create_commit("commit1", &[root_commit_id], tree1.id()); 241 let commit2 = create_commit("commit2", &[commit1.id()], tree2.id()); 242 let commit3 = create_commit("commit3", &[commit1.id()], tree3.id()); 243 let commit4 = create_commit("commit4", &[commit2.id(), commit3.id()], tree4.id()); 244 drop(create_commit); 245 246 insta::assert_snapshot!(annotate(tx.repo(), &commit4, file_path), @r" 247 commit2 : 2 248 commit1 : 1a 249 commit1 : 1b 250 commit3 : 3 251 commit4 : 4 252 "); 253} 254 255#[test] 256fn test_annotate_merge_split_interleaved() { 257 let test_repo = TestRepo::init(); 258 let repo = &test_repo.repo; 259 260 let root_commit_id = repo.store().root_commit_id(); 261 let file_path = RepoPath::from_internal_string("file"); 262 263 // 6 "1a 4 1b 6 2a 5 2b" 264 // |\ 265 // | 5 "1b 5 2b" 266 // | | 267 // 4 | "1a 4 2a" 268 // |/ 269 // 3 "1a 1b 2a 2b" 270 // |\ 271 // | 2 "2a 2b" 272 // | 273 // 1 "1a 1b" 274 let mut tx = repo.start_transaction(); 275 let mut create_commit = create_commit_fn(tx.repo_mut()); 276 let content1 = "1a\n1b\n"; 277 let content2 = "2a\n2b\n"; 278 let content3 = "1a\n1b\n2a\n2b\n"; 279 let content4 = "1a\n4\n2a\n"; 280 let content5 = "1b\n5\n2b\n"; 281 let content6 = "1a\n4\n1b\n6\n2a\n5\n2b\n"; 282 let tree1 = create_tree(repo, &[(file_path, content1)]); 283 let tree2 = create_tree(repo, &[(file_path, content2)]); 284 let tree3 = create_tree(repo, &[(file_path, content3)]); 285 let tree4 = create_tree(repo, &[(file_path, content4)]); 286 let tree5 = create_tree(repo, &[(file_path, content5)]); 287 let tree6 = create_tree(repo, &[(file_path, content6)]); 288 let commit1 = create_commit("commit1", &[root_commit_id], tree1.id()); 289 let commit2 = create_commit("commit2", &[root_commit_id], tree2.id()); 290 let commit3 = create_commit("commit3", &[commit1.id(), commit2.id()], tree3.id()); 291 let commit4 = create_commit("commit4", &[commit3.id()], tree4.id()); 292 let commit5 = create_commit("commit5", &[commit3.id()], tree5.id()); 293 let commit6 = create_commit("commit6", &[commit4.id(), commit5.id()], tree6.id()); 294 drop(create_commit); 295 296 insta::assert_snapshot!(annotate(tx.repo(), &commit6, file_path), @r" 297 commit1 : 1a 298 commit4 : 4 299 commit1 : 1b 300 commit6 : 6 301 commit2 : 2a 302 commit5 : 5 303 commit2 : 2b 304 "); 305} 306 307#[test] 308fn test_annotate_merge_dup() { 309 let test_repo = TestRepo::init(); 310 let repo = &test_repo.repo; 311 312 let root_commit_id = repo.store().root_commit_id(); 313 let file_path = RepoPath::from_internal_string("file"); 314 315 // 4 "2 1 1 3 4" 316 // |\ 317 // | 3 "1 3" 318 // | | 319 // 2 | "2 1" 320 // |/ 321 // 1 "1" 322 let mut tx = repo.start_transaction(); 323 let mut create_commit = create_commit_fn(tx.repo_mut()); 324 let content1 = "1\n"; 325 let content2 = "2\n1\n"; 326 let content3 = "1\n3\n"; 327 let content4 = "2\n1\n1\n3\n4\n"; 328 let tree1 = create_tree(repo, &[(file_path, content1)]); 329 let tree2 = create_tree(repo, &[(file_path, content2)]); 330 let tree3 = create_tree(repo, &[(file_path, content3)]); 331 let tree4 = create_tree(repo, &[(file_path, content4)]); 332 let commit1 = create_commit("commit1", &[root_commit_id], tree1.id()); 333 let commit2 = create_commit("commit2", &[commit1.id()], tree2.id()); 334 let commit3 = create_commit("commit3", &[commit1.id()], tree3.id()); 335 let commit4 = create_commit("commit4", &[commit2.id(), commit3.id()], tree4.id()); 336 drop(create_commit); 337 338 // Both "1"s can be propagated to commit1 through commit2 and commit3. 339 // Alternatively, it's also good to interpret that one of the "1"s was 340 // produced at commit2, commit3, or commit4. 341 insta::assert_snapshot!(annotate(tx.repo(), &commit4, file_path), @r" 342 commit2 : 2 343 commit1 : 1 344 commit1 : 1 345 commit3 : 3 346 commit4 : 4 347 "); 348 349 // For example, the parent tree of commit4 doesn't contain multiple "1"s. 350 // If annotation were computed compared to the parent tree, not trees of the 351 // parent commits, "1" would be inserted at commit4. 352 insta::assert_snapshot!(annotate_parent_tree(tx.repo(), &commit4, file_path), @r" 353 commit2 : 2 354 commit1 : 1 355 commit3 : 3 356 "); 357} 358 359#[test] 360fn test_annotate_file_directory_transition() { 361 let test_repo = TestRepo::init(); 362 let repo = &test_repo.repo; 363 364 let root_commit_id = repo.store().root_commit_id(); 365 let file_path1 = RepoPath::from_internal_string("file/was_dir"); 366 let file_path2 = RepoPath::from_internal_string("file"); 367 368 let mut tx = repo.start_transaction(); 369 let mut create_commit = create_commit_fn(tx.repo_mut()); 370 let tree1 = create_tree(repo, &[(file_path1, "1\n")]); 371 let tree2 = create_tree(repo, &[(file_path2, "2\n")]); 372 let commit1 = create_commit("commit1", &[root_commit_id], tree1.id()); 373 let commit2 = create_commit("commit2", &[commit1.id()], tree2.id()); 374 drop(create_commit); 375 376 insta::assert_snapshot!(annotate(tx.repo(), &commit2, file_path2), @"commit2 : 2"); 377}