Skip to content

Allow creation of git branches via checkout -b #56

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions include/parsers/CheckoutParser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#ifndef CHECKOUTPARSER_H
#define CHECKOUTPARSER_H

#include "tclap/CmdLine.h"
#include <string>
#include <vector>

class CheckoutParser {
public:
static CheckoutParser &get();
void parse(std::vector<std::string> &args);
bool isCommitSet() const;
std::string getCommit();
bool isCreateNewBranch() const;
std::string getStartPoint();
bool isStartPointSet() const;

private:
// hides constructors to avoid accidental instantiation
CheckoutParser();
CheckoutParser(const CheckoutParser &) = delete;
CheckoutParser &operator=(const CheckoutParser &) = delete;
CheckoutParser(CheckoutParser &&) = delete;
CheckoutParser &operator=(CheckoutParser &&) = delete;
~CheckoutParser() = default;
// data members
TCLAP::CmdLine cmd;
TCLAP::SwitchArg branchArg;
TCLAP::UnlabeledValueArg<std::string> commitArg;
TCLAP::UnlabeledValueArg<std::string> startPointArg;
};

#endif // CHECKOUTPARSER_H
5 changes: 5 additions & 0 deletions include/repository.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ class GitRepository {
fs::path dir(const fs::path &path, bool mkdir = false);
fs::path file(fs::path &gitdir, bool mkdir = false);

fs::path branch_path(const std::string &branch);

/* Checks if refs exists */
bool has_branch(const std::string &branch);

protected:
fs::path worktree;
fs::path gitdir;
Expand Down
80 changes: 47 additions & 33 deletions src/commands/checkout.cpp
Original file line number Diff line number Diff line change
@@ -1,50 +1,64 @@
#include "commands/checkout.h"
#include <iostream>
#include <stdexcept>

#include "commit.h"
#include "object.h"
#include "parsers/CheckoutParser.h"
#include "repository.h"
#include "tclap/CmdLine.h"
#include "tree.h"
#include "util.h"

namespace commands {
void checkout(std::vector<std::string> &args) {
TCLAP::CmdLine cmd("checkout", ' ', "0.1");

// defines arguments
TCLAP::UnlabeledValueArg<std::string> commitArg(
"commit", "hash of the commit to checkout to", true, "HEAD",
"commit hash");

cmd.add(commitArg);
cmd.parse(args);
std::string &hash = commitArg.getValue();
CheckoutParser &parser = CheckoutParser::get();
parser.parse(args);

bool isCreateNewBranch = parser.isCreateNewBranch();
// process args
try {
std::optional<GitRepository> repo = GitRepository::find();
if (repo) {
GitCommit *commit = dynamic_cast<GitCommit *>(
GitObject::read(*repo, GitObject::find(*repo, hash)));
if (!commit) {
throw std::runtime_error("Invalid commit object: " + hash);
}
GitTree *tree =
dynamic_cast<GitTree *>(GitObject::read(*repo, commit->get_tree()));
if (!tree) {
throw std::runtime_error("Invalid tree object: " + commit->get_tree());
}
std::string orig_head = GitObject::find(*repo, "HEAD");
GitCommit *head =
dynamic_cast<GitCommit *>(GitObject::read(*repo, orig_head));
GitTree *treeObj =
dynamic_cast<GitTree *>(GitObject::read(*repo, head->get_tree()));
GitTree::instantiate_tree(tree, treeObj, repo->worktree_path(""));
// TODO: fix bug in this one
std::optional<GitRepository> repo = GitRepository::find();
if (!repo) {
throw std::runtime_error("No git repository found");
}
// create a new branch with the name of the commit.
if (isCreateNewBranch) {
std::string branchName = parser.getCommit();
// creating at refs/heads/{branch_name}. check if the branch name exists.
if (repo->has_branch(branchName)) {
throw std::runtime_error("fatal: a branch named '" + branchName +
"' already exists.");
}
bool isStartPointSet = parser.isStartPointSet();
std::string hash = GitObject::find(
*repo, isStartPointSet ? parser.getStartPoint() : "HEAD");
create_file(repo->branch_path(branchName), hash);
repo->update_head("ref: refs/heads/" + branchName);
} else {
std::string hash = parser.getCommit();
GitCommit *commit = dynamic_cast<GitCommit *>(
GitObject::read(*repo, GitObject::find(*repo, hash)));
if (!commit) {
throw std::runtime_error("Invalid commit object: " + hash);
}
GitTree *tree =
dynamic_cast<GitTree *>(GitObject::read(*repo, commit->get_tree()));
if (!tree) {
throw std::runtime_error("Invalid tree object: " + commit->get_tree());
}
std::string orig_head = GitObject::find(*repo, "HEAD");
GitCommit *head =
dynamic_cast<GitCommit *>(GitObject::read(*repo, orig_head));
GitTree *treeObj =
dynamic_cast<GitTree *>(GitObject::read(*repo, head->get_tree()));
GitTree::instantiate_tree(tree, treeObj, repo->worktree_path(""));
// TODO: fix bug in this one
if (fs::exists(repo->repo_path(get_commit_path(hash)))) {
repo->update_head(hash);
} else if (fs::exists(repo->repo_path("refs/tags/" + hash))) {
repo->update_head("ref: refs/tags/" + hash);
} else {
repo->update_head("ref: refs/heads/" + hash);
}
} catch (std::runtime_error &err) {
std::cerr << err.what() << "\n";
}
}
} // namespace commands
6 changes: 5 additions & 1 deletion src/commit.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#include "commit.h"
#include "repository.h"
#include <array>
#include <sstream>
#include <string>
#include <vector>

GitCommit::GitCommit(const std::string &data, const std::string &sha)
: GitObject("commit") {
this->deserialise(data);
Expand Down Expand Up @@ -85,4 +89,4 @@ bool GitCommit::has_parent() {

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

std::string GitCommit::get_tree() { return this->keyValuePairs["tree"]; }
std::string GitCommit::get_tree() { return this->keyValuePairs["tree"]; }
38 changes: 38 additions & 0 deletions src/parsers/CheckoutParser.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#include "parsers/CheckoutParser.h"
#include <string>
#include <vector>

void CheckoutParser::parse(std::vector<std::string> &args) {
cmd.reset();
cmd.parse(args);
}

bool CheckoutParser::isCommitSet() const { return commitArg.isSet(); }
std::string CheckoutParser::getCommit() { return commitArg.getValue(); }

bool CheckoutParser::isCreateNewBranch() const { return branchArg.getValue(); }

std::string CheckoutParser::getStartPoint() { return startPointArg.getValue(); }
bool CheckoutParser::isStartPointSet() const { return startPointArg.isSet(); }

CheckoutParser &CheckoutParser::get() {
static CheckoutParser instance;
return instance;
}

CheckoutParser::CheckoutParser()
: cmd("checkout", ' ', "0.2"),
branchArg("b", "branch",
"whether to create a new branch from the given point", false),
commitArg("commit", "hash of the commit to checkout to", true, "HEAD",
"commit hash"),
startPointArg("start-point",
"If a new branch is created with a branch name specified, "
"this argument "
"will be the starting point of the branch",
false, "HEAD", "commit hash") {
cmd.ignoreUnmatched(true);
cmd.add(branchArg);
cmd.add(commitArg);
cmd.add(startPointArg);
}
9 changes: 9 additions & 0 deletions src/repository.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,12 @@ std::optional<GitRepository> GitRepository::find(const fs::path &path,
void GitRepository::update_head(const std::string &new_head) {
create_file(gitdir / "HEAD", new_head + "\n");
}

bool GitRepository::has_branch(const std::string &branch) {
fs::path branchPath = gitdir / "refs/heads" / branch;
return fs::exists(branchPath);
}

fs::path GitRepository::branch_path(const std::string &branch) {
return gitdir / "refs/heads" / branch;
}
2 changes: 1 addition & 1 deletion tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
add_executable(tests tests.cpp repository_t.cpp tag_t.cpp object_t.cpp utils/gitreposetup.cpp)
add_executable(tests tests.cpp repository_t.cpp tag_t.cpp object_t.cpp checkout_t.cpp utils/gitreposetup.cpp)
target_include_directories(tests PUBLIC ../ext)

target_link_libraries(tests PUBLIC boost_libraries repository commands)
Expand Down
72 changes: 72 additions & 0 deletions tests/checkout_t.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#include "catch2/catch.hpp"
#include "commands/checkout.h"
#include "repository.h"
#include "utils/gitreposetup.h"
#include <filesystem>

// Helper function to read the contents of a file

TEST_CASE("checkout command", "[checkout]") {
GitRepoSetup gitRepoSetup;
SECTION("checkout existing branch", "tag name only") {
std::vector<std::string> args({"checkout", "test_branch"});
commands::checkout(args);

REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true));
REQUIRE(GitRepoSetup::get_file_contents(".git/HEAD") ==
"ref: refs/heads/test_branch");
}
SECTION("checkout existing commit", "tag name with commit") {
std::vector<std::string> args({"checkout", SECOND_COMMIT});
commands::checkout(args);

REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true));
// check file contents match second commit
REQUIRE(GitRepoSetup::get_file_contents(".git/HEAD") == SECOND_COMMIT);
}
SECTION("checkout existing tag", "tag name with commit") {
std::vector<std::string> args({"checkout", SECOND_COMMIT});
commands::checkout(args);

REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true));
// check file contents match second commit
REQUIRE(GitRepoSetup::get_file_contents(".git/HEAD") == SECOND_COMMIT);
}
SECTION("checkout new branch that didn't exist before") {
std::vector<std::string> args({"checkout", "-b", "new_branch"});
commands::checkout(args);

REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true));
// check that the new branch was created
// TODO check that file contents of the worktree matches the commit

