Skip to content

Commit 2c565bc

Browse files
Implement git log (#28)
* wip * allow printing of commits without parents * Implement git log * wip day 2 * Include SHA and use correct set of metadata to print commit messages * Add documentation for `log` * Implement gpgsig and remove newline character that is saved into the message * Add newline before every commit is printed * Remove redundant repository argument
1 parent 0d4a454 commit 2c565bc

File tree

10 files changed

+165
-43
lines changed

10 files changed

+165
-43
lines changed

app/adder_app.cpp

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "commands/cat-file.h"
22
#include "commands/hash-object.h"
33
#include "commands/init.h"
4+
#include "commands/log.h"
45
#include <iostream>
56
#include <string>
67
#include <vector>
@@ -21,6 +22,8 @@ int main(int argc, char **argv) {
2122
catfile(args);
2223
} else if (command == "hash-object") {
2324
hashobject(args);
25+
} else if (command == "log") {
26+
log(args);
2427
} else {
2528
std::cerr << "Unknown command: " << command << "\n";
2629
return -1;

doc/commands/log.md

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# log
2+
3+
`git log` will take a commit, and generate a list of commits starting from that commit all the way to the root commit.
4+
5+
## Command usage
6+
- `git log [commit hash]`
7+
8+
## Implementation details
9+
10+
1. Starting from the given commit, find and parse the commit metadata in ./git/objects
11+
2. Use the metadata to figure out the commit hash of its parent
12+
3. Keep repeating this until no parent can be found
13+
14+
## An interesting tidbit about PGP signatures
15+
16+
When I commit locally, these commits do not have PGP signatures.
17+
When I pull commits remotely (for example to merge with the main branch), these commits have PGP signatures.
18+
At what point are PGP signatures added?
19+
20+
Turns out that Github signs commits made to the web interface ([source](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification)) - all the commits created by merging in feature branches to `main` are signed. Commits that are created locally and pushed to the `main` branch are not signed.
21+
22+
## Future work
23+
- [ ] Make the commit argument optional, by defaulting the commit argument to the HEAD argument.
24+
- [ ] Pipe the output to programs like `less` for repositories with a large set of commits
25+
- [ ] Support more options, like `--oneline`

doc/commits.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Commits
2+
3+
## Structure of a commit
4+
tree - tree commit
5+
parent - reference to its parents
6+
author - author name
7+
committer - committer name
8+
pgp signature

include/commands/log.h

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#ifndef LOG_H
2+
#define LOG_H
3+
4+
#include <string>
5+
#include <vector>
6+
void log(std::vector<std::string> &args);
7+
8+
#endif // LOG_H

include/commit.h

+10-2
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,25 @@
88

99
class GitCommit : public GitObject {
1010
public:
11-
GitCommit(const std::string &data = std::string(""));
11+
/**
12+
Assumption here: a GitCommit object will be serialised from an existing Git
13+
object. The SHA1 hash will be read from the file name.
14+
*/
15+
GitCommit(const std::string &data = std::string(""),
16+
const std::string &sha = std::string(""));
1217

1318
void deserialise(
1419
const std::string &data) override; // convert string format to data object
1520
std::string
1621
serialise(GitRepository &repo) override; // convert this to a string format
17-
std::string print_commit(GitRepository &repo);
22+
std::string print_commit();
1823
void init();
24+
bool has_parent();
25+
std::string get_parent();
1926

2027
protected:
2128
std::unordered_map<std::string, std::string> keyValuePairs;
29+
std::string sha;
2230
};
2331

2432
#endif // COMMIT_H

src/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ target_link_libraries(object PRIVATE repository boost_libraries)
2020
target_include_directories(object PUBLIC ../include)
2121
target_compile_features(object PUBLIC cxx_std_17)
2222

23-
add_library(commands init.cpp cat-file.cpp hash-object.cpp)
23+
add_library(commands init.cpp cat-file.cpp hash-object.cpp log.cpp)
2424
target_link_libraries(commands PRIVATE repository object boost_libraries)
2525
target_include_directories(commands PUBLIC ../include)
2626
target_compile_features(commands PUBLIC cxx_std_17)

src/cat-file.cpp

+25-26
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,35 @@
1-
#include <iostream>
21
#include "commands/cat-file.h"
2+
#include <iostream>
33

4-
#include "tclap/CmdLine.h"
5-
#include "repository.h"
64
#include "object.h"
5+
#include "repository.h"
6+
#include "tclap/CmdLine.h"
77

8-
/**
9-
Basic tests: e6c0a6d3b2ca0dbb3313843238d7e27f63259d3a should return CMakeLists.txt
10-
*/
118
void catfile(std::vector<std::string> &args) {
12-
TCLAP::CmdLine cmd("cat-file", ' ', "0.1");
9+
TCLAP::CmdLine cmd("cat-file", ' ', "0.1");
1310

14-
// defines arguments
15-
TCLAP::UnlabeledValueArg<std::string> typeArg("type","type of object",true,"blob","blob");
16-
TCLAP::UnlabeledValueArg<std::string> objArg("object","object hash",true,"e6c0a6d3b2ca0dbb3313843238d7e27f63259d3a","string");
11+
// defines arguments
12+
TCLAP::UnlabeledValueArg<std::string> typeArg("type", "type of object", true,
13+
"blob", "blob");
14+
TCLAP::UnlabeledValueArg<std::string> objArg(
15+
"object", "object hash", true, "e6c0a6d3b2ca0dbb3313843238d7e27f63259d3a",
16+
"string");
1717

18-
cmd.add(typeArg);
19-
cmd.add(objArg);
20-
cmd.parse(args);
21-
std::string& type = typeArg.getValue();
22-
std::string& hash = objArg.getValue();
18+
cmd.add(typeArg);
19+
cmd.add(objArg);
20+
cmd.parse(args);
21+
std::string &type = typeArg.getValue();
22+
std::string &hash = objArg.getValue();
2323

24-
// process args
25-
try {
26-
std::optional<GitRepository> repo = GitRepository::find();
27-
if (repo){
28-
GitObject* obj = GitObject::read(*repo, GitObject::find(*repo, hash, type));
29-
std::cout << obj->serialise(*repo) << "\n";
30-
}
31-
} catch (std::runtime_error& err) {
32-
std::cerr << err.what() << "\n";
24+
// process args
25+
try {
26+
std::optional<GitRepository> repo = GitRepository::find();
27+
if (repo) {
28+
GitObject *obj =
29+
GitObject::read(*repo, GitObject::find(*repo, hash, type));
30+
std::cout << obj->serialise(*repo) << "\n";
3331
}
34-
35-
32+
} catch (std::runtime_error &err) {
33+
std::cerr << err.what() << "\n";
34+
}
3635
}

src/commit.cpp

+34-11
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
#include "commit.h"
22
#include "repository.h"
33
#include <sstream>
4-
GitCommit::GitCommit(const std::string &data) : GitObject() {
4+
GitCommit::GitCommit(const std::string &data, const std::string &sha)
5+
: GitObject() {
56
this->deserialise(data);
7+
this->sha = sha;
68
};
79

810
std::string GitCommit::serialise(GitRepository &repo) {
911
std::stringstream ss;
1012
ss << "tree " << this->keyValuePairs["tree"] << "\n";
11-
ss << "parent " << this->keyValuePairs["parent"] << "\n";
13+
if (this->has_parent()) {
14+
ss << "parent " << this->keyValuePairs["parent"] << "\n";
15+
}
1216
ss << "author " << this->keyValuePairs["author_name"] << " <"
1317
<< this->keyValuePairs["author_email"] << "> "
1418
<< this->keyValuePairs["author_unix_timestamp"] << " "
@@ -17,32 +21,38 @@ std::string GitCommit::serialise(GitRepository &repo) {
1721
<< this->keyValuePairs["committer_email"] << "> "
1822
<< this->keyValuePairs["committer_unix_timestamp"] << " "
1923
<< this->keyValuePairs["committer_timezone"] << "\n";
24+
ss << "gpgsig " << this->keyValuePairs["gpgsig"] << "\n\n";
2025
ss << this->keyValuePairs["message"];
2126
return ss.str();
2227
}
2328

24-
std::string GitCommit::print_commit(GitRepository &repo) {
29+
std::string GitCommit::print_commit() {
2530
std::stringstream ss;
26-
ss << "commit " << this->keyValuePairs["commit"] << "\n";
27-
ss << "Author: " << this->keyValuePairs["author"] << "\n";
28-
ss << "Date: " << this->keyValuePairs["date"] << "\n";
29-
ss << this->keyValuePairs["message"] << "\n";
31+
ss << "commit " << sha << "\n";
32+
ss << "Author: " << this->keyValuePairs["author_name"] << " <"
33+
<< this->keyValuePairs["author_email"] << ">\n";
34+
ss << "Date: " << this->keyValuePairs["author_unix_timestamp"] << " "
35+
<< this->keyValuePairs["author_timezone"] << "\n\n";
36+
ss << " " << this->keyValuePairs["message"];
3037
return ss.str();
3138
}
3239

3340
void GitCommit::deserialise(const std::string &data) {
3441
const std::unordered_map<std::string, std::string> keyValuePairs;
3542
int pos = 0;
3643

37-
const std::array<std::string, 4> keys = {"tree", "parent", "author",
38-
"committer"};
44+
const std::array<std::string, 5> keys = {"tree", "parent", "author",
45+
"committer", "gpgsig"};
3946
const std::vector<std::pair<std::string, std::string>> sub_keys = {
4047
{"name", " <"},
4148
{"email", "> "},
4249
{"unix_timestamp", " "},
4350
{"timezone", "\n"}};
4451
for (auto &key : keys) {
4552
int space = data.find(key + " ", pos);
53+
if (space == std::string::npos) {
54+
continue;
55+
}
4656
int nl = data.find("\n", space);
4757
if (key == "author" || key == "committer") {
4858
pos = space + key.size() + 1;
@@ -52,12 +62,25 @@ void GitCommit::deserialise(const std::string &data) {
5262
data.substr(pos, space - pos);
5363
pos = space + sub_key.second.size();
5464
}
65+
pos = nl + 1;
66+
} else if (key == "gpgsig") {
67+
std::string gpgsig_end = "-----END PGP SIGNATURE-----";
68+
nl = data.find(gpgsig_end, space) + gpgsig_end.size();
69+
this->keyValuePairs[key] =
70+
data.substr(space + key.size() + 1, nl - space - key.size() - 1);
71+
pos = nl + 3;
5572
} else {
5673
this->keyValuePairs[key] =
5774
data.substr(space + key.size() + 1, nl - space - key.size() - 1);
75+
pos = nl + 1;
5876
}
59-
pos = nl + 1;
6077
}
6178
int nl = data.find("\n", pos + 1);
62-
this->keyValuePairs["message"] = data.substr(pos, nl - pos);
79+
this->keyValuePairs["message"] = data.substr(pos + 1, nl - pos);
6380
}
81+
82+
bool GitCommit::has_parent() {
83+
return this->keyValuePairs.find("parent") != this->keyValuePairs.end();
84+
}
85+
86+
std::string GitCommit::get_parent() { return this->keyValuePairs["parent"]; }

src/log.cpp

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#include "commands/log.h"
2+
#include "commit.h"
3+
#include "object.h"
4+
#include <iostream>
5+
6+
#include "repository.h"
7+
#include "tclap/CmdLine.h"
8+
9+
void log(std::vector<std::string> &args) {
10+
TCLAP::CmdLine cmd("log", ' ', "0.1");
11+
12+
// defines arguments
13+
TCLAP::UnlabeledValueArg<std::string> commitArg(
14+
"commit", "Display history of given commit", false, "HEAD",
15+
"commit to start at");
16+
cmd.ignoreUnmatched(true);
17+
cmd.add(commitArg);
18+
cmd.parse(args);
19+
// process args
20+
try {
21+
std::optional<GitRepository> repo = GitRepository::find();
22+
if (repo) {
23+
std::string commit = commitArg.getValue();
24+
GitCommit *commitObj;
25+
26+
do {
27+
GitObject *obj = GitObject::read(*repo, commit);
28+
commitObj = dynamic_cast<GitCommit *>(obj);
29+
if (commitObj) {
30+
std::cout << commitObj->print_commit() << "\n";
31+
commit = commitObj->get_parent();
32+
}
33+
} while (commitObj && commitObj->has_parent());
34+
}
35+
} catch (std::runtime_error &err) {
36+
std::cerr << err.what() << "\n";
37+
}
38+
}

src/object.cpp

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
#include "blob.h"
21
#include "object.h"
2+
#include "blob.h"
3+
#include "commit.h"
34
#include "util.h"
45
#include <boost/algorithm/string.hpp>
56
#include <boost/format.hpp>
@@ -9,8 +10,8 @@
910
#include <boost/iostreams/filtering_streambuf.hpp>
1011

1112
#include <fstream>
12-
#include <sstream>
1313
#include <iostream>
14+
#include <sstream>
1415
#include <stdexcept>
1516
#include <vector>
1617

@@ -59,7 +60,7 @@ GitObject *GitObject::read(GitRepository &repo, const std::string &sha) {
5960
std::cout << "Object size: " << size << std::endl;
6061
return new GitBlob(raw.substr(y + 1));
6162
} else if (fmt == "commit") {
62-
return new GitCommit(raw.substr(y + 1));
63+
return new GitCommit(raw.substr(y + 1), sha);
6364
} else {
6465
throw std::runtime_error("Unknown type");
6566
}
@@ -98,5 +99,14 @@ std::string GitObject::write(GitRepository &repo, std::string &type,
9899

99100
std::string GitObject::find(GitRepository &repo, std::string &name,
100101
std::string &fmt, bool follow) {
102+
if (name == "HEAD") {
103+
// todo resolve HEAD
104+
fs::path head = repo.repo_path(".git/HEAD");
105+
std::string file_data = read_file(head);
106+
std::string ref = file_data.substr(5);
107+
std::cout << "HEAD ref is in " << file_data << std::endl;
108+
fs::path ref_path = repo.repo_path(".git/" + ref);
109+
return read_file(ref_path);
110+
}
101111
return name;
102112
}

0 commit comments

Comments
 (0)