diff options
author | 2025-06-12 01:53:38 -0700 | |
---|---|---|
committer | 2025-06-12 01:53:38 -0700 | |
commit | c9adb09abc626cdcc35c345a635ad8c163fcff3e (patch) | |
tree | 93f78bf8e5910a044f96ed77aca498ac60ada804 /src | |
parent | 179679633a9fc3317585167f86c87a7fe8394945 (diff) | |
parent | b78875e2265339b58c7a67cc83e6db2988aa0d74 (diff) | |
download | github-mirror-c9adb09abc626cdcc35c345a635ad8c163fcff3e.tar.gz github-mirror-c9adb09abc626cdcc35c345a635ad8c163fcff3e.tar.zst github-mirror-c9adb09abc626cdcc35c345a635ad8c163fcff3e.zip |
Merge pull request #2 from ansg191/srht
Diffstat (limited to 'src')
-rw-r--r-- | src/client.c | 179 | ||||
-rw-r--r-- | src/client.h | 30 | ||||
-rw-r--r-- | src/config.c | 142 | ||||
-rw-r--r-- | src/config.h | 35 | ||||
-rw-r--r-- | src/git.c | 30 | ||||
-rw-r--r-- | src/git.h | 3 | ||||
-rw-r--r-- | src/github.c | 290 | ||||
-rw-r--r-- | src/github.h | 30 | ||||
-rw-r--r-- | src/github/client.c | 100 | ||||
-rw-r--r-- | src/github/client.h | 17 | ||||
-rw-r--r-- | src/github/types.c (renamed from src/github_types.c) | 11 | ||||
-rw-r--r-- | src/github/types.h (renamed from src/github_types.h) | 6 | ||||
-rw-r--r-- | src/main.c | 120 | ||||
-rw-r--r-- | src/srht/client.c | 65 | ||||
-rw-r--r-- | src/srht/client.h | 15 | ||||
-rw-r--r-- | src/srht/types.c | 149 | ||||
-rw-r--r-- | src/srht/types.h | 24 |
17 files changed, 850 insertions, 396 deletions
diff --git a/src/client.c b/src/client.c new file mode 100644 index 0000000..034023a --- /dev/null +++ b/src/client.c @@ -0,0 +1,179 @@ +// +// Created by Anshul Gupta on 6/10/25. +// + +#include "client.h" + +#include <stdlib.h> +#include <string.h> + +struct gql_impl { + struct gql_ctx ctx; + CURL *curl; +}; + +gql_client *gql_client_new(struct gql_ctx ctx) +{ + struct gql_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; +} + +gql_client *gql_client_dup(gql_client *client) +{ + struct gql_impl *c = client; + if (!c) + return NULL; + + struct gql_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 gql_client_free(gql_client *client) +{ + struct gql_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); +} + +/** + * 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; +} + +CURLcode gql_client_send(const gql_client *client, const char *query, + cJSON *args, buffer_t *buf) +{ + struct gql_impl *c = (struct gql_impl *) client; + struct curl_slist *headers = NULL; + char auth[1024]; + + // Set the URL + curl_easy_setopt(c->curl, CURLOPT_URL, c->ctx.endpoint); + + // Set the authorization header + snprintf(auth, sizeof(auth), "Authorization: Bearer %s", c->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(c->curl, CURLOPT_HTTPHEADER, headers); + + // Set user agent + curl_easy_setopt(c->curl, CURLOPT_USERAGENT, c->ctx.user_agent); + + // Set the request type to POST + curl_easy_setopt(c->curl, CURLOPT_CUSTOMREQUEST, "POST"); + + // Prepare request body + char *wrapped_query = wrap_query(query, args); + + // Set the request body + curl_easy_setopt(c->curl, CURLOPT_POSTFIELDS, wrapped_query); + curl_easy_setopt(c->curl, CURLOPT_POSTFIELDSIZE, strlen(wrapped_query)); + + // Set the write function to capture the response + curl_easy_setopt(c->curl, CURLOPT_WRITEFUNCTION, write_data); + curl_easy_setopt(c->curl, CURLOPT_WRITEDATA, (void *) buf); + + // Perform the request + const CURLcode ret = curl_easy_perform(c->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 + */ +int gql_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/client.h b/src/client.h new file mode 100644 index 0000000..1e24f6f --- /dev/null +++ b/src/client.h @@ -0,0 +1,30 @@ +// +// Created by Anshul Gupta on 6/10/25. +// + +#ifndef CLIENT_H +#define CLIENT_H + +#include <cjson/cJSON.h> +#include <curl/curl.h> + +#include "buffer.h" + +typedef void gql_client; + +struct gql_ctx { + const char *endpoint; + const char *token; + const char *user_agent; +}; + +gql_client *gql_client_new(struct gql_ctx ctx); +gql_client *gql_client_dup(gql_client *client); +void gql_client_free(gql_client *client); + +CURLcode gql_client_send(const gql_client *client, const char *query, + cJSON *args, buffer_t *buf); + +int gql_handle_error(const cJSON *root); + +#endif // CLIENT_H diff --git a/src/config.c b/src/config.c index 1556e68..f27e72f 100644 --- a/src/config.c +++ b/src/config.c @@ -30,6 +30,7 @@ const char *config_locations[] = { enum config_section { section_none, section_github, + section_srht, section_git, }; @@ -89,7 +90,7 @@ static char *trim(char *start, char *end) * Parse a token value from the config file. * If the token is a file path, read the file and set the token to its contents. * If not a file path, the raw token value is checked for validity. - * It then checks that the token starts with: + * If it is a github token, it then checks that the token starts with: * - "ghp_" * - "gho_" * - "ghu_" @@ -99,7 +100,7 @@ static char *trim(char *start, char *end) * @param value token value * @return Owned token value or NULL on error */ -static char *parse_token(char *value) +static char *parse_token(char *value, enum remote_type tp) { char *token = NULL; @@ -146,6 +147,8 @@ static char *parse_token(char *value) close(fd); token_check: + if (tp == remote_type_srht) + return token; if (!strncmp(token, "ghp_", 4) || !strncmp(token, "gho_", 4) || !strncmp(token, "ghu_", 4) || !strncmp(token, "ghs_", 4) || !strncmp(token, "ghf_", 4) || !strncmp(token, "github_pat_", 11)) { @@ -182,15 +185,16 @@ static int parse_line_inner(struct config *cfg, enum config_section section, return -1; case section_github: if (!strcmp(key, "endpoint")) - cfg->head->endpoint = value; + cfg->head->gh.endpoint = value; else if (!strcmp(key, "token")) { - cfg->head->token = parse_token(value); + cfg->head->gh.token = + parse_token(value, cfg->head->type); } else if (!strcmp(key, "user_agent")) - cfg->head->user_agent = value; + cfg->head->gh.user_agent = value; else if (!strcmp(key, "owner")) - cfg->head->owner = value; + cfg->head->gh.owner = value; else if (!strcmp(key, "skip-forks")) { - if (parse_bool(value, &cfg->head->skip_forks) < 0) { + if (parse_bool(value, &cfg->head->gh.skip_forks) < 0) { fprintf(stderr, "Error parsing config file: " "invalid value for skip-forks: %s\n", @@ -198,7 +202,8 @@ static int parse_line_inner(struct config *cfg, enum config_section section, return -1; } } else if (!strcmp(key, "skip-private")) { - if (parse_bool(value, &cfg->head->skip_private) < 0) { + if (parse_bool(value, &cfg->head->gh.skip_private) < + 0) { fprintf(stderr, "Error parsing config file: " "invalid value for skip-private: %s\n", @@ -212,6 +217,23 @@ static int parse_line_inner(struct config *cfg, enum config_section section, return -1; } break; + case section_srht: + if (!strcmp(key, "endpoint")) + cfg->head->srht.endpoint = value; + else if (!strcmp(key, "token")) { + cfg->head->srht.token = + parse_token(value, cfg->head->type); + } else if (!strcmp(key, "user_agent")) { + cfg->head->srht.user_agent = value; + } else if (!strcmp(key, "owner")) { + cfg->head->srht.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; @@ -250,16 +272,31 @@ static int parse_line(struct config *cfg, char *line, if (!strcmp(section_name, "github")) { *section = section_github; - // Add the new owner to the list - struct github_cfg *owner = calloc(1, sizeof(*owner)); - if (!owner) { + // Add the new remote to the list + struct remote_cfg *remote = calloc(1, sizeof(*remote)); + if (!remote) { perror("Error allocating owner"); return -1; } - owner->endpoint = GH_DEFAULT_ENDPOINT; - owner->user_agent = GH_DEFAULT_USER_AGENT; - owner->next = cfg->head; - cfg->head = owner; + remote->type = remote_type_github; + remote->gh.endpoint = GH_DEFAULT_ENDPOINT; + remote->gh.user_agent = DEFAULT_USER_AGENT; + remote->next = cfg->head; + cfg->head = remote; + } else if (!strcmp(section_name, "srht")) { + *section = section_srht; + + // Add the new remote to the list + struct remote_cfg *remote = calloc(1, sizeof(*remote)); + if (!remote) { + perror("Error allocating owner"); + return -1; + } + remote->type = remote_type_srht; + remote->srht.endpoint = SRHT_DEFAULT_ENDPOINT; + remote->srht.user_agent = DEFAULT_USER_AGENT; + remote->next = cfg->head; + cfg->head = remote; } else if (!strcmp(section_name, "git")) *section = section_git; else { @@ -329,21 +366,52 @@ static void config_defaults(struct config *cfg) cfg->git_base = "/srv/git"; } +static int github_cfg_validate(const struct github_cfg *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; +} + +static int srht_cfg_validate(const struct srht_cfg *cfg) +{ + if (!cfg->token) { + fprintf(stderr, "Error: missing required field: " + "srht.token\n"); + return -1; + } + + if (!cfg->owner) { + fprintf(stderr, "Error: missing required field: " + "srht.owner\n"); + return -1; + } + return 0; +} + static int config_validate(const struct config *cfg) { - const struct github_cfg *owner = cfg->head; - while (owner) { - if (!owner->token) { - fprintf(stderr, "Error: missing required field: " - "github.token\n"); - return -1; - } - if (!owner->owner) { - fprintf(stderr, "Error: missing required field: " - "github.owner\n"); - return -1; + const struct remote_cfg *remote = cfg->head; + while (remote) { + switch (remote->type) { + case remote_type_github: + if (github_cfg_validate(&remote->gh) < 0) + return -1; + break; + case remote_type_srht: + if (srht_cfg_validate(&remote->srht) < 0) + return -1; + break; } - owner = owner->next; + remote = remote->next; } return 0; } @@ -380,17 +448,29 @@ fail: return NULL; } +static void remote_cfg_free(struct remote_cfg *remote) +{ + switch (remote->type) { + case remote_type_github: + free((char *) remote->gh.token); + break; + case remote_type_srht: + free((char *) remote->srht.token); + break; + } +} + void config_free(struct config *config) { if (!config || !config->contents) return; munmap(config->contents, config->contents_len); - // Free the github_cfg list - struct github_cfg *owner = config->head; + // Free the remote_cfg list + struct remote_cfg *owner = config->head; while (owner) { - struct github_cfg *next = owner->next; - free((char *) owner->token); + struct remote_cfg *next = owner->next; + remote_cfg_free(owner); free(owner); owner = next; } diff --git a/src/config.h b/src/config.h index eb133b4..4d08b7a 100644 --- a/src/config.h +++ b/src/config.h @@ -8,7 +8,8 @@ #include <stdlib.h> #define GH_DEFAULT_ENDPOINT "https://api.github.com/graphql" -#define GH_DEFAULT_USER_AGENT "github-mirror/" GITHUB_MIRROR_VERSION +#define SRHT_DEFAULT_ENDPOINT "https://git.sr.ht/query" +#define DEFAULT_USER_AGENT "github-mirror/" GITHUB_MIRROR_VERSION extern const char *config_locations[]; @@ -29,8 +30,34 @@ struct github_cfg { // Owned /// Github auth token const char *token; - /// Next owner in the list - struct github_cfg *next; +}; + +struct srht_cfg { + // Borrowed + /// SourceHut graphql API endpoint + const char *endpoint; + /// Client user agent + const char *user_agent; + /// The owner of the repositories + const char *owner; + + // Owned + const char *token; +}; + +enum remote_type { + remote_type_github, + remote_type_srht, +}; + +struct remote_cfg { + enum remote_type type; + union { + struct github_cfg gh; + struct srht_cfg srht; + }; + /// Next remote in the list + struct remote_cfg *next; }; struct config { @@ -42,7 +69,7 @@ struct config { int quiet; /// Repo owners to mirror - struct github_cfg *head; + struct remote_cfg *head; /// The filepath to the git mirrors /// Default: /srv/git @@ -53,25 +53,36 @@ static char *get_git_path(const char *base, const char *owner, const char *name) } /** + * Prepares a repository URL for use with git. * Adds authentication information to the HTTPS URL for git. + * Does nothing for ssh URLs. * @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) +static char *prepare_git_url(const char *url, const char *user, + const char *token) { const char *https_prefix = "https://"; - const size_t prefix_len = strlen(https_prefix); + const char *ssh_prefix = "ssh://"; + const size_t http_prefix_len = strlen(https_prefix); + const size_t ssh_prefix_len = strlen(ssh_prefix); size_t new_len; char *new_url; if (!url || !user || !token) return NULL; + // Check if the URL starts with "ssh://" + if (strncmp(url, ssh_prefix, ssh_prefix_len) == 0) { + // If it's an SSH URL, return it unchanged + return strdup(url); + } + // Find the position of "https://" - if (strncmp(url, https_prefix, prefix_len) != 0) { + if (strncmp(url, https_prefix, http_prefix_len) != 0) { fprintf(stderr, "Error: URL does not start with https://\n"); return NULL; } @@ -88,7 +99,7 @@ static char *add_url_auth(const char *url, const char *user, const char *token) // Construct the new URL snprintf(new_url, new_len + 1, "https://%s:%s@%s", user, token, - url + prefix_len); + url + http_prefix_len); return new_url; } @@ -172,8 +183,8 @@ static int create_mirror(const char *path, const struct repo_ctx *ctx, 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); + char *url = prepare_git_url(ctx->url, ctx->username, + ctx->token); // Run git char *args[7]; @@ -216,7 +227,7 @@ static int create_mirror(const char *path, const struct repo_ctx *ctx, 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->git_base, ctx->cfg->owner, NULL); + char *owner_path = get_git_path(ctx->git_base, ctx->owner, NULL); if (!owner_path) return -1; if (mkdir(owner_path, 0755) == -1 && errno != EEXIST) { @@ -227,8 +238,7 @@ static int create_git_path(const struct repo_ctx *ctx) free(owner_path); // Create repo directory if it doesn't exist - char *repo_path = - get_git_path(ctx->git_base, ctx->cfg->owner, ctx->name); + char *repo_path = get_git_path(ctx->git_base, ctx->owner, ctx->name); if (!repo_path) return -1; if (mkdir(repo_path, 0755) == -1 && errno != EEXIST) { @@ -293,7 +303,7 @@ static int update_mirror(const char *path, int quiet) int git_mirror_repo(const struct repo_ctx *ctx, int quiet) { int ret = 0; - char *path = get_git_path(ctx->git_base, ctx->cfg->owner, ctx->name); + char *path = get_git_path(ctx->git_base, ctx->owner, ctx->name); if (!path) { perror("get_git_path"); return -1; @@ -9,7 +9,8 @@ struct repo_ctx { const char *git_base; - const struct github_cfg *cfg; + const char *owner; + const char *token; /// Name of the repo const char *name; diff --git a/src/github.c b/src/github.c deleted file mode 100644 index 38c28a0..0000000 --- a/src/github.c +++ /dev/null @@ -1,290 +0,0 @@ -// -// Created by Anshul Gupta on 4/4/25. -// - -#include "github.h" - -#include <stddef.h> -#include <stdlib.h> -#include <string.h> - -#include <cjson/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 deleted file mode 100644 index 4979fde..0000000 --- a/src/github.h +++ /dev/null @@ -1,30 +0,0 @@ -// -// 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/client.c b/src/github/client.c new file mode 100644 index 0000000..0c0034e --- /dev/null +++ b/src/github/client.c @@ -0,0 +1,100 @@ +// +// Created by Anshul Gupta on 4/4/25. +// + +#include "client.h" + +#include <stdlib.h> + +#include <cjson/cJSON.h> +#include <curl/curl.h> + +#include "queries/github/gh_identity.h" +#include "queries/github/gh_list_repos.h" + +#include "../buffer.h" +#include "types.h" + +char *github_identity(const gql_client *client) +{ + char *login = NULL; + buffer_t buf = buffer_new(4096); + + const CURLcode ret = gql_client_send(client, gh_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 (gql_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_list_user_repos(const gql_client *client, const char *username, + const char *after, struct gh_list_repos_res *res) +{ + int status = 0; + 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 = gql_client_send(client, gh_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 (gql_handle_error(root) < 0) { + status = -1; + goto end; + } + + // Convert json to struct + if (gh_list_repos_from_json(root, res) < 0) { + fprintf(stderr, "Failed to parse response\n"); + status = -1; + } + +end: + buffer_free(buf); + return status; +} diff --git a/src/github/client.h b/src/github/client.h new file mode 100644 index 0000000..723c24c --- /dev/null +++ b/src/github/client.h @@ -0,0 +1,17 @@ +// +// Created by Anshul Gupta on 4/4/25. +// + +#ifndef GITHUB_CLIENT_H +#define GITHUB_CLIENT_H + +#include "../client.h" + +#include "types.h" + +char *github_identity(const gql_client *client); + +int github_list_user_repos(const gql_client *client, const char *username, + const char *after, struct gh_list_repos_res *res); + +#endif // GITHUB_CLIENT_H diff --git a/src/github_types.c b/src/github/types.c index 88f495c..ce9441e 100644 --- a/src/github_types.c +++ b/src/github/types.c @@ -8,7 +8,7 @@ #include <cjson/cJSON.h> -#include "github_types.h" +#include "types.h" char *identity_from_json(const cJSON *root) @@ -32,7 +32,7 @@ char *identity_from_json(const cJSON *root) return strdup(login->valuestring); } -int list_repos_from_json(cJSON *root, struct list_repos_res *res) +int gh_list_repos_from_json(cJSON *root, struct gh_list_repos_res *res) { int status = 0; cJSON *repo; @@ -137,18 +137,19 @@ int list_repos_from_json(cJSON *root, struct list_repos_res *res) 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[res->repos_len].is_private = cJSON_IsTrue(is_private); + res->repos[res->repos_len].is_private = + cJSON_IsTrue(is_private); res->repos_len++; } end: cJSON_Delete(root); if (status != 0) - list_repos_res_free(*res); + gh_list_repos_res_free(*res); return status; } -void list_repos_res_free(struct list_repos_res res) +void gh_list_repos_res_free(struct gh_list_repos_res res) { free(res.end_cursor); for (size_t i = 0; i < res.repos_len; i++) { diff --git a/src/github_types.h b/src/github/types.h index 8f3e273..dec3551 100644 --- a/src/github_types.h +++ b/src/github/types.h @@ -9,7 +9,7 @@ char *identity_from_json(const cJSON *root); -struct list_repos_res { +struct gh_list_repos_res { int has_next_page; char *end_cursor; @@ -23,7 +23,7 @@ struct list_repos_res { 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); +int gh_list_repos_from_json(cJSON *root, struct gh_list_repos_res *res); +void gh_list_repos_res_free(struct gh_list_repos_res res); #endif // GITHUB_TYPES_H @@ -4,11 +4,14 @@ #include <curl/curl.h> +#include "client.h" #include "config.h" #include "git.h" -#include "github.h" -#include "github_types.h" +#include "github/client.h" +#include "github/types.h" #include "precheck.h" +#include "srht/client.h" +#include "srht/types.h" static int load_config(int argc, char **argv, struct config **cfg_out) { @@ -81,29 +84,32 @@ static int load_config(int argc, char **argv, struct config **cfg_out) return 1; } -static int mirror_owner(const char *git_base, const struct github_cfg *cfg, - int quiet) +static int mirror_github(const char *git_base, const struct github_cfg *cfg, + int quiet) { - const struct github_ctx ctx = { + if (!quiet) + printf("Mirroring Github owner: %s\n", cfg->owner); + + const struct gql_ctx ctx = { .endpoint = cfg->endpoint, .token = cfg->token, .user_agent = cfg->user_agent, }; - github_client *client = github_client_new(ctx); + gql_client *client = gql_client_new(ctx); if (!client) { fprintf(stderr, "Failed to create GitHub client\n"); return 1; } // Get identity - char *login = github_client_identity(client); + char *login = github_identity(client); - struct list_repos_res res; + struct gh_list_repos_res res; char *end_cursor = NULL; int status = 0; do { - if (github_client_list_user_repos(client, cfg->owner, - end_cursor, &res)) + if (github_list_user_repos(client, cfg->owner, end_cursor, + &res)) return -1; for (size_t i = 0; i < res.repos_len; i++) { @@ -126,7 +132,8 @@ static int mirror_owner(const char *git_base, const struct github_cfg *cfg, const struct repo_ctx repo = { .git_base = git_base, - .cfg = cfg, + .owner = cfg->owner, + .token = cfg->token, .name = res.repos[i].name, .url = res.repos[i].url, .username = login, @@ -141,15 +148,72 @@ static int mirror_owner(const char *git_base, const struct github_cfg *cfg, free(end_cursor); end_cursor = strdup(res.end_cursor); - list_repos_res_free(res); + gh_list_repos_res_free(res); } while (res.has_next_page); free(end_cursor); free(login); - github_client_free(client); + gql_client_free(client); + return status; +} + +static int mirror_srht(const char *git_base, const struct srht_cfg *cfg, + int quiet) +{ + if (!quiet) + printf("Mirroring sr.ht owner: %s\n", cfg->owner); + + const struct gql_ctx ctx = { + .endpoint = cfg->endpoint, + .token = cfg->token, + .user_agent = cfg->user_agent, + }; + gql_client *client = gql_client_new(ctx); + if (!client) { + fprintf(stderr, "Failed to create sr.ht client\n"); + return 1; + } + + struct srht_list_repos_res res; + char *cursor = NULL; + int status = 0; + do { + if (srht_list_user_repos(client, cfg->owner, cursor, &res)) + return -1; + + for (size_t i = 0; i < res.repos_len; i++) { + if (!quiet) + printf("Repo: %s\t%s\n", res.repos[i].name, + res.repos[i].url); + + const struct repo_ctx repo = { + .git_base = git_base, + .owner = res.canonical_name, + .token = cfg->token, + .name = res.repos[i].name, + .url = res.repos[i].url, + .username = res.canonical_name, + }; + if (git_mirror_repo(&repo, quiet) != 0) { + fprintf(stderr, "Failed to mirror repo\n"); + status = -1; + break; + } + } + + if (res.cursor) { + free(cursor); + cursor = strdup(res.cursor); + } + srht_list_repos_res_free(res); + } while (res.cursor != NULL); + + free(cursor); + gql_client_free(client); return status; } + int main(int argc, char **argv) { setbuf(stdout, NULL); @@ -167,16 +231,28 @@ int main(int argc, char **argv) } int status = 0; - const struct github_cfg *owner = cfg->head; - while (owner) { - if (!cfg->quiet) - printf("Mirroring owner: %s\n", owner->owner); - if (mirror_owner(cfg->git_base, owner, cfg->quiet)) { - fprintf(stderr, "Failed to mirror owner: %s\n", - owner->owner); - status = 1; + const struct remote_cfg *remote = cfg->head; + while (remote) { + switch (remote->type) { + case remote_type_github: + if (mirror_github(cfg->git_base, &remote->gh, + cfg->quiet)) { + fprintf(stderr, "Failed to mirror owner: %s\n", + remote->gh.owner); + status = 1; + } + break; + case remote_type_srht: + if (mirror_srht(cfg->git_base, &remote->srht, + cfg->quiet)) { + fprintf(stderr, + "Failed to mirror sr.ht owner: %s\n", + remote->srht.owner); + status = 1; + } + break; } - owner = owner->next; + remote = remote->next; } config_free(cfg); diff --git a/src/srht/client.c b/src/srht/client.c new file mode 100644 index 0000000..b6d9a50 --- /dev/null +++ b/src/srht/client.c @@ -0,0 +1,65 @@ +// +// Created by Anshul Gupta on 6/10/25. +// + +#include "client.h" + +#include "queries/srht/srht_list_repos.h" + +#include "../buffer.h" + +int srht_list_user_repos(const gql_client *client, const char *username, + const char *cursor, struct srht_list_repos_res *res) +{ + int status = 0; + buffer_t buf = buffer_new(4096); + + cJSON *args = cJSON_CreateObject(); + if (!args) { + status = -1; + goto end; + } + cJSON_AddItemToObject(args, "username", cJSON_CreateString(username)); + + if (cursor != NULL) + cJSON_AddItemToObject(args, "cursor", + cJSON_CreateString(cursor)); + else + cJSON_AddItemToObject(args, "cursor", cJSON_CreateNull()); + + const CURLcode ret = + gql_client_send(client, srht_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 (gql_handle_error(root) < 0) { + cJSON_Delete(root); + status = -1; + goto end; + } + + // Convert json to struct + if (srht_list_repos_from_json(root, res) < 0) { + fprintf(stderr, "Failed to parse response\n"); + status = -1; + } + +end: + buffer_free(buf); + return status; +} diff --git a/src/srht/client.h b/src/srht/client.h new file mode 100644 index 0000000..2ab1f79 --- /dev/null +++ b/src/srht/client.h @@ -0,0 +1,15 @@ +// +// Created by Anshul Gupta on 6/10/25. +// + +#ifndef SRHT_CLIENT_H +#define SRHT_CLIENT_H + +#include "../client.h" + +#include "types.h" + +int srht_list_user_repos(const gql_client *client, const char *username, + const char *cursor, struct srht_list_repos_res *res); + +#endif // SRHT_CLIENT_H diff --git a/src/srht/types.c b/src/srht/types.c new file mode 100644 index 0000000..65fde91 --- /dev/null +++ b/src/srht/types.c @@ -0,0 +1,149 @@ +// +// Created by Anshul Gupta on 6/10/25. +// + +#include "types.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#define SRHT_GIT_BASE_URL "ssh://git@git.sr.ht/" + +static char *srht_url(const char *canonical_name, const char *repo_name) +{ + if (!canonical_name || !repo_name) { + fprintf(stderr, "Error: canonical_name or repo_name is NULL\n"); + return NULL; + } + + size_t len = strlen(SRHT_GIT_BASE_URL) + strlen(canonical_name) + + strlen(repo_name) + 2; // +2 for '/' and '\0' + char *url = malloc(len); + if (!url) { + fprintf(stderr, "Error: memory allocation failed for url\n"); + return NULL; + } + snprintf(url, len, "%s%s/%s", SRHT_GIT_BASE_URL, canonical_name, + repo_name); + return url; +} + +int srht_list_repos_from_json(cJSON *root, struct srht_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 user object + cJSON *user = cJSON_GetObjectItemCaseSensitive(data, "user"); + if (!user || !cJSON_IsObject(user)) { + fprintf(stderr, "Error: user object not found\n"); + status = -1; + goto end; + } + + // Get the canonicalName value + cJSON *canonical_name = + cJSON_GetObjectItemCaseSensitive(user, "canonicalName"); + if (!canonical_name || !cJSON_IsString(canonical_name)) { + fprintf(stderr, "Error: canonicalName not found\n"); + status = -1; + goto end; + } + res->canonical_name = strdup(canonical_name->valuestring); + + // Get the repositories object + cJSON *repositories = + cJSON_GetObjectItemCaseSensitive(user, "repositories"); + if (!repositories || !cJSON_IsObject(repositories)) { + fprintf(stderr, "Error: repositories object not found\n"); + status = -1; + goto end; + } + + // Get the cursor value + cJSON *cursor = cJSON_GetObjectItemCaseSensitive(repositories, + "cursor"); + if (!cursor) { + fprintf(stderr, "Error: cursor not found\n"); + status = -1; + goto end; + } + if (cJSON_IsNull(cursor)) + res->cursor = NULL; + else if (cJSON_IsString(cursor)) + res->cursor = strdup(cursor->valuestring); + else { + fprintf(stderr, "Error: cursor is not a string or null\n"); + status = -1; + goto end; + } + + // Get the results array + cJSON *results = cJSON_GetObjectItemCaseSensitive(repositories, + "results"); + if (!results || !cJSON_IsArray(results)) { + fprintf(stderr, "Error: results array not found\n"); + status = -1; + goto end; + } + + // Iterate over the results array + size_t len = cJSON_GetArraySize(results); + res->repos = malloc(sizeof(*res->repos) * len); + if (!res->repos) { + fprintf(stderr, "Error: memory allocation failed for repos\n"); + status = -1; + goto end; + } + cJSON_ArrayForEach(repo, results) + { + if (!cJSON_IsObject(repo)) { + fprintf(stderr, + "Error: expected an object in results array\n"); + status = -1; + goto end; + } + + cJSON *name = cJSON_GetObjectItemCaseSensitive(repo, "name"); + if (!name || !cJSON_IsString(name)) { + fprintf(stderr, + "Error: name not found in repo object\n"); + status = -1; + goto end; + } + res->repos[res->repos_len].name = strdup(name->valuestring); + res->repos[res->repos_len].url = + srht_url(res->canonical_name, + res->repos[res->repos_len].name); + res->repos_len++; + } + +end: + cJSON_Delete(root); + if (status != 0) + srht_list_repos_res_free(*res); + return status; +} + +void srht_list_repos_res_free(struct srht_list_repos_res res) +{ + free(res.cursor); + free(res.canonical_name); + 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/srht/types.h b/src/srht/types.h new file mode 100644 index 0000000..6d9f5be --- /dev/null +++ b/src/srht/types.h @@ -0,0 +1,24 @@ +// +// Created by Anshul Gupta on 6/10/25. +// + +#ifndef SRHT_TYPES_H +#define SRHT_TYPES_H + +#include <cjson/cJSON.h> + +struct srht_list_repos_res { + char *cursor; + char *canonical_name; + + struct { + char *name; + char *url; + } *repos; + size_t repos_len; +}; + +int srht_list_repos_from_json(cJSON *root, struct srht_list_repos_res *res); +void srht_list_repos_res_free(struct srht_list_repos_res res); + +#endif // SRHT_TYPES_H |