REQUIRE(GitRepoSetup::get_file_contents(".git/HEAD") ==
"ref: refs/heads/new_branch");
REQUIRE(GitRepoSetup::get_file_contents(".git/refs/heads/new_branch") ==
SECOND_COMMIT);
}
SECTION("checkout new branch that didn't exist before with a specified start "
"point") {
std::vector<std::string> args(
{"checkout", "-b", "new_branch", FIRST_COMMIT});
commands::checkout(args);
REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true));
// check that the new branch was created
// TODO check that file contents of the worktree matches the commit
REQUIRE(GitRepoSetup::get_file_contents(".git/HEAD") ==
"ref: refs/heads/new_branch");
REQUIRE(GitRepoSetup::get_file_contents(".git/refs/heads/new_branch") ==
FIRST_COMMIT);
}
}

TEST_CASE("checkout with errors", "[checkout errors]") {
GitRepoSetup gitRepoSetup;
SECTION("branch already exists") {
std::vector<std::string> args({"checkout", "-b", "test_branch"});

REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true));
REQUIRE_THROWS_WITH(commands::checkout(args),
"fatal: a branch named 'test_branch' already exists.");
}
}
25 changes: 6 additions & 19 deletions tests/tag_t.cpp
Original file line number Diff line number Diff line change
@@ -1,27 +1,11 @@
#include "boost/algorithm/string/trim.hpp"
#include "catch2/catch.hpp"
#include "commands/tag.h"
#include "repository.h"
#include "utils/gitreposetup.h"
#include <filesystem>
#include <fstream>

