refactor(cli): make main more clean and moving subcommands to the commands files

This commit is contained in:
lisk77 2025-11-23 00:38:25 +01:00
parent 58079f26da
commit 8aa95da2a1
3 changed files with 796 additions and 786 deletions

760
src/commands.c Normal file
View file

@ -0,0 +1,760 @@
#include "../include/commands.h"
void usage(int exitcode) {
printf("usage: merk <command> [<args>]\n\
\n init Initializes a repository in the current directory\
\n diff Displays the difference between the given two files\
\n status Displays a list of modified and untracked files in the repository\
\n commit Record changes made to the repository\n\
\n log Displays the list of commits on the current branch\n"
);
exit(exitcode);
}
int init() {
if (mkdir(".merk", 0755) == 0) {
printf("Initialized merk repository\n");
}
if (errno == EEXIST) {
struct stat st;
if (stat(".merk", &st) == 0 && S_ISDIR(st.st_mode)) {
printf("This directory is already a merk repository\n");
return 1;
}
}
int branch = open(".merk/BRANCH", O_WRONLY | O_CREAT, 0666);
if (branch < 0) return 0;
write(branch, "main", 4);
close(branch);
mkdir(".merk/objects", 0755);
mkdir(".merk/refs", 0755);
mkdir(".merk/refs/branches", 0755);
mkdir(".merk/info", 0755);
mkdir(".merk/logs", 0755);
mkdir(".merk/logs/branches", 0755);
int main_branch = open(".merk/refs/branches/main", O_WRONLY | O_CREAT, 0666);
if (main_branch < 0) return 0;
close(main_branch);
int main_info = open(".merk/info/main", O_WRONLY | O_CREAT, 0666);
if (main_info < 0) return 0;
close(main_info);
int main_log = open(".merk/logs/branches/main", O_WRONLY | O_CREAT, 0666);
if (main_log < 0) return 0;
close(main_log);
CommitLog* empty = commit_log_new();
if (!empty) { perror("ERROR: failed to create commit log for branch main!"); return 0; }
if (!write_commit_log(empty, ".merk/logs/branches/main")) { perror("ERROR: failed to write initial main commit log!"); commit_log_free(empty); return 0; }
commit_log_free(empty);
return 1;
}
int diff(char* file1_path, char* file2_path) {
File* file1;
switch (get_path_type(file1_path)) {
case PT_FILE: file1 = new_file(file1_path); break;
case PT_DIR: file1 = NULL; printf("ERROR: first path is a directory and no file!"); break;
case PT_NOEXIST: file1 = NULL; printf("ERROR: first path does not exist!"); break;
default: file1 = NULL; printf("ERROR: unknown first path!");
}
if (!file1) exit(1);
File* file2;
switch (get_path_type(file2_path)) {
case PT_FILE: file2 = new_file(file2_path); break;
case PT_DIR: file2 = NULL; printf("ERROR: second path is a directory and no file!"); break;
case PT_NOEXIST: file2 = NULL; printf("ERROR: second path does not exist!"); break;
default: file2 = NULL; printf("ERROR: unknown second path!");
}
if (!file2) exit(1);
ActionList* actions = myers_diff(file1, file2, 0, 0);
if (!actions) printf("ERROR: something went wrong while taking the diff!");
else visualize_diff(file1, file2, actions);
return 1;
}
int status() {
FileInfoBuffer* files = file_info_buffer_new();
char relative[PATH_MAX] = "";
char* root = find_root(relative);
if (!root) {
list_free(files);
return 1;
}
walk(root, relative, relative, files, 0, root);
file_info_buffer_sort(files);
char branch_path[PATH_MAX];
snprintf(branch_path, sizeof(branch_path), "%s/.merk/BRANCH", root);
char* branch = get_file_content(branch_path);
char info_path[PATH_MAX];
snprintf(info_path, sizeof(info_path), "%s/.merk/info/%s", root, branch);
char log_path[PATH_MAX];
snprintf(log_path, sizeof(log_path), "%s/.merk/logs/branches/%s", root, branch);
CommitLog* log = commit_log_new();
read_commit_log(log, log_path);
char* last_tree_hash = NULL;
if (log->len != 0) {
last_tree_hash = ((Commit*)log->items + (log->len))->tree_hash;
}
BaseFileBuffer* tracked_list = base_file_buffer_new();
read_base_file_list(tracked_list, info_path);
StringBuffer* modified_files = string_buffer_new();
StringBuffer* deleted_files = string_buffer_new();
StringBuffer* untracked_files = string_buffer_new();
Changes* changes = calloc(1, sizeof(Changes));
if (!changes) {
printf("ERROR: unable to allocate memory for changes!\n");
return 1;
}
changes->insertions = 0;
changes->deletions = 0;
for (size_t idx = 0; idx < files->len; idx++) {
char* file_name = ((FileInfo*)files->items + idx)->name;
if (base_file_buffer_search(tracked_list, file_name)) continue;
string_buffer_push(untracked_files, file_name);
}
for (size_t idx = 0; idx < files->len; idx++) {
char* file_name = ((FileInfo*)files->items + idx)->name;
if (base_file_buffer_search(tracked_list, file_name)) {
BaseFileInfo* base_file = base_file_buffer_search(tracked_list, file_name);
char base_file_hash[41];
char id[3 + snprintf(NULL, 0, "%lu", base_file->base_num) + strlen(file_name) + strlen(branch)];
snprintf(id, sizeof(id), "%s %lu %s", file_name, base_file->base_num, branch);
object_hash(BaseFileObject, id, base_file_hash);
File* basefile = parse_object(base_file_hash, BaseFileObject, NULL, NULL);
if (!basefile) {
printf("ERROR: unable to parse base file %s!\n", file_name);
return 1;
}
File* modified_file = new_file(file_name);
if (!modified_file) {
printf("ERROR: unable to read modified file %s!\n", file_name);
return 1;
}
File* comparison_file = basefile;
if (base_file->diff_num > 0) {
size_t prev_diff = base_file->diff_num - 1;
char prev_diff_hash[41];
char id2[3 + snprintf(NULL, 0, "%zu", prev_diff) + strlen(file_name) + strlen(branch)];
snprintf(id2, sizeof(id2), "%s %zu %s", file_name, prev_diff, branch);
object_hash(FileDiffObject, id2, prev_diff_hash);
char hash[41];
ActionList* last_diff = parse_object(prev_diff_hash, FileDiffObject, NULL, hash);
if (last_diff) {
File* last_version = apply_diff(basefile, last_diff);
if (last_version) {
comparison_file = last_version;
}
free_action_list(last_diff);
}
}
ActionList* diff = myers_diff(comparison_file, modified_file, 0, 0);
if (!diff) {
printf("ERROR: unable to compute diff for file %s!\n", file_name);
return 1;
}
if (diff->len != 0) {
for (size_t idx = 0; idx < diff->len; idx++) {
Action action = diff->actions[idx];
if (action.type == DELETE) {
changes->deletions++;
} else if (action.type == INSERT) {
changes->insertions++;
}
}
string_buffer_push(modified_files, file_name);
}
free_action_list(diff);
free_file(modified_file);
if (comparison_file != basefile) {
free_file(comparison_file);
}
free_file(basefile);
}
}
for (size_t idx = 0; idx < tracked_list->len; idx++) {
char* file_name = ((BaseFileInfo*)tracked_list->items + idx)->name;
if (file_info_buffer_search(files, file_name)) continue;
else string_buffer_push(deleted_files, file_name);
}
for (size_t idx = 0; idx < modified_files->len; idx++) {
printf("\x1b[36;1mM\x1b[0m \x1b[36m%s\x1b[0m\n", (char*)modified_files->items[idx]);
}
for (size_t idx = 0; idx < deleted_files->len; idx++) {
printf("\x1b[31;4mD\x1b[0m \x1b[31;9m%s\x1b[0m\n", (char*)deleted_files->items[idx]);
}
if (modified_files->len != 0 || deleted_files->len != 0) printf("\n");
for (size_t idx = 0; idx < untracked_files->len; idx++) {
printf("\x1b[31;1mU\x1b[0m \x1b[31m%s\x1b[0m\n", (char*)untracked_files->items[idx]);
}
printf("\n\x1b[32;1m%d+\x1b[0m \x1b[31;1m%d-\x1b[0m on branch \x1b[39;1;4m%s\x1b[0m with %lu commits\n", changes->insertions, changes->deletions, branch, log->len);
file_info_buffer_free(files);
free(root);
return 1;
}
int commit(int argc, char** argv) {
char* commit_message = NULL;
int file_start_index = 2;
if (argc >= 4 && strcmp(argv[2], "-m") == 0) {
if (argv[3][0] == '-') {
printf("ERROR: Invalid commit message provided after -m flag!\n");
exit(1);
}
commit_message = strdup(argv[3]);
if (!commit_message) {
fprintf(stderr, "ERROR: Memory allocation failed\n");
exit(1);
}
file_start_index = 4;
} else if (argc == 3 && strcmp(argv[2], "-m") == 0) {
printf("ERROR: Missing commit message after -m flag!\n");
exit(1);
}
for (int i = file_start_index; i < argc; i++) {
if (strcmp(argv[i], "-m") == 0) {
printf("ERROR: Unexpected -m flag in file list!\n");
exit(1);
}
}
Config config = {0};
if (load_config(&config) < 0) {
free(commit_message);
return 1;
}
if (validate_config(&config) < 0) {
free_config(&config);
free(commit_message);
return 1;
}
char tmp_path[PATH_MAX] = ".merk/BRANCH";
char branch_path[PATH_MAX];
realpath(tmp_path, branch_path);
char* branch = get_file_content(branch_path);
if (!branch) {
free_config(&config);
free(commit_message);
return 1;
}
char tmp_path2[PATH_MAX];
char info_path[PATH_MAX];
snprintf(tmp_path2, sizeof(tmp_path2), ".merk/info/%s", branch);
realpath(tmp_path2, info_path);
BaseFileBuffer* base_files = base_file_buffer_new();
if (!base_files) {
free(branch);
free_config(&config);
free(commit_message);
return 1;
}
read_base_file_list(base_files, info_path);
StringBuffer* files = string_buffer_new();
if (!files) {
base_file_buffer_free(base_files);
free(branch);
free_config(&config);
free(commit_message);
return 1;
}
char* root = find_root(NULL);
if (!root) {
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free_config(&config);
free(commit_message);
return 1;
}
for (int i = file_start_index; i < argc; i++) {
char* real = realpath(argv[i], NULL);
if (!real) {
fprintf(stderr, "ERROR: Cannot resolve path: %s\n", argv[i]);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free_config(&config);
free(commit_message);
return 1;
}
if (get_path_type(real) == PT_FILE && is_in_repo(root, real) == 0) {
char* repo_path = get_repo_path(root, real);
if (repo_path) {
string_buffer_push(files, repo_path);
free(repo_path);
}
}
else {
fprintf(stderr, "ERROR: %s is not a file or not inside the repo!\n", real);
free(real);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(commit_message);
return 1;
}
free(real);
}
if (!commit_message) {
printf("Enter commit message: ");
fflush(stdout);
char buffer[1024];
if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
size_t len = strlen(buffer);
if (len > 0 && buffer[len-1] == '\n') {
buffer[len-1] = '\0';
}
if (strlen(buffer) == 0) {
printf("ERROR: Empty commit message!\n");
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
return 1;
}
commit_message = strdup(buffer);
if (!commit_message) {
fprintf(stderr, "ERROR: Memory allocation failed\n");
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
return 1;
}
} else {
printf("ERROR: Failed to read commit message!\n");
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
return 1;
}
}
FlatMap* file_hash_map = flat_map_new();
Changes* changes = calloc(1, sizeof(Changes));
if (!changes) {
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
return 1;
}
changes->insertions = 0;
changes->deletions = 0;
for (size_t idx = 0; idx < files->len; idx++) {
BaseFileInfo* base_file = base_file_buffer_search(base_files, files->items[idx]);
if (!base_file) {
printf("this is a basefile as it should be\n");
BaseFileInfo info = (BaseFileInfo){.base_num = 0, .diff_num = 0, .name = strdup(files->items[idx])};
base_file_buffer_push(base_files, info);
// Compress the files content and put it into the object database
File* modified_file = new_file(files->items[idx]);
if (!modified_file) {
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(changes);
return 1;
}
changes->insertions += modified_file->lines;
char file_hash[41];
snapshot_file(files->items[idx], root, 0, file_hash);
flat_map_put(file_hash_map, files->items[idx], file_hash);
continue;
}
char base_file_hash[41];
char id[3 + snprintf(NULL, 0, "%lu", base_file->base_num) + strlen(files->items[idx]) + strlen(branch)];
snprintf(id, sizeof(id), "%s %lu %s", (char*)files->items[idx], base_file->base_num, branch);
object_hash(BaseFileObject, id, base_file_hash);
File* basefile = parse_object(base_file_hash, BaseFileObject, NULL, NULL);
if (!basefile) {
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(changes);
return 1;
}
File* modified_file = new_file(files->items[idx]);
if (!modified_file) {
free_file(basefile);
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(changes);
return 1;
}
ActionList* diff = myers_diff(basefile, modified_file, 0, 0);
if (!diff) {
free_file(basefile);
free_file(modified_file);
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(changes);
return 1;
}
if (base_file->diff_num == 0) {
for (size_t idx = 0; idx < diff->len; idx++) {
Action action = diff->actions[idx];
if (action.type == INSERT) {
changes->insertions += 1;
} else if (action.type == DELETE) {
changes->deletions += 1;
}
}
}
if (base_file->diff_num > 0) {
size_t prev_diff = base_file->diff_num - 1;
char prev_diff_hash[41];
char id2[3 + snprintf(NULL, 0, "%zu", prev_diff) + strlen(files->items[idx]) + strlen(branch)];
snprintf(id2, sizeof(id2), "%s %zu %s", (char*)files->items[idx], prev_diff, branch);
object_hash(FileDiffObject, id2, prev_diff_hash);
char hash[41];
ActionList* last_diff = parse_object(prev_diff_hash, FileDiffObject, NULL, hash);
if (!last_diff) {
free_action_list(diff);
free_file(modified_file);
free_file(basefile);
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(changes);
return 1;
}
File* last_version = apply_diff(basefile, last_diff);
if (!last_version) {
free_action_list(last_diff);
free_action_list(diff);
free_file(modified_file);
free_file(basefile);
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(changes);
return 1;
}
ActionList* modified_diff = myers_diff(last_version, modified_file, 0, 0);
if (!modified_diff) {
free_action_list(last_diff);
free_action_list(diff);
free_file(modified_file);
free_file(basefile);
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(changes);
return 1;
}
for (size_t idx = 0; idx < modified_diff->len; idx++) {
Action action = modified_diff->actions[idx];
if (action.type == INSERT) {
changes->insertions += 1;
} else if (action.type == DELETE) {
changes->deletions += 1;
}
}
}
if (diff->len > 200) {
char new_base_hash[41];
snapshot_file(files->items[idx], root, base_file->base_num+1, new_base_hash);
flat_map_put(file_hash_map, files->items[idx], new_base_hash);
base_file_buffer_remove(base_files, files->items[idx]);
BaseFileInfo new_info = (BaseFileInfo){
.base_num = base_file->base_num + 1,
.diff_num = base_file->diff_num,
.name = strdup(files->items[idx])
};
base_file_buffer_push(base_files, new_info);
continue;
}
else {
save_diff(diff, files->items[idx], root, base_file->diff_num, base_file_hash);
char diff_hash[41];
char id[3 + snprintf(NULL, 0, "%lu", base_file->diff_num) + strlen(files->items[idx]) + strlen(branch)];
snprintf(id, sizeof(id), "%s %lu %s", (char*)files->items[idx], base_file->diff_num, branch);
object_hash(BaseFileObject, id, diff_hash);
flat_map_put(file_hash_map, files->items[idx], diff_hash);
base_file_buffer_remove(base_files, files->items[idx]);
BaseFileInfo new_info = (BaseFileInfo){
.base_num = base_file->base_num,
.diff_num = base_file->diff_num + 1,
.name = strdup(files->items[idx])
};
base_file_buffer_push(base_files, new_info);
}
}
base_file_buffer_sort(base_files);
write_base_file_list(base_files, info_path);
FileInfoBuffer* tree = basefilebuffer_to_fileinfobuffer(base_files);
if (!tree) {
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
return 1;
}
for (size_t idx = 0; idx < tree->len; idx++) {
char* hash = flat_map_get(file_hash_map, ((FileInfo*)tree->items + idx)->name);
if (hash) ((FileInfo*)tree->items + idx)->hash = strdup(hash);
}
char tree_hash[41];
snapshot_tree(tree, tree_hash);
char log_path[PATH_MAX];
snprintf(log_path, sizeof(log_path), "%s/.merk/logs/branches/%s", root, branch);
CommitLog* log = commit_log_new();
if (!log) {
file_info_buffer_free(tree);
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
return 1;
}
read_commit_log(log, log_path);
char parent_hash[41];
if (log->len != 0) {
snprintf(parent_hash, sizeof(parent_hash), "%s", ((Commit*)log->items + (log->len - 1))->hash);
}
char timestamp[20];
snprintf(timestamp, sizeof(timestamp), "%ld", time(NULL));
Author author = {
.name = strdup(config.user.name),
.email = strdup(config.user.email),
.timestamp = strdup(timestamp)
};
if (log->len != 0) {
char parent_hash[41];
snprintf(parent_hash, sizeof(parent_hash), "%s", ((Commit*)log->items + (log->len - 1))->hash);
}
char commit_hash_content[4096];
int offset = 0;
offset += snprintf(commit_hash_content + offset, sizeof(commit_hash_content) - offset, "tree %s\n", tree_hash);
if (log->len != 0) {
offset += snprintf(commit_hash_content + offset, sizeof(commit_hash_content) - offset, "parent %s\n", parent_hash);
}
offset += snprintf(commit_hash_content + offset, sizeof(commit_hash_content) - offset, "author %s <%s> %s\n", author.name, author.email, author.timestamp);
offset += snprintf(commit_hash_content + offset, sizeof(commit_hash_content) - offset, "committer %s <%s> %s\n", author.name, author.email, author.timestamp);
offset += snprintf(commit_hash_content + offset, sizeof(commit_hash_content) - offset, "message %s\n", commit_message);
char commit_hash[41];
object_hash(LogObject, commit_hash_content, commit_hash);
Commit commit = {
.hash = commit_hash,
.tree_hash = strdup(tree_hash),
.authors = &author,
.authors_count = 1,
.committer = author,
.message = strdup(commit_message)
};
commit_log_push(log, commit);
if (!write_commit_log(log, log_path)) {
commit_log_free(log);
file_info_buffer_free(tree);
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
return 1;
}
char ref_path[PATH_MAX];
snprintf(ref_path, sizeof(ref_path), "%s/.merk/refs/branches/%s", root, branch);
int ref = open(ref_path, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (ref < 0) return -1;
write(ref, commit_hash, 40);
close(ref);
printf("(\x1b[33;1m%.7s\x1b[0m on \x1b[39;1;4m%s\x1b[0m \x1b[32;1m%d+\x1b[0m \x1b[31;1m%d-\x1b[0m) %s\n",
commit_hash,
branch,
changes->insertions,
changes->deletions,
commit_message
);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(commit_message);
return 1;
}
int commit_log() {
CommitLog* log = commit_log_new();
if (!log) return 0;
char* root = find_root(NULL);
if (!root) {
return 0;
}
char branch_path[PATH_MAX];
snprintf(branch_path, sizeof(branch_path), "%s/.merk/BRANCH", root);
char* branch = get_file_content(branch_path);
if (!branch) {
free(root);
return 0;
}
char log_path[PATH_MAX];
snprintf(log_path, sizeof(log_path), "%s/.merk/logs/branches/%s", root, branch);
read_commit_log(log, log_path);
char ref_path[PATH_MAX];
snprintf(ref_path, sizeof(ref_path), "%s/.merk/refs/branches/%s", root, branch);
int ref = open(ref_path, O_RDONLY);
if (ref < 0) {
free(branch);
free(root);
commit_log_free(log);
return 0;
}
char head[41] = "";
read(ref, head, 40);
close(ref);
for (size_t i = log->len; i != 0; i--) {
Commit* commit = (Commit*)log->items + i - 1;
char human_readable_time[32];
time_t timestamp = atol(commit->committer.timestamp);
strftime(human_readable_time, sizeof(human_readable_time), "%Y-%m-%d %H:%M:%S %z", localtime(&timestamp));
char* marker = (strcmp(commit->hash, head) == 0) ? "\x1b[32m@\x1b[0m" : "\x1b[34m◯\x1b[0m";
printf("%s \x1b[33;1m%.7s\x1b[0m: %s\n│ %s <\x1b[38;5;240m\x1b]8;;mailto:%s\x1b\\%s\x1b]8;;\x1b\\\x1b[0m> (\x1b[36m%s\x1b[0m)\n\n",
marker,
commit->hash,
commit->message,
commit->committer.name,
commit->committer.email,
commit->committer.email,
human_readable_time
);
}
if (log->len != 0) printf("\n");
free(branch);
free(root);
commit_log_free(log);
return 1;
}

