Skip to content

Commit 1ec65aa

Browse files
authored
Add -b flag to init subcommand (#119)
* Add -b flag to init subcommand * address review comments * address review comment
1 parent 372f1ef commit 1ec65aa

File tree

10 files changed

+166
-248
lines changed

10 files changed

+166
-248
lines changed

src/subcommand/init_subcommand.cpp

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// #include <filesystem>
1+
#include <filesystem>
22
#include "init_subcommand.hpp"
33
#include "../wrapper/repository_wrapper.hpp"
44

@@ -12,11 +12,39 @@ init_subcommand::init_subcommand(const libgit2_object&, CLI::App& app)
1212
sub->add_option("directory", m_directory, "info about directory arg")
1313
->check(CLI::ExistingDirectory | CLI::NonexistentPath)
1414
->default_val(get_current_git_path());
15+
sub->add_option("-b,--initial-branch", m_branch, "Use <branch-name> for the initial branch in the newly created repository. If not specified, fall back to the default name.");
1516

1617
sub->callback([this]() { this->run(); });
1718
}
1819

1920
void init_subcommand::run()
2021
{
21-
repository_wrapper::init(m_directory, m_bare);
22+
std::filesystem::path target_dir = m_directory;
23+
bool reinit = std::filesystem::exists(target_dir / ".git" / "HEAD");
24+
25+
repository_wrapper repo = [this]() {
26+
if (m_branch.empty())
27+
{
28+
return repository_wrapper::init(m_directory, m_bare);
29+
}
30+
else
31+
{
32+
git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;
33+
if (m_bare) { opts.flags |= GIT_REPOSITORY_INIT_BARE; }
34+
opts.initial_head = m_branch.c_str();
35+
36+
return repository_wrapper::init_ext(m_directory, &opts);
37+
}
38+
}();
39+
40+
std::string path = repo.path();
41+
42+
if (reinit)
43+
{
44+
std::cout << "Reinitialized existing Git repository in " << path <<std::endl;
45+
}
46+
else
47+
{
48+
std::cout << "Initialized empty Git repository in " << path <<std::endl;
49+
}
2250
}

src/subcommand/init_subcommand.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ class init_subcommand
1616
private:
1717
bool m_bare = false;
1818
std::string m_directory;
19+
std::string m_branch;
1920
};

src/wrapper/repository_wrapper.cpp

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,21 @@ repository_wrapper repository_wrapper::init(std::string_view directory, bool bar
3131
return rw;
3232
}
3333

34+
repository_wrapper repository_wrapper::init_ext(std::string_view directory, git_repository_init_options* opts)
35+
{
36+
repository_wrapper rw;
37+
throw_if_error(git_repository_init_ext(&(rw.p_resource), directory.data(), opts));
38+
return rw;
39+
}
40+
3441
repository_wrapper repository_wrapper::clone(std::string_view url, std::string_view path, const git_clone_options& opts)
3542
{
3643
repository_wrapper rw;
3744
throw_if_error(git_clone(&(rw.p_resource), url.data(), path.data(), &opts));
3845
return rw;
3946
}
4047

41-
std::string repository_wrapper::git_path() const
48+
std::string repository_wrapper::path() const
4249
{
4350
return git_repository_path(*this);
4451
}
@@ -343,8 +350,8 @@ size_t repository_wrapper::shallow_depth_from_head() const
343350
return 0u;
344351
}
345352

346-
std::string git_path = this->git_path();
347-
std::string shallow_path = git_path + "shallow";
353+
std::string path = this->path();
354+
std::string shallow_path = path + "shallow";
348355

349356
std::vector<git_oid> boundaries_list;
350357
std::ifstream f(shallow_path);

src/wrapper/repository_wrapper.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,11 @@ class repository_wrapper : public wrapper_base<git_repository>
4040
repository_wrapper& operator=(repository_wrapper&&) noexcept = default;
4141

4242
static repository_wrapper init(std::string_view directory, bool bare);
43+
static repository_wrapper init_ext(std::string_view repo_path, git_repository_init_options* opts);
4344
static repository_wrapper open(std::string_view directory);
4445
static repository_wrapper clone(std::string_view url, std::string_view path, const git_clone_options& opts);
4546

