summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/alloc.h27
-rw-r--r--src/buffer.c51
-rw-r--r--src/buffer.h34
-rw-r--r--src/config.c307
-rw-r--r--src/config.h51
-rw-r--r--src/git.c362
-rw-r--r--src/git.h24
-rw-r--r--src/github.c290
-rw-r--r--src/github.h30
-rw-r--r--src/github_types.c151
-rw-r--r--src/github_types.h28
-rw-r--r--src/main.c132
-rw-r--r--src/precheck.c112
-rw-r--r--src/precheck.h16
14 files changed, 1615 insertions, 0 deletions
diff --git a/src/alloc.h b/src/alloc.h
new file mode 100644
index 0000000..0a09a30
--- /dev/null
+++ b/src/alloc.h
@@ -0,0 +1,27 @@
+//
+// Created by Anshul Gupta on 4/9/25.
+//
+
+#ifndef ALLOC_H
+#define ALLOC_H
+
+#ifdef TEST_ALLOC
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include <cmocka.h>
+#define gmalloc(size) test_malloc(size)
+#define gcalloc(num, size) test_calloc(num, size)
+#define grealloc(ptr, size) test_realloc(ptr, size)
+#define gfree(ptr) test_free(ptr)
+#else
+#include <stdlib.h>
+#define gmalloc(size) malloc(size)
+#define gcalloc(num, size) calloc(num, size)
+#define grealloc(ptr, size) realloc(ptr, size)
+#define gfree(ptr) free(ptr)
+#endif
+
+#endif // ALLOC_H
diff --git a/src/buffer.c b/src/buffer.c
new file mode 100644
index 0000000..51a3e3c
--- /dev/null
+++ b/src/buffer.c
@@ -0,0 +1,51 @@
+//
+// Created by Anshul Gupta on 4/4/25.
+//
+
+#include "buffer.h"
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "alloc.h"
+
+buffer_t buffer_new(size_t cap)
+{
+ buffer_t buf;
+
+ if (cap == 0)
+ return (buffer_t) {NULL, 0, 0};
+
+ cap = (cap + 7) & ~7; // Align to 8 bytes
+ buf.data = gmalloc(cap);
+ if (!buf.data)
+ abort();
+
+ return (buffer_t) {buf.data, 0, cap};
+}
+
+void buffer_free(buffer_t buf) { gfree(buf.data); }
+
+void buffer_reserve(buffer_t *buf, size_t cap)
+{
+ if (buf->cap >= cap)
+ return;
+
+ cap = (cap + 7) & ~7; // Align to 8 bytes
+ uint8_t *new_data = grealloc(buf->data, cap);
+ if (!new_data)
+ abort();
+ buf->data = new_data;
+ buf->cap = cap;
+}
+
+void buffer_append(buffer_t *buf, const void *data, size_t len)
+{
+ if (buf->len + len > buf->cap)
+ buffer_reserve(buf, buf->len + len);
+
+ memcpy(buf->data + buf->len, data, len);
+ buf->len += len;
+ assert(buf->len <= buf->cap);
+}
diff --git a/src/buffer.h b/src/buffer.h
new file mode 100644
index 0000000..ea7c7c3
--- /dev/null
+++ b/src/buffer.h
@@ -0,0 +1,34 @@
+//
+// Created by Anshul Gupta on 4/4/25.
+//
+
+#ifndef BUFFER_H
+#define BUFFER_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+typedef struct {
+ /// Pointer to the start of the buffer
+ uint8_t *data;
+ /// Size of the buffer
+ size_t len;
+ /// Size of the allocated buffer
+ size_t cap;
+} buffer_t;
+
+/// Create a new buffer with the given initial capacity
+buffer_t buffer_new(size_t cap);
+
+/// Free the buffer
+void buffer_free(buffer_t buf);
+
+/// Resize the buffer to the given capacity
+/// Does nothing if the new capacity is less than or equal to the
+/// current capacity.
+void buffer_reserve(buffer_t *buf, size_t cap);
+
+/// Append data to the buffer
+void buffer_append(buffer_t *buf, const void *data, size_t len);
+
+#endif // BUFFER_H
diff --git a/src/config.c b/src/config.c
new file mode 100644
index 0000000..d20f4f5
--- /dev/null
+++ b/src/config.c
@@ -0,0 +1,307 @@
+//
+// Created by Anshul Gupta on 4/6/25.
+//
+
+#include "config.h"
+
+#include <ctype.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+const char *config_locations[] = {
+ "/etc/github_mirror/config.ini",
+ "/usr/local/etc/github_mirror/config.ini",
+ "/usr/local/github_mirror/config.ini",
+ "config.ini",
+ NULL,
+};
+
+enum config_section {
+ section_none,
+ section_github,
+ section_git,
+};
+
+
+static char *file_read(const char *path, size_t *size_out)
+{
+ char *contents = NULL;
+
+ // Open the file for reading
+ const int fd = open(path, O_RDONLY);
+ if (fd < 0) {
+ perror("Error reading config file");
+ return NULL;
+ }
+
+ // Stat the file to get its size
+ struct stat st;
+ if (fstat(fd, &st) < 0) {
+ perror("Error getting file size");
+ goto end;
+ }
+
+ // Get the size of the file
+ const size_t size = st.st_size;
+ if (size == 0) {
+ fprintf(stderr, "Error reading config file: file is empty\n");
+ goto end;
+ }
+
+ // Map the file into memory
+ contents = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
+ if (contents == MAP_FAILED) {
+ perror("Error reading config file");
+ goto end;
+ }
+
+ // Output file size
+ if (size_out)
+ *size_out = size;
+
+end:
+ close(fd);
+ return contents;
+}
+
+static char *trim(char *start, char *end)
+{
+ while (start < end && isspace(*start))
+ start++;
+ while (end > start && isspace(*(end - 1)))
+ end--;
+ *end = '\0';
+ return start;
+}
+
+static int parse_line_inner(struct config *cfg, enum config_section section,
+ char *key, char *value)
+{
+ char *endptr;
+ struct passwd *pw;
+ struct group *gr;
+
+ switch (section) {
+ case section_none:
+ fprintf(stderr,
+ "Unexpected key-value pair outside of section: %s=%s\n",
+ key, value);
+ return -1;
+ case section_github:
+ if (!strcmp(key, "endpoint"))
+ cfg->endpoint = value;
+ else if (!strcmp(key, "token"))
+ cfg->token = value;
+ else if (!strcmp(key, "user_agent"))
+ cfg->user_agent = value;
+ else if (!strcmp(key, "owner"))
+ cfg->owner = value;
+ else {
+ fprintf(stderr,
+ "Error parsing config file: unknown key: %s\n",
+ key);
+ return -1;
+ }
+ break;
+ case section_git:
+ if (!strcmp(key, "base"))
+ cfg->git_base = value;
+ else if (!strcmp(key, "owner")) {
+ // If the value is a number, set the owner to that
+ // number
+ cfg->git_owner = strtol(value, &endptr, 10);
+ if (*endptr == '\0')
+ return 0;
+
+ // Otherwise, convert the string into a uid
+ if (!((pw = getpwnam(value)))) {
+ fprintf(stderr,
+ "Error parsing config file: unknown "
+ "user: %s\n",
+ value);
+ return -1;
+ }
+ cfg->git_owner = pw->pw_uid;
+ } else if (!strcmp(key, "group")) {
+ // If the value is a number, set the group to that
+ // number
+ cfg->git_group = strtol(value, &endptr, 10);
+ if (*endptr == '\0')
+ return 0;
+
+ // Otherwise, convert the string into a gid
+ if (!((gr = getgrnam(value)))) {
+ fprintf(stderr,
+ "Error parsing config file: unknown "
+ "group: %s\n",
+ value);
+ return -1;
+ }
+ cfg->git_group = gr->gr_gid;
+ } else {
+ fprintf(stderr,
+ "Error parsing config file: unknown key: %s\n",
+ key);
+ return -1;
+ }
+ break;
+ }
+ return 0;
+}
+
+static int parse_line(struct config *cfg, char *line,
+ enum config_section *section)
+{
+ switch (*line) {
+ case ';':
+ case '#':
+ case '\0':
+ // Ignore comments and empty lines
+ return 0;
+ case '[': {
+ // Handle section headers
+ char *close = strchr(line, ']');
+ if (!close) {
+ fprintf(stderr,
+ "Error parsing config file: invalid section "
+ "header: %s\n",
+ line);
+ return -1;
+ }
+ *close = '\0';
+ char *section_name = trim(line + 1, close);
+ if (!strcmp(section_name, "github"))
+ *section = section_github;
+ else if (!strcmp(section_name, "git"))
+ *section = section_git;
+ else {
+ fprintf(stderr,
+ "Error parsing config file: unknown section: "
+ "%s\n",
+ section_name);
+ return -1;
+ }
+ return 0;
+ }
+ default: {
+ // Handle key-value pairs
+ char *line_end = line + strlen(line);
+ char *equals = strchr(line, '=');
+ if (!equals) {
+ fprintf(stderr,
+ "Error parsing config file: invalid line: %s\n",
+ line);
+ return -1;
+ }
+ *equals = '\0';
+ char *key = trim(line, equals);
+ char *value = trim(equals + 1, line_end);
+ return parse_line_inner(cfg, *section, key, value);
+ }
+ }
+}
+
+static int config_parse(struct config *cfg)
+{
+ char *ptr = cfg->contents;
+ char *end = cfg->contents + cfg->contents_len;
+ enum config_section section = section_none;
+
+ while (ptr < end) {
+ // Find the end of the line
+ char *newline = memchr(ptr, '\n', end - ptr);
+ char *line_end = newline ? newline : end;
+
+ // Handle line endings
+ char *actual_end = line_end;
+ if (actual_end > ptr && *(actual_end - 1) == '\r')
+ actual_end--;
+
+ // Null-terminate the line
+ if (line_end < end)
+ *line_end = '\0';
+
+ // Trim whitespace
+ char *line = trim(ptr, actual_end);
+
+ // Parse the line
+ if (parse_line(cfg, line, &section) < 0)
+ return -1;
+
+ // Move to the next line
+ ptr = newline ? newline + 1 : end;
+ }
+
+ return 0;
+}
+
+static void config_defaults(struct config *cfg)
+{
+ cfg->endpoint = GH_DEFAULT_ENDPOINT;
+ cfg->user_agent = GH_DEFAULT_USER_AGENT;
+ cfg->git_base = "/srv/git";
+ cfg->git_owner = getuid();
+ cfg->git_group = getgid();
+}
+
+static int config_validate(const struct config *cfg)
+{
+ if (!cfg->token) {
+ fprintf(stderr,
+ "Error: missing required field: github.token\n");
+ return -1;
+ }
+ if (!cfg->owner) {
+ fprintf(stderr,
+ "Error: missing required field: github.owner\n");
+ return -1;
+ }
+ return 0;
+}
+
+
+struct config *config_read(const char *path)
+{
+ struct config *cfg = calloc(1, sizeof(*cfg));
+ if (!cfg) {
+ perror("error allocating config");
+ return NULL;
+ }
+ config_defaults(cfg);
+
+ // Read the config file
+ cfg->contents = file_read(path, &cfg->contents_len);
+ if (!cfg->contents)
+ goto fail;
+
+ // Parse the config file
+ if (config_parse(cfg) < 0)
+ goto fail2;
+
+ // Validate the config file
+ if (config_validate(cfg) < 0)
+ goto fail2;
+
+ return cfg;
+
+fail2:
+ munmap(cfg->contents, cfg->contents_len);
+fail:
+ free(cfg);
+ return NULL;
+}
+
+void config_free(struct config *config)
+{
+ if (!config || !config->contents)
+ return;
+ munmap(config->contents, config->contents_len);
+ free(config);
+}
diff --git a/src/config.h b/src/config.h
new file mode 100644
index 0000000..0d7d827
--- /dev/null
+++ b/src/config.h
@@ -0,0 +1,51 @@
+//
+// Created by Anshul Gupta on 4/6/25.
+//
+
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#include <stdlib.h>
+#include <sys/types.h>
+
+#define GH_DEFAULT_ENDPOINT "https://api.github.com/graphql"
+#define GH_DEFAULT_USER_AGENT "github_mirror/0.1"
+
+extern const char *config_locations[];
+
+struct config {
+ /// The content of the config file
+ char *contents;
+ size_t contents_len;
+
+ const char *endpoint;
+ const char *token;
+ const char *user_agent;
+
+ /// The owner of the repositories
+ const char *owner;
+
+ /// The filepath to the git mirrors
+ /// Default: /srv/git
+ const char *git_base;
+ /// User to give ownership of the git mirrors
+ uid_t git_owner;
+ /// Group to give ownership of the git mirrors
+ gid_t git_group;
+};
+
+/**
+ * Read the INI config file at the given path.
+ * Will print to stderr errors if the file cannot be read.
+ * @param path Path to the config file
+ * @return A pointer to the config struct, or NULL on error
+ */
+struct config *config_read(const char *path);
+
+/**
+ * Free the config struct
+ * @param config The config struct to free
+ */
+void config_free(struct config *config);
+
+#endif // CONFIG_H
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;
+}
diff --git a/src/git.h b/src/git.h
new file mode 100644
index 0000000..1240b0f
--- /dev/null
+++ b/src/git.h
@@ -0,0 +1,24 @@
+//
+// Created by Anshul Gupta on 4/6/25.
+//
+
+#ifndef GIT_H
+#define GIT_H
+
+#include "config.h"
+
+struct repo_ctx {
+ const struct config *cfg;
+
+ /// Name of the repo
+ const char *name;
+ /// HTTPS URL of the repo
+ const char *url;
+ /// GitHub username for authentication
+ const char *username;
+};
+
+int git_mirror_repo(const struct repo_ctx *ctx);
+
+
+#endif // GIT_H
diff --git a/src/github.c b/src/github.c
new file mode 100644
index 0000000..18bd992
--- /dev/null
+++ b/src/github.c
@@ -0,0 +1,290 @@
+//
+// Created by Anshul Gupta on 4/4/25.
+//
+
+#include "github.h"
+
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <cJSON.h>
+#include <curl/curl.h>
+
+#include "queries/identity.h"
+#include "queries/list_repos.h"
+
+#include "buffer.h"
+#include "github_types.h"
+
+struct gh_impl {
+ struct github_ctx ctx;
+ CURL *curl;
+};
+
+static CURLcode gh_impl_send(const struct gh_impl *client, const char *query,
+ cJSON *args, buffer_t *buf);
+
+static int gh_handle_error(const cJSON *root);
+
+github_client *github_client_new(const struct github_ctx ctx)
+{
+ struct gh_impl *c = malloc(sizeof(*c));
+ if (!c)
+ return NULL;
+
+ c->ctx.endpoint = strdup(ctx.endpoint);
+ c->ctx.token = strdup(ctx.token);
+ c->ctx.user_agent = strdup(ctx.user_agent);
+ c->curl = curl_easy_init();
+ return c;
+}
+
+github_client *github_client_dup(github_client *client)
+{
+ struct gh_impl *c = client;
+ if (!c)
+ return NULL;
+
+ struct gh_impl *dup = malloc(sizeof(*dup));
+ if (!dup)
+ return NULL;
+
+ dup->ctx.endpoint = strdup(c->ctx.endpoint);
+ dup->ctx.token = strdup(c->ctx.token);
+ dup->ctx.user_agent = strdup(c->ctx.user_agent);
+ dup->curl = curl_easy_duphandle(c->curl);
+
+ return dup;
+}
+
+void github_client_free(github_client *client)
+{
+ struct gh_impl *c = client;
+ if (!c)
+ return;
+ curl_easy_cleanup(c->curl);
+ free((char *) c->ctx.endpoint);
+ free((char *) c->ctx.token);
+ free((char *) c->ctx.user_agent);
+ free(c);
+}
+
+char *github_client_identity(const github_client *client)
+{
+ char *login = NULL;
+ const struct gh_impl *c = client;
+ buffer_t buf = buffer_new(4096);
+
+ const CURLcode ret = gh_impl_send(c, identity, NULL, &buf);
+ if (ret != CURLE_OK) {
+ fprintf(stderr, "Failed to send request: %s\n",
+ curl_easy_strerror(ret));
+ goto fail;
+ }
+
+ // Parse the response
+ cJSON *root = cJSON_Parse((const char *) buf.data);
+ if (!root) {
+ const char *err = cJSON_GetErrorPtr();
+ if (err)
+ fprintf(stderr, "Error parsing response: %s\n", err);
+ goto fail;
+ }
+
+ // Check for errors
+ if (gh_handle_error(root) < 0)
+ goto fail1;
+
+ // Get login from json
+ login = identity_from_json(root);
+
+fail1:
+ cJSON_Delete(root);
+fail:
+ buffer_free(buf);
+ return login;
+}
+
+int github_client_list_user_repos(const github_client *client,
+ const char *username, const char *after,
+ struct list_repos_res *res)
+{
+ int status = 0;
+ const struct gh_impl *c = client;
+ buffer_t buf = buffer_new(4096);
+
+ cJSON *args = cJSON_CreateObject();
+ if (!args) {
+ status = -1;
+ goto end;
+ }
+ cJSON_AddItemToObject(args, "username", cJSON_CreateString(username));
+ cJSON_AddItemToObject(args, "after", cJSON_CreateString(after));
+
+ const CURLcode ret = gh_impl_send(c, list_repos, args, &buf);
+ if (ret != CURLE_OK) {
+ fprintf(stderr, "Failed to send request: %s\n",
+ curl_easy_strerror(ret));
+ status = -1;
+ goto end;
+ }
+
+ // Parse the response
+ cJSON *root = cJSON_Parse((const char *) buf.data);
+ if (!root) {
+ const char *err = cJSON_GetErrorPtr();
+ if (err)
+ fprintf(stderr, "Error parsing response: %s\n", err);
+ status = -1;
+ goto end;
+ }
+
+ // Check for errors
+ if (gh_handle_error(root) < 0) {
+ status = -1;
+ goto end;
+ }
+
+ // Convert json to struct
+ if (list_repos_from_json(root, res) < 0) {
+ fprintf(stderr, "Failed to parse response\n");
+ status = -1;
+ }
+
+end:
+ buffer_free(buf);
+ return status;
+}
+
+/**
+ * Wraps the query in a JSON object with a "query" key and an optional
+ * "variables" key
+ * @param query GraphQL query
+ * @param args GraphQL arguments
+ * @return JSON string
+ */
+static char *wrap_query(const char *query, cJSON *args)
+{
+ cJSON *root = NULL, *query_str = NULL;
+ char *str = NULL;
+
+ root = cJSON_CreateObject();
+ if (!root)
+ return NULL;
+
+ query_str = cJSON_CreateString(query);
+ if (!query_str)
+ goto end;
+
+ // Transfer ownership of the string to root
+ cJSON_AddItemToObject(root, "query", query_str);
+
+ // Add the args object if it exists
+ if (args)
+ cJSON_AddItemToObject(root, "variables", args);
+
+ str = cJSON_Print(root);
+end:
+ cJSON_Delete(root);
+ return str;
+}
+
+static size_t write_data(const void *ptr, const size_t size, size_t nmemb,
+ void *stream)
+{
+ (void) size; // unused
+
+ buffer_t *buf = stream;
+ buffer_append(buf, ptr, nmemb);
+ return nmemb;
+}
+
+/**
+ * Send a GraphQL query to the GitHub API
+ * @param client Github Client
+ * @param query GraphQL query
+ * @return CURLcode
+ */
+static CURLcode gh_impl_send(const struct gh_impl *client, const char *query,
+ cJSON *args, buffer_t *buf)
+{
+ struct curl_slist *headers = NULL;
+ char auth[1024];
+
+ // Set the URL
+ curl_easy_setopt(client->curl, CURLOPT_URL, client->ctx.endpoint);
+
+ // Set the authorization header
+ snprintf(auth, sizeof(auth), "Authorization: bearer %s",
+ client->ctx.token);
+ headers = curl_slist_append(headers, auth);
+ // Set the content type to JSON
+ headers = curl_slist_append(headers, "Content-Type: application/json");
+ curl_easy_setopt(client->curl, CURLOPT_HTTPHEADER, headers);
+
+ // Set user agent
+ curl_easy_setopt(client->curl, CURLOPT_USERAGENT,
+ client->ctx.user_agent);
+
+ // Set the request type to POST
+ curl_easy_setopt(client->curl, CURLOPT_CUSTOMREQUEST, "POST");
+
+ // Prepare request body
+ char *wrapped_query = wrap_query(query, args);
+
+ // Set the request body
+ curl_easy_setopt(client->curl, CURLOPT_POSTFIELDS, wrapped_query);
+ curl_easy_setopt(client->curl, CURLOPT_POSTFIELDSIZE,
+ strlen(wrapped_query));
+
+ // Set the write function to capture the response
+ curl_easy_setopt(client->curl, CURLOPT_WRITEFUNCTION, write_data);
+ curl_easy_setopt(client->curl, CURLOPT_WRITEDATA, (void *) buf);
+
+ // Perform the request
+ const CURLcode ret = curl_easy_perform(client->curl);
+
+ // Append null terminator to the buffer
+ if (ret == CURLE_OK)
+ buffer_append(buf, "\0", 1);
+
+ // Cleanup
+ free(wrapped_query);
+ curl_slist_free_all(headers);
+
+ return ret;
+}
+
+/**
+ * Handle errors in the response.
+ * Will check for the "errors" key in the response and print the error messages.
+ * Returns failure if any errors are found.
+ * @param root Parsed JSON response
+ * @return 0 on success (no errors), -1 on failure
+ */
+static int gh_handle_error(const cJSON *root)
+{
+ // Check for errors
+ cJSON *errors = cJSON_GetObjectItemCaseSensitive(root, "errors");
+ if (!errors || !cJSON_IsArray(errors)) {
+ // No errors
+ return 0;
+ }
+
+ cJSON *err;
+ cJSON_ArrayForEach(err, errors)
+ {
+ // Get the error message
+ cJSON *message = cJSON_GetObjectItemCaseSensitive(err,
+ "message");
+ if (message && cJSON_IsString(message)) {
+ fprintf(stderr, "Github Error: %s\n",
+ message->valuestring);
+ } else {
+ fprintf(stderr, "Github Error: Unknown error\n");
+ }
+ }
+
+ return -1;
+}
diff --git a/src/github.h b/src/github.h
new file mode 100644
index 0000000..4979fde
--- /dev/null
+++ b/src/github.h
@@ -0,0 +1,30 @@
+//
+// Created by Anshul Gupta on 4/4/25.
+//
+
+#ifndef GITHUB_H
+#define GITHUB_H
+
+#include "github_types.h"
+
+typedef void github_client;
+
+struct github_ctx {
+ const char *endpoint;
+ const char *token;
+ const char *user_agent;
+};
+
+github_client *github_client_new(struct github_ctx ctx);
+
+github_client *github_client_dup(github_client *client);
+
+void github_client_free(github_client *client);
+
+char *github_client_identity(const github_client *client);
+
+int github_client_list_user_repos(const github_client *client,
+ const char *username, const char *after,
+ struct list_repos_res *res);
+
+#endif // GITHUB_H
diff --git a/src/github_types.c b/src/github_types.c
new file mode 100644
index 0000000..ea1d6c5
--- /dev/null
+++ b/src/github_types.c
@@ -0,0 +1,151 @@
+//
+// Created by Anshul Gupta on 4/4/25.
+//
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <cJSON.h>
+
+#include "github_types.h"
+
+
+char *identity_from_json(const cJSON *root)
+{
+ cJSON *data = cJSON_GetObjectItemCaseSensitive(root, "data");
+ if (!data || !cJSON_IsObject(data)) {
+ fprintf(stderr, "Error: data object not found\n");
+ return NULL;
+ }
+ cJSON *viewer = cJSON_GetObjectItemCaseSensitive(data, "viewer");
+ if (!viewer || !cJSON_IsObject(viewer)) {
+ fprintf(stderr, "Error: viewer object not found\n");
+ return NULL;
+ }
+ cJSON *login = cJSON_GetObjectItemCaseSensitive(viewer, "login");
+ if (!login || !cJSON_IsString(login)) {
+ fprintf(stderr, "Error: login not found\n");
+ return NULL;
+ }
+
+ return strdup(login->valuestring);
+}
+
+int list_repos_from_json(cJSON *root, struct list_repos_res *res)
+{
+ int status = 0;
+ cJSON *repo;
+
+ // Initialize the response structure
+ memset(res, 0, sizeof(*res));
+
+ // Get the data object
+ cJSON *data = cJSON_GetObjectItemCaseSensitive(root, "data");
+ if (!data || !cJSON_IsObject(data)) {
+ fprintf(stderr, "Error: data object not found\n");
+ status = -1;
+ goto end;
+ }
+
+ // Get the repositoryOwner object
+ cJSON *repositoryOwner = cJSON_GetObjectItemCaseSensitive(
+ data, "repositoryOwner");
+ if (!repositoryOwner || !cJSON_IsObject(repositoryOwner)) {
+ fprintf(stderr, "Error: repositoryOwner object not found\n");
+ status = -1;
+ goto end;
+ }
+
+ // Get the repositories object
+ cJSON *repositories = cJSON_GetObjectItemCaseSensitive(repositoryOwner,
+ "repositories");
+ if (!repositories || !cJSON_IsObject(repositories)) {
+ fprintf(stderr, "Error: repositories object not found\n");
+ status = -1;
+ goto end;
+ }
+
+ // Get the pageInfo object
+ cJSON *page_info = cJSON_GetObjectItemCaseSensitive(repositories,
+ "pageInfo");
+ if (!page_info || !cJSON_IsObject(page_info)) {
+ fprintf(stderr, "Error: pageInfo object not found\n");
+ status = -1;
+ goto end;
+ }
+
+ // Get the hasNextPage and endCursor values
+ cJSON *has_next_page = cJSON_GetObjectItemCaseSensitive(page_info,
+ "hasNextPage");
+ if (!has_next_page || !cJSON_IsBool(has_next_page)) {
+ fprintf(stderr, "Error: hasNextPage not found\n");
+ status = -1;
+ goto end;
+ }
+ res->has_next_page = cJSON_IsTrue(has_next_page);
+
+ cJSON *end_cursor = cJSON_GetObjectItemCaseSensitive(page_info,
+ "endCursor");
+ if (!end_cursor || !cJSON_IsString(end_cursor)) {
+ fprintf(stderr, "Error: endCursor not found\n");
+ status = -1;
+ goto end;
+ }
+ res->end_cursor = strdup(end_cursor->valuestring);
+
+ // Get the nodes array
+ cJSON *nodes = cJSON_GetObjectItemCaseSensitive(repositories, "nodes");
+ if (!nodes || !cJSON_IsArray(nodes)) {
+ fprintf(stderr, "Error: nodes array not found\n");
+ status = -1;
+ goto end;
+ }
+
+ // Iterate over the nodes array
+ size_t len = cJSON_GetArraySize(nodes);
+ res->repos = malloc(sizeof(*res->repos) * len);
+ cJSON_ArrayForEach(repo, nodes)
+ {
+ cJSON *name = cJSON_GetObjectItemCaseSensitive(repo, "name");
+ if (!name || !cJSON_IsString(name)) {
+ fprintf(stderr, "Error: name not found\n");
+ status = -1;
+ goto end;
+ }
+ cJSON *url = cJSON_GetObjectItemCaseSensitive(repo, "url");
+ if (!url || !cJSON_IsString(url)) {
+ fprintf(stderr, "Error: url not found\n");
+ status = -1;
+ goto end;
+ }
+ cJSON *is_fork = cJSON_GetObjectItemCaseSensitive(repo,
+ "isFork");
+ if (!is_fork || !cJSON_IsBool(is_fork)) {
+ fprintf(stderr, "Error: isFork not found\n");
+ status = -1;
+ goto end;
+ }
+
+ res->repos[res->repos_len].name = strdup(name->valuestring);
+ res->repos[res->repos_len].url = strdup(url->valuestring);
+ res->repos[res->repos_len].is_fork = cJSON_IsTrue(is_fork);
+ res->repos_len++;
+ }
+
+end:
+ cJSON_Delete(root);
+ if (status != 0)
+ list_repos_res_free(*res);
+ return status;
+}
+
+void list_repos_res_free(struct list_repos_res res)
+{
+ free(res.end_cursor);
+ for (size_t i = 0; i < res.repos_len; i++) {
+ free(res.repos[i].name);
+ free(res.repos[i].url);
+ }
+ free(res.repos);
+}
diff --git a/src/github_types.h b/src/github_types.h
new file mode 100644
index 0000000..ab3f884
--- /dev/null
+++ b/src/github_types.h
@@ -0,0 +1,28 @@
+//
+// Created by Anshul Gupta on 4/4/25.
+//
+
+#ifndef GITHUB_TYPES_H
+#define GITHUB_TYPES_H
+
+#include <cJSON.h>
+
+char *identity_from_json(const cJSON *root);
+
+struct list_repos_res {
+ int has_next_page;
+ char *end_cursor;
+
+ struct {
+ char *name;
+ char *url;
+ int is_fork;
+ } *repos;
+
+ size_t repos_len;
+};
+
+int list_repos_from_json(cJSON *root, struct list_repos_res *res);
+void list_repos_res_free(struct list_repos_res res);
+
+#endif // GITHUB_TYPES_H
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..db11d5a
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,132 @@
+#include <getopt.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <curl/curl.h>
+
+#include "config.h"
+#include "git.h"
+#include "github.h"
+#include "github_types.h"
+#include "precheck.h"
+
+static int load_config(int argc, char **argv, struct config **cfg_out)
+{
+ int opt, opt_idx = 0;
+ size_t i;
+ char *cfg_path = NULL;
+
+ static struct option long_options[] = {
+ {"config", required_argument, 0, 'c'},
+ {"help", no_argument, 0, 'h'},
+ {0, 0, 0, 0}};
+
+ while ((opt = getopt_long(argc, argv, "C:c:h", long_options,
+ &opt_idx)) != -1) {
+ switch (opt) {
+ case 'C':
+ case 'c':
+ cfg_path = optarg;
+ break;
+ case 'h':
+ fprintf(stderr,
+ "Usage: %s [--config <file>] [--help]\n",
+ argv[0]);
+ return 0;
+ default:
+ fprintf(stderr, "Unknown option: %c\n", opt);
+ fprintf(stderr,
+ "Usage: %s [--config <file>] [--help]\n",
+ argv[0]);
+ return 1;
+ }
+ }
+
+ // Config file given, use it
+ if (cfg_path) {
+ *cfg_out = config_read(cfg_path);
+ return *cfg_out == NULL;
+ }
+
+ // No config file given, try the default locations
+ for (i = 0; config_locations[i]; i++) {
+ fprintf(stderr, "Trying config file: %s\n",
+ config_locations[i]);
+ *cfg_out = config_read(config_locations[i]);
+ if (*cfg_out) {
+ fprintf(stderr, "Using config file: %s\n",
+ config_locations[i]);
+ return 0;
+ }
+ }
+ fprintf(stderr, "Failed to read config file\n");
+ return 1;
+}
+
+int main(int argc, char **argv)
+{
+ curl_global_init(CURL_GLOBAL_DEFAULT);
+
+ struct config *cfg = NULL;
+ const int ret = load_config(argc, argv, &cfg);
+ if (ret != 0 || !cfg)
+ return ret;
+
+ if (precheck_self(cfg)) {
+ fprintf(stderr, "Precheck failed\n");
+ config_free(cfg);
+ return 1;
+ }
+
+ const struct github_ctx ctx = {
+ .endpoint = cfg->endpoint,
+ .token = cfg->token,
+ .user_agent = cfg->user_agent,
+ };
+ github_client *client = github_client_new(ctx);
+ if (!client) {
+ fprintf(stderr, "Failed to create GitHub client\n");
+ return 1;
+ }
+
+ // Get identity
+ char *login = github_client_identity(client);
+
+ struct list_repos_res res;
+ char *end_cursor = NULL;
+ int status = 0;
+ do {
+ if (github_client_list_user_repos(client, cfg->owner,
+ end_cursor, &res))
+ return 1;
+
+ for (size_t i = 0; i < res.repos_len; i++) {
+ printf("Repo: %s\t%s\n", res.repos[i].name,
+ res.repos[i].url);
+
+ const struct repo_ctx repo = {
+ .cfg = cfg,
+ .name = res.repos[i].name,
+ .url = res.repos[i].url,
+ .username = login,
+ };
+ if (git_mirror_repo(&repo) != 0) {
+ fprintf(stderr, "Failed to mirror repo\n");
+ status = 1;
+ break;
+ }
+ }
+
+ free(end_cursor);
+ end_cursor = strdup(res.end_cursor);
+
+ list_repos_res_free(res);
+ } while (res.has_next_page);
+
+ free(end_cursor);
+ github_client_free(client);
+ free(login);
+ config_free(cfg);
+ curl_global_cleanup();
+ return status;
+}
diff --git a/src/precheck.c b/src/precheck.c
new file mode 100644
index 0000000..a9fce35
--- /dev/null
+++ b/src/precheck.c
@@ -0,0 +1,112 @@
+//
+// Created by Anshul Gupta on 4/7/25.
+//
+
+#include "precheck.h"
+
+#include <stdio.h>
+#include <sys/errno.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#ifdef __linux__
+#include <sys/capability.h>
+#endif
+
+/**
+ * Check if git is installed and available in the PATH.
+ * @return 1 if git is available, 0 if not.
+ */
+static int has_git(void)
+{
+ pid_t pid = fork();
+ if (pid < 0) {
+ perror("fork");
+ return 0;
+ }
+
+ if (pid == 0) {
+ // Child process
+ char *args[] = {"git", "--version", NULL};
+ execvp("git", args);
+ _exit(127); // execvp only returns on error
+ }
+
+ // Parent process
+ int status;
+ pid_t result;
+ do {
+ result = waitpid(pid, &status, 0);
+ } while (result == -1 && errno == EINTR);
+ if (result == -1) {
+ perror("waitpid");
+ return 0;
+ }
+
+ // Check exit status
+ if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
+ return 1; // git is available
+ }
+
+ fprintf(stderr, "Error: git is not installed or not found in PATH\n");
+ return 0; // git is not available
+}
+
+/**
+ * Check if we have CAP_CHOWN capability or if we are root.
+ * @return 1 if we have chown capability, 0 if not.
+ */
+static int has_chown(void)
+{
+#ifdef __linux__
+ cap_t caps = cap_get_proc();
+ if (caps == NULL) {
+ perror("cap_get_proc");
+ return 0;
+ }
+
+ cap_flag_value_t cap_value;
+ if (cap_get_flag(caps, CAP_CHOWN, CAP_EFFECTIVE, &cap_value) == -1) {
+ perror("cap_get_flag");
+ cap_free(caps);
+ return 0;
+ }
+ cap_free(caps);
+
+ if (cap_value == CAP_SET)
+ return 1; // We have CAP_CHOWN capability
+ fprintf(stderr, "Error: CAP_CHOWN capability is not set\n");
+ return 0; // We don't have CAP_CHOWN capability
+#else
+ if (geteuid() == 0)
+ return 1;
+ fprintf(stderr, "Error: not running as root\n");
+ return 0;
+#endif
+}
+
+/**
+ * Check if the git base directory exists.
+ * @param path Path to the directory to check
+ * @return 1 if the directory exists, 0 if not.
+ */
+static int git_base_exists(const char *path)
+{
+ struct stat st;
+ if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
+ return 1;
+ }
+ fprintf(stderr, "Error: git base directory does not exist: %s\n", path);
+ return 0;
+}
+
+int precheck_self(const struct config *cfg)
+{
+ if (!has_git())
+ return -1;
+ if (!has_chown())
+ return -1;
+ if (!git_base_exists(cfg->git_base))
+ return -1;
+ return 0;
+}
diff --git a/src/precheck.h b/src/precheck.h
new file mode 100644
index 0000000..b030016
--- /dev/null
+++ b/src/precheck.h
@@ -0,0 +1,16 @@
+//
+// Created by Anshul Gupta on 4/7/25.
+//
+
+#ifndef PRECHECK_H
+#define PRECHECK_H
+
+#include "config.h"
+
+/**
+ * Precheck the system for required dependencies and configurations.
+ * @return 0 if all checks pass, non-zero if any check fails.
+ */
+int precheck_self(const struct config *cfg);
+
+#endif // PRECHECK_H