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::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}