mirror of
https://github.com/tomasriveral/NoteWrapper.git
synced 2026-10-11 09:16:18 +02:00
feat: git version control for backup
This commit is contained in:
1 parent
2e03edac2b
commit
82d016fdd7
4 files changed
+271
-10
No files matched your search
@@ -4,9 +4,9 @@ VERSION := $(shell git describe --tags --always --dirty)
|
|||||||
|
|
||||||
CFLAGS := -Wall -Wextra -Werror -O2 \
|
CFLAGS := -Wall -Wextra -Werror -O2 \
|
||||||
-DVERSION=\"$(VERSION)\" \
|
-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
|
SRC := src/main.c src/ui.c src/utils.c src/notes.c
|
||||||
TARGET := notewrapper
|
TARGET := notewrapper
|
||||||
|
|||||||
+64
-7
@@ -96,7 +96,19 @@ int main(int argc, char *argv[]) {
|
|||||||
debug("Parsing the JSON config");
|
debug("Parsing the JSON config");
|
||||||
|
|
||||||
cJSON *json = cJSON_Parse(data);
|
cJSON *json = cJSON_Parse(data);
|
||||||
|
|
||||||
if (!json) {
|
if (!json) {
|
||||||
|
const char *error_ptr = cJSON_GetErrorPtr();
|
||||||
|
|
||||||
|
if (error_ptr) {
|
||||||
|
error(
|
||||||
|
1,
|
||||||
|
"program",
|
||||||
|
"JSON parse error near: %.30s",
|
||||||
|
error_ptr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
free(data);
|
free(data);
|
||||||
}
|
}
|
||||||
error(!json, "program", "JSON parse error");
|
error(!json, "program", "JSON parse error");
|
||||||
@@ -210,6 +222,9 @@ int main(int argc, char *argv[]) {
|
|||||||
char **backupDirectoriesArray = malloc(numDirectories * sizeof(char *));
|
char **backupDirectoriesArray = malloc(numDirectories * sizeof(char *));
|
||||||
char **rsyncArgs = NULL;
|
char **rsyncArgs = NULL;
|
||||||
int rsyncArgsNumber = 0;
|
int rsyncArgsNumber = 0;
|
||||||
|
int gitEnabled = 0;
|
||||||
|
char *gitSignatureName = NULL;
|
||||||
|
char *gitSignatureEmail = NULL;
|
||||||
cJSON *backupJSON = cJSON_GetObjectItem(json, "backup");
|
cJSON *backupJSON = cJSON_GetObjectItem(json, "backup");
|
||||||
if (backupJSON && cJSON_IsObject(backupJSON)) {
|
if (backupJSON && cJSON_IsObject(backupJSON)) {
|
||||||
cJSON *doesBackupJSON = cJSON_GetObjectItem(backupJSON, "enable");
|
cJSON *doesBackupJSON = cJSON_GetObjectItem(backupJSON, "enable");
|
||||||
@@ -285,11 +300,36 @@ int main(int argc, char *argv[]) {
|
|||||||
"value is from an unexpected type",
|
"value is from an unexpected type",
|
||||||
configPath);
|
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 {
|
} else {
|
||||||
error(1, "user",
|
debug("git in %s is disabled", configPath);
|
||||||
"%s did not contained a enable value inside the backup section or the value is "
|
}
|
||||||
"from an unexpected type",
|
} else {
|
||||||
configPath);
|
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);
|
||||||
}
|
}
|
||||||
// handle rsyncs array of arguments.
|
// handle rsyncs array of arguments.
|
||||||
cJSON *rsyncArgsJSON = cJSON_GetObjectItem(backupJSON, "rsyncArgs");
|
cJSON *rsyncArgsJSON = cJSON_GetObjectItem(backupJSON, "rsyncArgs");
|
||||||
@@ -314,9 +354,7 @@ int main(int argc, char *argv[]) {
|
|||||||
configPath);
|
configPath);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
debug("In %s, \"backup\" wasn't set or we encountered a abnormal type. Defaulting to "
|
debug("In %s, \"backup\" wasn't set or we encountered a abnormal type. Defaulting to {\"enable\": false}.", configPath);
|
||||||
"{\"enable\": false}.",
|
|
||||||
configPath);
|
|
||||||
}
|
}
|
||||||
backup_config_end:
|
backup_config_end:
|
||||||
|
|
||||||
@@ -541,6 +579,12 @@ backup_config_end:
|
|||||||
|
|
||||||
initscr(); // initialize ncurses
|
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;
|
int shouldExit = 0;
|
||||||
while (!shouldExit) {
|
while (!shouldExit) {
|
||||||
// this loop is the vault selector
|
// this loop is the vault selector
|
||||||
@@ -620,6 +664,11 @@ backup_config_end:
|
|||||||
int shouldChangeVault = 0;
|
int shouldChangeVault = 0;
|
||||||
// we must find the directory from which the vault comes again.
|
// we must find the directory from which the vault comes again.
|
||||||
char *notesDirectoryString = getDirectoryFromVault(vaultSelected, vaultsArray, vaultsCount, vaultsCountForEachDirectory, directoriesArray, numDirectories, shouldDebug);
|
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) {
|
while (!shouldExit && !shouldChangeVault) {
|
||||||
// this loop is the note selector
|
// this loop is the note selector
|
||||||
int filesCount = 0;
|
int filesCount = 0;
|
||||||
@@ -712,6 +761,14 @@ backup_config_end:
|
|||||||
appendToFile(fullPath, "\n", shouldDebug);
|
appendToFile(fullPath, "\n", shouldDebug);
|
||||||
}
|
}
|
||||||
openEditor(fullPath, editorToOpen, shouldRender, shouldJumpToEnd, 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);
|
free(fullPath);
|
||||||
} else if (strcmp(noteSelected, "Create new note") == 0) {
|
} else if (strcmp(noteSelected, "Create new note") == 0) {
|
||||||
note_creation:
|
note_creation:
|
||||||
|
|||||||
+195
-1
@@ -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) {
|
void initAppFilesAndDirs(const char *home, const int shouldDebug) {
|
||||||
// sometimes .cache and .config doesn't exist. Such as with github actions machines
|
// sometimes .cache and .config doesn't exist. Such as with github actions machines
|
||||||
char config_dir[PATH_MAX];
|
char config_dir[PATH_MAX];
|
||||||
@@ -159,13 +338,28 @@ void initAppFilesAndDirs(const char *home, const int shouldDebug) {
|
|||||||
" \"~/Documents/\": \"path/to/backup/\"\n"
|
" \"~/Documents/\": \"path/to/backup/\"\n"
|
||||||
" },\n"
|
" },\n"
|
||||||
" \"interval\": \"weekly\",\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"
|
||||||
"}\n");
|
"}\n");
|
||||||
|
|
||||||
fclose(w);
|
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,
|
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) {
|
const int forceBackup, int *backupMessage, const int shouldDebug) {
|
||||||
int shouldBackup = 0;
|
int shouldBackup = 0;
|
||||||
|
|||||||
+10
@@ -17,6 +17,8 @@
|
|||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <git2.h>
|
||||||
|
|
||||||
#ifndef VERSION
|
#ifndef VERSION
|
||||||
#define VERSION "dev"
|
#define VERSION "dev"
|
||||||
#endif
|
#endif
|
||||||
@@ -85,4 +87,12 @@ will be added in the function.
|
|||||||
backupMessage will be set to 1 if a backup was launched.*/
|
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,
|
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);
|
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
|
#endif
|
||||||
Reference in new issue
Block a user