Nico Sonack: 4 interactive: teach gcli_cmd_prompt to return NULL when an answer is optional pulls: Add an option for adding reviewers when creating a pull request gitlab: implement auto-adding reviewers when creating a merge request github: teach github pull creation to handle requesting reviews 11 files changed, 154 insertions(+), 58 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~herrhotzenplotz/gcli-devel/patches/55295/mbox | git am -3Learn more about email & git
This introduced two constants that I added for readability. These have been added in places where gcli_cmd_prompt is being used in the required way. Signed-off-by: Nico Sonack <nsonack@herrhotzenplotz.de> --- include/gcli/cmd/interactive.h | 3 +++ src/cmd/interactive.c | 25 +++++++++++++++++++------ src/cmd/issues.c | 4 ++-- src/cmd/pulls.c | 2 +- src/cmd/status_interactive.c | 8 ++++++-- 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/include/gcli/cmd/interactive.h b/include/gcli/cmd/interactive.h index be7e91a..b64c77d 100644 --- a/include/gcli/cmd/interactive.h +++ b/include/gcli/cmd/interactive.h @@ -34,6 +34,9 @@ #include <config.h> #endif +#define GCLI_PROMPT_RESULT_MANDATORY NULL +#define GCLI_PROMPT_RESULT_OPTIONAL "" + char *gcli_cmd_prompt(char const *const fmt, char const *const deflt, ...); int gcli_cmd_into_pager(int (*fn)(void *), void *data); diff --git a/src/cmd/interactive.c b/src/cmd/interactive.c index eb576df..eaa8ce2 100644 --- a/src/cmd/interactive.c +++ b/src/cmd/interactive.c @@ -135,31 +135,44 @@ get_input_line(char *const prompt) * capabilities. The prompt can be specified using a format string. * An optional default value can be specified. If the default value * is NULL the user will be repeatedly prompted until the input is - * non-empty. */ + * non-empty. If the default value is an empty string NULL will be + * returned if the user didn't give an answer. */ char * gcli_cmd_prompt(char const *const fmt, char const *const deflt, ...) { - va_list vp; + bool want_exit; char *result; char prompt[256] = {0}; size_t prompt_len; + va_list vp; va_start(vp, deflt); vsnprintf(prompt, sizeof(prompt), fmt, vp); va_end(vp); prompt_len = strlen(prompt); - if (deflt) { + if (deflt && *deflt) { snprintf(prompt + prompt_len, sizeof(prompt) - prompt_len, " [%s]: ", deflt); } else { strncat(prompt, ": ", sizeof(prompt) - prompt_len - 1); } - do { + for (;;) { result = get_input_line(prompt); - } while (deflt == NULL && result == NULL); - if (result == NULL) + want_exit = + /* default is empty string */ + (deflt && *deflt == '\0') || + /* result is empty but we have a default */ + (result == NULL && deflt != NULL) || + /* we have a non-empty response from the user */ + (result != NULL); + + if (want_exit) + break; + } + + if (result == NULL && deflt && *deflt) result = strdup(deflt); return result; diff --git a/src/cmd/issues.c b/src/cmd/issues.c index 6f27faf..aae952c 100644 --- a/src/cmd/issues.c +++ b/src/cmd/issues.c @@ -1,5 +1,5 @@ /* - * Copyright 2022 Nico Sonack <nsonack@herrhotzenplotz.de> + * Copyright 2022-2024 Nico Sonack <nsonack@herrhotzenplotz.de> * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -275,7 +275,7 @@ subcommand_issue_create_interactive(struct gcli_submit_issue_options *const opts if (!opts->repo) opts->repo = gcli_cmd_prompt("Repository", deflt_repo); - opts->title = gcli_cmd_prompt("Title", NULL); + opts->title = gcli_cmd_prompt("Title", GCLI_PROMPT_RESULT_MANDATORY); rc = create_issue(opts, false); if (rc < 0) { diff --git a/src/cmd/pulls.c b/src/cmd/pulls.c index 1bc4104..5096770 100644 --- a/src/cmd/pulls.c +++ b/src/cmd/pulls.c @@ -491,7 +491,7 @@ subcommand_pull_create_interactive(struct gcli_submit_pull_options *const opts) } /* Meta */ - opts->title = gcli_cmd_prompt("Title", NULL); + opts->title = gcli_cmd_prompt("Title", GCLI_PROMPT_RESULT_MANDATORY); opts->automerge = sn_yesno("Enable automerge?"); /* create_pull is going to pop up the editor */ diff --git a/src/cmd/status_interactive.c b/src/cmd/status_interactive.c index c9b9682..e306a59 100644 --- a/src/cmd/status_interactive.c +++ b/src/cmd/status_interactive.c @@ -95,7 +95,10 @@ handle_issue_notification(struct gcli_notification const *const notif) gcli_issue_print_summary(&issue); for (;;) { - user_input = gcli_cmd_prompt( "[%s] What? (status, discussion, quit)", NULL, notif->repository); + user_input = gcli_cmd_prompt( + "[%s] What? (status, discussion, quit)", + GCLI_PROMPT_RESULT_MANDATORY, + notif->repository); if (strcmp(user_input, "quit") == 0 || strcmp(user_input, "q") == 0) { @@ -161,7 +164,8 @@ gcli_status_interactive(void) print_notification_table(&list); for (;;) { - user_input = gcli_cmd_prompt("Enter number, list or quit", NULL); + user_input = gcli_cmd_prompt("Enter number, list or quit", + GCLI_PROMPT_RESULT_MANDATORY); if (strcmp(user_input, "q") == 0 || strcmp(user_input, "quit") == 0) { -- 2.45.2
Signed-off-by: Nico Sonack <nsonack@herrhotzenplotz.de> --- docs/gcli-pulls.1.in | 7 +++++++ include/gcli/pulls.h | 2 ++ src/cmd/issues.c | 4 +++- src/cmd/pulls.c | 27 ++++++++++++++++++++++++++- 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/docs/gcli-pulls.1.in b/docs/gcli-pulls.1.in index 0a598a0..06d7cfe 100644 --- a/docs/gcli-pulls.1.in +++ b/docs/gcli-pulls.1.in @@ -24,6 +24,7 @@ .Op Fl t Ar branch .Op Fl f Ar owner:branch .Op Fl y +.Op Fl R Ar reviewer .Op Ar "PR title..." .Sh DESCRIPTION Use @@ -121,6 +122,12 @@ omit this flag and gcli will try to infer this information. Do not ask for confirmation before creating the PR. Assume yes. .It Fl a , -automerge Enable the automerge feature when creating the PR. +.It Fl R , -reviewer Ar reviewer +Add the given +.Ar reviewer +as a reviewer for the pull request that is to be created. +To add multiple people as reviewers specify this option more than +once, one for each reviewer. .It Ar "PR Title..." The title of the Pull Request or Merge Request. .El diff --git a/include/gcli/pulls.h b/include/gcli/pulls.h index fa40944..faedece 100644 --- a/include/gcli/pulls.h +++ b/include/gcli/pulls.h @@ -102,6 +102,8 @@ struct gcli_submit_pull_options { char *body; char **labels; size_t labels_size; + char **reviewers; + size_t reviewers_size; int draft; bool automerge; /** Automatically merge the PR when a pipeline passes */ }; diff --git a/src/cmd/issues.c b/src/cmd/issues.c index aae952c..4bca8eb 100644 --- a/src/cmd/issues.c +++ b/src/cmd/issues.c @@ -50,7 +50,7 @@ static void usage(void) { - fprintf(stderr, "usage: gcli issues create [-o owner -r repo] [-y] [title...]\n"); + fprintf(stderr, "usage: gcli issues create [-o owner -r repo] [-y] [-R reviewer] [title...]\n"); fprintf(stderr, " gcli issues [-o owner -r repo] [-a] [-n number] [-A author] [-L label]\n"); fprintf(stderr, " [-M milestone] [-s] [search query...]\n"); fprintf(stderr, " gcli issues [-o owner -r repo] -i issue actions...\n"); @@ -65,6 +65,8 @@ usage(void) fprintf(stderr, " -s Print (sort) in reverse order\n"); fprintf(stderr, " -n number Number of issues to fetch (-1 = everything)\n"); fprintf(stderr, " -i issue ID of issue to perform actions on\n"); + fprintf(stderr, " -R reviewer Mark a person as a reviewer for the created PR\n"); + fprintf(stderr, " Can be specified more than once.\n"); fprintf(stderr, "ACTIONS:\n"); fprintf(stderr, " all Display both status and and op\n"); fprintf(stderr, " status Display status information\n"); diff --git a/src/cmd/pulls.c b/src/cmd/pulls.c index 5096770..27d0166 100644 --- a/src/cmd/pulls.c +++ b/src/cmd/pulls.c @@ -494,6 +494,22 @@ subcommand_pull_create_interactive(struct gcli_submit_pull_options *const opts) opts->title = gcli_cmd_prompt("Title", GCLI_PROMPT_RESULT_MANDATORY); opts->automerge = sn_yesno("Enable automerge?"); + /* Reviewers */ + for (;;) { + char *response; + + response = gcli_cmd_prompt("Add reviewer? (name or leave empty)", + GCLI_PROMPT_RESULT_OPTIONAL); + if (response == NULL) + break; + + opts->reviewers = realloc( + opts->reviewers, + (opts->reviewers_size + 1) * sizeof(*opts->reviewers)); + + opts->reviewers[opts->reviewers_size++] = response; + } + /* create_pull is going to pop up the editor */ rc = create_pull(opts, false); if (rc < 0) { @@ -541,10 +557,14 @@ subcommand_pull_create(int argc, char *argv[]) .has_arg = required_argument, .flag = NULL, .val = 'a' }, + { .name = "reviewer", + .has_arg = required_argument, + .flag = NULL, + .val = 'R' }, {0}, }; - while ((ch = getopt_long(argc, argv, "ayf:t:do:r:l:", options, NULL)) != -1) { + while ((ch = getopt_long(argc, argv, "ayf:t:do:r:l:R:", options, NULL)) != -1) { switch (ch) { case 'f': opts.from = optarg; @@ -566,6 +586,11 @@ subcommand_pull_create(int argc, char *argv[]) opts.labels, sizeof(*opts.labels) * (opts.labels_size + 1)); opts.labels[opts.labels_size++] = optarg; break; + case 'R': /* add a reviewer */ + opts.reviewers = realloc( + opts.reviewers, sizeof(*opts.reviewers) * (opts.reviewers_size + 1)); + opts.reviewers[opts.reviewers_size++] = optarg; + break; case 'y': always_yes = 1; break; -- 2.45.2
Signed-off-by: Nico Sonack <nsonack@herrhotzenplotz.de> --- src/gitlab/merge_requests.c | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/gitlab/merge_requests.c b/src/gitlab/merge_requests.c index d8b9bf5..aa7924f 100644 --- a/src/gitlab/merge_requests.c +++ b/src/gitlab/merge_requests.c @@ -1,5 +1,5 @@ /* - * Copyright 2021, 2022 Nico Sonack <nsonack@herrhotzenplotz.de> + * Copyright 2021-2024 Nico Sonack <nsonack@herrhotzenplotz.de> * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -678,6 +678,7 @@ gitlab_perform_submit_mr(struct gcli_ctx *ctx, struct gcli_submit_pull_options * gcli_jsongen_objmember(&gen, "target_project_id"); gcli_jsongen_number(&gen, target.id); + /* Labels if any */ if (opts->labels_size) { gcli_jsongen_objmember(&gen, "labels"); @@ -686,6 +687,23 @@ gitlab_perform_submit_mr(struct gcli_ctx *ctx, struct gcli_submit_pull_options * gcli_jsongen_string(&gen, opts->labels[i]); gcli_jsongen_end_array(&gen); } + + /* Reviewers if any */ + if (opts->reviewers_size) { + gcli_jsongen_objmember(&gen, "reviewer_ids"); + + gcli_jsongen_begin_array(&gen); + for (size_t i = 0; i < opts->reviewers_size; ++i) { + int uid; + + uid = gitlab_user_id(ctx, opts->reviewers[i]); + if (uid < 0) + return uid; + + gcli_jsongen_number(&gen, uid); + } + gcli_jsongen_end_array(&gen); + } } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); -- 2.45.2
Signed-off-by: Nico Sonack <nsonack@herrhotzenplotz.de> --- src/github/pulls.c | 110 +++++++++++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 44 deletions(-) diff --git a/src/github/pulls.c b/src/github/pulls.c index d40ad15..6e28760 100644 --- a/src/github/pulls.c +++ b/src/github/pulls.c @@ -1,5 +1,5 @@ /* - * Copyright 2021, 2022 Nico Sonack <nsonack@herrhotzenplotz.de> + * Copyright 2021-2024 Nico Sonack <nsonack@herrhotzenplotz.de> * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -431,6 +431,52 @@ github_pull_set_automerge(struct gcli_ctx *const ctx, char const *const node_id) return rc; } +static int +github_pull_add_reviewers(struct gcli_ctx *ctx, char const *owner, + char const *repo, gcli_id pr_number, + char const *const *users, size_t users_size) +{ + int rc = 0; + char *url, *payload, *e_owner, *e_repo; + struct gcli_jsongen gen = {0}; + + /* URL-encode repo and owner */ + e_owner = gcli_urlencode(owner); + e_repo = gcli_urlencode(repo); + + /* /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers */ + url = sn_asprintf("%s/repos/%s/%s/pulls/%"PRIid"/requested_reviewers", + gcli_get_apibase(ctx), e_owner, e_repo, pr_number); + + /* Generate payload */ + gcli_jsongen_init(&gen); + gcli_jsongen_begin_object(&gen); + { + gcli_jsongen_objmember(&gen, "reviewers"); + + gcli_jsongen_begin_array(&gen); + for (size_t i = 0; i < users_size; ++i) + gcli_jsongen_string(&gen, users[i]); + + gcli_jsongen_end_array(&gen); + } + gcli_jsongen_end_object(&gen); + + payload = gcli_jsongen_to_string(&gen); + gcli_jsongen_free(&gen); + + /* Perform request */ + rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, NULL); + + /* Cleanup */ + free(payload); + free(url); + free(e_repo); + free(e_owner); + + return rc; +} + int github_perform_submit_pull(struct gcli_ctx *ctx, struct gcli_submit_pull_options *opts) @@ -473,9 +519,12 @@ github_perform_submit_pull(struct gcli_ctx *ctx, rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, &buffer); - /* Add labels if requested. GitHub doesn't allow us to do this all - * with one request. */ - if (rc == 0 && (opts->labels_size || opts->automerge)) { + /* Add labels or reviewers if requested or set automerge. + * GitHub doesn't allow us to do this all with one request. */ + if (rc == 0 && (opts->labels_size || + opts->automerge || + opts->reviewers_size)) + { struct json_stream json = {0}; struct gcli_pull pull = {0}; @@ -483,9 +532,17 @@ github_perform_submit_pull(struct gcli_ctx *ctx, parse_github_pull(ctx, &json, &pull); if (opts->labels_size) { - rc = github_issue_add_labels(ctx, opts->owner, opts->repo, pull.id, - (char const *const *)opts->labels, - opts->labels_size); + rc = github_issue_add_labels( + ctx, opts->owner, opts->repo, pull.number, + (char const *const *)opts->labels, + opts->labels_size); + } + + if (rc == 0 && opts->reviewers_size) { + rc = github_pull_add_reviewers( + ctx, opts->owner, opts->repo, pull.number, + (char const *const *)opts->reviewers, + opts->reviewers_size); } if (rc == 0 && opts->automerge) { @@ -497,7 +554,6 @@ github_perform_submit_pull(struct gcli_ctx *ctx, json_close(&json); } - gcli_fetch_buffer_free(&buffer); free(payload); free(url); @@ -594,42 +650,8 @@ github_pull_add_reviewer(struct gcli_ctx *ctx, char const *owner, char const *repo, gcli_id pr_number, char const *username) { - int rc = 0; - char *url, *payload, *e_owner, *e_repo; - struct gcli_jsongen gen = {0}; - - /* URL-encode repo and owner */ - e_owner = gcli_urlencode(owner); - e_repo = gcli_urlencode(repo); - - /* /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers */ - url = sn_asprintf("%s/repos/%s/%s/pulls/%"PRIid"/requested_reviewers", - gcli_get_apibase(ctx), e_owner, e_repo, pr_number); - - /* Generate payload */ - gcli_jsongen_init(&gen); - gcli_jsongen_begin_object(&gen); - { - gcli_jsongen_objmember(&gen, "reviewers"); - gcli_jsongen_begin_array(&gen); - gcli_jsongen_string(&gen, username); - gcli_jsongen_end_array(&gen); - } - gcli_jsongen_end_object(&gen); - - payload = gcli_jsongen_to_string(&gen); - gcli_jsongen_free(&gen); - - /* Perform request */ - rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, NULL); - - /* Cleanup */ - free(payload); - free(url); - free(e_repo); - free(e_owner); - - return rc; + return github_pull_add_reviewers(ctx, owner, repo, pr_number, + &username, 1); } int -- 2.45.2