Compare commits

...

25 commits

Author SHA1 Message Date
e937523a25
feat: support for delete option in push 2024-08-05 22:02:40 +02:00
9fa645fe69
fix: get current branch name correctly 2024-08-03 21:24:20 +02:00
67489a6c60
fix: space indentation for cli help 2024-07-29 22:30:23 +02:00
5a6a2e056b
feat: gimi provider help message 2024-07-29 22:29:53 +02:00
6015636ae7
fix: print new line at the end of cli help 2024-07-28 22:46:25 +02:00
1ee984bd69
refactor: use bitwise for options instead two booleans 2024-07-28 22:45:33 +02:00
f9e9a5868c
feat: ci help message 2024-07-27 22:15:01 +02:00
aaabf9edeb
refactor: remove OPTION_WITH_ARG 2024-07-26 22:59:39 +02:00
f61fa52851
fix: push tags correctly 2024-07-24 07:34:58 +02:00
7031935db2
fix: missing semicolon 2024-07-24 07:33:05 +02:00
5312dd135e
feat: add support for tag pushing 2024-07-24 07:32:10 +02:00
780ca9eb94
docs: add newline 2024-07-23 18:25:44 +02:00
21ceb3a40e
docs: use markdown for readme 2024-07-23 18:24:21 +02:00
382dc02e3f
docs: add issue tracker & miling list 2024-07-23 18:23:24 +02:00
c4222ac9e6
fix: free config & provider after using 2024-07-23 18:19:37 +02:00
4c3271703d
fix: add push option only for specific providers 2024-07-23 18:13:38 +02:00
45d13856ec
feat: check if cfg exists 2024-07-23 18:07:58 +02:00
cdc87b444a
feat: skip ci if gimi push is used 2024-07-23 18:02:59 +02:00
e0e29bfd28
fix: add ssh keys to ~/.ssh/config 2024-07-23 18:01:18 +02:00
91479c8f94
fix: separate branch pushing & tags pushing 2024-07-23 17:51:05 +02:00
d9fdea2a26
feat: add support for sourcehut ci generation 2024-07-23 17:49:52 +02:00
7b8e6d5eb4
ci: go to the source dir 2024-07-23 12:49:18 +02:00
331abf287e
ci: add secret & ssh-keyscan 2024-07-23 12:48:13 +02:00
c199fde849
feat: ci generation 2024-07-23 12:36:43 +02:00
31c457851a
build: add codeberg mirror 2024-07-22 22:56:56 +02:00
14 changed files with 331 additions and 37 deletions

26
.builds/alpine.yml Normal file
View file

@ -0,0 +1,26 @@
image: alpine/latest
packages:
- git
sources:
- "https://git.sr.ht/~hyro/gimi"
secrets:
- 55691174-b52f-477c-81ea-dd32deff19b8 # github
- 3eaa5e52-e929-4822-842b-e817e0be7e39 # codeberg
tasks:
- sync: |
cd gimi
set +x
git remote add gimi-github git@github.com:xhyrom/gimi.git
ssh-keyscan github.com >> ~/.ssh/known_hosts
echo -e "Host github.com\nUser git\nIdentityFile ${HOME}/.ssh/55691174-b52f-477c-81ea-dd32deff19b8" >> ~/.ssh/config
git push -f --all gimi-github
git push -f --tags gimi-github
git remote add gimi-codeberg git@codeberg.org:xHyroM/gimi.git
ssh-keyscan codeberg.org >> ~/.ssh/known_hosts
echo -e "Host codeberg.org\nUser git\nIdentityFile ${HOME}/.ssh/3eaa5e52-e929-4822-842b-e817e0be7e39" >> ~/.ssh/config
git push -f --all gimi-codeberg
git push -f --tags gimi-codeberg
set -x

2
.gimi/config.toml Normal file → Executable file
View file

@ -4,3 +4,5 @@ ssh = "git@git.sr.ht:~hyro/gimi"
primary = true primary = true
[providers.github] [providers.github]
ssh = "git@github.com:xhyrom/gimi.git" ssh = "git@github.com:xhyrom/gimi.git"
[providers.codeberg]
ssh = "git@codeberg.org:xHyroM/gimi.git"

1
README
View file

@ -1 +0,0 @@
Gimi is a tool for managing multiple git remotes as mirrors written in C.

4
README.md Normal file
View file

