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::fs;
16use std::io::Write as _;
17use std::path::Path;
18use std::path::PathBuf;
19
20use testutils::git;
21
22use crate::common::TestEnvironment;
23
24fn read_git_config(repo_path: &Path) -> String {
25 let git_config = fs::read_to_string(repo_path.join(".jj/repo/store/git/config"))
26 .or_else(|_| fs::read_to_string(repo_path.join(".git/config")))
27 .unwrap();
28 git_config
29 .split_inclusive('\n')
30 .filter(|line| {
31 // Filter out non‐portable values.
32 [
33 "\tfilemode =",
34 "\tsymlinks =",
35 "\tignorecase =",
36 "\tprecomposeunicode =",
37 ]
38 .iter()
39 .all(|prefix| !line.to_ascii_lowercase().starts_with(prefix))
40 })
41 .collect()
42}
43
44#[test]
45fn test_git_remotes() {
46 let test_env = TestEnvironment::default();
47
48 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
49 let work_dir = test_env.work_dir("repo");
50
51 let output = work_dir.run_jj(["git", "remote", "list"]);
52 insta::assert_snapshot!(output, @"");
53 let output = work_dir.run_jj(["git", "remote", "add", "foo", "http://example.com/repo/foo"]);
54 insta::assert_snapshot!(output, @"");
55 let output = work_dir.run_jj(["git", "remote", "add", "bar", "http://example.com/repo/bar"]);
56 insta::assert_snapshot!(output, @"");
57 let output = work_dir.run_jj(["git", "remote", "list"]);
58 insta::assert_snapshot!(output, @r"
59 bar http://example.com/repo/bar
60 foo http://example.com/repo/foo
61 [EOF]
62 ");
63 let output = work_dir.run_jj(["git", "remote", "remove", "foo"]);
64 insta::assert_snapshot!(output, @"");
65 let output = work_dir.run_jj(["git", "remote", "list"]);
66 insta::assert_snapshot!(output, @r"
67 bar http://example.com/repo/bar
68 [EOF]
69 ");
70 let output = work_dir.run_jj(["git", "remote", "remove", "nonexistent"]);
71 insta::assert_snapshot!(output, @r"
72 ------- stderr -------
73 Error: No git remote named 'nonexistent'
74 [EOF]
75 [exit status: 1]
76 ");
77 insta::assert_snapshot!(read_git_config(work_dir.root()), @r#"
78 [core]
79 repositoryformatversion = 0
80 bare = true
81 logallrefupdates = false
82 [remote "bar"]
83 url = http://example.com/repo/bar
84 fetch = +refs/heads/*:refs/remotes/bar/*
85 "#);
86}
87
88#[test]
89fn test_git_remote_add() {
90 let test_env = TestEnvironment::default();
91
92 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
93 let work_dir = test_env.work_dir("repo");
94 work_dir
95 .run_jj(["git", "remote", "add", "foo", "http://example.com/repo/foo"])
96 .success();
97 let output = work_dir.run_jj([
98 "git",
99 "remote",
100 "add",
101 "foo",
102 "http://example.com/repo/foo2",
103 ]);
104 insta::assert_snapshot!(output, @r"
105 ------- stderr -------
106 Error: Git remote named 'foo' already exists
107 [EOF]
108 [exit status: 1]
109 ");
110 let output = work_dir.run_jj(["git", "remote", "add", "git", "http://example.com/repo/git"]);
111 insta::assert_snapshot!(output, @r"
112 ------- stderr -------
113 Error: Git remote named 'git' is reserved for local Git repository
114 [EOF]
115 [exit status: 1]
116 ");
117 let output = work_dir.run_jj(["git", "remote", "list"]);
118 insta::assert_snapshot!(output, @r"
119 foo http://example.com/repo/foo
120 [EOF]
121 ");
122}
123
124#[test]
125fn test_git_remote_set_url() {
126 let test_env = TestEnvironment::default();
127
128 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
129 let work_dir = test_env.work_dir("repo");
130 work_dir
131 .run_jj(["git", "remote", "add", "foo", "http://example.com/repo/foo"])
132 .success();
133 let output = work_dir.run_jj([
134 "git",
135 "remote",
136 "set-url",
137 "bar",
138 "http://example.com/repo/bar",
139 ]);
140 insta::assert_snapshot!(output, @r"
141 ------- stderr -------
142 Error: No git remote named 'bar'
143 [EOF]
144 [exit status: 1]
145 ");
146 let output = work_dir.run_jj([
147 "git",
148 "remote",
149 "set-url",
150 "git",
151 "http://example.com/repo/git",
152 ]);
153 insta::assert_snapshot!(output, @r"
154 ------- stderr -------
155 Error: Git remote named 'git' is reserved for local Git repository
156 [EOF]
157 [exit status: 1]
158 ");
159 let output = work_dir.run_jj([
160 "git",
161 "remote",
162 "set-url",
163 "foo",
164 "http://example.com/repo/bar",
165 ]);
166 insta::assert_snapshot!(output, @"");
167 let output = work_dir.run_jj(["git", "remote", "list"]);
168 insta::assert_snapshot!(output, @r"
169 foo http://example.com/repo/bar
170 [EOF]
171 ");
172 insta::assert_snapshot!(read_git_config(work_dir.root()), @r#"
173 [core]
174 repositoryformatversion = 0
175 bare = true
176 logallrefupdates = false
177 [remote "foo"]
178 url = http://example.com/repo/bar
179 fetch = +refs/heads/*:refs/remotes/foo/*
180 "#);
181}
182
183#[test]
184fn test_git_remote_relative_path() {
185 let test_env = TestEnvironment::default();
186 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
187 let work_dir = test_env.work_dir("repo");
188
189 // Relative path using OS-native separator
190 let path = PathBuf::from_iter(["..", "native", "sep"]);
191 work_dir
192 .run_jj(["git", "remote", "add", "foo", path.to_str().unwrap()])
193 .success();
194 let output = work_dir.run_jj(["git", "remote", "list"]);
195 insta::assert_snapshot!(output, @r"
196 foo $TEST_ENV/native/sep
197 [EOF]
198 ");
199
200 // Relative path using UNIX separator
201 test_env
202 .run_jj_in(
203 ".",
204 ["-Rrepo", "git", "remote", "set-url", "foo", "unix/sep"],
205 )
206 .success();
207 let output = work_dir.run_jj(["git", "remote", "list"]);
208 insta::assert_snapshot!(output, @r"
209 foo $TEST_ENV/unix/sep
210 [EOF]
211 ");
212}
213
214#[test]
215fn test_git_remote_rename() {
216 let test_env = TestEnvironment::default();
217
218 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
219 let work_dir = test_env.work_dir("repo");
220 work_dir
221 .run_jj(["git", "remote", "add", "foo", "http://example.com/repo/foo"])
222 .success();
223 work_dir
224 .run_jj(["git", "remote", "add", "baz", "http://example.com/repo/baz"])
225 .success();
226 let output = work_dir.run_jj(["git", "remote", "rename", "bar", "foo"]);
227 insta::assert_snapshot!(output, @r"
228 ------- stderr -------
229 Error: No git remote named 'bar'
230 [EOF]
231 [exit status: 1]
232 ");
233 let output = work_dir.run_jj(["git", "remote", "rename", "foo", "baz"]);
234 insta::assert_snapshot!(output, @r"
235 ------- stderr -------
236 Error: Git remote named 'baz' already exists
237 [EOF]
238 [exit status: 1]
239 ");
240 let output = work_dir.run_jj(["git", "remote", "rename", "foo", "git"]);
241 insta::assert_snapshot!(output, @r"
242 ------- stderr -------
243 Error: Git remote named 'git' is reserved for local Git repository
244 [EOF]
245 [exit status: 1]
246 ");
247 let output = work_dir.run_jj(["git", "remote", "rename", "foo", "bar"]);
248 insta::assert_snapshot!(output, @"");
249 let output = work_dir.run_jj(["git", "remote", "list"]);
250 insta::assert_snapshot!(output, @r"
251 bar http://example.com/repo/foo
252 baz http://example.com/repo/baz
253 [EOF]
254 ");
255 insta::assert_snapshot!(read_git_config(work_dir.root()), @r#"
256 [core]
257 repositoryformatversion = 0
258 bare = true
259 logallrefupdates = false
260 [remote "baz"]
261 url = http://example.com/repo/baz
262 fetch = +refs/heads/*:refs/remotes/baz/*
263 [remote "bar"]
264 url = http://example.com/repo/foo
265 fetch = +refs/heads/*:refs/remotes/bar/*
266 "#);
267}
268
269#[test]
270fn test_git_remote_named_git() {
271 let test_env = TestEnvironment::default();
272
273 // Existing remote named 'git' shouldn't block the repo initialization.
274 let work_dir = test_env.work_dir("repo");
275 git::init(work_dir.root());
276 git::add_remote(work_dir.root(), "git", "http://example.com/repo/repo");
277 work_dir.run_jj(["git", "init", "--git-repo=."]).success();
278 work_dir
279 .run_jj(["bookmark", "create", "-r@", "main"])
280 .success();
281
282 // The remote can be renamed.
283 let output = work_dir.run_jj(["git", "remote", "rename", "git", "bar"]);
284 insta::assert_snapshot!(output, @"");
285 let output = work_dir.run_jj(["git", "remote", "list"]);
286 insta::assert_snapshot!(output, @r"
287 bar http://example.com/repo/repo
288 [EOF]
289 ");
290 insta::assert_snapshot!(read_git_config(work_dir.root()), @r#"
291 [core]
292 repositoryformatversion = 0
293 bare = false
294 logallrefupdates = true
295 [remote "bar"]
296 url = http://example.com/repo/repo
297 fetch = +refs/heads/*:refs/remotes/bar/*
298 "#);
299 // @git bookmark shouldn't be renamed.
300 let output = work_dir.run_jj(["log", "-rmain@git", "-Tbookmarks"]);
301 insta::assert_snapshot!(output, @r"
302 @ main
303 │
304 ~
305 [EOF]
306 ");
307
308 // The remote cannot be renamed back by jj.
309 let output = work_dir.run_jj(["git", "remote", "rename", "bar", "git"]);
310 insta::assert_snapshot!(output, @r"
311 ------- stderr -------
312 Error: Git remote named 'git' is reserved for local Git repository
313 [EOF]
314 [exit status: 1]
315 ");
316
317 // Reinitialize the repo with remote named 'git'.
318 work_dir.remove_dir_all(".jj");
319 git::rename_remote(work_dir.root(), "bar", "git");
320 work_dir.run_jj(["git", "init", "--git-repo=."]).success();
321 insta::assert_snapshot!(read_git_config(work_dir.root()), @r#"
322 [core]
323 repositoryformatversion = 0
324 bare = false
325 logallrefupdates = true
326 [remote "git"]
327 url = http://example.com/repo/repo
328 fetch = +refs/heads/*:refs/remotes/git/*
329 "#);
330
331 // The remote can also be removed.
332 let output = work_dir.run_jj(["git", "remote", "remove", "git"]);
333 insta::assert_snapshot!(output, @"");
334 let output = work_dir.run_jj(["git", "remote", "list"]);
335 insta::assert_snapshot!(output, @"");
336 insta::assert_snapshot!(read_git_config(work_dir.root()), @r#"
337 [core]
338 repositoryformatversion = 0
339 bare = false
340 logallrefupdates = true
341 "#);
342 // @git bookmark shouldn't be removed.
343 let output = work_dir.run_jj(["log", "-rmain@git", "-Tbookmarks"]);
344 insta::assert_snapshot!(output, @r"
345 ○ main
346 │
347 ~
348 [EOF]
349 ");
350}
351
352#[test]
353fn test_git_remote_with_slashes() {
354 let test_env = TestEnvironment::default();
355
356 // Existing remote with slashes shouldn't block the repo initialization.
357 let work_dir = test_env.work_dir("repo");
358 git::init(work_dir.root());
359 git::add_remote(
360 work_dir.root(),
361 "slash/origin",
362 "http://example.com/repo/repo",
363 );
364 work_dir.run_jj(["git", "init", "--git-repo=."]).success();
365 work_dir
366 .run_jj(["bookmark", "create", "-r@", "main"])
367 .success();
368
369 // Cannot add remote with a slash via `jj`
370 let output = work_dir.run_jj([
371 "git",
372 "remote",
373 "add",
374 "another/origin",
375 "http://examples.org/repo/repo",
376 ]);
377 insta::assert_snapshot!(output, @r"
378 ------- stderr -------
379 Error: Git remotes with slashes are incompatible with jj: another/origin
380 [EOF]
381 [exit status: 1]
382 ");
383 let output = work_dir.run_jj(["git", "remote", "list"]);
384 insta::assert_snapshot!(output, @r"
385 slash/origin http://example.com/repo/repo
386 [EOF]
387 ");
388
389 // The remote can be renamed.
390 let output = work_dir.run_jj(["git", "remote", "rename", "slash/origin", "origin"]);
391 insta::assert_snapshot!(output, @"");
392 let output = work_dir.run_jj(["git", "remote", "list"]);
393 insta::assert_snapshot!(output, @r"
394 origin http://example.com/repo/repo
395 [EOF]
396 ");
397
398 // The remote cannot be renamed back by jj.
399 let output = work_dir.run_jj(["git", "remote", "rename", "origin", "slash/origin"]);
400 insta::assert_snapshot!(output, @r"
401 ------- stderr -------
402 Error: Git remotes with slashes are incompatible with jj: slash/origin
403 [EOF]
404 [exit status: 1]
405 ");
406
407 // Reinitialize the repo with remote with slashes
408 work_dir.remove_dir_all(".jj");
409 git::rename_remote(work_dir.root(), "origin", "slash/origin");
410 work_dir.run_jj(["git", "init", "--git-repo=."]).success();
411
412 // The remote can also be removed.
413 let output = work_dir.run_jj(["git", "remote", "remove", "slash/origin"]);
414 insta::assert_snapshot!(output, @"");
415 let output = work_dir.run_jj(["git", "remote", "list"]);
416 insta::assert_snapshot!(output, @"");
417 // @git bookmark shouldn't be removed.
418 let output = work_dir.run_jj(["log", "-rmain@git", "-Tbookmarks"]);
419 insta::assert_snapshot!(output, @r"
420 ○ main
421 │
422 ~
423 [EOF]
424 ");
425}
426
427#[test]
428fn test_git_remote_with_branch_config() {
429 let test_env = TestEnvironment::default();
430
431 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
432 let work_dir = test_env.work_dir("repo");
433
434 let output = work_dir.run_jj(["git", "remote", "add", "foo", "http://example.com/repo"]);
435 insta::assert_snapshot!(output, @"");
436
437 let mut config_file = fs::OpenOptions::new()
438 .append(true)
439 .open(work_dir.root().join(".jj/repo/store/git/config"))
440 .unwrap();
441 // `git clone` adds branch configuration like this.
442 let eol = if cfg!(windows) { "\r\n" } else { "\n" };
443 write!(config_file, "[branch \"test\"]{eol}").unwrap();
444 write!(config_file, "\tremote = foo{eol}").unwrap();
445 write!(config_file, "\tmerge = refs/heads/test{eol}").unwrap();
446 drop(config_file);
447
448 let output = work_dir.run_jj(["git", "remote", "rename", "foo", "bar"]);
449 insta::assert_snapshot!(output, @"");
450
451 insta::assert_snapshot!(read_git_config(work_dir.root()), @r#"
452 [core]
453 repositoryformatversion = 0
454 bare = true
455 logallrefupdates = false
456 [branch "test"]
457 remote = bar
458 merge = refs/heads/test
459 [remote "bar"]
460 url = http://example.com/repo
461 fetch = +refs/heads/*:refs/remotes/bar/*
462 "#);
463}