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