@ -0,0 +1,4 @@
gimi is a tool for managing multiple git remotes as mirrors written in C.
issue tracker: https://todo.sr.ht/~hyro/gimi \
mailing list (PRs): https://lists.sr.ht/~hyro/gimi

View file

@ -1,4 +1,5 @@
#include "../gimi_constants.h" #include "../gimi_constants.h"
#include "command/ci.h"
#include "command/config.h" #include "command/config.h"
#include "command/init.h" #include "command/init.h"
#include "command/provider.h" #include "command/provider.h"
@ -9,7 +10,15 @@
#define HELP \ #define HELP \
"Usage: gimi [-h | --help] [-v | --version]\n\n" \ "Usage: gimi [-h | --help] [-v | --version]\n\n" \
"A simple tool for managing multiple git remotes as mirrors\n\n" \ "A simple tool for managing multiple git remotes as mirrors\n\n" \
"Commands\n" "Useful commands for working with gimi:\n" \
" ci Generate CI configuration for a specific provider\n" \
" config Manage gimi configuration settings\n" \
" init Initialize a gimi project in the current workspace\n" \
" provider Manage providers (git remotes as mirrors)\n" \
" push Push all branches or tags to all providers\n\n" \
"Options:\n" \
" -h, --help Show this help message and exit\n" \
" -v, --version Show the version of gimi\n"
void cli_print_help() { printf("%s", HELP); } void cli_print_help() { printf("%s", HELP); }
void cli_print_version() { void cli_print_version() {
@ -20,14 +29,18 @@ void cli_print_version() {
int cli_handle(int argc, char **argv) { int cli_handle(int argc, char **argv) {
char *sub_command = argv[0]; char *sub_command = argv[0];
if (strcmp(sub_command, "init") == 0) { if (strcmp(sub_command, "ci") == 0) {
return cli_command_init(argc, argv); return cli_command_ci(argc, argv);
} }
if (strcmp(sub_command, "config") == 0) { if (strcmp(sub_command, "config") == 0) {
return cli_command_config(argc, argv); return cli_command_config(argc, argv);
} }
if (strcmp(sub_command, "init") == 0) {
return cli_command_init(argc, argv);
}
if (strcmp(sub_command, "provider") == 0) { if (strcmp(sub_command, "provider") == 0) {
return cli_command_provider(argc, argv); return cli_command_provider(argc, argv);
} }

View file

@ -13,9 +13,15 @@
argc -= optind; \ argc -= optind; \
argv += optind; argv += optind;
#define OPTION(opt, func) \ #define OPTION(opt, func, ...) \
case opt: { \ case opt: { \
func(); \ func(__VA_ARGS__); \
break; \
}
#define MASK_OPTION(opt, mask, field) \
case opt: { \
mask |= field; \
break; \ break; \
} }

184
src/cli/command/ci.c Normal file
View file

@ -0,0 +1,184 @@
#include "../../config.h"
#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#define SOURCEHUT \
"image: alpine/latest\n" \
"packages:\n" \
" - git\n" \
"sources:\n" \
" - \"https://%s/%s/%s\"\n" \
"secrets:\n" \
" - <secret>\n" \
"tasks:\n" \
" - sync: |\n" \
" cd %s\n" \
" set +x" \
" %s\n\n" \
" set -x\n"
struct repository {
char *domain;
char *owner;
char *repo;
};
struct repository *translate_ssh_to_repository(char *ssh) {
const char *at = strchr(ssh, '@');
const char *colon = strchr(ssh, ':');
const char *slash = strchr(colon, '/');
if (!at || !colon || at > colon) {
return NULL;
}
struct repository *repo =
(struct repository *)malloc(sizeof(struct repository));
if (!repo) {
return NULL;
}
int domain_len = colon - at - 1;
repo->domain = (char *)malloc(domain_len + 1);
snprintf(repo->domain, domain_len + 1, "%.*s", domain_len, at + 1);
int repo_len = slash - colon - 1;
repo->owner = (char *)malloc(repo_len + 1);
snprintf(repo->owner, repo_len + 1, "%.*s", repo_len, colon + 1);
int slash_len = strlen(slash) - 1;
repo->repo = (char *)malloc(slash_len + 1);
snprintf(repo->repo, slash_len + 1, "%s", slash + 1);
return repo;
}
void repository_free(struct repository *repo) {
free(repo->owner);
free(repo->repo);
free(repo);
}
char *generate_sourcehut(struct gimi_config *cfg,
struct gimi_config_provider *main_provider) {
char remotes[1024] = "";
for (int i = 0; i < cfg->providers_size; i++) {
struct gimi_config_provider *provider = cfg->providers[i];
if (strcmp(provider->name, main_provider->name) == 0)
continue;
struct repository *repo = translate_ssh_to_repository(provider->ssh);
char buf[1024];
snprintf(buf, sizeof(buf),
"\n"
"\n git remote add gimi-%s %s "
"\n ssh-keyscan %s >> ~/.ssh/known_hosts"
"\n echo -e \"Host %s\\nUser git\\nIdentityFile "
"${HOME}/.ssh/<secret>\" >> ~/.ssh/config"
"\n git push -f --all gimi-%s"
"\n git push -f --tags gimi-%s",
provider->name, provider->ssh, repo->domain, repo->domain,
provider->name, provider->name);
strcat(remotes, buf);
repository_free(repo);
}
char *buf = (char *)malloc(2048);
struct repository *repo = translate_ssh_to_repository(main_provider->ssh);
snprintf(buf, 2048, SOURCEHUT, repo->domain, repo->owner, repo->repo,
repo->repo, remotes);
repository_free(repo);
return buf;
}
int generate(int argc, char **argv) {
struct gimi_config *cfg = config_read();
ASSERT_CONFIG_EXIST(cfg);
struct gimi_config_provider *provider;
if (argc == 2) {
provider = config_find_provider(cfg, argv[1]);
if (provider == NULL) {
printf("error: no such provider '%s'", argv[1]);
return 1;
}
} else {
for (int i = 0; i < cfg->providers_size; i++) {
if (cfg->providers[i]->primary) {
provider = cfg->providers[i];
break;
}
}
if (provider == NULL) {
printf("error: can't find primary provider");
return 1;
}
};
char *template;
if (strcmp(provider->name, "sourcehut") == 0) {
template = generate_sourcehut(cfg, provider);
int ret = mkdir(".builds", S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
if (ret != 0 && errno != EEXIST) {
printf("error: failed to create directory .builds with errno %d", errno);
return ret;
}
FILE *file_ptr;
file_ptr = fopen(".builds/gimi.yml", "w");
if (file_ptr == NULL) {
printf("error: failed to open file .builds/gimi.yml");
return 1;
}
fprintf(file_ptr, "%s", template);
fclose(file_ptr);
} else {
printf("error: provider '%s' is not supported, create the ci yourself.",
provider->name);
config_free(cfg);
free(provider);
return 1;
}
config_free(cfg);
free(provider);
return 0;
}
int cli_command_ci(int argc, char **argv) {
if (argc == 1) {
printf("usage: gimi ci generate [provider]");
return 1;
}
// remove "ci" from args
argc -= 1;
argv += 1;
char *subcommand = argv[0];
if (strcmp(subcommand, "generate") == 0) {
return generate(argc, argv);
}
return 0;
}

1
src/cli/command/ci.h Normal file
View file

@ -0,0 +1 @@
int cli_command_ci(int argc, char **argv);

View file

@ -6,11 +6,7 @@
int cli_command_config(int argc, char **argv) { int cli_command_config(int argc, char **argv) {
struct gimi_config *cfg = config_read(); struct gimi_config *cfg = config_read();
if (!cfg) { ASSERT_CONFIG_EXIST(cfg);
printf(
"error: missing gimi config, initialize it using gimi init command.");
return 1;
}
for (int i = 0; i < cfg->providers_size; i++) { for (int i = 0; i < cfg->providers_size; i++) {
struct gimi_config_provider *provider = cfg->providers[i]; struct gimi_config_provider *provider = cfg->providers[i];

View file

@ -10,7 +10,7 @@
int cli_command_init(int argc, char **argv) { int cli_command_init(int argc, char **argv) {
errno = 0; errno = 0;
int ret = mkdir(".gimi", S_IRWXU); int ret = mkdir(".gimi", S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
if (ret == -1 && errno != EEXIST) { if (ret == -1 && errno != EEXIST) {
printf("error: failed to initialize gimi.\n"); printf("error: failed to initialize gimi.\n");
return 1; return 1;

View file

@ -5,10 +5,17 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#define HELP \
"Usage: gimi provider [-h | --help]\n\n" \
"Commands for working with gimi provider:\n" \
" info Print info about the provider\n" \
" sync Synchronize gimi provider with git\n\n" \
"Options:\n" \
" -h, --help Show this help message and exit\n"
int providers() { int providers() {
struct gimi_config *cfg = config_read(); struct gimi_config *cfg = config_read();
if (!cfg) ASSERT_CONFIG_EXIST(cfg);
return 1;
for (int i = 0; i < cfg->providers_size; i++) { for (int i = 0; i < cfg->providers_size; i++) {
struct gimi_config_provider *provider = cfg->providers[i]; struct gimi_config_provider *provider = cfg->providers[i];
@ -27,11 +34,9 @@ int provider_info(int argc, char **argv) {
} }
struct gimi_config *cfg = config_read(); struct gimi_config *cfg = config_read();
if (!cfg) ASSERT_CONFIG_EXIST(cfg);
return 1;
struct gimi_config_provider *provider = config_find_provider(cfg, argv[1]); struct gimi_config_provider *provider = config_find_provider(cfg, argv[1]);
config_free(cfg);
if (!provider) { if (!provider) {
printf("error: no such provider '%s'", argv[1]); printf("error: no such provider '%s'", argv[1]);
@ -42,6 +47,7 @@ int provider_info(int argc, char **argv) {
printf("ssh: %s\n", provider->ssh); printf("ssh: %s\n", provider->ssh);
printf("primary: %d\n", provider->primary); printf("primary: %d\n", provider->primary);
config_free(cfg);
free(provider); free(provider);
return 0; return 0;
@ -54,11 +60,9 @@ int provider_sync(int argc, char **argv) {
} }
struct gimi_config *cfg = config_read(); struct gimi_config *cfg = config_read();
if (!cfg) ASSERT_CONFIG_EXIST(cfg);
return 1;
struct gimi_config_provider *provider = config_find_provider(cfg, argv[1]); struct gimi_config_provider *provider = config_find_provider(cfg, argv[1]);
config_free(cfg);
char command[100]; char command[100];
snprintf(command, sizeof(command), "git remote add gimi-%s %s", snprintf(command, sizeof(command), "git remote add gimi-%s %s",
@ -73,6 +77,9 @@ int provider_sync(int argc, char **argv) {
provider->name); provider->name);
} }
config_free(cfg);
free(provider);
return 0; return 0;
} }
@ -87,6 +94,11 @@ int cli_command_provider(int argc, char **argv) {
char *subcommand = argv[0]; char *subcommand = argv[0];
if (strcmp(argv[0], "--help") == 0 || strcmp(argv[0], "-h") == 0) {
printf("%s", HELP);
return 0;
}
if (strcmp(subcommand, "info") == 0) { if (strcmp(subcommand, "info") == 0) {
return provider_info(argc, argv); return provider_info(argc, argv);
} }

View file

@ -1,41 +1,68 @@
#include "../../config.h" #include "../../config.h"
#include "../cli.h"
#include <linux/limits.h> #include <linux/limits.h>
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <unistd.h>
#define PUSH_TAGS_OPTION 1 << 0
#define VERBOSE_OPTION 1 << 1
#define DELETE_BRANCH_OPTION 1 << 2
char *get_current_branch_name() { char *get_current_branch_name() {
FILE *file_ptr; FILE *file_ptr;
static char output[256]; char output[256];
file_ptr = popen("git rev-parse --abbrev-ref HEAD", "r"); file_ptr = popen("git rev-parse --abbrev-ref HEAD", "r");
if (file_ptr == NULL) { if (file_ptr == NULL) {
return NULL; return NULL;
} }
char *branch = fgets(output, sizeof(output), file_ptr); if (fgets(output, sizeof(output), file_ptr) == NULL) {
if (branch != NULL) { pclose(file_ptr);
// remove new line return NULL;
size_t len = strlen(branch); }
if (len > 0 && branch[len - 1] == '\n') {
branch[len - 1] = '\0'; // remove new line
} size_t len = strlen(output);
if (len > 0 && output[len - 1] == '\n') {
output[len - 1] = '\0';
} }
pclose(file_ptr); pclose(file_ptr);
return branch; return strdup(output);
} }
int git_push(char *provider_name, char *branch_name, bool verbose) { int git_push(char *provider_name, char *branch_name, int options) {
FILE *file_ptr; FILE *file_ptr;
char output[1024]; char output[1024];
char command[256]; char command[256];
snprintf(command, sizeof(command), "git push gimi-%s %s 2>&1", provider_name, char push_options[50] = "";
branch_name);
if (options & DELETE_BRANCH_OPTION) {
strcat(push_options, " -d");
} else {
if (strcmp(provider_name, "sourcehut") == 0) {
strcat(push_options, " -o skip-ci");
} else if (strcmp(provider_name, "gitlab") == 0) {
strcat(push_options, " -o ci.skip");
}
}
char branch_or_tags[256] = "";
if (options & PUSH_TAGS_OPTION) {
strcat(branch_or_tags, "--tags");
} else {
strcat(branch_or_tags, branch_name);
}
snprintf(command, sizeof(command), "git push%s gimi-%s %s 2>&1", push_options,
provider_name, branch_or_tags);
file_ptr = popen(command, "r"); file_ptr = popen(command, "r");
if (file_ptr == NULL) { if (file_ptr == NULL) {
@ -44,7 +71,7 @@ int git_push(char *provider_name, char *branch_name, bool verbose) {
while (fgets(output, sizeof(output), file_ptr) != while (fgets(output, sizeof(output), file_ptr) !=
NULL) { // need to process for valid exit code NULL) { // need to process for valid exit code
if (verbose) { if (options & VERBOSE_OPTION) {
printf("%s", output); printf("%s", output);
} }
} }
@ -55,17 +82,27 @@ int git_push(char *provider_name, char *branch_name, bool verbose) {
int cli_command_push(int argc, char **argv) { int cli_command_push(int argc, char **argv) {
struct gimi_config *cfg = config_read(); struct gimi_config *cfg = config_read();
if (!cfg) ASSERT_CONFIG_EXIST(cfg);
return 1;
bool verbose = argc == 2 && strcmp(argv[1], "--verbose") == 0; int options = 0;
HANDLE_OPTIONS(argc, argv, "tvd",
MASK_OPTION('t', options, PUSH_TAGS_OPTION)
MASK_OPTION('v', options, VERBOSE_OPTION)
MASK_OPTION('d', options, DELETE_BRANCH_OPTION));
if ((options & DELETE_BRANCH_OPTION) && (options & PUSH_TAGS_OPTION)) {
printf(
"error: options '-d' (delete) and '-t' (tags) cannot be used together");
return 1;
}
char *branch_name = get_current_branch_name(); char *branch_name = get_current_branch_name();
for (int i = 0; i < cfg->providers_size; i++) { for (int i = 0; i < cfg->providers_size; i++) {
struct gimi_config_provider *provider = cfg->providers[i]; struct gimi_config_provider *provider = cfg->providers[i];
int ret = git_push(provider->name, branch_name, verbose); int ret = git_push(provider->name, branch_name, options);
if (ret != 0) { if (ret != 0) {
printf("error: failed to push into '%s' with git's exit code %d.\n", printf("error: failed to push into '%s' with git's exit code %d.\n",
provider->name, ret); provider->name, ret);
@ -75,6 +112,7 @@ int cli_command_push(int argc, char **argv) {
printf("info: successfully pushed into '%s'.\n", provider->name); printf("info: successfully pushed into '%s'.\n", provider->name);
} }
free(branch_name);
config_free(cfg); config_free(cfg);
return 0; return 0;

View file

@ -64,6 +64,11 @@ struct gimi_config *config_read() {
} }
void config_free(struct gimi_config *cfg) { void config_free(struct gimi_config *cfg) {
for (int i = 0; i < cfg->providers_size; i++) {
free(cfg->providers[i]->name);
free(cfg->providers[i]->ssh);
}
free(cfg->providers); free(cfg->providers);
free(cfg); free(cfg);
} }
@ -74,6 +79,7 @@ struct gimi_config_provider *config_find_provider(struct gimi_config *cfg,
for (int i = 0; i < cfg->providers_size; i++) { for (int i = 0; i < cfg->providers_size; i++) {
if (strcmp(cfg->providers[i]->name, name) == 0) { if (strcmp(cfg->providers[i]->name, name) == 0) {
provider = cfg->providers[i]; provider = cfg->providers[i];
break;
} }
} }

View file

@ -1,5 +1,12 @@
#include <stdbool.h> #include <stdbool.h>
#define ASSERT_CONFIG_EXIST(cfg) \
if (!cfg) { \
printf( \
"error: missing gimi config, initialize it using gimi init command."); \
return 1; \
}
struct gimi_config_provider { struct gimi_config_provider {
char *name; char *name;
char *ssh; char *ssh;