View file

@ -1,793 +1,19 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <unistd.h>
#include <time.h>
#include "base_file_buffer.h"
#include "config.h"
#include "commit.h"
#include "hash.h"
#include "object.h"
#include "utilities.h"
static void usage(int exitcode) {
printf("usage: merk <command> [<args>]\n\
\n init Initializes a repository in the current directory\
\n diff Displays the difference between the given two files\
\n status Displays a list of modified and untracked files in the repository\
\n commit Record changes made to the repository\n\
\n log Displays the list of commits on the current branch\n"
);
exit(exitcode);
}
static int init() {
if (mkdir(".merk", 0755) == 0) {
printf("Initialized merk repository\n");
}
if (errno == EEXIST) {
struct stat st;
if (stat(".merk", &st) == 0 && S_ISDIR(st.st_mode)) {
printf("This directory is already a merk repository\n");
return 1;
}
}
int branch = open(".merk/BRANCH", O_WRONLY | O_CREAT, 0666);
if (branch < 0) return 0;
write(branch, "main", 4);
close(branch);
mkdir(".merk/objects", 0755);
mkdir(".merk/refs", 0755);
mkdir(".merk/refs/branches", 0755);
mkdir(".merk/info", 0755);
mkdir(".merk/logs", 0755);
mkdir(".merk/logs/branches", 0755);
int main_branch = open(".merk/refs/branches/main", O_WRONLY | O_CREAT, 0666);
if (main_branch < 0) return 0;
close(main_branch);
int main_info = open(".merk/info/main", O_WRONLY | O_CREAT, 0666);
if (main_info < 0) return 0;
close(main_info);
int main_log = open(".merk/logs/branches/main", O_WRONLY | O_CREAT, 0666);
if (main_log < 0) return 0;
close(main_log);
CommitLog* empty = commit_log_new();
if (!empty) { perror("ERROR: failed to create commit log for branch main!"); return 0; }
if (!write_commit_log(empty, ".merk/logs/branches/main")) { perror("ERROR: failed to write initial main commit log!"); commit_log_free(empty); return 0; }
commit_log_free(empty);
return 1;
}
static int diff(char* file1_path, char* file2_path) {
File* file1;
switch (get_path_type(file1_path)) {
case PT_FILE: file1 = new_file(file1_path); break;
case PT_DIR: file1 = NULL; printf("ERROR: first path is a directory and no file!"); break;
case PT_NOEXIST: file1 = NULL; printf("ERROR: first path does not exist!"); break;
default: file1 = NULL; printf("ERROR: unknown first path!");
}
if (!file1) exit(1);
File* file2;
switch (get_path_type(file2_path)) {
case PT_FILE: file2 = new_file(file2_path); break;
case PT_DIR: file2 = NULL; printf("ERROR: second path is a directory and no file!"); break;
case PT_NOEXIST: file2 = NULL; printf("ERROR: second path does not exist!"); break;
default: file2 = NULL; printf("ERROR: unknown second path!");
}
if (!file2) exit(1);
ActionList* actions = myers_diff(file1, file2, 0, 0);
if (!actions) printf("ERROR: something went wrong while taking the diff!");
else visualize_diff(file1, file2, actions);
return 1;
}
static int status() {
FileInfoBuffer* files = file_info_buffer_new();
char relative[PATH_MAX] = "";
char* root = find_root(relative);
if (!root) {
list_free(files);
return 1;
}
walk(root, relative, relative, files, 0, root);
file_info_buffer_sort(files);
char branch_path[PATH_MAX];
snprintf(branch_path, sizeof(branch_path), "%s/.merk/BRANCH", root);
char* branch = get_file_content(branch_path);
char info_path[PATH_MAX];
snprintf(info_path, sizeof(info_path), "%s/.merk/info/%s", root, branch);
char log_path[PATH_MAX];
snprintf(log_path, sizeof(log_path), "%s/.merk/logs/branches/%s", root, branch);
CommitLog* log = commit_log_new();
read_commit_log(log, log_path);
char* last_tree_hash = NULL;
if (log->len != 0) {
last_tree_hash = ((Commit*)log->items + (log->len))->tree_hash;
}
BaseFileBuffer* tracked_list = base_file_buffer_new();
read_base_file_list(tracked_list, info_path);
StringBuffer* modified_files = string_buffer_new();
StringBuffer* deleted_files = string_buffer_new();
StringBuffer* untracked_files = string_buffer_new();
Changes* changes = calloc(1, sizeof(Changes));
if (!changes) {
printf("ERROR: unable to allocate memory for changes!\n");
return 1;
}
changes->insertions = 0;
changes->deletions = 0;
for (size_t idx = 0; idx < files->len; idx++) {
char* file_name = ((FileInfo*)files->items + idx)->name;
if (base_file_buffer_search(tracked_list, file_name)) continue;
string_buffer_push(untracked_files, file_name);
}
for (size_t idx = 0; idx < files->len; idx++) {
char* file_name = ((FileInfo*)files->items + idx)->name;
if (base_file_buffer_search(tracked_list, file_name)) {
BaseFileInfo* base_file = base_file_buffer_search(tracked_list, file_name);
char base_file_hash[41];
char id[3 + snprintf(NULL, 0, "%lu", base_file->base_num) + strlen(file_name) + strlen(branch)];
snprintf(id, sizeof(id), "%s %lu %s", file_name, base_file->base_num, branch);
object_hash(BaseFileObject, id, base_file_hash);
File* basefile = parse_object(base_file_hash, BaseFileObject, NULL, NULL);
if (!basefile) {
printf("ERROR: unable to parse base file %s!\n", file_name);
return 1;
}
File* modified_file = new_file(file_name);
if (!modified_file) {
printf("ERROR: unable to read modified file %s!\n", file_name);
return 1;
}
File* comparison_file = basefile;
if (base_file->diff_num > 0) {
size_t prev_diff = base_file->diff_num - 1;
char prev_diff_hash[41];
char id2[3 + snprintf(NULL, 0, "%zu", prev_diff) + strlen(file_name) + strlen(branch)];
snprintf(id2, sizeof(id2), "%s %zu %s", file_name, prev_diff, branch);
object_hash(FileDiffObject, id2, prev_diff_hash);
char hash[41];
ActionList* last_diff = parse_object(prev_diff_hash, FileDiffObject, NULL, hash);
if (last_diff) {
File* last_version = apply_diff(basefile, last_diff);
if (last_version) {
comparison_file = last_version;
}
free_action_list(last_diff);
}
}
ActionList* diff = myers_diff(comparison_file, modified_file, 0, 0);
if (!diff) {
printf("ERROR: unable to compute diff for file %s!\n", file_name);
return 1;
}
if (diff->len != 0) {
for (size_t idx = 0; idx < diff->len; idx++) {
Action action = diff->actions[idx];
if (action.type == DELETE) {
changes->deletions++;
} else if (action.type == INSERT) {
changes->insertions++;
}
}
string_buffer_push(modified_files, file_name);
}
free_action_list(diff);
free_file(modified_file);
if (comparison_file != basefile) {
free_file(comparison_file);
}
free_file(basefile);
}
}
for (size_t idx = 0; idx < tracked_list->len; idx++) {
char* file_name = ((BaseFileInfo*)tracked_list->items + idx)->name;
if (file_info_buffer_search(files, file_name)) continue;
else string_buffer_push(deleted_files, file_name);
}
for (size_t idx = 0; idx < modified_files->len; idx++) {
printf("\x1b[36;1mM\x1b[0m \x1b[36m%s\x1b[0m\n", (char*)modified_files->items[idx]);
}
for (size_t idx = 0; idx < deleted_files->len; idx++) {
printf("\x1b[31;4mD\x1b[0m \x1b[31;9m%s\x1b[0m\n", (char*)deleted_files->items[idx]);
}
if (modified_files->len != 0 || deleted_files->len != 0) printf("\n");
for (size_t idx = 0; idx < untracked_files->len; idx++) {
printf("\x1b[31;1mU\x1b[0m \x1b[31m%s\x1b[0m\n", (char*)untracked_files->items[idx]);
}
printf("\n\x1b[32;1m%d+\x1b[0m \x1b[31;1m%d-\x1b[0m on branch \x1b[39;1;4m%s\x1b[0m with %lu commits\n", changes->insertions, changes->deletions, branch, log->len);
file_info_buffer_free(files);
free(root);
return 1;
}
static int commit(int argc, char** argv) {
char* commit_message = NULL;
int file_start_index = 2;
if (argc >= 4 && strcmp(argv[2], "-m") == 0) {
if (argv[3][0] == '-') {
printf("ERROR: Invalid commit message provided after -m flag!\n");
exit(1);
}
commit_message = strdup(argv[3]);
if (!commit_message) {
fprintf(stderr, "ERROR: Memory allocation failed\n");
exit(1);
}
file_start_index = 4;
} else if (argc == 3 && strcmp(argv[2], "-m") == 0) {
printf("ERROR: Missing commit message after -m flag!\n");
exit(1);
}
for (int i = file_start_index; i < argc; i++) {
if (strcmp(argv[i], "-m") == 0) {
printf("ERROR: Unexpected -m flag in file list!\n");
exit(1);
}
}
Config config = {0};
if (load_config(&config) < 0) {
free(commit_message);
return 1;
}
if (validate_config(&config) < 0) {
free_config(&config);
free(commit_message);
return 1;
}
char tmp_path[PATH_MAX] = ".merk/BRANCH";
char branch_path[PATH_MAX];
realpath(tmp_path, branch_path);
char* branch = get_file_content(branch_path);
if (!branch) {
free_config(&config);
free(commit_message);
return 1;
}
char tmp_path2[PATH_MAX];
char info_path[PATH_MAX];
snprintf(tmp_path2, sizeof(tmp_path2), ".merk/info/%s", branch);
realpath(tmp_path2, info_path);
BaseFileBuffer* base_files = base_file_buffer_new();
if (!base_files) {
free(branch);
free_config(&config);
free(commit_message);
return 1;
}
read_base_file_list(base_files, info_path);
StringBuffer* files = string_buffer_new();
if (!files) {
base_file_buffer_free(base_files);
free(branch);
free_config(&config);
free(commit_message);
return 1;
}
char* root = find_root(NULL);
if (!root) {
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free_config(&config);
free(commit_message);
return 1;
}
for (int i = file_start_index; i < argc; i++) {
char* real = realpath(argv[i], NULL);
if (!real) {
fprintf(stderr, "ERROR: Cannot resolve path: %s\n", argv[i]);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free_config(&config);
free(commit_message);
return 1;
}
if (get_path_type(real) == PT_FILE && is_in_repo(root, real) == 0) {
char* repo_path = get_repo_path(root, real);
if (repo_path) {
string_buffer_push(files, repo_path);
free(repo_path);
}
}
else {
fprintf(stderr, "ERROR: %s is not a file or not inside the repo!\n", real);
free(real);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(commit_message);
return 1;
}
free(real);
}
if (!commit_message) {
printf("Enter commit message: ");
fflush(stdout);
char buffer[1024];
if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
size_t len = strlen(buffer);
if (len > 0 && buffer[len-1] == '\n') {
buffer[len-1] = '\0';
}
if (strlen(buffer) == 0) {
printf("ERROR: Empty commit message!\n");
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
return 1;
}
commit_message = strdup(buffer);
if (!commit_message) {
fprintf(stderr, "ERROR: Memory allocation failed\n");
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
return 1;
}
} else {
printf("ERROR: Failed to read commit message!\n");
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
return 1;
}
}
FlatMap* file_hash_map = flat_map_new();
Changes* changes = calloc(1, sizeof(Changes));
if (!changes) {
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
return 1;
}
changes->insertions = 0;
changes->deletions = 0;
for (size_t idx = 0; idx < files->len; idx++) {
BaseFileInfo* base_file = base_file_buffer_search(base_files, files->items[idx]);
if (!base_file) {
printf("this is a basefile as it should be\n");
BaseFileInfo info = (BaseFileInfo){.base_num = 0, .diff_num = 0, .name = strdup(files->items[idx])};
base_file_buffer_push(base_files, info);
// Compress the files content and put it into the object database
File* modified_file = new_file(files->items[idx]);
if (!modified_file) {
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(changes);
return 1;
}
changes->insertions += modified_file->lines;
char file_hash[41];
snapshot_file(files->items[idx], root, 0, file_hash);
flat_map_put(file_hash_map, files->items[idx], file_hash);
continue;
}
char base_file_hash[41];
char id[3 + snprintf(NULL, 0, "%lu", base_file->base_num) + strlen(files->items[idx]) + strlen(branch)];
snprintf(id, sizeof(id), "%s %lu %s", (char*)files->items[idx], base_file->base_num, branch);
object_hash(BaseFileObject, id, base_file_hash);
File* basefile = parse_object(base_file_hash, BaseFileObject, NULL, NULL);
if (!basefile) {
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(changes);
return 1;
}
File* modified_file = new_file(files->items[idx]);
if (!modified_file) {
free_file(basefile);
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(changes);
return 1;
}
ActionList* diff = myers_diff(basefile, modified_file, 0, 0);
if (!diff) {
free_file(basefile);
free_file(modified_file);
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(changes);
return 1;
}
if (base_file->diff_num == 0) {
for (size_t idx = 0; idx < diff->len; idx++) {
Action action = diff->actions[idx];
if (action.type == INSERT) {
changes->insertions += 1;
} else if (action.type == DELETE) {
changes->deletions += 1;
}
}
}
if (base_file->diff_num > 0) {
size_t prev_diff = base_file->diff_num - 1;
char prev_diff_hash[41];
char id2[3 + snprintf(NULL, 0, "%zu", prev_diff) + strlen(files->items[idx]) + strlen(branch)];
snprintf(id2, sizeof(id2), "%s %zu %s", (char*)files->items[idx], prev_diff, branch);
object_hash(FileDiffObject, id2, prev_diff_hash);
char hash[41];
ActionList* last_diff = parse_object(prev_diff_hash, FileDiffObject, NULL, hash);
if (!last_diff) {
free_action_list(diff);
free_file(modified_file);
free_file(basefile);
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(changes);
return 1;
}
File* last_version = apply_diff(basefile, last_diff);
if (!last_version) {
free_action_list(last_diff);
free_action_list(diff);
free_file(modified_file);
free_file(basefile);
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(changes);
return 1;
}
ActionList* modified_diff = myers_diff(last_version, modified_file, 0, 0);
if (!modified_diff) {
free_action_list(last_diff);
free_action_list(diff);
free_file(modified_file);
free_file(basefile);
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(changes);
return 1;
}
for (size_t idx = 0; idx < modified_diff->len; idx++) {
Action action = modified_diff->actions[idx];
if (action.type == INSERT) {
changes->insertions += 1;
} else if (action.type == DELETE) {
changes->deletions += 1;
}
}
}
if (diff->len > 200) {
char new_base_hash[41];
snapshot_file(files->items[idx], root, base_file->base_num+1, new_base_hash);
flat_map_put(file_hash_map, files->items[idx], new_base_hash);
base_file_buffer_remove(base_files, files->items[idx]);
BaseFileInfo new_info = (BaseFileInfo){
.base_num = base_file->base_num + 1,
.diff_num = base_file->diff_num,
.name = strdup(files->items[idx])
};
base_file_buffer_push(base_files, new_info);
continue;
}
else {
save_diff(diff, files->items[idx], root, base_file->diff_num, base_file_hash);
char diff_hash[41];
char id[3 + snprintf(NULL, 0, "%lu", base_file->diff_num) + strlen(files->items[idx]) + strlen(branch)];
snprintf(id, sizeof(id), "%s %lu %s", (char*)files->items[idx], base_file->diff_num, branch);
object_hash(BaseFileObject, id, diff_hash);
flat_map_put(file_hash_map, files->items[idx], diff_hash);
base_file_buffer_remove(base_files, files->items[idx]);
BaseFileInfo new_info = (BaseFileInfo){
.base_num = base_file->base_num,
.diff_num = base_file->diff_num + 1,
.name = strdup(files->items[idx])
};
base_file_buffer_push(base_files, new_info);
}
}
base_file_buffer_sort(base_files);
write_base_file_list(base_files, info_path);
FileInfoBuffer* tree = basefilebuffer_to_fileinfobuffer(base_files);
if (!tree) {
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
return 1;
}
for (size_t idx = 0; idx < tree->len; idx++) {
char* hash = flat_map_get(file_hash_map, ((FileInfo*)tree->items + idx)->name);
if (hash) ((FileInfo*)tree->items + idx)->hash = strdup(hash);
}
char tree_hash[41];
snapshot_tree(tree, tree_hash);
char log_path[PATH_MAX];
snprintf(log_path, sizeof(log_path), "%s/.merk/logs/branches/%s", root, branch);
CommitLog* log = commit_log_new();
if (!log) {
file_info_buffer_free(tree);
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
return 1;
}
read_commit_log(log, log_path);
char parent_hash[41];
if (log->len != 0) {
snprintf(parent_hash, sizeof(parent_hash), "%s", ((Commit*)log->items + (log->len - 1))->hash);
}
char timestamp[20];
snprintf(timestamp, sizeof(timestamp), "%ld", time(NULL));
Author author = {
.name = strdup(config.user.name),
.email = strdup(config.user.email),
.timestamp = strdup(timestamp)
};
if (log->len != 0) {
char parent_hash[41];
snprintf(parent_hash, sizeof(parent_hash), "%s", ((Commit*)log->items + (log->len - 1))->hash);
}
char commit_hash_content[4096];
int offset = 0;
offset += snprintf(commit_hash_content + offset, sizeof(commit_hash_content) - offset, "tree %s\n", tree_hash);
if (log->len != 0) {
offset += snprintf(commit_hash_content + offset, sizeof(commit_hash_content) - offset, "parent %s\n", parent_hash);
}
offset += snprintf(commit_hash_content + offset, sizeof(commit_hash_content) - offset, "author %s <%s> %s\n", author.name, author.email, author.timestamp);
offset += snprintf(commit_hash_content + offset, sizeof(commit_hash_content) - offset, "committer %s <%s> %s\n", author.name, author.email, author.timestamp);
offset += snprintf(commit_hash_content + offset, sizeof(commit_hash_content) - offset, "message %s\n", commit_message);
char commit_hash[41];
object_hash(LogObject, commit_hash_content, commit_hash);
Commit commit = {
.hash = commit_hash,
.tree_hash = strdup(tree_hash),
.authors = &author,
.authors_count = 1,
.committer = author,
.message = strdup(commit_message)
};
commit_log_push(log, commit);
if (!write_commit_log(log, log_path)) {
commit_log_free(log);
file_info_buffer_free(tree);
free(commit_message);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
return 1;
}
char ref_path[PATH_MAX];
snprintf(ref_path, sizeof(ref_path), "%s/.merk/refs/branches/%s", root, branch);
int ref = open(ref_path, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (ref < 0) return -1;
write(ref, commit_hash, 40);
close(ref);
printf("(\x1b[33;1m%.7s\x1b[0m on \x1b[39;1;4m%s\x1b[0m \x1b[32;1m%d+\x1b[0m \x1b[31;1m%d-\x1b[0m) %s\n",
commit_hash,
branch,
changes->insertions,
changes->deletions,
commit_message
);
list_free(files);
base_file_buffer_free(base_files);
free(branch);
free(root);
free_config(&config);
free(commit_message);
return 1;
}
static int commit_log() {
CommitLog* log = commit_log_new();
if (!log) return 0;
char* root = find_root(NULL);
if (!root) {
return 0;
}
char branch_path[PATH_MAX];
snprintf(branch_path, sizeof(branch_path), "%s/.merk/BRANCH", root);
char* branch = get_file_content(branch_path);
if (!branch) {
free(root);
return 0;
}
char log_path[PATH_MAX];
snprintf(log_path, sizeof(log_path), "%s/.merk/logs/branches/%s", root, branch);
read_commit_log(log, log_path);
char ref_path[PATH_MAX];
snprintf(ref_path, sizeof(ref_path), "%s/.merk/refs/branches/%s", root, branch);
int ref = open(ref_path, O_RDONLY);
if (ref < 0) {
free(branch);
free(root);
commit_log_free(log);
return 0;
}
char head[41] = "";
read(ref, head, 40);
close(ref);
for (size_t i = log->len; i != 0; i--) {
Commit* commit = (Commit*)log->items + i - 1;
char human_readable_time[32];
time_t timestamp = atol(commit->committer.timestamp);
strftime(human_readable_time, sizeof(human_readable_time), "%Y-%m-%d %H:%M:%S %z", localtime(&timestamp));
char* marker = (strcmp(commit->hash, head) == 0) ? "\x1b[32m@\x1b[0m" : "\x1b[34m◯\x1b[0m";
printf("%s \x1b[33;1m%.7s\x1b[0m: %s\n│ %s <\x1b[38;5;240m\x1b]8;;mailto:%s\x1b\\%s\x1b]8;;\x1b\\\x1b[0m> (\x1b[36m%s\x1b[0m)\n\n",
marker,
commit->hash,
commit->message,
commit->committer.name,
commit->committer.email,
commit->committer.email,
human_readable_time
);
}
if (log->len != 0) printf("\n");
free(branch);
free(root);
commit_log_free(log);
return 1;
}
#include "../include/commands.h"
int main(int argc, char** argv) {
if (argc == 2 && (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help"))) {
usage(0);
usage(0);
}
const char* subcmd = argv[1];
if (strcmp(subcmd, "diff") != 0 &&
strcmp(subcmd, "init") != 0 &&
strcmp(subcmd, "status") != 0 &&
strcmp(subcmd, "commit") != 0 &&
strcmp(subcmd, "log") != 0 &&
strcmp(subcmd, "test") != 0
) {
fprintf(stderr, "ERROR: Unknown subcommand: '%s'\n", subcmd);
usage(2);
if (strcmp(subcmd, "diff") != 0
&& strcmp(subcmd, "init") != 0
&& strcmp(subcmd, "status") != 0
&& strcmp(subcmd, "commit") != 0
&& strcmp(subcmd, "log") != 0
&& strcmp(subcmd, "test") != 0 ) {
fprintf(stderr, "ERROR: Unknown subcommand: '%s'\n", subcmd); usage(2);
}
// Initializes a merk repository in the current directory
@ -811,6 +37,7 @@ int main(int argc, char** argv) {
diff(argv[2], argv[3]);
}
// Gives a list of untracked files in the repo as well as meta information
else if (strcmp(subcmd, "status") == 0) {
if (argc != 2) {
@ -821,6 +48,7 @@ int main(int argc, char** argv) {
status();
}
// Commits changes to the repo
else if (strcmp(subcmd, "commit") == 0) {
if (argc < 3) {
@ -831,6 +59,7 @@ int main(int argc, char** argv) {
commit(argc, argv);
}
else if (strcmp(subcmd, "log") == 0) {
if (argc != 2) {
printf("ERROR: too many arguments given!\n");
@ -840,10 +69,10 @@ int main(int argc, char** argv) {
commit_log();
}
else if (strcmp(subcmd, "test") == 0) {}
else {
usage(1);
}
else { usage(1); }
return 0;
}