#include #include #include #include #include #include #include #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 []\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(×tamp)); 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; } int main(int argc, char** argv) { if (argc == 2 && (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help"))) { 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); } // Initializes a merk repository in the current directory if (strcmp(subcmd, "init") == 0) { if (argc != 2) { printf("ERROR: too many arguments given!\n"); printf("Usage: merk init"); exit(1); } init(); } // Prints out a visual representation of the diff of the files provided else if (strcmp(subcmd, "diff") == 0) { if (argc != 4) { printf("ERROR: too many/little arguments given!\n"); printf("Usage: merk diff "); exit(1); } 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) { printf("ERROR: too many arguments given!\n"); printf("Usage: merk status"); exit(1); } status(); } // Commits changes to the repo else if (strcmp(subcmd, "commit") == 0) { if (argc < 3) { printf("ERROR: too little arguments given!\n"); printf("Usage: merk commit [-m ] ...\n"); exit(1); } commit(argc, argv); } else if (strcmp(subcmd, "log") == 0) { if (argc != 2) { printf("ERROR: too many arguments given!\n"); printf("Usage: merk log"); exit(1); } commit_log(); } else if (strcmp(subcmd, "test") == 0) {} else { usage(1); } return 0; }