summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar Anshul Gupta <ansg191@anshulg.com> 2025-06-12 01:53:38 -0700
committerGravatar GitHub <noreply@github.com> 2025-06-12 01:53:38 -0700
commitc9adb09abc626cdcc35c345a635ad8c163fcff3e (patch)
tree93f78bf8e5910a044f96ed77aca498ac60ada804 /src
parent179679633a9fc3317585167f86c87a7fe8394945 (diff)
parentb78875e2265339b58c7a67cc83e6db2988aa0d74 (diff)
downloadgithub-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.c179
-rw-r--r--src/client.h30
-rw-r--r--src/config.c142
-rw-r--r--src/config.h35
-rw-r--r--src/git.c30
-rw-r--r--src/git.h3
-rw-r--r--src/github.c290
-rw-r--r--src/github.h30
-rw-r--r--src/github/client.c100
-rw-r--r--src/github/client.h17
-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.c120
-rw-r--r--src/srht/client.c65
-rw-r--r--src/srht/client.h15
-rw-r--r--src/srht/types.c149
-rw-r--r--src/srht/types.h24
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
diff --git a/src/git.c b/src/git.c
index b8156ce..75f9888 100644
--- a/src/git.c
+++ b/src/git.c
@@ -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;
diff --git a/src/git.h b/src/git.h
index 06d3e92..ae222b8 100644
--- a/src/git.h
+++ b/src/git.h
@@ -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
diff --git a/src/main.c b/src/main.c
index 70bf253..44dd901 100644
--- a/src/main.c
+++ b/src/main.c
@@ -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