46-
std::string git_path() const;
47+
std::string path() const;
4748
git_repository_state_t state() const;
4849
void state_cleanup();
4950

test/conftest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def commit_env_config(monkeypatch):
5252

5353
@pytest.fixture
5454
def repo_init_with_commit(commit_env_config, git2cpp_path, tmp_path, run_in_tmp_path):
55-
cmd_init = [git2cpp_path, "init", "."]
55+
cmd_init = [git2cpp_path, "init", ".", "-b", "main"]
5656
p_init = subprocess.run(cmd_init, capture_output=True, cwd=tmp_path, text=True)
5757
assert p_init.returncode == 0
5858

@@ -70,7 +70,7 @@ def repo_init_with_commit(commit_env_config, git2cpp_path, tmp_path, run_in_tmp_
7070

7171
@pytest.fixture(scope="session")
7272
def private_test_repo():
73-
# Fixture containing everything needed to access private github repo.
73+
# Fixture containing everything needed to access private github repo.
7474
# GIT2CPP_TEST_PRIVATE_TOKEN is the fine-grained Personal Access Token for private test repo.
7575
# If this is not available as an environment variable, tests that use this fixture are skipped.
7676
token = os.getenv("GIT2CPP_TEST_PRIVATE_TOKEN")
@@ -82,5 +82,5 @@ def private_test_repo():
8282
"repo_name": repo_name,
8383
"http_url": f"http://github.com/{org_name}/{repo_name}",
8484
"https_url": f"https://github.com/{org_name}/{repo_name}",
85-
"token": token
85+
"token": token,
8686
}

test/test_checkout.py

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,6 @@
66
def test_checkout(repo_init_with_commit, git2cpp_path, tmp_path):
77
assert (tmp_path / "initial.txt").exists()
88

9-
default_branch = subprocess.run(
10-
["git", "branch", "--show-current"],
11-
capture_output=True,
12-
cwd=tmp_path,
13-
text=True,
14-
check=True,
15-
).stdout.strip() # TODO: use git2cpp when "branch --show-current" is implemented
16-
179
create_cmd = [git2cpp_path, "branch", "foregone"]
1810
p_create = subprocess.run(create_cmd, capture_output=True, cwd=tmp_path, text=True)
1911
assert p_create.returncode == 0
@@ -28,27 +20,19 @@ def test_checkout(repo_init_with_commit, git2cpp_path, tmp_path):
2820
branch_cmd = [git2cpp_path, "branch"]
2921
p_branch = subprocess.run(branch_cmd, capture_output=True, cwd=tmp_path, text=True)
3022
assert p_branch.returncode == 0
31-
assert p_branch.stdout == f"* foregone\n {default_branch}\n"
23+
assert p_branch.stdout == "* foregone\n main\n"
3224

33-
checkout_cmd[2] = default_branch
25+
checkout_cmd[2] = "main"
3426
p_checkout2 = subprocess.run(
3527
checkout_cmd, capture_output=True, cwd=tmp_path, text=True
3628
)
3729
assert p_checkout2.returncode == 0
38-
assert f"Switched to branch '{default_branch}'" in p_checkout2.stdout
30+
assert "Switched to branch 'main'" in p_checkout2.stdout
3931

4032

4133
def test_checkout_b(repo_init_with_commit, git2cpp_path, tmp_path):
4234
assert (tmp_path / "initial.txt").exists()
4335

