just playing with tangled
1// Copyright 2022 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::path::{self, Path, PathBuf};
16
17use crate::common::{get_stderr_string, get_stdout_string, TestEnvironment};
18
19fn set_up_non_empty_git_repo(git_repo: &git2::Repository) {
20 let signature =
21 git2::Signature::new("Some One", "some.one@example.com", &git2::Time::new(0, 0)).unwrap();
22 let mut tree_builder = git_repo.treebuilder(None).unwrap();
23 let file_oid = git_repo.blob(b"content").unwrap();
24 tree_builder
25 .insert("file", file_oid, git2::FileMode::Blob.into())
26 .unwrap();
27 let tree_oid = tree_builder.write().unwrap();
28 let tree = git_repo.find_tree(tree_oid).unwrap();
29 git_repo
30 .commit(
31 Some("refs/heads/main"),
32 &signature,
33 &signature,
34 "message",
35 &tree,
36 &[],
37 )
38 .unwrap();
39 git_repo.set_head("refs/heads/main").unwrap();
40}
41
42#[test]
43fn test_git_clone() {
44 let test_env = TestEnvironment::default();
45 test_env.add_config("git.auto-local-branch = true");
46 let git_repo_path = test_env.env_root().join("source");
47 let git_repo = git2::Repository::init(git_repo_path).unwrap();
48
49 // Clone an empty repo
50 let (stdout, stderr) =
51 test_env.jj_cmd_ok(test_env.env_root(), &["git", "clone", "source", "empty"]);
52 insta::assert_snapshot!(stdout, @"");
53 insta::assert_snapshot!(stderr, @r###"
54 Fetching into new repo in "$TEST_ENV/empty"
55 Nothing changed.
56 "###);
57
58 set_up_non_empty_git_repo(&git_repo);
59
60 // Clone with relative source path
61 let (stdout, stderr) =
62 test_env.jj_cmd_ok(test_env.env_root(), &["git", "clone", "source", "clone"]);
63 insta::assert_snapshot!(stdout, @"");
64 insta::assert_snapshot!(stderr, @r###"
65 Fetching into new repo in "$TEST_ENV/clone"
66 branch: main@origin [new] tracked
67 Working copy now at: uuqppmxq 1f0b881a (empty) (no description set)
68 Parent commit : mzyxwzks 9f01a0e0 main | message
69 Added 1 files, modified 0 files, removed 0 files
70 "###);
71 assert!(test_env.env_root().join("clone").join("file").exists());
72
73 // Subsequent fetch should just work even if the source path was relative
74 let (stdout, stderr) =
75 test_env.jj_cmd_ok(&test_env.env_root().join("clone"), &["git", "fetch"]);
76 insta::assert_snapshot!(stdout, @"");
77 insta::assert_snapshot!(stderr, @r###"
78 Nothing changed.
79 "###);
80
81 // Failed clone should clean up the destination directory
82 std::fs::create_dir(test_env.env_root().join("bad")).unwrap();
83 let assert = test_env
84 .jj_cmd(test_env.env_root(), &["git", "clone", "bad", "failed"])
85 .assert()
86 .code(1);
87 let stdout = test_env.normalize_output(&get_stdout_string(&assert));
88 let stderr = test_env.normalize_output(&get_stderr_string(&assert));
89 insta::assert_snapshot!(stdout, @"");
90 insta::assert_snapshot!(stderr, @r###"
91 Fetching into new repo in "$TEST_ENV/failed"
92 Error: could not find repository at '$TEST_ENV/bad'; class=Repository (6)
93 "###);
94 assert!(!test_env.env_root().join("failed").exists());
95
96 // Failed clone shouldn't remove the existing destination directory
97 std::fs::create_dir(test_env.env_root().join("failed")).unwrap();
98 let assert = test_env
99 .jj_cmd(test_env.env_root(), &["git", "clone", "bad", "failed"])
100 .assert()
101 .code(1);
102 let stdout = test_env.normalize_output(&get_stdout_string(&assert));
103 let stderr = test_env.normalize_output(&get_stderr_string(&assert));
104 insta::assert_snapshot!(stdout, @"");
105 insta::assert_snapshot!(stderr, @r###"
106 Fetching into new repo in "$TEST_ENV/failed"
107 Error: could not find repository at '$TEST_ENV/bad'; class=Repository (6)
108 "###);
109 assert!(test_env.env_root().join("failed").exists());
110 assert!(!test_env.env_root().join("failed").join(".jj").exists());
111
112 // Failed clone (if attempted) shouldn't remove the existing workspace
113 let stderr = test_env.jj_cmd_failure(test_env.env_root(), &["git", "clone", "bad", "clone"]);
114 insta::assert_snapshot!(stderr, @r###"
115 Error: Destination path exists and is not an empty directory
116 "###);
117 assert!(test_env.env_root().join("clone").join(".jj").exists());
118
119 // Try cloning into an existing workspace
120 let stderr = test_env.jj_cmd_failure(test_env.env_root(), &["git", "clone", "source", "clone"]);
121 insta::assert_snapshot!(stderr, @r###"
122 Error: Destination path exists and is not an empty directory
123 "###);
124
125 // Try cloning into an existing file
126 std::fs::write(test_env.env_root().join("file"), "contents").unwrap();
127 let stderr = test_env.jj_cmd_failure(test_env.env_root(), &["git", "clone", "source", "file"]);
128 insta::assert_snapshot!(stderr, @r###"
129 Error: Destination path exists and is not an empty directory
130 "###);
131
132 // Try cloning into non-empty, non-workspace directory
133 std::fs::remove_dir_all(test_env.env_root().join("clone").join(".jj")).unwrap();
134 let stderr = test_env.jj_cmd_failure(test_env.env_root(), &["git", "clone", "source", "clone"]);
135 insta::assert_snapshot!(stderr, @r###"
136 Error: Destination path exists and is not an empty directory
137 "###);
138}
139
140#[test]
141fn test_git_clone_colocate() {
142 let test_env = TestEnvironment::default();
143 test_env.add_config("git.auto-local-branch = true");
144 let git_repo_path = test_env.env_root().join("source");
145 let git_repo = git2::Repository::init(git_repo_path).unwrap();
146
147 // Clone an empty repo
148 let (stdout, stderr) = test_env.jj_cmd_ok(
149 test_env.env_root(),
150 &["git", "clone", "source", "empty", "--colocate"],
151 );
152 insta::assert_snapshot!(stdout, @"");
153 insta::assert_snapshot!(stderr, @r###"
154 Fetching into new repo in "$TEST_ENV/empty"
155 Nothing changed.
156 "###);
157
158 // git_target path should be relative to the store
159 let store_path = test_env
160 .env_root()
161 .join(PathBuf::from_iter(["empty", ".jj", "repo", "store"]));
162 let git_target_file_contents = std::fs::read_to_string(store_path.join("git_target")).unwrap();
163 insta::assert_snapshot!(
164 git_target_file_contents.replace(path::MAIN_SEPARATOR, "/"),
165 @"../../../.git");
166
167 set_up_non_empty_git_repo(&git_repo);
168
169 // Clone with relative source path
170 let (stdout, stderr) = test_env.jj_cmd_ok(
171 test_env.env_root(),
172 &["git", "clone", "source", "clone", "--colocate"],
173 );
174 insta::assert_snapshot!(stdout, @"");
175 insta::assert_snapshot!(stderr, @r###"
176 Fetching into new repo in "$TEST_ENV/clone"
177 branch: main@origin [new] tracked
178 Working copy now at: uuqppmxq 1f0b881a (empty) (no description set)
179 Parent commit : mzyxwzks 9f01a0e0 main | message
180 Added 1 files, modified 0 files, removed 0 files
181 "###);
182 assert!(test_env.env_root().join("clone").join("file").exists());
183 assert!(test_env.env_root().join("clone").join(".git").exists());
184
185 eprintln!(
186 "{:?}",
187 git_repo.head().expect("Repo head should be set").name()
188 );
189
190 let jj_git_repo = git2::Repository::open(test_env.env_root().join("clone"))
191 .expect("Could not open clone repo");
192 assert_eq!(
193 jj_git_repo
194 .head()
195 .expect("Clone Repo HEAD should be set.")
196 .symbolic_target(),
197 git_repo
198 .head()
199 .expect("Repo HEAD should be set.")
200 .symbolic_target()
201 );
202 // ".jj" directory should be ignored at Git side.
203 #[allow(clippy::format_collect)]
204 let git_statuses: String = jj_git_repo
205 .statuses(None)
206 .unwrap()
207 .iter()
208 .map(|entry| format!("{:?} {}\n", entry.status(), entry.path().unwrap()))
209 .collect();
210 insta::assert_snapshot!(git_statuses, @r###"
211 Status(IGNORED) .jj/.gitignore
212 Status(IGNORED) .jj/repo/
213 Status(IGNORED) .jj/working_copy/
214 "###);
215
216 // The old default branch "master" shouldn't exist.
217 insta::assert_snapshot!(
218 get_branch_output(&test_env, &test_env.env_root().join("clone")), @r###"
219 main: mzyxwzks 9f01a0e0 message
220 @git: mzyxwzks 9f01a0e0 message
221 @origin: mzyxwzks 9f01a0e0 message
222 "###);
223
224 // Subsequent fetch should just work even if the source path was relative
225 let (stdout, stderr) =
226 test_env.jj_cmd_ok(&test_env.env_root().join("clone"), &["git", "fetch"]);
227 insta::assert_snapshot!(stdout, @"");
228 insta::assert_snapshot!(stderr, @r###"
229 Nothing changed.
230 "###);
231
232 // Failed clone should clean up the destination directory
233 std::fs::create_dir(test_env.env_root().join("bad")).unwrap();
234 let assert = test_env
235 .jj_cmd(
236 test_env.env_root(),
237 &["git", "clone", "--colocate", "bad", "failed"],
238 )
239 .assert()
240 .code(1);
241 let stdout = test_env.normalize_output(&get_stdout_string(&assert));
242 let stderr = test_env.normalize_output(&get_stderr_string(&assert));
243 insta::assert_snapshot!(stdout, @"");
244 insta::assert_snapshot!(stderr, @r###"
245 Fetching into new repo in "$TEST_ENV/failed"
246 Error: could not find repository at '$TEST_ENV/bad'; class=Repository (6)
247 "###);
248 assert!(!test_env.env_root().join("failed").exists());
249
250 // Failed clone shouldn't remove the existing destination directory
251 std::fs::create_dir(test_env.env_root().join("failed")).unwrap();
252 let assert = test_env
253 .jj_cmd(
254 test_env.env_root(),
255 &["git", "clone", "--colocate", "bad", "failed"],
256 )
257 .assert()
258 .code(1);
259 let stdout = test_env.normalize_output(&get_stdout_string(&assert));
260 let stderr = test_env.normalize_output(&get_stderr_string(&assert));
261 insta::assert_snapshot!(stdout, @"");
262 insta::assert_snapshot!(stderr, @r###"
263 Fetching into new repo in "$TEST_ENV/failed"
264 Error: could not find repository at '$TEST_ENV/bad'; class=Repository (6)
265 "###);
266 assert!(test_env.env_root().join("failed").exists());
267 assert!(!test_env.env_root().join("failed").join(".git").exists());
268 assert!(!test_env.env_root().join("failed").join(".jj").exists());
269
270 // Failed clone (if attempted) shouldn't remove the existing workspace
271 let stderr = test_env.jj_cmd_failure(
272 test_env.env_root(),
273 &["git", "clone", "--colocate", "bad", "clone"],
274 );
275 insta::assert_snapshot!(stderr, @r###"
276 Error: Destination path exists and is not an empty directory
277 "###);
278 assert!(test_env.env_root().join("clone").join(".git").exists());
279 assert!(test_env.env_root().join("clone").join(".jj").exists());
280
281 // Try cloning into an existing workspace
282 let stderr = test_env.jj_cmd_failure(
283 test_env.env_root(),
284 &["git", "clone", "source", "clone", "--colocate"],
285 );
286 insta::assert_snapshot!(stderr, @r###"
287 Error: Destination path exists and is not an empty directory
288 "###);
289
290 // Try cloning into an existing file
291 std::fs::write(test_env.env_root().join("file"), "contents").unwrap();
292 let stderr = test_env.jj_cmd_failure(
293 test_env.env_root(),
294 &["git", "clone", "source", "file", "--colocate"],
295 );
296 insta::assert_snapshot!(stderr, @r###"
297 Error: Destination path exists and is not an empty directory
298 "###);
299
300 // Try cloning into non-empty, non-workspace directory
301 std::fs::remove_dir_all(test_env.env_root().join("clone").join(".jj")).unwrap();
302 let stderr = test_env.jj_cmd_failure(
303 test_env.env_root(),
304 &["git", "clone", "source", "clone", "--colocate"],
305 );
306 insta::assert_snapshot!(stderr, @r###"
307 Error: Destination path exists and is not an empty directory
308 "###);
309}
310
311#[test]
312fn test_git_clone_remote_default_branch() {
313 let test_env = TestEnvironment::default();
314 let git_repo_path = test_env.env_root().join("source");
315 let git_repo = git2::Repository::init(git_repo_path).unwrap();
316 set_up_non_empty_git_repo(&git_repo);
317 // Create non-default branch in remote
318 let oid = git_repo
319 .find_reference("refs/heads/main")
320 .unwrap()
321 .target()
322 .unwrap();
323 git_repo
324 .reference("refs/heads/feature1", oid, false, "")
325 .unwrap();
326
327 // All fetched branches will be imported if auto-local-branch is on
328 test_env.add_config("git.auto-local-branch = true");
329 let (_stdout, stderr) =
330 test_env.jj_cmd_ok(test_env.env_root(), &["git", "clone", "source", "clone1"]);
331 insta::assert_snapshot!(stderr, @r###"
332 Fetching into new repo in "$TEST_ENV/clone1"
333 branch: feature1@origin [new] tracked
334 branch: main@origin [new] tracked
335 Working copy now at: sqpuoqvx cad212e1 (empty) (no description set)
336 Parent commit : mzyxwzks 9f01a0e0 feature1 main | message
337 Added 1 files, modified 0 files, removed 0 files
338 "###);
339 insta::assert_snapshot!(
340 get_branch_output(&test_env, &test_env.env_root().join("clone1")), @r###"
341 feature1: mzyxwzks 9f01a0e0 message
342 @origin: mzyxwzks 9f01a0e0 message
343 main: mzyxwzks 9f01a0e0 message
344 @origin: mzyxwzks 9f01a0e0 message
345 "###);
346
347 // Only the default branch will be imported if auto-local-branch is off
348 test_env.add_config("git.auto-local-branch = false");
349 let (_stdout, stderr) =
350 test_env.jj_cmd_ok(test_env.env_root(), &["git", "clone", "source", "clone2"]);
351 insta::assert_snapshot!(stderr, @r###"
352 Fetching into new repo in "$TEST_ENV/clone2"
353 branch: feature1@origin [new] untracked
354 branch: main@origin [new] untracked
355 Working copy now at: pmmvwywv fa729b1e (empty) (no description set)
356 Parent commit : mzyxwzks 9f01a0e0 feature1@origin main | message
357 Added 1 files, modified 0 files, removed 0 files
358 "###);
359 insta::assert_snapshot!(
360 get_branch_output(&test_env, &test_env.env_root().join("clone2")), @r###"
361 feature1@origin: mzyxwzks 9f01a0e0 message
362 main: mzyxwzks 9f01a0e0 message
363 @origin: mzyxwzks 9f01a0e0 message
364 "###);
365}
366
367fn get_branch_output(test_env: &TestEnvironment, repo_path: &Path) -> String {
368 test_env.jj_cmd_success(repo_path, &["branch", "list", "--all-remotes"])
369}