Skip to content

Commit cb4839e

Browse files
wip (#56)
1 parent 2d39fba commit cb4839e

File tree

11 files changed

+233
-54
lines changed

11 files changed

+233
-54
lines changed

include/parsers/CheckoutParser.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#ifndef CHECKOUTPARSER_H
2+
#define CHECKOUTPARSER_H
3+
4+
#include "tclap/CmdLine.h"
5+
#include <string>
6+
#include <vector>
7+
8+
class CheckoutParser {
9+
public:
10+
static CheckoutParser &get();
11+
void parse(std::vector<std::string> &args);
12+
bool isCommitSet() const;
13+
std::string getCommit();
14+
bool isCreateNewBranch() const;
15+
std::string getStartPoint();
16+
bool isStartPointSet() const;
17+
18+
private:
19+
// hides constructors to avoid accidental instantiation
20+
CheckoutParser();
21+
CheckoutParser(const CheckoutParser &) = delete;
22+
CheckoutParser &operator=(const CheckoutParser &) = delete;
23+
CheckoutParser(CheckoutParser &&) = delete;
24+
CheckoutParser &operator=(CheckoutParser &&) = delete;
25+
~CheckoutParser() = default;
26+
// data members
27+
TCLAP::CmdLine cmd;
28+
TCLAP::SwitchArg branchArg;
29+
TCLAP::UnlabeledValueArg<std::string> commitArg;
30+
TCLAP::UnlabeledValueArg<std::string> startPointArg;
31+
};
32+
33+
#endif // CHECKOUTPARSER_H

include/repository.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ class GitRepository {
2424
fs::path dir(const fs::path &path, bool mkdir = false);
2525
fs::path file(fs::path &gitdir, bool mkdir = false);
2626

27+
fs::path branch_path(const std::string &branch);
28+
29+
/* Checks if refs exists */
30+
bool has_branch(const std::string &branch);
31+
2732
protected:
2833
fs::path worktree;
2934
fs::path gitdir;

src/commands/checkout.cpp

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,64 @@
11
#include "commands/checkout.h"
2-
#include <iostream>
2+
#include <stdexcept>
33

44
#include "commit.h"
55
#include "object.h"
6+
#include "parsers/CheckoutParser.h"
67
#include "repository.h"
7-
#include "tclap/CmdLine.h"
88
#include "tree.h"
9+
#include "util.h"
910

1011
namespace commands {
1112
void checkout(std::vector<std::string> &args) {
12-
TCLAP::CmdLine cmd("checkout", ' ', "0.1");
13-
1413
// defines arguments
15-
TCLAP::UnlabeledValueArg<std::string> commitArg(
16-
"commit", "hash of the commit to checkout to", true, "HEAD",
17-
"commit hash");
18-
19-
cmd.add(commitArg);
20-
cmd.parse(args);
21-
std::string &hash = commitArg.getValue();
14+
CheckoutParser &parser = CheckoutParser::get();
15+
parser.parse(args);
2216

17+
bool isCreateNewBranch = parser.isCreateNewBranch();
2318
// process args
24-
try {
25-
std::optional<GitRepository> repo = GitRepository::find();
26-
if (repo) {
27-
GitCommit *commit = dynamic_cast<GitCommit *>(
28-
GitObject::read(*repo, GitObject::find(*repo, hash)));
29-
if (!commit) {
30-
throw std::runtime_error("Invalid commit object: " + hash);
31-
}
32-
GitTree *tree =
33-
dynamic_cast<GitTree *>(GitObject::read(*repo, commit->get_tree()));
34-
if (!tree) {
35-
throw std::runtime_error("Invalid tree object: " + commit->get_tree());
36-
}
37-
std::string orig_head = GitObject::find(*repo, "HEAD");
38-
GitCommit *head =
39-
dynamic_cast<GitCommit *>(GitObject::read(*repo, orig_head));
40-
GitTree *treeObj =
41-
dynamic_cast<GitTree *>(GitObject::read(*repo, head->get_tree()));
42-
GitTree::instantiate_tree(tree, treeObj, repo->worktree_path(""));
43-
// TODO: fix bug in this one
19+
std::optional<GitRepository> repo = GitRepository::find();
20+
if (!repo) {
21+
throw std::runtime_error("No git repository found");
22+
}
23+
// create a new branch with the name of the commit.
24+
if (isCreateNewBranch) {
25+
std::string branchName = parser.getCommit();
26+
// creating at refs/heads/{branch_name}. check if the branch name exists.
27+
if (repo->has_branch(branchName)) {
28+
throw std::runtime_error("fatal: a branch named '" + branchName +
29+
"' already exists.");
30+
}
31+
bool isStartPointSet = parser.isStartPointSet();
32+
std::string hash = GitObject::find(
33+
*repo, isStartPointSet ? parser.getStartPoint() : "HEAD");
34+
create_file(repo->branch_path(branchName), hash);
35+
repo->update_head("ref: refs/heads/" + branchName);
36+
} else {
37+
std::string hash = parser.getCommit();
38+
GitCommit *commit = dynamic_cast<GitCommit *>(
39+
GitObject::read(*repo, GitObject::find(*repo, hash)));
40+
if (!commit) {
41+
throw std::runtime_error("Invalid commit object: " + hash);
42+
}
43+
GitTree *tree =
44+
dynamic_cast<GitTree *>(GitObject::read(*repo, commit->get_tree()));
45+
if (!tree) {
46+
throw std::runtime_error("Invalid tree object: " + commit->get_tree());
47+
}
48+
std::string orig_head = GitObject::find(*repo, "HEAD");
49+
GitCommit *head =
50+
dynamic_cast<GitCommit *>(GitObject::read(*repo, orig_head));
51+
GitTree *treeObj =
52+
dynamic_cast<GitTree *>(GitObject::read(*repo, head->get_tree()));
53+
GitTree::instantiate_tree(tree, treeObj, repo->worktree_path(""));
54+
// TODO: fix bug in this one
55+
if (fs::exists(repo->repo_path(get_commit_path(hash)))) {
4456
repo->update_head(hash);
57+
} else if (fs::exists(repo->repo_path("refs/tags/" + hash))) {
58+
repo->update_head("ref: refs/tags/" + hash);
59+
} else {
60+
repo->update_head("ref: refs/heads/" + hash);
4561
}
46-
} catch (std::runtime_error &err) {
47-
std::cerr << err.what() << "\n";
4862
}
4963
}
5064
} // namespace commands

src/commit.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
#include "commit.h"
22
#include "repository.h"
3+
#include <array>
34
#include <sstream>
5+
#include <string>
6+
#include <vector>
7+
48
GitCommit::GitCommit(const std::string &data, const std::string &sha)
59
: GitObject("commit") {
610
this->deserialise(data);
@@ -85,4 +89,4 @@ bool GitCommit::has_parent() {
8589

8690
std::string GitCommit::get_parent() { return this->keyValuePairs["parent"]; }
8791

88-
std::string GitCommit::get_tree() { return this->keyValuePairs["tree"]; }
92+
std::string GitCommit::get_tree() { return this->keyValuePairs["tree"]; }

src/parsers/CheckoutParser.cpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#include "parsers/CheckoutParser.h"
2+
#include <string>
3+
#include <vector>
4+
5+
void CheckoutParser::parse(std::vector<std::string> &args) {
6+
cmd.reset();
7+
cmd.parse(args);
8+
}
9+
10+
bool CheckoutParser::isCommitSet() const { return commitArg.isSet(); }
11+
std::string CheckoutParser::getCommit() { return commitArg.getValue(); }
12+
13+
bool CheckoutParser::isCreateNewBranch() const { return branchArg.getValue(); }
14+
15+
std::string CheckoutParser::getStartPoint() { return startPointArg.getValue(); }
16+
bool CheckoutParser::isStartPointSet() const { return startPointArg.isSet(); }
17+
18+
CheckoutParser &CheckoutParser::get() {
19+
static CheckoutParser instance;
20+
return instance;
21+
}
22+
23+
CheckoutParser::CheckoutParser()
24+
: cmd("checkout", ' ', "0.2"),
25+
branchArg("b", "branch",
26+
"whether to create a new branch from the given point", false),
27+
commitArg("commit", "hash of the commit to checkout to", true, "HEAD",
28+
"commit hash"),
29+
startPointArg("start-point",
30+
"If a new branch is created with a branch name specified, "
31+
"this argument "
32+
"will be the starting point of the branch",
33+
false, "HEAD", "commit hash") {
34+
cmd.ignoreUnmatched(true);
35+
cmd.add(branchArg);
36+
cmd.add(commitArg);
37+
cmd.add(startPointArg);
38+
}

src/repository.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,12 @@ std::optional<GitRepository> GitRepository::find(const fs::path &path,
112112
void GitRepository::update_head(const std::string &new_head) {
113113
create_file(gitdir / "HEAD", new_head + "\n");
114114
}
115+
116+
bool GitRepository::has_branch(const std::string &branch) {
117+
fs::path branchPath = gitdir / "refs/heads" / branch;
118+
return fs::exists(branchPath);
119+
}
120+
121+
fs::path GitRepository::branch_path(const std::string &branch) {
122+
return gitdir / "refs/heads" / branch;
123+
}

tests/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
add_executable(tests tests.cpp repository_t.cpp tag_t.cpp object_t.cpp utils/gitreposetup.cpp)
1+
add_executable(tests tests.cpp repository_t.cpp tag_t.cpp object_t.cpp checkout_t.cpp utils/gitreposetup.cpp)
22
target_include_directories(tests PUBLIC ../ext)
33

44
target_link_libraries(tests PUBLIC boost_libraries repository commands)

tests/checkout_t.cpp

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#include "catch2/catch.hpp"
2+
#include "commands/checkout.h"
3+
#include "repository.h"
4+
#include "utils/gitreposetup.h"
5+
#include <filesystem>
6+
7+
// Helper function to read the contents of a file
8+
9+
TEST_CASE("checkout command", "[checkout]") {
10+
GitRepoSetup gitRepoSetup;
11+
SECTION("checkout existing branch", "tag name only") {
12+
std::vector<std::string> args({"checkout", "test_branch"});
13+
commands::checkout(args);
14+
15+
REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true));
16+
REQUIRE(GitRepoSetup::get_file_contents(".git/HEAD") ==
17+
"ref: refs/heads/test_branch");
18+
}
19+
SECTION("checkout existing commit", "tag name with commit") {
20+
std::vector<std::string> args({"checkout", SECOND_COMMIT});
21+
commands::checkout(args);
22+
23+
REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true));
24+
// check file contents match second commit
25+
REQUIRE(GitRepoSetup::get_file_contents(".git/HEAD") == SECOND_COMMIT);
26+
}
27+
SECTION("checkout existing tag", "tag name with commit") {
28+
std::vector<std::string> args({"checkout", SECOND_COMMIT});
29+
commands::checkout(args);
30+
31+
REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true));
32+
// check file contents match second commit
33+
REQUIRE(GitRepoSetup::get_file_contents(".git/HEAD") == SECOND_COMMIT);
34+
}
35+
SECTION("checkout new branch that didn't exist before") {
36+
std::vector<std::string> args({"checkout", "-b", "new_branch"});
37+
commands::checkout(args);
38+
39+
REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true));
40+
// check that the new branch was created
41+
// TODO check that file contents of the worktree matches the commit
42+
43+
REQUIRE(GitRepoSetup::get_file_contents(".git/HEAD") ==
44+
"ref: refs/heads/new_branch");
45+
REQUIRE(GitRepoSetup::get_file_contents(".git/refs/heads/new_branch") ==
46+
SECOND_COMMIT);
47+
}
48+
SECTION("checkout new branch that didn't exist before with a specified start "
49+
"point") {
50+
std::vector<std::string> args(
51+
{"checkout", "-b", "new_branch", FIRST_COMMIT});
52+
commands::checkout(args);
53+
REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true));
54+
// check that the new branch was created
55+
// TODO check that file contents of the worktree matches the commit
56+
REQUIRE(GitRepoSetup::get_file_contents(".git/HEAD") ==
57+
"ref: refs/heads/new_branch");
58+
REQUIRE(GitRepoSetup::get_file_contents(".git/refs/heads/new_branch") ==
59+
FIRST_COMMIT);
60+
}
61+
}
62+
63+
TEST_CASE("checkout with errors", "[checkout errors]") {
64+
GitRepoSetup gitRepoSetup;
65+
SECTION("branch already exists") {
66+
std::vector<std::string> args({"checkout", "-b", "test_branch"});
67+
68+
REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true));
69+
REQUIRE_THROWS_WITH(commands::checkout(args),
70+
"fatal: a branch named 'test_branch' already exists.");
71+
}
72+
}

