diff --git a/src/main.c b/src/main.c index 505ae9e..3167a1b 100644 --- a/src/main.c +++ b/src/main.c @@ -4,12 +4,16 @@ #include #include #include +#include #include "action_list.h" #include "file.h" #include "myers.h" #include "tree.h" #include "base_file_buffer.h" +#include "config.h" +#include "commit.h" +#include "hash.h" static void usage(int exitcode) { printf("usage: merk []\n\ @@ -30,7 +34,9 @@ int main(int argc, char **argv) { if (strcmp(subcmd, "diff") != 0 && strcmp(subcmd, "init") != 0 && strcmp(subcmd, "status") != 0 && - strcmp(subcmd, "commit") != 0 + strcmp(subcmd, "commit") != 0 && + strcmp(subcmd, "log") != 0 && + strcmp(subcmd, "test") != 0 ) { fprintf(stderr, "ERROR: Unknown subcommand: '%s'\n", subcmd); usage(2); @@ -65,6 +71,8 @@ int main(int argc, char **argv) { 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 -1; @@ -73,6 +81,16 @@ int main(int argc, char **argv) { int main_info = open(".merk/info/main", O_WRONLY | O_CREAT, 0666); if (main_info < 0) return -1; close(main_info); + + int main_log = open(".merk/logs/branches/main", O_WRONLY | O_CREAT, 0666); + if (main_log < 0) return -1; + close(main_log); + + CommitLog* empty = commit_log_new(); + if (!empty) { perror("ERROR: failed to create commit log for branch main!"); return -1; } + if (!write_commit_log(empty, ".merk/logs/branches/main")) { perror("ERROR: failed to write initial main commit log!"); commit_log_free(empty); return -1; } + + commit_log_free(empty); } // Prints out a visual representation of the diff of the files provided @@ -126,40 +144,56 @@ int main(int argc, char **argv) { 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(); for (size_t idx = 0; idx < files->len; idx++) { char* file_name = ((FileInfo*)files->items + idx)->name; - // This if is just for debugging purposes right now and will be - // changed later to the actual implementation - if (base_file_buffer_search(tracked_list, file_name)) { - string_buffer_push(modified_files, file_name); - continue; - } + if (base_file_buffer_search(tracked_list, file_name)) continue; string_buffer_push(untracked_files, file_name); } + 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", modified_files->items[idx]); } printf("\n"); + for (size_t idx = 0; idx < deleted_files->len; idx++) { + printf("\x1b[31;4mD\x1b[0m \x1b[31;9m%s\x1b[0m\n", deleted_files->items[idx]); + } + + if (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", untracked_files->items[idx]); } - printf("\nOn branch \x1b[39;1;4m%s\x1b[0m with 0 commits\n", branch); + printf("\nOn branch \x1b[39;1;4m%s\x1b[0m with %d commits\n", branch, log->len); file_info_buffer_free(files); free(root); @@ -167,40 +201,361 @@ int main(int argc, char **argv) { } // Commits changes to the repo else if (strcmp(subcmd, "commit") == 0) { - if (argc == 2) { + if (argc < 3) { printf("ERROR: too little arguments given!\n"); - printf("Usage: merk commit [-m ] ..."); + printf("Usage: merk commit [-m ] ...\n"); exit(1); } + 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); + } + + // Ensure files to commit are correctly identified + 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 relative[PATH_MAX] = ""; char* root = find_root(relative); + if (!root) { + free_config(&config); + free(commit_message); + return 1; + } 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); + free_config(&config); + free(commit_message); + return 1; + } + char info_path[PATH_MAX]; snprintf(info_path, sizeof(info_path), "%s/.merk/info/%s", root, branch); - char* base_file_list = get_file_content(info_path); - for (size_t i = 2; i < argc; i++) { + BaseFileBuffer* base_files = base_file_buffer_new(); + if (!base_files) { + free(branch); + free(root); + 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(root); + 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(root); + 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); - free(repo_path); + if (repo_path) { + string_buffer_push(files, repo_path); + free(repo_path); + } } else { - perror("ERROR: given arguments were not all files!"); + 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(base_file_list); + 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; } } + StringBuffer* file_hashes = string_buffer_new(); + if (!file_hashes) { + 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 < files->len; idx++) { + BaseFileInfo* base_file = base_file_buffer_search(base_files, files->items[idx]); + if (!base_file) { + 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 + char file_hash[41]; + snapshot_file(files->items[idx], root, 0, file_hash); + string_buffer_push(file_hashes, file_hash); + continue; + } + size_t base_num = base_file->base_num; + size_t diff_num = base_file->diff_num + 1; + base_file_buffer_remove(base_files, files->items[idx]); + BaseFileInfo new_info = (BaseFileInfo){.base_num = base_num, .diff_num = diff_num, .name = strdup(files->items[idx])}; + base_file_buffer_push(base_files, new_info); + // Take the diff and put it into the object database + char basefile_hash[41]; + char id[2 + snprintf(NULL, 0, "%d", base_num) + strlen(files->items[idx])]; + snprintf(id, sizeof(id), "%s %d", files->items[idx], base_num); + object_hash(BaseFileObject, id, basefile_hash); + + char diff_hash[41]; + save_file_diff(files->items[idx], root, base_num, basefile_hash, diff_hash); + string_buffer_push(file_hashes, diff_hash); + } + + base_file_buffer_sort(base_files); + write_base_file_list(base_files, info_path); + FileInfoBuffer* tree = basefilebuffer_to_fileinfobuffer(base_files); + if (!tree) { + list_free(file_hashes); + free(commit_message); + list_free(files); + base_file_buffer_free(base_files); + free(branch); + free(root); + free_config(&config); + return 1; + } + 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); + list_free(file_hashes); + 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); + list_free(file_hashes); + 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); + + printf("this is the ref path: %s\n", ref_path); + + int ref = open(ref_path, O_WRONLY | O_CREAT | O_TRUNC, 0666); + if (ref < 0) return -1; + write(ref, commit_hash, 40); + close(ref); + + // Cleanup + list_free(files); + base_file_buffer_free(base_files); free(branch); - free(base_file_list); + free(root); + free_config(&config); + free(commit_message); } + else if (strcmp(subcmd, "log") == 0) { + CommitLog* log = commit_log_new(); + if (!log) return 1; + + char* root = find_root(NULL); + if (!root) { + return 1; + } + + 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 1; + } + + char log_path[PATH_MAX]; + snprintf(log_path, sizeof(log_path), "%s/.merk/logs/branches/%s", root, branch); + + read_commit_log(log, log_path); + + 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(×tamp)); + + printf("\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", + commit->hash, + commit->message, + commit->committer.name, + commit->committer.email, + commit->committer.email, + human_readable_time + ); + } + + free(branch); + free(root); + commit_log_free(log); + } + else if (strcmp(subcmd, "test") == 0) {} else { usage(1); }