namespace fs = std::filesystem;

// Helper function to read the contents of a file
std::string file_contents(const fs::path &path) {
std::ifstream file(path);
if (file.is_open()) {
std::string contents((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
boost::trim_right(contents);
file.close();
return contents;
} else {
throw std::runtime_error("Could not open file: " + path.string());
}
}

TEST_CASE("tag command", "[tag]") {
GitRepoSetup gitRepoSetup;
SECTION("Valid git tag command - tag name only", "tag name only") {
Expand All @@ -30,7 +14,8 @@ TEST_CASE("tag command", "[tag]") {

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

REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true));
REQUIRE(fs::exists(".git/refs/tags/v1.0"));
REQUIRE(file_contents(".git/refs/tags/v1.0") == SECOND_COMMIT);
REQUIRE(GitRepoSetup::get_file_contents(".git/refs/tags/v1.0") ==
SECOND_COMMIT);
}
}

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

REQUIRE_NOTHROW(GitRepository(VALID_GIT_PATH, true));
REQUIRE(fs::exists(".git/refs/tags/v1.0"));
REQUIRE(file_contents(".git/refs/tags/v1.0") == SECOND_COMMIT);
REQUIRE(GitRepoSetup::get_file_contents(".git/refs/tags/v1.0") ==
SECOND_COMMIT);

std::vector<std::string> args2({"tag", "v1.0"});
REQUIRE_THROWS_WITH(commands::tag(args2), "tag 'v1.0' already exists");
Expand Down
15 changes: 15 additions & 0 deletions tests/utils/gitreposetup.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "gitreposetup.h"
#include "boost/algorithm/string/trim.hpp"
#include <filesystem>
#include <fstream>
/**
Uses RAII to manage the setup and teardown of a sample Git repo
*/
Expand All @@ -26,3 +28,16 @@ void GitRepoSetup::teardown() {
fs::remove_all(VALID_GIT_PATH);
fs::current_path(OLD_CWD);
}

std::string GitRepoSetup::get_file_contents(const fs::path &path) {
std::ifstream file(path);
if (file.is_open()) {
std::string contents((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
boost::trim_right(contents);
file.close();
return contents;
} else {
throw std::runtime_error("Could not open file: " + path.string());
}
}
2 changes: 2 additions & 0 deletions tests/utils/gitreposetup.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class GitRepoSetup {

void setup();
void teardown();

static std::string get_file_contents(const fs::path &path);
};

#endif // GITREPOSETUP_H
Loading