44-
default_branch = subprocess.run(
45-
["git", "branch", "--show-current"],
46-
capture_output=True,
47-
cwd=tmp_path,
48-
text=True,
49-
check=True,
50-
).stdout.strip() # TODO: use git2cpp when "branch --show-current" is implemented
51-
5236
checkout_cmd = [git2cpp_path, "checkout", "-b", "foregone"]
5337
p_checkout = subprocess.run(
5438
checkout_cmd, capture_output=True, cwd=tmp_path, text=True
@@ -59,16 +43,16 @@ def test_checkout_b(repo_init_with_commit, git2cpp_path, tmp_path):
5943
branch_cmd = [git2cpp_path, "branch"]
6044
p_branch = subprocess.run(branch_cmd, capture_output=True, cwd=tmp_path, text=True)
6145
assert p_branch.returncode == 0
62-
assert p_branch.stdout == f"* foregone\n {default_branch}\n"
46+
assert p_branch.stdout == "* foregone\n main\n"
6347

6448
checkout_cmd.remove("-b")
65-
checkout_cmd[2] = default_branch
49+
checkout_cmd[2] = "main"
6650
p_checkout2 = subprocess.run(checkout_cmd, cwd=tmp_path, text=True)
6751
assert p_checkout2.returncode == 0
6852

6953
p_branch2 = subprocess.run(branch_cmd, capture_output=True, cwd=tmp_path, text=True)
7054
assert p_branch2.returncode == 0
71-
assert p_branch2.stdout == f" foregone\n* {default_branch}\n"
55+
assert p_branch2.stdout == " foregone\n* main\n"
7256

7357

7458
def test_checkout_B_force_create(repo_init_with_commit, git2cpp_path, tmp_path):
@@ -143,14 +127,6 @@ def test_checkout_refuses_overwrite(
143127
initial_file = tmp_path / "initial.txt"
144128
assert (initial_file).exists()
145129

146-
default_branch = subprocess.run(
147-
["git", "branch", "--show-current"],
148-
capture_output=True,
149-
cwd=tmp_path,
150-
text=True,
151-
check=True,
152-
).stdout.strip() # TODO: use git2cpp when "branch --show-current" is implemented
153-
154130
# Create a new branch and switch to it
155131
create_cmd = [git2cpp_path, "checkout", "-b", "newbranch"]
156132
p_create = subprocess.run(create_cmd, capture_output=True, cwd=tmp_path, text=True)
@@ -166,14 +142,14 @@ def test_checkout_refuses_overwrite(
166142
subprocess.run(commit_cmd, cwd=tmp_path, text=True)
167143

168144
# Switch back to default branch
169-
checkout_default_cmd = [git2cpp_path, "checkout", default_branch]
145+
checkout_default_cmd = [git2cpp_path, "checkout", "main"]
170146
p_default = subprocess.run(
171147
checkout_default_cmd, capture_output=True, cwd=tmp_path, text=True
172148
)
173149
assert p_default.returncode == 0
174150

175151
# Now modify initial.txt locally (unstaged) on default branch
176-
initial_file.write_text(f"Local modification on {default_branch}")
152+
initial_file.write_text("Local modification on main")
177153

178154
# Try to checkout newbranch
179155
checkout_cmd = [git2cpp_path, "checkout"]
@@ -201,7 +177,7 @@ def test_checkout_refuses_overwrite(
201177
p_branch = subprocess.run(
202178
branch_cmd, capture_output=True, cwd=tmp_path, text=True
203179
)
204-
assert f"* {default_branch}" in p_branch.stdout
180+
assert "* main" in p_branch.stdout
205181
else:
206182
assert "Switched to branch 'newbranch'" in p_checkout.stdout
207183

test/test_init.py

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ def test_init_in_directory(git2cpp_path, tmp_path):
77
assert list(tmp_path.iterdir()) == []
88

99
cmd = [git2cpp_path, "init", "--bare", str(tmp_path)]
10-
p = subprocess.run(cmd, capture_output=True)
10+
p = subprocess.run(cmd, capture_output=True, text=True)
1111
assert p.returncode == 0
12-
assert p.stdout == b""
13-
assert p.stderr == b""
12+
assert p.stdout.startswith("Initialized empty Git repository in ")
13+
assert p.stdout.strip().endswith("/")
1414

1515
assert sorted(map(lambda path: path.name, tmp_path.iterdir())) == [
1616
"HEAD",
@@ -31,10 +31,10 @@ def test_init_in_cwd(git2cpp_path, tmp_path, run_in_tmp_path):
3131
assert Path.cwd() == tmp_path
3232

3333
cmd = [git2cpp_path, "init", "--bare"]
34-
p = subprocess.run(cmd, capture_output=True)
34+
p = subprocess.run(cmd, capture_output=True, text=True)
3535
assert p.returncode == 0
36-
assert p.stdout == b""
37-
assert p.stderr == b""
36+
assert p.stdout.startswith("Initialized empty Git repository in ")
37+
assert p.stdout.strip().endswith("/")
3838

3939
assert sorted(map(lambda path: path.name, tmp_path.iterdir())) == [
4040
"HEAD",
@@ -52,12 +52,12 @@ def test_init_in_cwd(git2cpp_path, tmp_path, run_in_tmp_path):
5252
def test_init_not_bare(git2cpp_path, tmp_path):
5353
# tmp_path exists and is empty.
5454
assert list(tmp_path.iterdir()) == []
55-
55+
assert not (tmp_path / ".git" / "HEAD").exists()
5656
cmd = [git2cpp_path, "init", "."]
57-
p = subprocess.run(cmd, capture_output=True, cwd=tmp_path)
57+
p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True)
5858
assert p.returncode == 0
59-
assert p.stdout == b""
60-
assert p.stderr == b""
59+
assert p.stdout.startswith("Initialized empty Git repository in ")
60+
assert p.stdout.strip().endswith(".git/")
6161

6262
# Directory contains just .git directory.
6363
assert sorted(map(lambda path: path.name, tmp_path.iterdir())) == [".git"]
@@ -79,6 +79,19 @@ def test_init_not_bare(git2cpp_path, tmp_path):
7979
assert b"does not have any commits yet" in p.stdout
8080

8181

82+
def test_init_reinitializes_existing_repo_message(git2cpp_path, tmp_path):
83+
cmd = [git2cpp_path, "init", "."]
84+
85+
p1 = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True)
86+
assert p1.returncode == 0
87+
88+
p2 = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True)
89+
assert p2.returncode == 0
90+
assert p2.stderr == ""
91+
assert p2.stdout.startswith("Reinitialized existing Git repository in ")
92+
assert p2.stdout.strip().endswith("/.git/")
93+
94+
8295
def test_error_on_unknown_option(git2cpp_path):
8396
cmd = [git2cpp_path, "init", "--unknown"]
8497
p = subprocess.run(cmd, capture_output=True)
@@ -93,3 +106,52 @@ def test_error_on_repeated_directory(git2cpp_path):
93106
assert p.returncode == 109
94107
assert p.stdout == b""
95108
assert p.stderr.startswith(b"The following argument was not expected: def")
109+
110+
111+
def test_init_creates_missing_parent_directories(git2cpp_path, tmp_path):
112+
# Parent "does-not-exist" does not exist yet.
113+
repo_dir = tmp_path / "does-not-exist" / "repo"
114+
assert not repo_dir.parent.exists()
115+
116+
cmd = [git2cpp_path, "init", "--bare", str(repo_dir)]
117+
p = subprocess.run(cmd, capture_output=True, text=True)
118+
119+
assert p.returncode == 0
120+
assert p.stdout.startswith("Initialized empty Git repository in ")
121+
assert p.stdout.strip().endswith("/")
122+
assert ".git" not in p.stdout
123+
124+
assert repo_dir.exists()
125+
assert sorted(p.name for p in repo_dir.iterdir()) == [
126+
"HEAD",
127+
"config",
128+
"description",
129+
"hooks",
130+
"info",
131+
"objects",
132+
"refs",
133+
]
134+
135+
136+
def test_init_initial_branch_non_bare(git2cpp_path, tmp_path):
137+
cmd = [git2cpp_path, "init", "-b", "main", "."]
138+
p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True)
139+
assert p.returncode == 0
140+
assert p.stderr == ""
141+
assert p.stdout.startswith("Initialized empty Git repository in ")
142+
assert p.stdout.strip().endswith("/.git/")
143+
144+
head = (tmp_path / ".git" / "HEAD").read_text()
145+
assert "refs/heads/main" in head
146+
147+
148+
def test_init_initial_branch_bare(git2cpp_path, tmp_path):
149+
cmd = [git2cpp_path, "init", "--bare", "-b", "main", str(tmp_path)]
150+
p = subprocess.run(cmd, capture_output=True, text=True)
151+
assert p.returncode == 0
152+
assert p.stderr == ""
153+
assert p.stdout.startswith("Initialized empty Git repository in ")
154+
assert p.stdout.strip().endswith("/")
155+
156+
head = (tmp_path / "HEAD").read_text()
157+
assert "refs/heads/main" in head

0 commit comments

Comments
 (0)