tests/tag_t.cpp

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,11 @@
1-
#include "boost/algorithm/string/trim.hpp"
21
#include "catch2/catch.hpp"
32
#include "commands/tag.h"
43
#include "repository.h"
54
#include "utils/gitreposetup.h"
65
#include <filesystem>
7-
#include <fstream>
86

97
namespace fs = std::filesystem;
108

11-
// Helper function to read the contents of a file
12-
std::string file_contents(const fs::path &path) {
13-
std::ifstream file(path);
14-
if (file.is_open()) {
15-
std::string contents((std::istreambuf_iterator<char>(file)),
16-
std::istreambuf_iterator<char>());
17-
boost::trim_right(contents);
18-
file.close();
19-
return contents;
20-
} else {
21-
throw std::runtime_error("Could not open file: " + path.string());
22-
}
23-
}
24-
259
TEST_CASE("tag command", "[tag]") {
2610
GitRepoSetup gitRepoSetup;
2711
SECTION("Valid git tag command - tag name only", "tag name only") {
@@ -30,7 +14,8 @@ TEST_CASE("tag command", "[tag]") {
3014

3115
REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true));
3216
REQUIRE(fs::exists(".git/refs/tags/v1.0"));
33-
REQUIRE(file_contents(".git/refs/tags/v1.0") == SECOND_COMMIT);
17+
REQUIRE(GitRepoSetup::get_file_contents(".git/refs/tags/v1.0") ==
18+
SECOND_COMMIT);
3419
}
3520
SECTION("Valid git tag command - tag name with commit",
3621
"tag name with commit") {
@@ -39,7 +24,8 @@ TEST_CASE("tag command", "[tag]") {
3924

4025
REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true));
4126
REQUIRE(fs::exists(".git/refs/tags/v1.0"));
42-
REQUIRE(file_contents(".git/refs/tags/v1.0") == SECOND_COMMIT);
27+
REQUIRE(GitRepoSetup::get_file_contents(".git/refs/tags/v1.0") ==
28+
SECOND_COMMIT);
4329
}
4430
}
4531

