diff options
author | 2025-04-09 13:24:53 -0700 | |
---|---|---|
committer | 2025-04-11 00:15:18 -0700 | |
commit | f277f41d83d450a1f28d350bb7d6da075d1d2741 (patch) | |
tree | e197a7de7e26b47cec57a46acdde4b6c74889ad5 /src/git.c | |
download | github-mirror-f277f41d83d450a1f28d350bb7d6da075d1d2741.tar.gz github-mirror-f277f41d83d450a1f28d350bb7d6da075d1d2741.tar.zst github-mirror-f277f41d83d450a1f28d350bb7d6da075d1d2741.zip |
Initial Commit
Add cmake github workflow
Install dependencies in gh action
Fix missing directory
Fix compiler errors
Fix group name to gid conversion
Force cjson to statically link
Add header guards to query headers
Add install and cpack to CMakeLists.txt
Add config search and CLI arg parsing
Improve docs for git.c
Fix program continuing on -h flag
Diffstat (limited to 'src/git.c')
-rw-r--r-- | src/git.c | 362 |
1 files changed, 362 insertions, 0 deletions
diff --git a/src/git.c b/src/git.c new file mode 100644 index 0000000..6720e54 --- /dev/null +++ b/src/git.c @@ -0,0 +1,362 @@ +// +// Created by Anshul Gupta on 4/6/25. +// + +#include "git.h" + +#include <errno.h> +#include <grp.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/fcntl.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +extern char **environ; + +/** + * Constructs the full path to the git repository based on the base path, owner, + * and name. If the name is NULL, it constructs the path to the owner's + * directory. + * @param base Base path for the git repository + * @param owner Owner of the repository + * @param name Name of the repository + * @return A string containing the full path to the git repository or owner's + * directory. + */ +static char *get_git_path(const char *base, const char *owner, const char *name) +{ + if (!base || !owner) + return NULL; + + const char *format = name ? "%s/%s/%s.git" : "%s/%s"; + + // Calculate the length of the string + const int len = snprintf(NULL, 0, format, base, owner, name); + if (len < 0) + return NULL; + + // Allocate memory for the string + char *path = malloc(len + 1); + if (!path) + return NULL; + + // Format the string + if (snprintf(path, len + 1, format, base, owner, name) < 0) { + free(path); + return NULL; + } + return path; +} + +/** + * Adds authentication information to the HTTPS URL for git. + * @param url The HTTPS URL to modify + * @param user The username for authentication + * @param token The token for authentication + * @return A new string containing the modified URL with authentication + * information + */ +static char *add_url_auth(const char *url, const char *user, const char *token) +{ + const char *https_prefix = "https://"; + const size_t prefix_len = strlen(https_prefix); + size_t new_len; + char *new_url; + + if (!url || !user || !token) + return NULL; + + // Find the position of "https://" + if (strncmp(url, https_prefix, prefix_len) != 0) { + fprintf(stderr, "Error: URL does not start with https://\n"); + return NULL; + } + + // Calculate the length of the new URL + new_len = strlen(url) + strlen(user) + strlen(token) + + 2; // 2 for "@" and ":" + + // Allocate memory for the new URL + if (!((new_url = malloc(new_len + 1)))) { + perror("malloc"); + return NULL; + } + + // Construct the new URL + snprintf(new_url, new_len + 1, "https://%s:%s@%s", user, token, + url + prefix_len); + + return new_url; +} + +/** + * Drops the permissions of the current process to the specified user and group. + * @param ctx Repository context + * @return 0 on success, -1 on error + */ +static int drop_perms(const struct repo_ctx *ctx) +{ + // Drop supplementary groups + if (setgroups(0, NULL) != 0) { + perror("setgroups"); + return -1; + } + // Set gid + if (setgid(ctx->cfg->git_group) == -1) { + perror("setgid"); + return -1; + } + // Set uid + if (setuid(ctx->cfg->git_owner) == -1) { + perror("setuid"); + return -1; + } + return 0; +} + +/** + * Checks if the git repository at the specified path is a mirror. + * @param path Path to the git repository + * @param ctx Repository context + * @return 1 if the repository is a mirror, 0 if not + */ +static int contains_mirror(const char *path, const struct repo_ctx *ctx) +{ + const pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + return 0; + } + + if (pid == 0) { + // Child process + + // Redirect stdout to /dev/null + const int devnull = open("/dev/null", O_WRONLY); + if (devnull == -1) { + perror("open"); + _exit(127); + } + if (dup2(devnull, STDOUT_FILENO) == -1) { + perror("dup2"); + close(devnull); + _exit(127); + } + close(devnull); + + // Change uid and gid to the user specified in the config + if (drop_perms(ctx)) + _exit(127); + char *args[] = { + "git", "--git-dir", (char *) path, + "config", "--get", "remote.origin.mirror", + NULL, + }; + execvp("git", args); + perror("execvp"); + _exit(127); // execvp only returns on error + } + + int status; + pid_t result; + while ((result = waitpid(pid, &status, 0)) == -1 && errno == EINTR) { + } + if (result == -1) { + perror("waitpid"); + return 0; + } + if (WIFEXITED(status)) { + // Check if the exit status is 0 (success) + if (WEXITSTATUS(status) == 0) + return 1; // Repo exists + if (WEXITSTATUS(status) == 1) + return 0; // Repo does not exist + } + fprintf(stderr, "Error: git command failed with status %d\n", + WEXITSTATUS(status)); + return 0; // Error occurred +} + +/** + * Creates a mirror of the git repository at the specified path. + * @param path Full path to the git repository + * @param ctx Context containing the repository information + * @return 0 on success, -1 on error + */ +static int create_mirror(const char *path, const struct repo_ctx *ctx) +{ + const pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + return -1; + } + + if (pid == 0) { + // Child process + // Convert the URL to a format that git can use + char *url = add_url_auth(ctx->url, ctx->username, + ctx->cfg->token); + + // Change uid and gid to the user specified in the config + if (drop_perms(ctx)) + _exit(127); + + char *args[] = { + "git", "clone", "--mirror", + url, (char *) path, NULL, + }; + execvp("git", args); + perror("execvp"); + _exit(127); // execvp only returns on error + } + + int status; + pid_t result; + while ((result = waitpid(pid, &status, 0)) == -1 && errno == EINTR) { + } + if (result == -1) { + perror("waitpid"); + return -1; + } + + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) + return 0; // Success + fprintf(stderr, "Error: git clone failed with status %d\n", + WEXITSTATUS(status)); + return -1; // Error occurred +} + +/** + * Creates the directory structure for the git repository. + * @param ctx Repository context + * @return 0 on success, -1 on error + */ +static int create_git_path(const struct repo_ctx *ctx) +{ + // Create owner directory if it doesn't exist + char *owner_path = + get_git_path(ctx->cfg->git_base, ctx->cfg->owner, NULL); + if (!owner_path) + return -1; + if (mkdir(owner_path, 0755) == -1 && errno != EEXIST) { + perror("mkdir"); + free(owner_path); + return -1; + } + // Set the permissions of the owner directory to 0775 + if (chmod(owner_path, 0775) == -1) { + perror("chmod"); + free(owner_path); + return -1; + } + free(owner_path); + + // Create repo directory if it doesn't exist + char *repo_path = get_git_path(ctx->cfg->git_base, ctx->cfg->owner, + ctx->name); + if (!repo_path) + return -1; + if (mkdir(repo_path, 0755) == -1 && errno != EEXIST) { + perror("mkdir"); + free(repo_path); + return -1; + } + + // Chown the repo directory to the specified user and group + if (chown(repo_path, ctx->cfg->git_owner, ctx->cfg->git_group) == -1) { + perror("chown"); + free(repo_path); + return -1; + } + + free(repo_path); + return 0; +} + +/** + * Updates the git repository at the specified path from the remote. + * @param path Full path to the git repository + * @param ctx Context containing the repository information + * @return 0 on success, -1 on error + */ +static int update_mirror(const char *path, const struct repo_ctx *ctx) +{ + const pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + return -1; + } + + if (pid == 0) { + // Child process + // Change uid and gid to the user specified in the config + if (drop_perms(ctx)) + _exit(127); + + char *args[] = { + "git", "--git-dir", (char *) path, "remote", + "update", "--prune", NULL, + }; + execvp("git", args); + perror("execvp"); + _exit(127); // execvp only returns on error + } + + int status; + pid_t result; + while ((result = waitpid(pid, &status, 0)) == -1 && errno == EINTR) { + } + if (result == -1) { + perror("waitpid"); + return -1; + } + + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) + return 0; // Success + fprintf(stderr, "Error: git remote update failed with status %d\n", + WEXITSTATUS(status)); + return -1; // Error occurred +} + +int git_mirror_repo(const struct repo_ctx *ctx) +{ + char *path = get_git_path(ctx->cfg->git_base, ctx->cfg->owner, + ctx->name); + if (!path) { + perror("get_git_path"); + return -1; + } + + // Check whether repo exists + if (contains_mirror(path, ctx)) { + // Repo exists, so we can just update it + printf("Repo already exists, updating...\n"); + if (update_mirror(path, ctx) == -1) { + perror("update_mirror"); + free(path); + return -1; + } + free(path); + return 0; + } + + // Repo does not exist, so we need to clone it + printf("Repo does not exist, cloning...\n"); + if (create_git_path(ctx) == -1) { + perror("create_git_path"); + free(path); + return -1; + } + if (create_mirror(path, ctx) == -1) { + perror("create_mirror"); + free(path); + return -1; + } + + free(path); + return 0; +} |