From 82d016fdd7a38202f302a600c849aa563a610dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Rivera?= Date: Fri, 14 Aug 2026 17:28:15 +0200 Subject: [PATCH] feat: git version control for backup --- makefile | 4 +- src/main.c | 77 +++++++++++++++++--- src/utils.c | 198 +++++++++++++++++++++++++++++++++++++++++++++++++++- src/utils.h | 10 +++ 4 files changed, 275 insertions(+), 14 deletions(-) diff --git a/makefile b/makefile index 6c91b0a..189d565 100644 --- a/makefile +++ b/makefile @@ -4,9 +4,9 @@ VERSION := $(shell git describe --tags --always --dirty) CFLAGS := -Wall -Wextra -Werror -O2 \ -DVERSION=\"$(VERSION)\" \ - $(shell pkg-config --cflags libcjson ncurses) + $(shell pkg-config --cflags libcjson ncurses libgit2) -LDFLAGS := $(shell pkg-config --libs libcjson ncurses) +LDFLAGS := $(shell pkg-config --libs libcjson ncurses libgit2) SRC := src/main.c src/ui.c src/utils.c src/notes.c TARGET := notewrapper diff --git a/src/main.c b/src/main.c index 1916e4a..31dc783 100644 --- a/src/main.c +++ b/src/main.c @@ -96,9 +96,21 @@ int main(int argc, char *argv[]) { debug("Parsing the JSON config"); cJSON *json = cJSON_Parse(data); - if (!json) { - free(data); - } + + if (!json) { + const char *error_ptr = cJSON_GetErrorPtr(); + + if (error_ptr) { + error( + 1, + "program", + "JSON parse error near: %.30s", + error_ptr + ); + } + + free(data); + } error(!json, "program", "JSON parse error"); // Parse all of the directories which will( or do) contain the vaults @@ -210,6 +222,9 @@ int main(int argc, char *argv[]) { char **backupDirectoriesArray = malloc(numDirectories * sizeof(char *)); char **rsyncArgs = NULL; int rsyncArgsNumber = 0; + int gitEnabled = 0; + char *gitSignatureName = NULL; + char *gitSignatureEmail = NULL; cJSON *backupJSON = cJSON_GetObjectItem(json, "backup"); if (backupJSON && cJSON_IsObject(backupJSON)) { cJSON *doesBackupJSON = cJSON_GetObjectItem(backupJSON, "enable"); @@ -285,11 +300,36 @@ int main(int argc, char *argv[]) { "value is from an unexpected type", configPath); } + + // Configure Git backup + cJSON *gitJSON = cJSON_GetObjectItem(backupJSON, "git"); + + if (gitJSON && cJSON_IsObject(gitJSON)) { + cJSON *gitEnableJSON = cJSON_GetObjectItem(gitJSON, "enable"); + cJSON *gitNameJSON = cJSON_GetObjectItem(gitJSON, "name"); + cJSON *gitEmailJSON = cJSON_GetObjectItem(gitJSON, "email"); + + error(!gitEnableJSON || !cJSON_IsBool(gitEnableJSON), "user", "Entry \"git.enable\" in %s must be a bool.", configPath); + error(!gitNameJSON || !cJSON_IsString(gitNameJSON), "user", "Entry \"git.name\" in %s must be a string.", configPath); + error(!gitEmailJSON || !cJSON_IsString(gitEmailJSON), "user", "Entry \"git.email\" in %s must be a string.", configPath); + + gitEnabled = cJSON_IsTrue(gitEnableJSON) ? 1 : 0; + + if (gitEnabled) { + gitSignatureName = strdup(cJSON_GetStringValue(gitNameJSON)); + gitSignatureEmail = strdup(cJSON_GetStringValue(gitEmailJSON)); + + error(gitSignatureName == NULL || gitSignatureEmail == NULL, "program", "malloc failed"); + + debug("git in %s is enabled with name \"%s\" and email \"%s\"", configPath, gitSignatureName, gitSignatureEmail); + } else { + debug("git in %s is disabled", configPath); + } + } else { + error(1, "user", "Entry \"git\" in %s must be an object containing \"enable\", \"name\", and \"email\".", configPath); + } } else { - error(1, "user", - "%s did not contained a enable value inside the backup section or the value is " - "from an unexpected type", - configPath); + error(1, "user", "%s did not contained a enable value inside the backup section or the value is from an unexpected type", configPath); } // handle rsyncs array of arguments. cJSON *rsyncArgsJSON = cJSON_GetObjectItem(backupJSON, "rsyncArgs"); @@ -314,9 +354,7 @@ int main(int argc, char *argv[]) { configPath); } } else { - debug("In %s, \"backup\" wasn't set or we encountered a abnormal type. Defaulting to " - "{\"enable\": false}.", - configPath); + debug("In %s, \"backup\" wasn't set or we encountered a abnormal type. Defaulting to {\"enable\": false}.", configPath); } backup_config_end: @@ -541,6 +579,12 @@ backup_config_end: initscr(); // initialize ncurses + // initialized libgit2 + if (gitEnabled) { + int libgit2InitializationTimes = git_libgit2_init(); + error(libgit2InitializationTimes != 1, "program", "libgit2 has been initialized %d times (instead of one)", libgit2InitializationTimes); + } + int shouldExit = 0; while (!shouldExit) { // this loop is the vault selector @@ -620,6 +664,11 @@ backup_config_end: int shouldChangeVault = 0; // we must find the directory from which the vault comes again. char *notesDirectoryString = getDirectoryFromVault(vaultSelected, vaultsArray, vaultsCount, vaultsCountForEachDirectory, directoriesArray, numDirectories, shouldDebug); + + if (gitEnabled) { // we check if we need to git init the vault each time we enter it + ensureGitDirectory(notesDirectoryString, vaultSelected, gitSignatureName, gitSignatureEmail, shouldDebug); + } + while (!shouldExit && !shouldChangeVault) { // this loop is the note selector int filesCount = 0; @@ -712,6 +761,14 @@ backup_config_end: appendToFile(fullPath, "\n", shouldDebug); } openEditor(fullPath, editorToOpen, shouldRender, shouldJumpToEnd, shouldDebug); + if (gitEnabled) { // After closing a file, we need to update the git repo (if git is enable + char *date = getDateAndTime(); + char *commitMsg = malloc(29); + snprintf(commitMsg, 29, "Update - %s", date); + gitBackupUpdate(notesDirectoryString, vaultSelected, gitSignatureName, gitSignatureEmail, commitMsg, shouldDebug); + free(date); + free(commitMsg); + } free(fullPath); } else if (strcmp(noteSelected, "Create new note") == 0) { note_creation: diff --git a/src/utils.c b/src/utils.c index 0f49b6e..d8f6bba 100644 --- a/src/utils.c +++ b/src/utils.c @@ -113,6 +113,185 @@ static void ensureDir(const char *path, const int shouldDebug) { } } +void ensureGitDirectory(const char *path, const char *vault, const char *signatureName, const char *signatureEmail, const int shouldDebug) { + git_repository *repo = NULL; + char *fullPath = malloc(PATH_MAX); + snprintf(fullPath, PATH_MAX, "%s/%s", path, vault); + + int git_repo_error_code = git_repository_open(&repo, fullPath); + if (git_repo_error_code) { // error (most probably that the git repo wasn't initialized) + int git_init_error_code = git_repository_init(&repo, fullPath, 0); // the 0 indicates that the directory isn't bare. See https://libgit2.org/docs/reference/main/repository/git_repository_init.html + error(git_init_error_code, "program", "git initialization didn't worked. Error code : %d\nSee https://libgit2.org/docs/reference/main/repository/git_repository_init.html for documentation\n\n%s", git_init_error_code, git_error_last()->message); + debug("git repo %s was initialized.", fullPath); + + // we need to setup the .gitignore + char *gitignorePath = malloc(PATH_MAX); + snprintf(gitignorePath, PATH_MAX, "%s/.gitignore", fullPath); + FILE *gitignore = fopen(gitignorePath, "a"); + error(gitignore == NULL, "program", "failed to open/create %s\n%s", gitignorePath, git_error_last()->message); + fputs("*\n", gitignore); + fputs("!*/\n", gitignore); + fputs("!*.md\n", gitignore); + fclose(gitignore); + debug("Succesfully wrote .gitignore to %s", gitignorePath); + git_repository_free(repo); // we free to be able to open it in gitBackupUpdate() + char *date = getDateAndTime(); + char *commitMsg = malloc(37); + snprintf(commitMsg, 37, "Initial commit - %s", date); + gitBackupUpdate(path, vault, signatureName, signatureEmail, commitMsg, shouldDebug); + free(date); + free(commitMsg); + } else { // the git repo already exists + debug("%s is already a git repo. Skipping initialization...", fullPath); + git_repository_free(repo); + } + + free(fullPath); +} + +void gitBackupUpdate(const char *path, const char *vault, const char *signatureName, const char *signatureEmail, const char *commitMsg, const int shouldDebug) { + git_repository *repo = NULL; + git_index *index = NULL; + git_tree *tree = NULL; + git_tree *head_tree = NULL; + git_commit *head_commit = NULL; + git_signature *signature = NULL; + git_diff *diff = NULL; + + git_oid tree_id; + git_oid commit_id; + + char *fullPath = malloc(PATH_MAX); + + error(fullPath == NULL, "program", "Failed to allocate path"); + + snprintf(fullPath, PATH_MAX, "%s/%s", path, vault); + + /* Open repository */ + int return_code = git_repository_open(&repo, fullPath); + + error(return_code, "program", "Failed to open Git repository\n%s", git_error_last()->message); + + /* Get the index */ + return_code = git_repository_index(&index, repo); + error(return_code, "program", "Failed to get Git index\n%s", git_error_last()->message); + + /* git add . */ + return_code = git_index_add_all(index, NULL, 0, NULL, NULL); + error(return_code, "program", "Failed to add files\n%s", git_error_last()->message); + + return_code = git_index_write(index); + error(return_code, "program", "Failed to write index\n%s", git_error_last()->message); + + /* + * Check whether there are changes to commit. + * + * For a new repository there is no HEAD yet, so the index + * itself tells us whether there is anything to commit. + */ + int has_changes = 0; + + if (git_repository_head_unborn(repo)) { + has_changes = git_index_entrycount(index) > 0; + } else { + git_oid head_oid; + + return_code = git_reference_name_to_id( + &head_oid, + repo, + "HEAD" + ); + error(return_code, "program", "Failed to get HEAD\n%s", git_error_last()->message); + + return_code = git_commit_lookup( + &head_commit, + repo, + &head_oid + ); + error(return_code, "program", "Failed to lookup HEAD commit\n%s", git_error_last()->message); + + return_code = git_commit_tree( + &head_tree, + head_commit + ); + error(return_code, "program", "Failed to get HEAD tree\n%s", git_error_last()->message); + + return_code = git_diff_tree_to_index( + &diff, + repo, + head_tree, + index, + NULL + ); + + error(return_code, "program", "Failed to create diff\n%s", git_error_last()->message); + + has_changes = git_diff_num_deltas(diff) > 0; + } + + /* Nothing changed, so don't create a commit */ + if (!has_changes) { + debug("No changes to commit in %s", fullPath); + goto cleanup; + } + + /* index → tree */ + return_code = git_index_write_tree(&tree_id, index); + error(return_code, "program", "Failed to write tree\n%s", git_error_last()->message); + + return_code = git_tree_lookup(&tree, repo, &tree_id); + error(return_code, "program", "Failed to lookup tree\n%s", git_error_last()->message); + + /* Author/committer */ + return_code = git_signature_now( + &signature, + signatureName, + signatureEmail + ); + error(return_code, "program", "Failed to create signature\n%s", git_error_last()->message); + + /* Create commit */ + if (git_repository_head_unborn(repo)) { + return_code = git_commit_create_v( + &commit_id, + repo, + "HEAD", + signature, + signature, + NULL, + commitMsg, + tree, + 0 + ); + } else { + return_code = git_commit_create_v( + &commit_id, + repo, + "HEAD", + signature, + signature, + NULL, + commitMsg, + tree, + 1, + head_commit + ); + } + error(return_code, "program", "Failed to create commit\n%s", git_error_last()->message); + + debug("Created commit %s in %s", commitMsg, fullPath); + +cleanup: + git_diff_free(diff); + git_signature_free(signature); + git_tree_free(tree); + git_tree_free(head_tree); + git_commit_free(head_commit); + git_index_free(index); + git_repository_free(repo); + free(fullPath); +} + void initAppFilesAndDirs(const char *home, const int shouldDebug) { // sometimes .cache and .config doesn't exist. Such as with github actions machines char config_dir[PATH_MAX]; @@ -157,15 +336,30 @@ void initAppFilesAndDirs(const char *home, const int shouldDebug) { " \"enable\": false,\n" " \"directory\": {\n" " \"~/Documents/\": \"path/to/backup/\"\n" - " },\n" + " },\n" " \"interval\": \"weekly\",\n" - " \"rsyncArgs\": [\"-Lqah\", \"--update\"]\n" + " \"rsyncArgs\": [\"-Lqah\", \"--update\"],\n" + " \"git\": {\n" + " \"enable\": false,\n" + " \"email\": \"mail@example.com\",\n" + " \"name\": \"Example Example\"\n" + " }\n" " }\n" "}\n"); fclose(w); } +char *getDateAndTime() { + char *return_value = malloc(20); + time_t now = time(NULL); + struct tm tm_now; + localtime_r(&now, &tm_now); + + strftime(return_value, sizeof(return_value), "%Y-%m-%%d %H:%M:%%S", &tm_now); + return return_value; +} + void handleBackups(char **sourceDirectoryArray, const int sourceNumber, char **destinationDirectoryArray, const char *homeDir, const int interval, const char **rsyncArguments, const int rsyncArgumentsNumber, const int forceBackup, int *backupMessage, const int shouldDebug) { int shouldBackup = 0; diff --git a/src/utils.h b/src/utils.h index be1d260..207aee9 100644 --- a/src/utils.h +++ b/src/utils.h @@ -17,6 +17,8 @@ #include #include #include +#include + #ifndef VERSION #define VERSION "dev" #endif @@ -85,4 +87,12 @@ will be added in the function. backupMessage will be set to 1 if a backup was launched.*/ void handleBackups(char **sourceDirectoryArray, const int sourceDirectoryNumber, char **destinationDirectoryArray, const char *homeDir, const int interval, const char **rsyncArgs, const int rsyncArgsNumber, const int forceBackup, int *backupMessage, const int shouldDebug); +/*Equivalent of git init. +If the directory wasn't git initialized. We do it.*/ +void ensureGitDirectory(const char *path, const char *vault, const char *signatureName, const char *signatureEmail, const int shouldDebug); +/*Configures git, stages everything, commits everything*/ +void gitBackupUpdate(const char *path, const char *vault, const char *signatureName, const char *signatureEmail, const char *commitMsg, const int shouldDebug); +// yyyy-mm-dd hh:mm:ss +// Don't forget to free the pointer. +char *getDateAndTime(); #endif