Skip to content
Open
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
9 changes: 9 additions & 0 deletions .cursor/rules/project-instructions.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
alwaysApply: true
---

This project is my implementation of git: git_d. This is purely a self-learning project in which I am trying to implement it the best way I think it should be implemented.

I DONT WANT AI CODING IN THIS PROJECT AT ALL. As this is a project that is for self learning - I want to be the one implementing all of the code in the project. This means no cursor suggestions AT ALL unless I specifically ask for them.

When I want to consult with you on architectural topics - I want you to reply on the real implementation of git in your answers. But only offer architectural design suggestions and explanations with pros and cons regarding these suggestions. DO NOT add code unless specifically requested.
127 changes: 127 additions & 0 deletions src/ConfigDiskManager.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#include "CryptoUtils.h"
#include "ConfigDiskManager.h"
#include <filesystem>
#include <iterator>
#include <stdexcept>
#include <string>
#include <system_error>
#include <fstream>
#include <iostream>
#include <cassert>

static void touch_file(const std::filesystem::path& p) {
std::ofstream ofs(p, std::ios::app);
if (!ofs) throw std::runtime_error("Failed creating the file: " + p.string());
}

void ConfigDiskManager::initRepoDisk() {
if (std::filesystem::exists(this->configPath)) {
std::cout << "repository already initialized, .git_d already exists" << std::endl;
readIndexFiles();
return;
}
std::error_code ec;
std::filesystem::create_directory(this->configPath, ec);
if (ec) throw std::runtime_error("Error in creating .git_d");
std::filesystem::create_directory(this->configPath / "objects", ec);
if (ec) throw std::runtime_error("Error in creating the objects dir");
touch_file(this->configPath / "config");
touch_file(this->configPath / "index");
touch_file(this->configPath / "HEAD");
}

ConfigDiskManager::ConfigDiskManager(const std::filesystem::path& repoPath) : configPath(repoPath / ".git_d") {}

ConfigDiskManager& ConfigDiskManager::instance(const std::filesystem::path& repoPath) {
static ConfigDiskManager inst(repoPath);
return inst;
}

void ConfigDiskManager::stageFile(const std::filesystem::path& p) {
std::string fileBlob = this->createBlob(p);
std::string fileHash = CryptoUtils::sha256_hex(fileBlob);
this->saveBlobToObjects(fileBlob, fileHash);
// TODO: should make sure that here or in main.cpp the path is always relative to the root repo
this->saveBlobHashToIndex(fileHash, p);
}

std::string ConfigDiskManager::createBlob(const std::filesystem::path& p) const {
std::ifstream file(p, std::ios::binary);
if (!file) throw std::runtime_error("failed to read the file!");

std::string content((std::istream_iterator<char>(file)), std::istream_iterator<char>());

std::string blob = "blob" + std::to_string(content.size()) + '\0' + content;

// TODO: should compress the blob before returning it

return blob;
}

void ConfigDiskManager::saveBlobToObjects(const std::string& blob, std::string hash) const {
std::filesystem::path objectsPath = this->configPath / "objects";
assert(std::filesystem::exists(objectsPath));
// save the two chars of the hash as a separate folder - to decrease the amount of objects
std::string twoChars = hash.substr(0, 2);
hash.erase(0,2);
std::filesystem::path blobDir = objectsPath / twoChars;
if (!std::filesystem::exists(blobDir)) std::filesystem::create_directory(blobDir);
std::filesystem::path blobPath = blobDir / hash;
std::ofstream outFile(blobPath, std::ios::binary);
if (!outFile) {
throw std::runtime_error("Failed to create blob file at " + blobPath.string());
}
outFile.write(blob.data(), blob.size());
outFile.close();
}

