use std::fs;
use std::io::Write as _;
use std::path::Path;
use std::path::PathBuf;
use indoc::indoc;
use testutils::git;
use crate::common::TestEnvironment;
fn read_git_config(repo_path: &Path) -> String {
let git_config = fs::read_to_string(repo_path.join(".jj/repo/store/git/config"))
.or_else(|_| fs::read_to_string(repo_path.join(".git/config")))
.unwrap();
git_config
.split_inclusive('\n')
.filter(|line| {
[
"\tfilemode =",
"\tsymlinks =",
"\tignorecase =",
"\tprecomposeunicode =",
]
.iter()
.all(|prefix| !line.to_ascii_lowercase().starts_with(prefix))
})
.collect()
}
#[test]
fn test_git_remotes() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj(["git", "remote", "list"]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj(["git", "remote", "add", "foo", "http://example.com/repo/foo"]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj(["git", "remote", "add", "bar", "http://example.com/repo/bar"]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj(["git", "remote", "list"]);
insta::assert_snapshot!(output, @r"
bar http://example.com/repo/bar
foo http://example.com/repo/foo
[EOF]
");
let output = work_dir.run_jj(["git", "remote", "remove", "foo"]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj(["git", "remote", "list"]);
insta::assert_snapshot!(output, @r"
bar http://example.com/repo/bar
[EOF]
");
let output = work_dir.run_jj(["git", "remote", "remove", "nonexistent"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: No git remote named 'nonexistent'
[EOF]
[exit status: 1]
");
insta::assert_snapshot!(read_git_config(work_dir.root()), @r#"
[core]
repositoryformatversion = 0
bare = true
logallrefupdates = false
[remote "bar"]
url = http://example.com/repo/bar
fetch = +refs/heads/*:refs/remotes/bar/*
"#);
}
#[test]
fn test_git_remote_add() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir
.run_jj(["git", "remote", "add", "foo", "http://example.com/repo/foo"])
.success();
let output = work_dir.run_jj([
"git",
"remote",
"add",
"foo",
"http://example.com/repo/foo2",
]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Git remote named 'foo' already exists
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["git", "remote", "add", "git", "http://example.com/repo/git"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Git remote named 'git' is reserved for local Git repository
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["git", "remote", "list"]);
insta::assert_snapshot!(output, @r"
foo http://example.com/repo/foo
[EOF]
");
}
#[test]
fn test_git_remote_with_fetch_tags() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj(["git", "remote", "add", "foo", "http://example.com/repo"]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj([
"git",
"remote",
"add",
"foo-included",
"http://example.com/repo",
"--fetch-tags",
"included",
]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj([
"git",
"remote",
"add",
"foo-all",
"http://example.com/repo",
"--fetch-tags",
"all",
]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj([
"git",
"remote",
"add",
"foo-none",
"http://example.com/repo",
"--fetch-tags",
"none",
]);
insta::assert_snapshot!(output, @"");
insta::assert_snapshot!(read_git_config(work_dir.root()), @r#"
[core]
repositoryformatversion = 0
bare = true
logallrefupdates = false
[remote "foo"]
url = http://example.com/repo
fetch = +refs/heads/*:refs/remotes/foo/*
[remote "foo-included"]
url = http://example.com/repo
fetch = +refs/heads/*:refs/remotes/foo-included/*
[remote "foo-all"]
url = http://example.com/repo
tagOpt = --tags
fetch = +refs/heads/*:refs/remotes/foo-all/*
[remote "foo-none"]
url = http://example.com/repo
tagOpt = --no-tags
fetch = +refs/heads/*:refs/remotes/foo-none/*
"#);
}
#[test]
fn test_git_remote_set_url() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir
.run_jj(["git", "remote", "add", "foo", "http://example.com/repo/foo"])
.success();
let output = work_dir.run_jj([
"git",
"remote",
"set-url",
"bar",
"http://example.com/repo/bar",
]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: No git remote named 'bar'
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj([
"git",
"remote",
"set-url",
"git",
"http://example.com/repo/git",
]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Git remote named 'git' is reserved for local Git repository
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj([
"git",
"remote",
"set-url",
"foo",
"http://example.com/repo/bar",
]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj(["git", "remote", "list"]);
insta::assert_snapshot!(output, @r"
foo http://example.com/repo/bar
[EOF]
");
insta::assert_snapshot!(read_git_config(work_dir.root()), @r#"
[core]
repositoryformatversion = 0
bare = true
logallrefupdates = false
[remote "foo"]
url = http://example.com/repo/bar
fetch = +refs/heads/*:refs/remotes/foo/*
"#);
}
#[test]
fn test_git_remote_relative_path() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let path = PathBuf::from_iter(["..", "native", "sep"]);
work_dir
.run_jj(["git", "remote", "add", "foo", path.to_str().unwrap()])
.success();
let output = work_dir.run_jj(["git", "remote", "list"]);
insta::assert_snapshot!(output, @r"
foo $TEST_ENV/native/sep
[EOF]
");
test_env
.run_jj_in(
".",
["-Rrepo", "git", "remote", "set-url", "foo", "unix/sep"],
)
.success();
let output = work_dir.run_jj(["git", "remote", "list"]);
insta::assert_snapshot!(output, @r"
foo $TEST_ENV/unix/sep
[EOF]
");
}
#[test]
fn test_git_remote_rename() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir
.run_jj(["git", "remote", "add", "foo", "http://example.com/repo/foo"])
.success();
work_dir
.run_jj(["git", "remote", "add", "baz", "http://example.com/repo/baz"])
.success();
let output = work_dir.run_jj(["git", "remote", "rename", "bar", "foo"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: No git remote named 'bar'
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["git", "remote", "rename", "foo", "baz"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Git remote named 'baz' already exists
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["git", "remote", "rename", "foo", "git"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Git remote named 'git' is reserved for local Git repository
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["git", "remote", "rename", "foo", "bar"]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj(["git", "remote", "list"]);
insta::assert_snapshot!(output, @r"
bar http://example.com/repo/foo
baz http://example.com/repo/baz
[EOF]
");
insta::assert_snapshot!(read_git_config(work_dir.root()), @r#"
[core]
repositoryformatversion = 0
bare = true
logallrefupdates = false
[remote "baz"]
url = http://example.com/repo/baz
fetch = +refs/heads/*:refs/remotes/baz/*
[remote "bar"]
url = http://example.com/repo/foo
fetch = +refs/heads/*:refs/remotes/bar/*
"#);
}
#[test]
fn test_git_remote_named_git() {
let test_env = TestEnvironment::default();
let work_dir = test_env.work_dir("repo");
git::init(work_dir.root());
git::add_remote(work_dir.root(), "git", "http://example.com/repo/repo");
work_dir.run_jj(["git", "init", "--git-repo=."]).success();
work_dir
.run_jj(["bookmark", "create", "-r@", "main"])
.success();
let output = work_dir.run_jj(["git", "remote", "rename", "git", "bar"]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj(["git", "remote", "list"]);
insta::assert_snapshot!(output, @r"
bar http://example.com/repo/repo
[EOF]
");
insta::assert_snapshot!(read_git_config(work_dir.root()), @r#"
[core]
repositoryformatversion = 0
bare = false
logallrefupdates = true
[remote "bar"]
url = http://example.com/repo/repo
fetch = +refs/heads/*:refs/remotes/bar/*
"#);
let output = work_dir.run_jj(["log", "-rmain@git", "-Tbookmarks"]);
insta::assert_snapshot!(output, @r"
@ main
│
~
[EOF]
");
let output = work_dir.run_jj(["git", "remote", "rename", "bar", "git"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Git remote named 'git' is reserved for local Git repository
[EOF]
[exit status: 1]
");
work_dir.remove_dir_all(".jj");
git::rename_remote(work_dir.root(), "bar", "git");
work_dir.run_jj(["git", "init", "--git-repo=."]).success();
insta::assert_snapshot!(read_git_config(work_dir.root()), @r#"
[core]
repositoryformatversion = 0
bare = false
logallrefupdates = true
[remote "git"]
url = http://example.com/repo/repo
fetch = +refs/heads/*:refs/remotes/git/*
"#);
let output = work_dir.run_jj(["git", "remote", "remove", "git"]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj(["git", "remote", "list"]);
insta::assert_snapshot!(output, @"");
insta::assert_snapshot!(read_git_config(work_dir.root()), @r#"
[core]
repositoryformatversion = 0
bare = false
logallrefupdates = true
"#);
let output = work_dir.run_jj(["log", "-rmain@git", "-Tbookmarks"]);
insta::assert_snapshot!(output, @r"
○ main
│
~
[EOF]
");
}
#[test]
fn test_git_remote_with_slashes() {
let test_env = TestEnvironment::default();
let work_dir = test_env.work_dir("repo");
git::init(work_dir.root());
git::add_remote(
work_dir.root(),
"slash/origin",
"http://example.com/repo/repo",
);
work_dir.run_jj(["git", "init", "--git-repo=."]).success();
work_dir
.run_jj(["bookmark", "create", "-r@", "main"])
.success();
let output = work_dir.run_jj([
"git",
"remote",
"add",
"another/origin",
"http://examples.org/repo/repo",
]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Git remotes with slashes are incompatible with jj: another/origin
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["git", "remote", "list"]);
insta::assert_snapshot!(output, @r"
slash/origin http://example.com/repo/repo
[EOF]
");
let output = work_dir.run_jj(["git", "remote", "rename", "slash/origin", "origin"]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj(["git", "remote", "list"]);
insta::assert_snapshot!(output, @r"
origin http://example.com/repo/repo
[EOF]
");
let output = work_dir.run_jj(["git", "remote", "rename", "origin", "slash/origin"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Git remotes with slashes are incompatible with jj: slash/origin
[EOF]
[exit status: 1]
");
work_dir.remove_dir_all(".jj");
git::rename_remote(work_dir.root(), "origin", "slash/origin");
work_dir.run_jj(["git", "init", "--git-repo=."]).success();
let output = work_dir.run_jj(["git", "remote", "remove", "slash/origin"]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj(["git", "remote", "list"]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj(["log", "-rmain@git", "-Tbookmarks"]);
insta::assert_snapshot!(output, @r"
○ main
│
~
[EOF]
");
}
#[test]
fn test_git_remote_with_branch_config() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj(["git", "remote", "add", "foo", "http://example.com/repo"]);
insta::assert_snapshot!(output, @"");
let mut config_file = fs::OpenOptions::new()
.append(true)
.open(work_dir.root().join(".jj/repo/store/git/config"))
.unwrap();
let eol = if cfg!(windows) { "\r\n" } else { "\n" };
write!(config_file, "[branch \"test\"]{eol}").unwrap();
write!(config_file, "\tremote = foo{eol}").unwrap();
write!(config_file, "\tmerge = refs/heads/test{eol}").unwrap();
drop(config_file);
let output = work_dir.run_jj(["git", "remote", "rename", "foo", "bar"]);
insta::assert_snapshot!(output, @"");
insta::assert_snapshot!(read_git_config(work_dir.root()), @r#"
[core]
repositoryformatversion = 0
bare = true
logallrefupdates = false
[branch "test"]
remote = bar
merge = refs/heads/test
[remote "bar"]
url = http://example.com/repo
fetch = +refs/heads/*:refs/remotes/bar/*
"#);
}
#[test]
fn test_git_remote_with_global_git_remote_config() {
let mut test_env = TestEnvironment::default();
test_env.work_dir("").write_file(
"git-config",
indoc! {r#"
[remote "origin"]
prune = true
[remote "foo"]
url = htps://example.com/repo/foo
fetch = +refs/heads/*:refs/remotes/foo/*
"#},
);
test_env.add_env_var(
"GIT_CONFIG_GLOBAL",
test_env.env_root().join("git-config").to_str().unwrap(),
);
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj(["git", "remote", "list"]);
insta::assert_snapshot!(output, @r"
foo htps://example.com/repo/foo
[EOF]
");
let output = work_dir.run_jj(["git", "remote", "rename", "foo", "bar"]);
insta::assert_snapshot!(output, @"");
insta::assert_snapshot!(read_git_config(work_dir.root()), @r#"
[core]
repositoryformatversion = 0
bare = true
logallrefupdates = false
[remote "bar"]
url = htps://example.com/repo/foo
fetch = +refs/heads/*:refs/remotes/bar/*
"#);
let output = work_dir.run_jj(["git", "remote", "list"]);
insta::assert_snapshot!(output, @r"
bar htps://example.com/repo/foo
foo htps://example.com/repo/foo
[EOF]
");
let output = work_dir.run_jj([
"git",
"remote",
"add",
"origin",
"http://example.com/repo/origin/1",
]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj([
"git",
"remote",
"set-url",
"origin",
"https://example.com/repo/origin/2",
]);
insta::assert_snapshot!(output, @"");
let output = work_dir.run_jj(["git", "remote", "list"]);
insta::assert_snapshot!(output, @r"
bar htps://example.com/repo/foo
foo htps://example.com/repo/foo
origin https://example.com/repo/origin/2
[EOF]
");
insta::assert_snapshot!(read_git_config(work_dir.root()), @r#"
[core]
repositoryformatversion = 0
bare = true
logallrefupdates = false
[remote "bar"]
url = htps://example.com/repo/foo
fetch = +refs/heads/*:refs/remotes/bar/*
[remote "origin"]
url = https://example.com/repo/origin/2
fetch = +refs/heads/*:refs/remotes/origin/*
"#);
}