@@ -51,7 +37,8 @@ TEST_CASE("tag with errors", "[tag errors]") {
5137

5238
REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true));
5339
REQUIRE(fs::exists(".git/refs/tags/v1.0"));
54-
REQUIRE(file_contents(".git/refs/tags/v1.0") == SECOND_COMMIT);
40+
REQUIRE(GitRepoSetup::get_file_contents(".git/refs/tags/v1.0") ==
41+
SECOND_COMMIT);
5542

5643
std::vector<std::string> args2({"tag", "v1.0"});
5744
REQUIRE_THROWS_WITH(commands::tag(args2), "tag 'v1.0' already exists");

tests/utils/gitreposetup.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include "gitreposetup.h"
2+
#include "boost/algorithm/string/trim.hpp"
23
#include <filesystem>
4+
#include <fstream>
35
/**
46
Uses RAII to manage the setup and teardown of a sample Git repo
57
*/
@@ -26,3 +28,16 @@ void GitRepoSetup::teardown() {
2628
fs::remove_all(VALID_GIT_PATH);
2729
fs::current_path(OLD_CWD);
2830
}
31+
32+
std::string GitRepoSetup::get_file_contents(const fs::path &path) {
33+
std::ifstream file(path);
34+
if (file.is_open()) {
35+
std::string contents((std::istreambuf_iterator<char>(file)),
36+
std::istreambuf_iterator<char>());
37+
boost::trim_right(contents);
38+
file.close();
39+
return contents;
40+
} else {
41+
throw std::runtime_error("Could not open file: " + path.string());
42+
}
43+
}

tests/utils/gitreposetup.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class GitRepoSetup {
2121

2222
void setup();
2323
void teardown();
24+
25+
static std::string get_file_contents(const fs::path &path);
2426
};
2527

2628
#endif // GITREPOSETUP_H

0 commit comments

Comments
 (0)