void ConfigDiskManager::readIndexFiles() {
assert(indexFiles.empty());
assert(std::filesystem::exists(this->configPath));
std::filesystem::path indexPath = this->configPath / "index";
std::ifstream indexFile(indexPath, std::ios::binary);
if (!indexFile) {
throw std::runtime_error("Failed to open index file: " + indexPath.string());
}
std::string line;
std::string::size_type pos;
while (std::getline(indexFile, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back(); // for windows devices
if (line.empty()) continue;
pos = line.find(" ");
if (pos == std::string::npos) {
throw std::runtime_error("Failed to parse the index file!");
}
std::string pathRelative(line.substr(0, pos));
std::string latestFileHash(line.substr(pos + 1));
IndexFile indexFile(pathRelative, latestFileHash);
this->indexFiles.emplace(pathRelative, indexFile);
}
}

void ConfigDiskManager::addToIndexFiles(const std::string& pathRelative, const std::string& latestFileHash) {
auto item = this->indexFiles.find(pathRelative);
if (item != this->indexFiles.end()) {
if (item->second.latestFileHash == latestFileHash) return;
}
IndexFile indexFile(pathRelative, latestFileHash);

// assign or overwrite the previous one
this->indexFiles.insert_or_assign(pathRelative, indexFile);

std::filesystem::path indexPath = this->configPath / "index";
std::ofstream indexFileStream(indexPath, std::ios::binary | std::ios::trunc);
if (!indexFileStream) {
throw std::runtime_error("Failed to open index file for writing: " + indexPath.string());
}
for (const auto& [key, fileObj] : this->indexFiles) {
indexFileStream << fileObj.pathRelative << " " << fileObj.latestFileHash << "\n";
}
indexFileStream.close();
}


void ConfigDiskManager::saveBlobHashToIndex(const std::string& latestFileHash, const std::filesystem::path& pathReltive) {
// TODO: Make sure that all of the paths that are used here are relative to the root repo and not absolute
this->addToIndexFiles(pathReltive.string(), latestFileHash);
}
30 changes: 30 additions & 0 deletions src/ConfigDiskManager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#pragma once
#include <filesystem>
#include <string>
#include <map>


struct IndexFile {
std::string pathRelative;
std::string latestFileHash;
IndexFile(std::string _pathRelative, std::string _latestFileHash): pathRelative(_pathRelative),latestFileHash(_latestFileHash) {}
};


class ConfigDiskManager {

public:
static ConfigDiskManager& instance(const std::filesystem::path&);
void initRepoDisk(); // file to init and / or initialize all git objects
void stageFile(const std::filesystem::path&);

private:
std::string createBlob(const std::filesystem::path&) const;
void saveBlobToObjects(const std::string&, std::string) const;
void saveBlobHashToIndex(const std::string&, const std::filesystem::path&);
ConfigDiskManager(const std::filesystem::path&);
void readIndexFiles();
void addToIndexFiles(const std::string&, const std::string&);
const std::filesystem::path configPath;
std::map<std::string, IndexFile> indexFiles;
};
76 changes: 49 additions & 27 deletions src/GitConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,26 @@
#include <filesystem>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <system_error>
#include <fstream>
#include <cassert>
#include <stdexcept>
#include "GitConfig.h"
#include "ConfigDiskManager.h"
#include "GitFolder.h"
#include "GitObject.h"

static void touch_file(std::filesystem::path p) {
std::ofstream ofs(p, std::ios::app);
if (!ofs) throw std::runtime_error("Failed creating the file: " + p.string());
}


std::shared_ptr<GitFolder> GitConfig::getRoot() {
return this->rootObject;
}

void GitConfig::initRepo(const std::filesystem::path &repoPath) {
if (!std::filesystem::exists(repoPath)) throw std::runtime_error("Repository path not found");
std::filesystem::path gitConfigPath = repoPath / ".git_d";
if (std::filesystem::exists(gitConfigPath)) {
std::cout << "repository already initialized, .git_d already exists" << std::endl;
return;
}
std::error_code ec;
std::filesystem::create_directory(gitConfigPath, ec);
if (ec) throw std::runtime_error("Error in creating .git_d");
std::filesystem::create_directory(gitConfigPath / "config", ec);
if (ec) throw std::runtime_error("Error in creating .git_d/config");
touch_file(gitConfigPath / "index");
touch_file(gitConfigPath / "HEAD");
void GitConfig::initRepo() {
this->configDiskManager.initRepoDisk();
}

GitConfig& GitConfig::instance(const std::filesystem::path &p) {
GitConfig& GitConfig::instance() {
// static initialization makes sure that it's calld only once
static GitConfig inst(p);
static GitConfig inst(std::filesystem::current_path());
return inst;
}

Expand All @@ -58,10 +42,8 @@ std::shared_ptr<GitObject> GitConfig::getFromPath(const std::filesystem::path& p

}

GitConfig::GitConfig(const std::filesystem::path &p) {
if (!std::filesystem::exists(p / ".git_d")) {
initRepo(p);
}
GitConfig::GitConfig(const std::filesystem::path &p) : configDiskManager(ConfigDiskManager::instance(p)) {
initRepo();
this->rootObject = GitFolder::create(p, false);
}

Expand Down Expand Up @@ -128,4 +110,44 @@ bool GitConfig::addGitObj(const std::filesystem::path& p) {
}

return true;
}

void GitConfig::printTree() const {
this->printGitTreeFromFolder(this->rootObject, "");
}

void GitConfig::printGitTreeFromFolder(const std::shared_ptr<GitFolder>& root, std::string printPrefix) const {
for (auto& subObj : root->getSubObjects()) {
if (subObj->isDirectory()) {
std::cout << printPrefix << subObj->getName() << ":" << std::endl;
this->printGitTreeFromFolder(std::dynamic_pointer_cast<GitFolder>(subObj), printPrefix + "|- ");
} else {
std::cout << printPrefix << subObj->getName() << std::endl;
}
}
}

bool GitConfig::isInitialized() {
return std::filesystem::exists(std::filesystem::current_path() / ".git_d");
}

void GitConfig::stageDir(const std::filesystem::path& p) const {
assert(std::filesystem::is_directory(p));
for (const auto& entry : std::filesystem::directory_iterator(p)) {
const std::filesystem::path& entryPath = entry.path();
if (std::filesystem::is_directory(entryPath)) {
this->stageDir(entryPath);
} else {
this->stageFile(entryPath);
}
}
}

void GitConfig::stageFile(const std::filesystem::path& p) const {
assert(!std::filesystem::is_directory(p));
try {
this->configDiskManager.stageFile(p);
} catch (std::runtime_error e) {
std::cerr << "Failed to stage file: " << e.what() << std::endl;
}
}
16 changes: 11 additions & 5 deletions src/GitConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,29 @@
#include "GitCommit.h"
#include "GitFolder.h"
#include "GitObject.h"
#include "ConfigDiskManager.h"
#include <filesystem>
#include <memory>

class GitConfig {

public:
static GitConfig& instance(const std::filesystem::path&);
static void initRepo(const std::filesystem::path&);
static GitConfig& instance();
static bool isInitialized();
bool addGitObj(const std::filesystem::path&);
std::shared_ptr<GitFolder> getRoot();

void printTree() const;
void stageFile(const std::filesystem::path&) const;
void stageDir(const std::filesystem::path&) const;

private:
void initRepo();
GitConfig(const std::filesystem::path&);
void initFromConfig(const std::filesystem::path&);
std::shared_ptr<GitObject> getFromPath(const std::filesystem::path&) const;
void printGitTreeFromFolder(const std::shared_ptr<GitFolder>&, std::string printPrefix) const;

GitConfig* conf;
ConfigDiskManager& configDiskManager;
std::vector<GitCommit> commitList;
std::shared_ptr<GitFolder> rootObject;
};
47 changes: 13 additions & 34 deletions src/main.cpp
Original file line number Diff line number Diff line change
@@ -1,36 +1,10 @@
#include "GitConfig.h"
#include "GitFile.h"
#include "GitFolder.h"
#include "GitObject.h"
#include <exception>
#include <filesystem>
#include <iostream>
#include <memory>
#include <ostream>
#include <string>


GitConfig& getRepoConfig() {
std::filesystem::path currPath = std::filesystem::current_path();
return GitConfig::instance(currPath);
}

void printGitTree(std::shared_ptr<GitFolder> root, const std::string& prefix = "", bool isLast = true) {
std::cout << prefix << (isLast ? "└── " : "├── ") << root->getName() << "/" << std::endl;
const auto& children = root->getSubObjects();
for (size_t i = 0; i < children.size(); ++i) {
const auto& child = children[i];
const bool last = (i == children.size() - 1);
const std::string childPrefix = prefix + (isLast ? " " : "│ ");
if (child->isDirectory()) {
printGitTree(std::dynamic_pointer_cast<GitFolder>(child), childPrefix, last);
} else {
std::cout << childPrefix << (last ? "└── " : "├── ") << child->getName() << std::endl;
}
}
}


int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "Usage: git_d <init|add|commit>" << std::endl;
Expand All @@ -40,19 +14,22 @@ int main(int argc, char* argv[]) {
std::string command = argv[1];

if (command == "init") {
std::filesystem::path repoPath = std::filesystem::current_path();
if (argc >= 3) {
std::cerr << "init currently does not support dynamic paths" << std::endl;
// repoPath = argv[2];
}
try {
GitConfig::initRepo(repoPath);
GitConfig::instance(); // initializes the repo and creates the git folders in the repoPath
} catch (const std::exception& ex) {
std::cerr << "Init failed: " << ex.what() << std::endl;
}
return 0;
}

if (!GitConfig::isInitialized()) {
std::cerr << "The repository is not initialized. Please init before doing any git acion" << std::endl;
return 0;
}

if (command == "add") {

do {
Expand All @@ -65,12 +42,14 @@ int main(int argc, char* argv[]) {
std::cerr << "add failed. Please provide a valid path" << std::endl;
break;
}
addPath = std::filesystem::canonical(addPath);
GitConfig& config = getRepoConfig();
if (!config.addGitObj(addPath)) {
std::cerr << "Adding this path to git failed!" << std::endl;
if (addPath.is_absolute()) {
std::cerr << "git only supports relative paths from the root repo" << std::endl;
}
if (std::filesystem::is_directory(addPath)) {
GitConfig::instance().stageDir(addPath);
} else {
GitConfig::instance().stageFile(addPath);
}
printGitTree(config.getRoot());

} while (false);

Expand Down
Loading