summaryrefslogtreecommitdiff
path: root/src/git.c
diff options
context:
space:
mode:
authorGravatar Anshul Gupta <ansg191@anshulg.com> 2025-04-09 13:24:53 -0700
committerGravatar Anshul Gupta <ansg191@anshulg.com> 2025-04-11 00:15:18 -0700
commitf277f41d83d450a1f28d350bb7d6da075d1d2741 (patch)
treee197a7de7e26b47cec57a46acdde4b6c74889ad5 /src/git.c
downloadgithub-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.c362
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;
+}