diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3115065e..4378d813 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,9 +8,9 @@ alpine-amd64: - linux image: alpine:3.17 script: - - apk add libcurl gcc autoconf automake libtool make pkgconf musl-dev curl-dev flex bison xz gzip bzip2 libbsd-dev kyua atf-dev + - apk add libcurl gcc autoconf automake libtool make pkgconf musl-dev curl-dev flex bison xz gzip bzip2 libbsd-dev kyua atf-dev libedit-dev - ./autogen.sh - - ./configure CFLAGS='-std=c99 -pedantic -Wall -Wextra -Werror' CPPFLAGS='-D_XOPEN_SOURCE=600' --disable-silent-rules || (cat config.log && exit 42) + - ./configure CFLAGS='-std=c99 -pedantic -Wall -Wextra -Werror' CPPFLAGS='-D_XOPEN_SOURCE=600' --disable-silent-rules --with-libedit || (cat config.log && exit 42) - make -j - make -j distcheck @@ -49,8 +49,8 @@ debian-amd64: image: debian:bullseye script: - apt-get update - - apt-get install -y --no-install-recommends build-essential libcurl4-openssl-dev pkgconf autotools-dev bison flex make autoconf automake libtool libbsd-dev libatf-dev kyua + - apt-get install -y --no-install-recommends build-essential libcurl4-openssl-dev pkgconf autotools-dev bison flex make autoconf automake libtool libbsd-dev libatf-dev kyua libreadline-dev - ./autogen.sh - - ./configure CFLAGS='-std=c99 -pedantic -Wall -Wextra -Werror -Wno-misleading-indentation' CPPFLAGS='-D_XOPEN_SOURCE=600' --disable-silent-rules || (cat config.log && exit 42) + - ./configure CFLAGS='-std=c99 -pedantic -Wall -Wextra -Werror -Wno-misleading-indentation' CPPFLAGS='-D_XOPEN_SOURCE=600' --disable-silent-rules --with-readline || (cat config.log && exit 42) - make -j - make -j distcheck diff --git a/Changelog.md b/Changelog.md index 1f1cb8f2..c056c5d4 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,56 @@ This changelog does not follow semantic versioning. +## 2.3.0 (2024-Mar-25) + +### Added + +- It is now possible to build gcli against libgcli as a DLL on cygwin. + Submitted by: Daisuke Fujimura + +- The pulls subcommand now allows searching for pull requests with + a given search term. The search terms can be appended to the + regular pull subcommand for listing PRs: + + ```console + $ gcli pulls -L bug segmentation fault + ``` + + The above will search for pull requests containing »segmentation + fault« and the label »bug«. + +- An interactive mode for creating both PRs and issues has been added. + You can now interactively create pull requests and issues by omitting their title: + + ```console + $ gcli issues create + Owner [herrhotzenplotz]: + Repository [gcli]: + Title: foo + The following issue will be created: + + TITLE : foo + OWNER : herrhotzenplotz + REPO : gcli + MESSAGE : + No message + + Do you want to continue? [yN] + ``` + +### Fixed + +- gcli was incorrectly using an environment variable *XDG_CONFIG_DIR*. + This variable has now been fixed to be *XDG_CONFIG_HOME*. + Submitted by: Jakub Wilk + +- Fixed a segmentation fault when listing forks + +- Fixed error when submitting a comment on Gitlab issues + +- The build on Haiku has been fixed. GCLI can now be compiled and + used on this platform. + ## 2.2.0 (2024-Feb-05) ### Added diff --git a/Makefile.am b/Makefile.am index 9af5865f..2c09679e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -13,6 +13,7 @@ AM_CFLAGS = -Wno-gnu-zero-variadic-macro-arguments noinst_PROGRAMS = pgen$(EXEEXT) pgen_SOURCES = \ include/gcli/pgen.h \ + thirdparty/sn/sn.c \ src/pgen/dump_c.c \ src/pgen/dump_h.c \ src/pgen/dump_plain.c \ @@ -20,13 +21,13 @@ pgen_SOURCES = \ src/pgen/lexer.l lib_LTLIBRARIES = libgcli.la -noinst_LTLIBRARIES = libsn.la libpdjson.la +noinst_LTLIBRARIES = libpdjson.la # For testing puproses I'll reenable parallel builds. If it breaks again, uncomment. # .NOTPARALLEL: pgen$(EXEEXT) $(builddir)/src/pgen/parser.c $(builddir)/src/pgen/parser.h $(builddir)/src/pgen/lexer.c src/pgen/lexer.c: src/pgen/parser.h -libgcli_la_DEPENDENCIES = pgen$(EXEEXT) libpdjson.la libsn.la +libgcli_la_DEPENDENCIES = pgen$(EXEEXT) libpdjson.la $(BUILT_SOURCES): pgen$(EXEEXT) @@ -40,7 +41,8 @@ SUFFIXES = .t bin_PROGRAMS = gcli$(EXEEXT) gcli_LDADD = libgcli.la -libgcli_la_LIBADD = libpdjson.la libsn.la +libgcli_la_LIBADD = libpdjson.la +libgcli_la_LDFLAGS = -no-undefined dist_man_MANS = \ docs/gcli-api.1 \ @@ -82,13 +84,10 @@ gcli_SOURCES = \ include/gcli/cmd/snippets.h src/cmd/snippets.c \ include/gcli/cmd/status.h src/cmd/status.c \ include/gcli/cmd/table.h src/cmd/table.c \ + include/gcli/cmd/interactive.h src/cmd/interactive.c \ src/cmd/api.c \ src/cmd/gcli.c -libsn_la_SOURCES = \ - thirdparty/sn/sn.c \ - thirdparty/sn/sn.h - libpdjson_la_SOURCES = \ thirdparty/pdjson/pdjson.c \ thirdparty/pdjson/pdjson.h @@ -200,7 +199,8 @@ libgcli_la_SOURCES = \ src/bugzilla/bugs.c include/gcli/bugzilla/bugs.h \ src/bugzilla/bugs-parser.c include/gcli/bugzilla/bugs-parser.h \ src/bugzilla/config.c include/gcli/bugzilla/config.h \ - $(TEMPLATES) + $(TEMPLATES) \ + thirdparty/sn/sn.c thirdparty/sn/sn.h libgcli_la_CPPFLAGS = \ $(AM_CPPFLAGS) \ @@ -245,57 +245,84 @@ do_test: $(check_PROGRAMS) tests/Kyuafile tests_json_escape_SOURCES = \ tests/json-escape.c +tests_json_escape_CFLAGS = \ + $(AM_CFLAGS) \ + $(LIBATFC_CFLAGS) tests_json_escape_LDADD = \ - libgcli.la libpdjson.la libsn.la \ - $(LIBATFC_LDFLAGS) + libgcli.la libpdjson.la \ + $(LIBATFC_LIBS) tests_github_parse_tests_SOURCES = \ tests/github-parse-tests.c +tests_github_parse_tests_CFLAGS = \ + $(AM_CFLAGS) \ + $(LIBATFC_CFLAGS) tests_github_parse_tests_LDADD = \ - libgcli.la libpdjson.la libsn.la \ - $(LIBATFC_LDFLAGS) + libgcli.la libpdjson.la \ + $(LIBATFC_LIBS) tests_gitlab_parse_tests_SOURCES = \ tests/gitlab-parse-tests.c +tests_gitlab_parse_tests_CFLAGS = \ + $(AM_CFLAGS) \ + $(LIBATFC_CFLAGS) tests_gitlab_parse_tests_LDADD = \ - libgcli.la libpdjson.la libsn.la \ - $(LIBATFC_LDFLAGS) + libgcli.la libpdjson.la \ + $(LIBATFC_LIBS) tests_gitea_parse_tests_SOURCES = \ tests/gitea-parse-tests.c +tests_gitea_parse_tests_CFLAGS = \ + $(AM_CFLAGS) \ + $(LIBATFC_CFLAGS) tests_gitea_parse_tests_LDADD = \ - libgcli.la libpdjson.la libsn.la \ - $(LIBATFC_LDFLAGS) + libgcli.la libpdjson.la \ + $(LIBATFC_LIBS) tests_bugzilla_parse_tests_SOURCES = \ tests/bugzilla-parse-tests.c +tests_bugzilla_parse_tests_CFLAGS = \ + $(AM_CFLAGS) \ + $(LIBATFC_CFLAGS) tests_bugzilla_parse_tests_LDADD = \ - libgcli.la libpdjson.la libsn.la \ - $(LIBATFC_LDFLAGS) + libgcli.la libpdjson.la \ + $(LIBATFC_LIBS) tests_base64_tests_SOURCES = \ tests/base64-tests.c +tests_base64_tests_CFLAGS = \ + $(AM_CFLAGS) \ + $(LIBATFC_CFLAGS) tests_base64_tests_LDADD = \ - libgcli.la libpdjson.la libsn.la \ - $(LIBATFC_LDFLAGS) + libgcli.la libpdjson.la \ + $(LIBATFC_LIBS) tests_url_encode_SOURCES = \ tests/url-encode.c +tests_url_encode_CFLAGS = \ + $(AM_CFLAGS) \ + $(LIBATFC_CFLAGS) tests_url_encode_LDADD = \ - libgcli.la libpdjson.la libsn.la \ - $(LIBATFC_LDFLAGS) + libgcli.la libpdjson.la \ + $(LIBATFC_LIBS) tests_pretty_print_test_SOURCES = \ - tests/pretty_print_test.c + tests/pretty_print_test.c \ + thirdparty/sn/sn.c thirdparty/sn/sn.h +tests_pretty_print_test_CFLAGS = \ + $(AM_CFLAGS) \ + $(LIBATFC_CFLAGS) tests_pretty_print_test_LDADD = \ - libsn.la \ - $(LIBATFC_LDFLAGS) + $(LIBATFC_LIBS) tests_test_jsongen_SOURCES = \ tests/test-jsongen.c +tests_test_jsongen_CFLAGS = \ + $(AM_CFLAGS) \ + $(LIBATFC_CFLAGS) tests_test_jsongen_LDADD = \ libgcli.la \ - $(LIBATFC_LDFLAGS) + $(LIBATFC_LIBS) EXTRA_DIST += tests/samples/github_simple_comment.json \ tests/samples/github_simple_fork.json \ diff --git a/README.md b/README.md index bb8773a5..6365c878 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,9 @@ consider using the link above. There are official packages available: - [FreeBSD](https://freshports.org/devel/gcli) +- [Debian Testing](https://packages.debian.org/trixie/gcli) +- [ArchLinux AUR](https://aur.archlinux.org/packages/gcli) +- [NixPkgs Unstable](https://search.nixos.org/packages?channel=unstable&show=gcli&from=0&size=50&sort=relevance&type=packages&query=gcli) ### Dependencies diff --git a/configure.ac b/configure.ac index c683c784..0238c0e0 100644 --- a/configure.ac +++ b/configure.ac @@ -3,14 +3,14 @@ AC_PREREQ([2.69]) AC_INIT([gcli], - [2.2.0], + [2.3.0], [~herrhotzenplotz/gcli-discuss@lists.sr.ht], [gcli], [https://herrhotzenplotz.de/gcli]) AM_INIT_AUTOMAKE([1.0 foreign subdir-objects dist-bzip2 dist-xz -Wall]) dnl Release Date. -PACKAGE_DATE="2024-Feb-05" +PACKAGE_DATE="2024-May-25" AC_SUBST([PACKAGE_DATE]) dnl Silent by default. @@ -42,10 +42,13 @@ if ! test -z "${CCACHE}"; then CC="${CCACHE} ${CC}" fi +dnl #################################################################################### +dnl LIBCURL +dnl #################################################################################### dnl Go looking for libcurl OPT_LIBCURL=check AC_ARG_WITH([libcurl], -[AS_HELP_STRING([--with-libcurl], +[AS_HELP_STRING([--with-libcurl[[=DIR]]], [Give an alternate path to libcurl.])], OPT_LIBCURL=$withval ) @@ -63,10 +66,82 @@ AC_CHECK_HEADER([curl/curl.h],,[AC_MSG_ERROR([Cannot find libcurl headers])]) dnl FIXME find a better way for this dnl AC_CHECK_LIB([curl],[curl_easy_init],,[AC_MSG_ERROR([-lcurl doesn not contain curl_easy_init])]) +dnl #################################################################################### +dnl LIBEDIT +dnl #################################################################################### +dnl Check for libedit +OPT_LIBEDIT=check +AC_ARG_WITH([libedit], + [AS_HELP_STRING([--with-libedit[[=DIR]]], + [Use libedit at the given prefix for interactive editing])], + OPT_LIBEDIT=$withval, + OPT_LIBEDIT=no +) + +HAVE_LIBEDIT=0 +if test "x$OPT_LIBEDIT" = "xyes" || test "x$OPT_LIBEDIT" = "xcheck"; then + HAVE_LIBEDIT=1 + + PKG_CHECK_MODULES([LIBEDIT], [libedit],,[AC_MSG_ERROR([Could not find libedit])]) + CFLAGS="$LIBEDIT_CFLAGS $CFLAGS" + LDFLAGS="$LIBEDIT_LIBS $LDFLAGS" +elif ! test "x$OPT_LIBEDIT" = "xno"; then + HAVE_LIBEDIT=1 + + CPPFLAGS="-I$OPT_LIBEDIT/include $CPPFLAGS" + LDFLAGS="-L$OPT_LIBEDIT/lib $LDFLAGS" +fi + +if test $HAVE_LIBEDIT -eq 1; then + AC_CHECK_HEADER([histedit.h],,[AC_MSG_ERROR([Cannot find libedit headers])]) +fi +AC_DEFINE_UNQUOTED([HAVE_LIBEDIT], [$HAVE_LIBEDIT], [Define if we link against libedit]) + +dnl #################################################################################### +dnl READLINE (second option) +dnl #################################################################################### +if ! test $HAVE_LIBEDIT -eq 1; then + OPT_READLINE=check + AC_ARG_WITH([readline], + [AS_HELP_STRING([--with-readline[[=DIR]]], + [Use readline at the given prefix for interactive editing. + When libedit is available this option is a no-op.])], + OPT_READLINE=$withval, + OPT_READLINE=no + ) + + HAVE_READLINE=0 + if test "x$OPT_READLINE" = "xyes" || test "x$OPT_READLINE" = "xcheck"; then + HAVE_READLINE=1 + # FIXME: This is an ugly hack because the readline headers are + # not C99 clean. Clang seems to not like them and causes + # the configure script to fail. + CFLAGS="${CFLAGS} -Wno-strict-prototypes" + + PKG_CHECK_MODULES([READLINE], [readline],,[AC_MSG_ERROR([Could not find readline])]) + CFLAGS="$READLINE_CFLAGS $CFLAGS" + LDFLAGS="$READLINE_LIBS $LDFLAGS" + elif ! test "x$OPT_READLINE" = "xno"; then + HAVE_READLINE=1 + + CPPFLAGS="-I$OPT_READLINE/include $CPPFLAGS" + LDFLAGS="-L$OPT_READLINE/lib $LDFLAGS" + fi + + if test $HAVE_READLINE -eq 1; then + AC_CHECK_HEADER([readline/readline.h],,[AC_MSG_ERROR([Cannot find readline headers])]) + fi + AC_DEFINE_UNQUOTED([HAVE_READLINE], [$HAVE_READLINE], [Define if we link against readline]) +fi + +dnl #################################################################################### +dnl TEST SUITE STUFF +dnl #################################################################################### + dnl For the test suite we require libatf-c and Kyua OPT_LIBATFC=check AC_ARG_WITH([libatf-c], -[AS_HELP_STRING([--with-libatf-c], +[AS_HELP_STRING([--with-libatf-c[[=DIR]]], [Give an alternate path to libatf-c.])], OPT_LIBATFC=$withval ) @@ -75,23 +150,32 @@ HAVE_ATFC=no if test "x$OPT_LIBATFC" = "xcheck" || test "x$OPT_LIBATFC" = "xyes" then PKG_CHECK_MODULES([LIBATFC], [atf-c], [HAVE_ATFC=yes],[HAVE_ATFC=no]) - CFLAGS="$LIBATFC_CFLAGS $CFLAGS" - LDFLAGS="$LIBATFC_LIBS $LDFLAGS" elif test "x$OPT_LIBATFC" = "xno" then HAVE_ATFC=no else - CPPFLAGS="-I$OPT_LIBATFC/include $CPPFLAGS" - LDFLAGS="-L$OPT_LIBATFC/lib $LDFLAGS" + LIBATFC_CFLAGS="-I$OPT_LIBATFC/include" + LIBATFC_LIBS="-L$OPT_LIBATFC/lib" HAVE_ATFC=yes fi -AS_IF([test "x$HAVE_ATFC" = "xyes"], - [AC_CHECK_HEADER([atf-c.h],,[AC_MSG_ERROR([Cannot find libatf-c headers])]) - HAVE_ATFC=yes]) +if test "x$HAVE_ATFC" = "xyes"; then + AC_SUBST([LIBATFC_CFLAGS]) + AC_SUBST([LIBATFC_LIBS]) + + _OLD_CFLAGS="$CFLAGS" + _OLD_LDFLAGS="$LDFLAGS" + + CFLAGS="$CFLAGS $LIBATFC_CFLAGS" + LDFLAGS="$LDFLAGS $LIBATFC_LIBS" -dnl FIXME find a better way to do this -dnl AC_CHECK_LIB([atf-c],[atf_no_error],,[AC_MSG_ERROR([-latf-c doesn not contain atf_no_error])]) + AC_CHECK_HEADER([atf-c.h],,[AC_MSG_ERROR([Cannot find libatf-c headers])]) + + CFLAGS="$_OLD_CFLAGS" + LDFLAGS="$_OLD_LDFLAGS" + + HAVE_ATFC=yes +fi AC_CHECK_PROG([KYUA], [kyua], [kyua]) AC_CHECK_PROGS([REALPATH], [realpath grealpath], []) diff --git a/docs/gcli-comment.1.in b/docs/gcli-comment.1.in index 58b34836..c4e3427b 100644 --- a/docs/gcli-comment.1.in +++ b/docs/gcli-comment.1.in @@ -20,7 +20,7 @@ issues on GitHub and Gitea, making the .Fl i and .Fl p -flags exchangable without changing the overall effect of creating the +flags exchangeable without changing the overall effect of creating the comment. .Nm will open an editor, either specified in your environment through diff --git a/docs/gcli-issues.1.in b/docs/gcli-issues.1.in index d28658ab..40706460 100644 --- a/docs/gcli-issues.1.in +++ b/docs/gcli-issues.1.in @@ -22,6 +22,7 @@ .Cm create .Op Fl o Ar owner Fl r Ar repo .Op Fl y +.Op Ar issue-title .Sh DESCRIPTION Use .Nm @@ -50,9 +51,9 @@ option. .It Fl A , Fl -author Ar user Only list issues authored by the given user. .It Fl L , Fl -label Ar label -Filter issues by the given label. This option may only be specfied once. +Filter issues by the given label. This option may only be specified once. .It Fl M , Fl -milestone Ar milestone -Filter issues by the given milestone. This option may only be specfied once. +Filter issues by the given milestone. This option may only be specified once. .It Fl n , -count Ar n Fetch at least .Ar n @@ -75,6 +76,9 @@ for the specified Create a new issue in the given or autodetected repository. The editor will come up and ask you to enter an issue message. .Pp +When the issue title is omitted gcli will interactively prompt you +for all the details to create an issue. +.Pp The following flags can be specified: .Bl -tag -width indent .It Fl i , -in Ar owner/repo @@ -139,7 +143,15 @@ in contour-terminal/contour on GitHub including closed issues: $ gcli -t github issues -o contour-terminal -r contour -a crash .Ed .Pp -Report a new issue in the current project: +Report a new issue in the current project; interactively asking for +details: +.Bd -literal -offset indent +$ gcli issues create +.Ed +.Pp +Report a new issue titled +.Dq summary here +in the current project: .Bd -literal -offset indent $ gcli issues create "summary here" .Ed diff --git a/docs/gcli-pulls.1.in b/docs/gcli-pulls.1.in index 7b6c44bb..6fea8b02 100644 --- a/docs/gcli-pulls.1.in +++ b/docs/gcli-pulls.1.in @@ -13,6 +13,7 @@ .Op Fl s .Op Fl n Ar n .Op Fl o Ar owner Fl r Ar repo +.Op Ar search-terms... .Nm .Fl i Ar pr .Op Fl o Ar owner Fl r Ar repo @@ -23,7 +24,7 @@ .Op Fl t Ar branch .Op Fl f Ar owner:branch .Op Fl y -.Ar "PR title..." +.Op Ar "PR title..." .Sh DESCRIPTION Use .Nm @@ -93,6 +94,9 @@ on the specified Create a new PR in the given or autodetected repository. The editor will come up and ask you to enter the PR message. .Pp +When the title is omitted gcli will interactively prompt the various +options listed below, including the title. +.Pp The following flags can be specified: .Bl -tag -width indent .It Fl o , -owner Ar owner @@ -184,6 +188,11 @@ Print a list of open PRs in the current project: $ gcli pulls .Ed .Pp +Create a new PR and let gcli interactively prompt you for details: +.Bd -literal -offset indent +$ gcli pr create +.Ed +.Pp Create a new PR in the current Project, the head is the currently checked out branch of git. See .Xr git-status 1 diff --git a/docs/gcli.1.in b/docs/gcli.1.in index b7a2fc9b..09f8835c 100644 --- a/docs/gcli.1.in +++ b/docs/gcli.1.in @@ -28,7 +28,7 @@ command). .Pp The default behaviour of .Nm -can be overriden to accomodate more nuanced use cases. Manual +can be overridden to accommodate more nuanced use cases. Manual overrides must be passed before subcommands and their options. .Sh SUBCOMMANDS Most of these subcommands are documented in dedicated man pages. @@ -104,7 +104,7 @@ unless stdout is not a tty. See This is useful in combination with modern pagers such as .Xr less 1 . .It Fl q , -quiet -Supresses most output of +Suppresses most output of .Nm . .It Fl v , -verbose Be very verbose. This means that warnings about missing config files @@ -153,12 +153,12 @@ Operate on the given numeric identifier. Other options specific to the context are documented in the respective man pages. .Sh ENVIRONMENT -.Bl -tag -width XDG_CONFIG_DIR +.Bl -tag -width XDG_CONFIG_HOME .It Ev EDITOR If the gcli config file does not name an editor, .Nm may use this editor. -.It Ev XDG_CONFIG_DIR +.It Ev XDG_CONFIG_HOME There should be a subdirectory called gcli in the directory this environment variable points to where .Nm @@ -183,8 +183,8 @@ escape sequences. See (--colours). .El .Sh FILES -.Bl -tag -width ${XDG_CONFIG_DIR}/gcli/config -compact -.It Pa ${XDG_CONFIG_DIR}/gcli/config +.Bl -tag -width ${XDG_CONFIG_HOME}/gcli/config -compact +.It Pa ${XDG_CONFIG_HOME}/gcli/config The user configuration file for gcli. It contains account definitions as well as sensible default values. See .Xr gcli 5 . diff --git a/docs/gcli.5.in b/docs/gcli.5.in index d6ebdd77..64f33b8c 100644 --- a/docs/gcli.5.in +++ b/docs/gcli.5.in @@ -15,7 +15,7 @@ into the repository and provide these default values to other users as well. .Ss User Configuration File The user configuration file is located in -.Pa ${XDG_CONFIG_DIR}/gcli/config . +.Pa ${XDG_CONFIG_HOME}/gcli/config . On most systems this equal to .Pa ${HOME}/.config/gcli/config . .Pp @@ -103,7 +103,8 @@ It contains a list of key-value pairs. Allowed keys are: Name of a branch that the changes should be merged into by default. Usually this is one of .Em master , -.Em main or +.Em main +or .Em trunk . .It pr.upstream Name of the upstream repository to submit the pull request to by default. diff --git a/docs/pgen.org b/docs/pgen.org index d76742b2..02036c70 100644 --- a/docs/pgen.org +++ b/docs/pgen.org @@ -69,7 +69,7 @@ "things" => things as array of thing use parse_thing); #+end_src - This will generate two functions with the follwing signatures: + This will generate two functions with the following signatures: #+begin_src shell :exports results :results output ../pgen -th <General

-

GCLI is available or coming to various distributions

+

GCLI is available in various distributions

If you want gcli to be available in your favourite operating system please submit them to the respective packaging tree.
diff --git a/include/gcli/bugzilla/bugs.h b/include/gcli/bugzilla/bugs.h index c11440ec..879e3660 100644 --- a/include/gcli/bugzilla/bugs.h +++ b/include/gcli/bugzilla/bugs.h @@ -56,7 +56,8 @@ int bugzilla_bug_get_attachments(struct gcli_ctx *ctx, char const *const product gcli_id const bug_id, struct gcli_attachment_list *const out); -int bugzilla_bug_submit(struct gcli_ctx *ctx, struct gcli_submit_issue_options opts, - struct gcli_fetch_buffer *out); +int bugzilla_bug_submit(struct gcli_ctx *ctx, + struct gcli_submit_issue_options *opts, + struct gcli_issue *out); #endif /* GCLI_BUGZILLA_BUGS_H */ diff --git a/include/gcli/cmd/cmdconfig.h b/include/gcli/cmd/cmdconfig.h index 26aebee2..b60fbe72 100644 --- a/include/gcli/cmd/cmdconfig.h +++ b/include/gcli/cmd/cmdconfig.h @@ -64,7 +64,7 @@ sn_sv gcli_config_get_base(struct gcli_ctx *ctx); gcli_forge_type gcli_config_get_forge_type(struct gcli_ctx *ctx); sn_sv gcli_config_get_override_default_account(struct gcli_ctx *ctx); bool gcli_config_pr_inhibit_delete_source_branch(struct gcli_ctx *ctx); -void gcli_config_get_repo(struct gcli_ctx *ctx, char const **, char const **); +int gcli_config_get_repo(struct gcli_ctx *ctx, char const **, char const **); int gcli_config_have_colours(struct gcli_ctx *ctx); struct gcli_config_entries const *gcli_config_get_section_entries( struct gcli_ctx *ctx, char const *section_name); diff --git a/include/gcli/cmd/gitconfig.h b/include/gcli/cmd/gitconfig.h index efe13055..0a4edeb6 100644 --- a/include/gcli/cmd/gitconfig.h +++ b/include/gcli/cmd/gitconfig.h @@ -50,8 +50,8 @@ void gcli_gitconfig_add_fork_remote(char const *org, char const *repo); int gcli_gitconfig_get_forgetype(struct gcli_ctx *ctx, char const *remote_name); -int gcli_gitconfig_repo_by_remote(char const *remote_name, - char const **owner, - char const **repo); +int gcli_gitconfig_repo_by_remote(struct gcli_ctx *ctx, char const *const remote_name, + char const **const owner, char const **const repo, + int *const forge); #endif /* GCLI_CMD_GITCONFIG_H */ diff --git a/include/gcli/cmd/interactive.h b/include/gcli/cmd/interactive.h new file mode 100644 index 00000000..99b3d32b --- /dev/null +++ b/include/gcli/cmd/interactive.h @@ -0,0 +1,39 @@ +/* + * Copyright 2024 Nico Sonack + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef GCLI_CMD_INTERACTIVE_H +#define GCLI_CMD_INTERACTIVE_H + +#ifdef HAVE_CONFIG_H +#include +#endif + +char *gcli_cmd_prompt(char const *const fmt, char const *const deflt, ...); + +#endif /* GCLI_CMD_INTERACTIVE_H */ diff --git a/include/gcli/cmd/table.h b/include/gcli/cmd/table.h index 2e36155d..5c1d9aa5 100644 --- a/include/gcli/cmd/table.h +++ b/include/gcli/cmd/table.h @@ -67,7 +67,6 @@ enum gcli_tblcoltype { GCLI_TBLCOLTYPE_LONG, /* signed long int */ GCLI_TBLCOLTYPE_ID, /* some ID type (uint64_t) */ GCLI_TBLCOLTYPE_STRING, /* C string */ - GCLI_TBLCOLTYPE_SV, /* sn_sv */ GCLI_TBLCOLTYPE_DOUBLE, /* double precision float */ GCLI_TBLCOLTYPE_BOOL, /* yes/no */ }; diff --git a/include/gcli/forges.h b/include/gcli/forges.h index a704bb6d..db19a5b2 100644 --- a/include/gcli/forges.h +++ b/include/gcli/forges.h @@ -165,8 +165,8 @@ struct gcli_forge_descriptor { * Submit an issue */ int (*perform_submit_issue)( struct gcli_ctx *ctx, - struct gcli_submit_issue_options opts, - struct gcli_fetch_buffer *out); + struct gcli_submit_issue_options *opts, + struct gcli_issue *out); /** * Change the title of an issue */ @@ -279,7 +279,7 @@ struct gcli_forge_descriptor { /** * Get a list of PRs/MRs on the given repo */ - int (*get_pulls)( + int (*search_pulls)( struct gcli_ctx *ctx, char const *owner, char const *reponame, @@ -341,7 +341,7 @@ struct gcli_forge_descriptor { * Submit PR/MR */ int (*perform_submit_pull)( struct gcli_ctx *ctx, - struct gcli_submit_pull_options opts); + struct gcli_submit_pull_options *opts); /** * Get a list of commits in the given PR/MR */ diff --git a/include/gcli/gitea/issues.h b/include/gcli/gitea/issues.h index 12742c49..58a59fa0 100644 --- a/include/gcli/gitea/issues.h +++ b/include/gcli/gitea/issues.h @@ -44,8 +44,9 @@ int gitea_get_issue_summary(struct gcli_ctx *ctx, char const *owner, char const *repo, gcli_id issue_number, struct gcli_issue *out); -int gitea_submit_issue(struct gcli_ctx *ctx, struct gcli_submit_issue_options opts, - struct gcli_fetch_buffer *out); +int gitea_submit_issue(struct gcli_ctx *ctx, + struct gcli_submit_issue_options *opts, + struct gcli_issue *out); int gitea_issue_close(struct gcli_ctx *ctx, char const *owner, char const *repo, gcli_id issue_number); diff --git a/include/gcli/gitea/pulls.h b/include/gcli/gitea/pulls.h index 401575b5..881f9811 100644 --- a/include/gcli/gitea/pulls.h +++ b/include/gcli/gitea/pulls.h @@ -37,9 +37,10 @@ #include #include -int gitea_get_pulls(struct gcli_ctx *ctx, char const *owner, char const *repo, - struct gcli_pull_fetch_details const *details, int max, - struct gcli_pull_list *out); +int gitea_search_pulls(struct gcli_ctx *ctx, char const *owner, + char const *repo, + struct gcli_pull_fetch_details const *details, + int const max, struct gcli_pull_list *const out); int gitea_get_pull(struct gcli_ctx *ctx, char const *owner, char const *repo, gcli_id pr_number, struct gcli_pull *out); @@ -48,7 +49,7 @@ int gitea_get_pull_commits(struct gcli_ctx *ctx, char const *owner, char const *repo, gcli_id pr_number, struct gcli_commit_list *out); -int gitea_pull_submit(struct gcli_ctx *ctx, struct gcli_submit_pull_options opts); +int gitea_pull_submit(struct gcli_ctx *ctx, struct gcli_submit_pull_options *opts); int gitea_pull_merge(struct gcli_ctx *ctx, char const *owner, char const *repo, gcli_id pr_number, enum gcli_merge_flags flags); diff --git a/include/gcli/github/issues.h b/include/gcli/github/issues.h index 54e5e456..c97da2f0 100644 --- a/include/gcli/github/issues.h +++ b/include/gcli/github/issues.h @@ -55,8 +55,8 @@ int github_issue_reopen(struct gcli_ctx *ctx, char const *owner, char const *repo, gcli_id issue_number); int github_perform_submit_issue(struct gcli_ctx *ctx, - struct gcli_submit_issue_options opts, - struct gcli_fetch_buffer *out); + struct gcli_submit_issue_options *opts, + struct gcli_issue *out); int github_issue_assign(struct gcli_ctx *ctx, char const *owner, char const *repo, gcli_id issue_number, diff --git a/include/gcli/github/pulls.h b/include/gcli/github/pulls.h index 30f07abc..9ec08e01 100644 --- a/include/gcli/github/pulls.h +++ b/include/gcli/github/pulls.h @@ -37,9 +37,9 @@ #include #include -int github_get_pulls(struct gcli_ctx *ctx, char const *owner, char const *repo, - struct gcli_pull_fetch_details const *details, int max, - struct gcli_pull_list *out); +int github_search_pulls(struct gcli_ctx *ctx, char const *owner, char const *repo, + struct gcli_pull_fetch_details const *details, int max, + struct gcli_pull_list *out); int github_pull_get_diff(struct gcli_ctx *ctx, FILE *stream, char const *owner, char const *reponame, gcli_id pr_number); @@ -61,7 +61,8 @@ int github_pull_reopen(struct gcli_ctx *ctx, char const *owner, int github_pull_close(struct gcli_ctx *ctx, char const *owner, char const *repo, gcli_id pr_number); -int github_perform_submit_pull(struct gcli_ctx *ctx, struct gcli_submit_pull_options opts); +int github_perform_submit_pull(struct gcli_ctx *ctx, + struct gcli_submit_pull_options *opts); int github_get_pull_commits(struct gcli_ctx *ctx, char const *owner, char const *repo, gcli_id pr_number, diff --git a/include/gcli/gitlab/issues.h b/include/gcli/gitlab/issues.h index bdb1dade..b9f7b42c 100644 --- a/include/gcli/gitlab/issues.h +++ b/include/gcli/gitlab/issues.h @@ -59,8 +59,8 @@ int gitlab_issue_assign(struct gcli_ctx *ctx, char const *owner, char const *assignee); int gitlab_perform_submit_issue(struct gcli_ctx *ctx, - struct gcli_submit_issue_options opts, - struct gcli_fetch_buffer *out); + struct gcli_submit_issue_options *opts, + struct gcli_issue *out); int gitlab_issue_add_labels(struct gcli_ctx *ctx, char const *owner, char const *repo, gcli_id issue, diff --git a/include/gcli/gitlab/merge_requests.h b/include/gcli/gitlab/merge_requests.h index 889f3ac7..d89d451b 100644 --- a/include/gcli/gitlab/merge_requests.h +++ b/include/gcli/gitlab/merge_requests.h @@ -90,7 +90,7 @@ int gitlab_get_pull_commits(struct gcli_ctx *ctx, char const *owner, char const *repo, gcli_id mr_number, struct gcli_commit_list *out); -int gitlab_perform_submit_mr(struct gcli_ctx *ctx, struct gcli_submit_pull_options opts); +int gitlab_perform_submit_mr(struct gcli_ctx *ctx, struct gcli_submit_pull_options *opts); int gitlab_mr_add_labels(struct gcli_ctx *ctx, char const *owner, char const *repo, gcli_id mr_number, diff --git a/include/gcli/issues.h b/include/gcli/issues.h index b223279e..baa38ec3 100644 --- a/include/gcli/issues.h +++ b/include/gcli/issues.h @@ -103,7 +103,7 @@ int gcli_issue_close(struct gcli_ctx *ctx, char const *owner, char const *repo, int gcli_issue_reopen(struct gcli_ctx *ctx, char const *owner, char const *repo, gcli_id issue_number); -int gcli_issue_submit(struct gcli_ctx *ctx, struct gcli_submit_issue_options); +int gcli_issue_submit(struct gcli_ctx *ctx, struct gcli_submit_issue_options *); int gcli_issue_assign(struct gcli_ctx *ctx, char const *owner, char const *repo, gcli_id issue_number, char const *assignee); diff --git a/include/gcli/pulls.h b/include/gcli/pulls.h index d9724561..6a159651 100644 --- a/include/gcli/pulls.h +++ b/include/gcli/pulls.h @@ -104,10 +104,11 @@ struct gcli_submit_pull_options { }; struct gcli_pull_fetch_details { - bool all; - char const *author; - char const *label; - char const *milestone; + bool all; /** Ignore status of the pull requests */ + char const *author; /** Author of the pull request or NULL */ + char const *label; /** a label attached to the pull request or NULL */ + char const *milestone; /** a milestone this pull request is a part of or NULL */ + char const *search_term; /** some text to match in the pull request or NULL */ }; /** Generic list of checks ran on a pull request @@ -124,9 +125,9 @@ struct gcli_pull_checks_list { int forge_type; }; -int gcli_get_pulls(struct gcli_ctx *ctx, char const *owner, char const *repo, - struct gcli_pull_fetch_details const *details, int max, - struct gcli_pull_list *out); +int gcli_search_pulls(struct gcli_ctx *ctx, char const *owner, char const *repo, + struct gcli_pull_fetch_details const *details, int max, + struct gcli_pull_list *out); void gcli_pull_free(struct gcli_pull *it); @@ -150,7 +151,7 @@ void gcli_commits_free(struct gcli_commit_list *list); int gcli_get_pull(struct gcli_ctx *ctx, char const *owner, char const *repo, gcli_id pr_number, struct gcli_pull *out); -int gcli_pull_submit(struct gcli_ctx *ctx, struct gcli_submit_pull_options); +int gcli_pull_submit(struct gcli_ctx *ctx, struct gcli_submit_pull_options *); enum gcli_merge_flags { GCLI_PULL_MERGE_SQUASH = 0x1, /* squash commits when merging */ diff --git a/src/bugzilla/bugs.c b/src/bugzilla/bugs.c index 3820727a..dc28f279 100644 --- a/src/bugzilla/bugs.c +++ b/src/bugzilla/bugs.c @@ -284,14 +284,16 @@ add_extra_options(struct gcli_nvlist const *list, struct gcli_jsongen *gen) } int -bugzilla_bug_submit(struct gcli_ctx *ctx, struct gcli_submit_issue_options opts, - struct gcli_fetch_buffer *out) +bugzilla_bug_submit(struct gcli_ctx *const ctx, + struct gcli_submit_issue_options *const opts, + struct gcli_issue *const out) { char *payload = NULL, *url = NULL; char *token; /* bugzilla wants the api token as a parameter in the url or the json payload */ - char const *product = opts.owner, *component = opts.repo, - *summary = opts.title, *description = opts.body; + char const *product = opts->owner, *component = opts->repo, + *summary = opts->title, *description = opts->body; struct gcli_jsongen gen = {0}; + struct gcli_fetch_buffer buffer = {0}, *_buffer = NULL; int rc = 0; /* prepare data for payload generation */ @@ -338,7 +340,7 @@ bugzilla_bug_submit(struct gcli_ctx *ctx, struct gcli_submit_issue_options opts, gcli_jsongen_objmember(&gen, "api_key"); gcli_jsongen_string(&gen, token); - add_extra_options(&opts.extra, &gen); + add_extra_options(&opts->extra, &gen); } gcli_jsongen_end_object(&gen); @@ -347,8 +349,24 @@ bugzilla_bug_submit(struct gcli_ctx *ctx, struct gcli_submit_issue_options opts, /* generate url and perform request */ url = sn_asprintf("%s/rest/bug", gcli_get_apibase(ctx)); - rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, out); + if (out) + _buffer = &buffer; + + rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, _buffer); + if (out && rc == 0) { + struct json_stream stream = {0}; + gcli_id id = 0; + + json_open_buffer(&stream, buffer.data, buffer.length); + rc = parse_bugzilla_bug_creation_result(ctx, &stream, &id); + json_close(&stream); + + if (rc == 0) + rc = bugzilla_get_bug(ctx, NULL, NULL, id, out); + } + + free(buffer.data); free(url); free(payload); diff --git a/src/cmd/cmd.c b/src/cmd/cmd.c index 56e7ed90..ed2e723c 100644 --- a/src/cmd/cmd.c +++ b/src/cmd/cmd.c @@ -78,8 +78,11 @@ check_owner_and_repo(const char **owner, const char **repo) if ((*owner == NULL) != (*repo == NULL)) errx(1, "gcli: error: missing either explicit owner or repo"); - if (*owner == NULL) - gcli_config_get_repo(g_clictx, owner, repo); + if (*owner == NULL) { + int rc = gcli_config_get_repo(g_clictx, owner, repo); + if (rc < 0) + errx(1, "gcli: error: %s", gcli_get_error(g_clictx)); + } } /* Parses (and updates) the given argument list into two seperate lists: diff --git a/src/cmd/cmdconfig.c b/src/cmd/cmdconfig.c index e412755e..074ca03b 100644 --- a/src/cmd/cmdconfig.c +++ b/src/cmd/cmdconfig.c @@ -32,6 +32,7 @@ #include #include +#include #include #include #include @@ -389,11 +390,11 @@ ensure_config(struct gcli_ctx *ctx) cfg->inited = true; - file_path = getenv("XDG_CONFIG_PATH"); + file_path = getenv("XDG_CONFIG_HOME"); if (!file_path) { file_path = getenv("HOME"); if (!file_path) { - warnx("Neither XDG_CONFIG_PATH nor HOME set in env"); + warnx("Neither XDG_CONFIG_HOME nor HOME set in env"); return cfg; } @@ -896,7 +897,7 @@ gcli_config_get_forge_type(struct gcli_ctx *ctx) return result; } -void +int gcli_config_get_repo(struct gcli_ctx *ctx, char const **const owner, char const **const repo) { @@ -906,30 +907,33 @@ gcli_config_get_repo(struct gcli_ctx *ctx, char const **const owner, cfg = ensure_config(ctx); if (cfg->override_remote) { - int const forge = gcli_gitconfig_repo_by_remote( - cfg->override_remote, owner, repo); + int forge = 0, rc = 0; + + rc = gcli_gitconfig_repo_by_remote(ctx, cfg->override_remote, owner, + repo, &forge); + + if (rc < 0) + return rc; if (forge >= 0) { if ((int)(gcli_config_get_forge_type(ctx)) != forge) - errx(1, "gcli: error: forge types are inconsistent"); + return gcli_error(ctx, "forge types are inconsistent"); } - return; + return 0; } if ((upstream = gcli_config_get_upstream(ctx)).length != 0) { sn_sv const owner_sv = sn_sv_chop_until(&upstream, '/'); - sn_sv const repo_sv = sn_sv_from_parts( - upstream.data + 1, - upstream.length - 1); + sn_sv const repo_sv = sn_sv_from_parts(upstream.data + 1, upstream.length - 1); *owner = sn_sv_to_cstr(owner_sv); *repo = sn_sv_to_cstr(repo_sv); - return; + return 0; } - gcli_gitconfig_repo_by_remote(NULL, owner, repo); + return gcli_gitconfig_repo_by_remote(ctx, NULL, owner, repo, NULL); } int diff --git a/src/cmd/forks.c b/src/cmd/forks.c index a3cc6d58..f38e2135 100644 --- a/src/cmd/forks.c +++ b/src/cmd/forks.c @@ -68,10 +68,10 @@ gcli_print_forks(enum gcli_output_flags const flags, size_t n; gcli_tbl table; struct gcli_tblcoldef cols[] = { - { .name = "OWNER", .type = GCLI_TBLCOLTYPE_SV, .flags = GCLI_TBLCOL_BOLD }, - { .name = "DATE", .type = GCLI_TBLCOLTYPE_SV, .flags = 0 }, - { .name = "FORKS", .type = GCLI_TBLCOLTYPE_INT, .flags = GCLI_TBLCOL_JUSTIFYR }, - { .name = "FULLNAME", .type = GCLI_TBLCOLTYPE_SV, .flags = 0 }, + { .name = "OWNER", .type = GCLI_TBLCOLTYPE_STRING, .flags = GCLI_TBLCOL_BOLD }, + { .name = "DATE", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, + { .name = "FORKS", .type = GCLI_TBLCOLTYPE_INT, .flags = GCLI_TBLCOL_JUSTIFYR }, + { .name = "FULLNAME", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, }; if (list->forks_size == 0) { diff --git a/src/cmd/gitconfig.c b/src/cmd/gitconfig.c index f851f0f4..e8cd0161 100644 --- a/src/cmd/gitconfig.c +++ b/src/cmd/gitconfig.c @@ -30,6 +30,7 @@ #include #include +#include #include #include @@ -507,29 +508,34 @@ gcli_gitconfig_get_forgetype(struct gcli_ctx *ctx, char const *const remote_name } int -gcli_gitconfig_repo_by_remote( - char const *const remote_name, - char const **const owner, - char const **const repo) +gcli_gitconfig_repo_by_remote(struct gcli_ctx *ctx, char const *const remote, + char const **const owner, char const **const repo, + int *const forge) { gcli_gitconfig_read_gitconfig(); - if (remote_name) { + if (remote) { for (size_t i = 0; i < remotes_size; ++i) { - if (sn_sv_eq_to(remotes[i].name, remote_name)) { + if (sn_sv_eq_to(remotes[i].name, remote)) { *owner = sn_sv_to_cstr(remotes[i].owner); *repo = sn_sv_to_cstr(remotes[i].repo); - return remotes[i].forge_type; + if (forge) + *forge = remotes[i].forge_type; + + return 0; } } - errx(1, "gcli: error: no such remote: %s", remote_name); + return gcli_error(ctx, "no such remote: %s", remote); } if (!remotes_size) - errx(1, "gcli: error: no remotes to auto-detect forge"); + return gcli_error(ctx, "no remotes to auto-detect forge"); *owner = sn_sv_to_cstr(remotes[0].owner); *repo = sn_sv_to_cstr(remotes[0].repo); - return remotes[0].forge_type; + if (forge) + *forge = remotes[0].forge_type; + + return 0; } diff --git a/src/cmd/interactive.c b/src/cmd/interactive.c new file mode 100644 index 00000000..e389522b --- /dev/null +++ b/src/cmd/interactive.c @@ -0,0 +1,162 @@ +/* + * Copyright 2024 Nico Sonack + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include +#include +#include +#include +#include + +#if defined(HAVE_LIBEDIT) +# if HAVE_LIBEDIT +# include +# define USE_EDITLINE 1 +# else +# define USE_EDITLINE 0 +# endif +#else +# define USE_EDITLINE 0 +#endif + +#if !USE_EDITLINE && defined(HAVE_READLINE) +# if HAVE_READLINE +# include +# define USE_READLINE 1 +# else +# define USE_READLINE 0 +# endif +#else +# define USE_READLINE 0 +#endif + +#if USE_EDITLINE +static char * +el_prompt_fn(EditLine *el_ctx) +{ + char *prompt = NULL; + if (el_get(el_ctx, EL_CLIENTDATA, &prompt) < 0) + return strdup(">"); + else + return prompt; +} +#endif + +/* Actual implementation for reading in the line */ +static char * +get_input_line(char *const prompt) +{ +#if USE_EDITLINE + static EditLine *el_ctx = NULL; + char const *txt = NULL; + char *result = NULL; + int len = 0; + + if (!el_ctx) { + el_ctx = el_init("gcli", stdin, stdout, stderr); + el_set(el_ctx, EL_PROMPT, el_prompt_fn); + el_set(el_ctx, EL_EDITOR, "emacs"); + } + + el_set(el_ctx, EL_CLIENTDATA, prompt); + + txt = el_gets(el_ctx, &len); + bool const is_error = txt == NULL || len < 0; + bool const is_empty = len == 1 && txt[0] == '\n'; + if (is_error || is_empty) + return NULL; + + result = strdup(txt); + result[len - 1] = '\0'; + + return result; + +#elif USE_READLINE + char *result = readline(prompt); + + /* readline() returns an empty string if the input is empty. Our interface + * returns NULL if the input was empty */ + if (result == NULL || result[0] == '\0') { + free(result); + result = NULL; + } + + return result; + +#else + char buf[256] = {0}; /* nobody types more than 256 characters, amirite? */ + + fputs(prompt, stdout); + fflush(stdout); + fgets(buf, sizeof buf, stdin); + + if (buf[0] == '\n') + return NULL; + + buf[strlen(buf)-1] = '\0'; + return strdup(buf); +#endif +} + +/** Prompt for input with an optional default + * + * This function prompts for user input, possibly with editline + * 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. */ +char * +gcli_cmd_prompt(char const *const fmt, char const *const deflt, ...) +{ + va_list vp; + char *result; + char prompt[256] = {0}; + size_t prompt_len; + + va_start(vp, deflt); + vsnprintf(prompt, sizeof(prompt), fmt, vp); + va_end(vp); + + prompt_len = strlen(prompt); + if (deflt) { + snprintf(prompt + prompt_len, sizeof(prompt) - prompt_len, " [%s]: ", deflt); + } else { + strncat(prompt, ": ", sizeof(prompt) - prompt_len - 1); + } + + do { + result = get_input_line(prompt); + } while (deflt == NULL && result == NULL); + + if (result == NULL) + result = strdup(deflt); + + return result; +} diff --git a/src/cmd/issues.c b/src/cmd/issues.c index 83203bfc..606226d5 100644 --- a/src/cmd/issues.c +++ b/src/cmd/issues.c @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -49,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] [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"); @@ -230,11 +231,11 @@ gcli_issue_get_user_message(struct gcli_submit_issue_options *opts) } static int -create_issue(struct gcli_submit_issue_options opts, int always_yes) +create_issue(struct gcli_submit_issue_options *opts, int always_yes) { int rc; - opts.body = gcli_issue_get_user_message(&opts); + opts->body = gcli_issue_get_user_message(opts); printf("The following issue will be created:\n" "\n" @@ -242,8 +243,8 @@ create_issue(struct gcli_submit_issue_options opts, int always_yes) "OWNER : %s\n" "REPO : %s\n" "MESSAGE :\n%s\n", - opts.title, opts.owner, opts.repo, - opts.body ? opts.body : "No message"); + opts->title, opts->owner, opts->repo, + opts->body ? opts->body : "No message"); putchar('\n'); @@ -254,12 +255,37 @@ create_issue(struct gcli_submit_issue_options opts, int always_yes) rc = gcli_issue_submit(g_clictx, opts); - free(opts.body); - free(opts.body); + free(opts->body); + free(opts->body); return rc; } +static int +subcommand_issue_create_interactive(struct gcli_submit_issue_options *const opts) +{ + char const *deflt_owner = NULL, *deflt_repo = NULL; + int rc = 0; + + gcli_config_get_repo(g_clictx, &deflt_owner, &deflt_repo); + + if (!opts->owner) + opts->owner = gcli_cmd_prompt("Owner", deflt_owner); + + if (!opts->repo) + opts->repo = gcli_cmd_prompt("Repository", deflt_repo); + + opts->title = gcli_cmd_prompt("Title", NULL); + + rc = create_issue(opts, false); + if (rc < 0) { + fprintf(stderr, "gcli: error: %s\n", gcli_get_error(g_clictx)); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} + static int parse_submit_issue_option(struct gcli_submit_issue_options *opts) { @@ -335,6 +361,9 @@ subcommand_issue_create(int argc, char *argv[]) argc -= optind; argv += optind; + if (argc == 0) + return subcommand_issue_create_interactive(&opts); + check_owner_and_repo(&opts.owner, &opts.repo); if (argc != 1) { @@ -345,7 +374,7 @@ subcommand_issue_create(int argc, char *argv[]) opts.title = argv[0]; - if (create_issue(opts, always_yes) < 0) + if (create_issue(&opts, always_yes) < 0) errx(1, "gcli: error: failed to submit issue: %s", gcli_get_error(g_clictx)); diff --git a/src/cmd/pulls.c b/src/cmd/pulls.c index 7344f479..b01eafdf 100644 --- a/src/cmd/pulls.c +++ b/src/cmd/pulls.c @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -56,9 +57,9 @@ static void usage(void) { fprintf(stderr, "usage: gcli pulls create [-o owner -r repo] [-f from]\n"); - fprintf(stderr, " [-t to] [-d] [-a] [-l label] pull-request-title\n"); + fprintf(stderr, " [-t to] [-d] [-a] [-l label] [pull-request-title]\n"); fprintf(stderr, " gcli pulls [-o owner -r repo] [-a] [-A author] [-n number]\n"); - fprintf(stderr, " [-L label] [-M milestone] [-s]\n"); + fprintf(stderr, " [-L label] [-M milestone] [-s] [search-terms...]\n"); fprintf(stderr, " gcli pulls [-o owner -r repo] -i pull-id actions...\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -o owner The repository owner\n"); @@ -381,9 +382,9 @@ gcli_pull_get_user_message(struct gcli_submit_pull_options *opts) } static int -create_pull(struct gcli_submit_pull_options opts, int always_yes) +create_pull(struct gcli_submit_pull_options *const opts, int always_yes) { - opts.body = gcli_pull_get_user_message(&opts); + opts->body = gcli_pull_get_user_message(opts); fprintf(stdout, "The following PR will be created:\n" @@ -393,8 +394,8 @@ create_pull(struct gcli_submit_pull_options opts, int always_yes) "HEAD : %s\n" "IN : %s/%s\n" "MESSAGE :\n%s\n", - opts.title, opts.to, opts.from, - opts.owner, opts.repo, opts.body ? opts.body : "No message."); + opts->title, opts->to, opts->from, + opts->owner, opts->repo, opts->body ? opts->body : "No message."); fputc('\n', stdout); @@ -429,6 +430,76 @@ pr_try_derive_head(void) return sn_asprintf("%s:"SV_FMT, account, SV_ARGS(branch)); } +static char * +derive_head(void) +{ + char const *account; + sn_sv branch = {0}; + + if ((account = gcli_config_get_account_name(g_clictx)) == NULL) + return NULL; + + branch = gcli_gitconfig_get_current_branch(); + if (branch.length == 0) + return NULL; + + return sn_asprintf("%s:"SV_FMT, account, SV_ARGS(branch)); +} + +/** Interactive version of the create subcommand */ +static int +subcommand_pull_create_interactive(struct gcli_submit_pull_options *const opts) +{ + char const *deflt_owner = NULL, *deflt_repo = NULL; + int rc = 0; + + gcli_config_get_repo(g_clictx, &deflt_owner, &deflt_repo); + + /* PR Source */ + if (!opts->from) { + char *tmp = NULL; + + tmp = derive_head(); + opts->from = gcli_cmd_prompt("From (owner:branch)", tmp); + free(tmp); + tmp = NULL; + } + + /* PR Target */ + if (!opts->owner) + opts->owner = gcli_cmd_prompt("Owner", deflt_owner); + + if (!opts->repo) + opts->repo = gcli_cmd_prompt("Repository", deflt_repo); + + if (!opts->to) { + char *tmp = NULL; + sn_sv base; + + base = gcli_config_get_base(g_clictx); + if (base.length != 0) + tmp = sn_sv_to_cstr(base); + + opts->to = gcli_cmd_prompt("To Branch", tmp); + + free(tmp); + tmp = NULL; + } + + /* Meta */ + opts->title = gcli_cmd_prompt("Title", NULL); + opts->automerge = sn_yesno("Enable automerge?"); + + /* create_pull is going to pop up the editor */ + rc = create_pull(opts, false); + if (rc < 0) { + fprintf(stderr, "gcli: error: %s\n", gcli_get_error(g_clictx)); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} + static int subcommand_pull_create(int argc, char *argv[]) { @@ -506,6 +577,9 @@ subcommand_pull_create(int argc, char *argv[]) argc -= optind; argv += optind; + if (argc == 0) + return subcommand_pull_create_interactive(&opts); + if (!opts.from) opts.from = pr_try_derive_head(); @@ -529,7 +603,7 @@ subcommand_pull_create(int argc, char *argv[]) opts.title = argv[0]; - if (create_pull(opts, always_yes) < 0) + if (create_pull(&opts, always_yes) < 0) errx(1, "gcli: error: failed to submit pull request: %s", gcli_get_error(g_clictx)); @@ -563,7 +637,7 @@ subcommand_pulls(int argc, char *argv[]) return subcommand_pull_create(argc, argv); } - const struct option options[] = { + struct option const options[] = { { .name = "all", .has_arg = no_argument, .flag = NULL, @@ -661,13 +735,24 @@ subcommand_pulls(int argc, char *argv[]) /* In case no explicit PR number was specified, list all * open PRs and exit */ if (pr < 0) { - if (gcli_get_pulls(g_clictx, owner, repo, &details, n, &pulls) < 0) + char *search_term = NULL; + + /* Trailing arguments indicate a search term */ + if (argc) + search_term = sn_join_with((char const *const *)argv, argc, " "); + + details.search_term = search_term; + + if (gcli_search_pulls(g_clictx, owner, repo, &details, n, &pulls) < 0) errx(1, "gcli: error: could not fetch pull requests: %s", gcli_get_error(g_clictx)); gcli_print_pulls(flags, &pulls, n); gcli_pulls_free(&pulls); + free(search_term); + details.search_term = search_term = NULL; + return EXIT_SUCCESS; } diff --git a/src/cmd/table.c b/src/cmd/table.c index fa97de8a..4227a373 100644 --- a/src/cmd/table.c +++ b/src/cmd/table.c @@ -159,11 +159,6 @@ tablerow_add_cell(struct gcli_tbl *const table, row->cells[col].text = strdup(it); cell_size = strlen(it); } break; - case GCLI_TBLCOLTYPE_SV: { - sn_sv src = va_arg(*vp, sn_sv); - row->cells[col].text = sn_sv_to_cstr(src); - cell_size = src.length; - } break; case GCLI_TBLCOLTYPE_DOUBLE: { row->cells[col].text = sn_asprintf("%lf", va_arg(*vp, double)); cell_size = strlen(row->cells[col].text); diff --git a/src/forges.c b/src/forges.c index 6835a52d..eff67819 100644 --- a/src/forges.c +++ b/src/forges.c @@ -115,7 +115,7 @@ github_forge_descriptor = .get_pull = github_get_pull, .get_pull_checks = github_pull_get_checks, .get_pull_commits = github_get_pull_commits, - .get_pulls = github_get_pulls, + .search_pulls = github_search_pulls, .perform_submit_pull = github_perform_submit_pull, .pull_add_reviewer = github_pull_add_reviewer, .pull_close = github_pull_close, @@ -211,7 +211,7 @@ gitlab_forge_descriptor = .get_pull = gitlab_get_pull, .get_pull_checks = (gcli_get_pull_checks_cb)gitlab_get_mr_pipelines, .get_pull_commits = gitlab_get_pull_commits, - .get_pulls = gitlab_get_mrs, + .search_pulls = gitlab_get_mrs, .perform_submit_pull = gitlab_perform_submit_mr, .pull_add_labels = gitlab_mr_add_labels, .pull_add_reviewer = gitlab_mr_add_reviewer, @@ -303,7 +303,7 @@ gitea_forge_descriptor = .get_pull = gitea_get_pull, .get_pull_checks = gitea_pull_get_checks, /* stub, will always return an error */ .get_pull_commits = gitea_get_pull_commits, - .get_pulls = gitea_get_pulls, + .search_pulls = gitea_search_pulls, .perform_submit_pull = gitea_pull_submit, .pull_add_labels = gitea_issue_add_labels, .pull_add_reviewer = gitea_pull_add_reviewer, diff --git a/src/gitea/issues.c b/src/gitea/issues.c index a73c08e4..c27318d3 100644 --- a/src/gitea/issues.c +++ b/src/gitea/issues.c @@ -38,6 +38,8 @@ #include +#include + int gitea_issues_search(struct gcli_ctx *ctx, char const *owner, char const *repo, struct gcli_issue_fetch_details const *details, @@ -46,6 +48,13 @@ gitea_issues_search(struct gcli_ctx *ctx, char const *owner, char const *repo, char *url = NULL, *e_owner = NULL, *e_repo = NULL, *e_author = NULL, *e_label = NULL, *e_milestone = NULL, *e_query = NULL; + struct gcli_fetch_list_ctx fl = { + .listp = &out->issues, + .sizep = &out->issues_size, + .parse = (parsefn)(parse_github_issues), + .max = max, + }; + if (details->milestone) { char *tmp = gcli_urlencode(details->milestone); e_milestone = sn_asprintf("&milestones=%s", tmp); @@ -73,7 +82,7 @@ gitea_issues_search(struct gcli_ctx *ctx, char const *owner, char const *repo, e_owner = gcli_urlencode(owner); e_repo = gcli_urlencode(repo); - url = sn_asprintf("%s/repos/%s/%s/issues?state=%s%s%s%s%s", + url = sn_asprintf("%s/repos/%s/%s/issues?type=issues&state=%s%s%s%s%s", gcli_get_apibase(ctx), e_owner, e_repo, details->all ? "all" : "open", @@ -89,7 +98,7 @@ gitea_issues_search(struct gcli_ctx *ctx, char const *owner, char const *repo, free(e_owner); free(e_repo); - return github_fetch_issues(ctx, url, max, out); + return gcli_fetch_list(ctx, url, &fl); } int @@ -101,10 +110,11 @@ gitea_get_issue_summary(struct gcli_ctx *ctx, char const *owner, } int -gitea_submit_issue(struct gcli_ctx *ctx, struct gcli_submit_issue_options opts, - struct gcli_fetch_buffer *const out) +gitea_submit_issue(struct gcli_ctx *const ctx, + struct gcli_submit_issue_options *const opts, + struct gcli_issue *const out) { - return github_perform_submit_issue(ctx,opts, out); + return github_perform_submit_issue(ctx, opts, out); } /* Gitea has closed, Github has close ... go figure */ diff --git a/src/gitea/pulls.c b/src/gitea/pulls.c index 66db5932..7489c586 100644 --- a/src/gitea/pulls.c +++ b/src/gitea/pulls.c @@ -33,12 +33,67 @@ #include +#include + int -gitea_get_pulls(struct gcli_ctx *ctx, char const *owner, char const *repo, - struct gcli_pull_fetch_details const *const details, int const max, - struct gcli_pull_list *const out) +gitea_search_pulls(struct gcli_ctx *ctx, char const *owner, char const *repo, + struct gcli_pull_fetch_details const *details, + int const max, struct gcli_pull_list *const out) { - return github_get_pulls(ctx, owner, repo, details, max, out); + char *url = NULL, *e_owner = NULL, *e_repo = NULL, *e_author = NULL, + *e_label = NULL, *e_milestone = NULL, *e_query = NULL; + + struct gcli_fetch_list_ctx fl = { + .listp = &out->pulls, + .sizep = &out->pulls_size, + .parse = (parsefn)(parse_github_pulls), + .max = max, + }; + + if (details->milestone) { + char *tmp = gcli_urlencode(details->milestone); + e_milestone = sn_asprintf("&milestones=%s", tmp); + free(tmp); + } + + if (details->author) { + char *tmp = gcli_urlencode(details->author); + e_author = sn_asprintf("&created_by=%s", tmp); + free(tmp); + } + + if (details->label) { + char *tmp = gcli_urlencode(details->label); + e_label = sn_asprintf("&labels=%s", tmp); + free(tmp); + } + + if (details->search_term) { + char *tmp = gcli_urlencode(details->search_term); + e_query = sn_asprintf("&q=%s", tmp); + free(tmp); + } + + e_owner = gcli_urlencode(owner); + e_repo = gcli_urlencode(repo); + + url = sn_asprintf("%s/repos/%s/%s/issues?type=pulls&state=%s%s%s%s%s", + gcli_get_apibase(ctx), + e_owner, e_repo, + details->all ? "all" : "open", + e_author ? e_author : "", + e_label ? e_label : "", + e_milestone ? e_milestone : "", + e_query ? e_query : ""); + + free(e_query); + free(e_milestone); + free(e_author); + free(e_label); + free(e_owner); + free(e_repo); + + return gcli_fetch_list(ctx, url, &fl); } int @@ -57,7 +112,7 @@ gitea_get_pull_commits(struct gcli_ctx *ctx, char const *owner, } int -gitea_pull_submit(struct gcli_ctx *ctx, struct gcli_submit_pull_options opts) +gitea_pull_submit(struct gcli_ctx *ctx, struct gcli_submit_pull_options *opts) { warnx("In case the following process errors out, see: " "https://github.com/go-gitea/gitea/issues/20175"); diff --git a/src/github/issues.c b/src/github/issues.c index f1185aa4..ea0d8f5d 100644 --- a/src/github/issues.c +++ b/src/github/issues.c @@ -322,11 +322,13 @@ github_issue_reopen(struct gcli_ctx *ctx, char const *owner, char const *repo, } int -github_perform_submit_issue(struct gcli_ctx *ctx, struct gcli_submit_issue_options opts, - struct gcli_fetch_buffer *out) +github_perform_submit_issue(struct gcli_ctx *const ctx, + struct gcli_submit_issue_options *const opts, + struct gcli_issue *const out) { char *e_owner = NULL, *e_repo = NULL, *payload = NULL, *url = NULL; struct gcli_jsongen gen = {0}; + struct gcli_fetch_buffer buffer = {0}, *_buffer = NULL; int rc = 0; /* Generate Payload */ @@ -334,12 +336,12 @@ github_perform_submit_issue(struct gcli_ctx *ctx, struct gcli_submit_issue_optio gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "title"); - gcli_jsongen_string(&gen, opts.title); + gcli_jsongen_string(&gen, opts->title); /* Body can be omitted and is NULL in that case */ - if (opts.body) { + if (opts->body) { gcli_jsongen_objmember(&gen, "body"); - gcli_jsongen_string(&gen, opts.body); + gcli_jsongen_string(&gen, opts->body); } } gcli_jsongen_begin_object(&gen); @@ -348,8 +350,8 @@ github_perform_submit_issue(struct gcli_ctx *ctx, struct gcli_submit_issue_optio gcli_jsongen_free(&gen); /* Generate URL */ - e_owner = gcli_urlencode(opts.owner); - e_repo = gcli_urlencode(opts.repo); + e_owner = gcli_urlencode(opts->owner); + e_repo = gcli_urlencode(opts->repo); url = sn_asprintf("%s/repos/%s/%s/issues", gcli_get_apibase(ctx), e_owner, e_repo); @@ -357,8 +359,20 @@ github_perform_submit_issue(struct gcli_ctx *ctx, struct gcli_submit_issue_optio free(e_owner); free(e_repo); - rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, out); + /* only read the resulting data if the issue data has been requested */ + if (out) + _buffer = &buffer; + rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, _buffer); + if (out && rc == 0) { + struct json_stream stream = {0}; + + json_open_buffer(&stream, buffer.data, buffer.length); + rc = parse_github_issue(ctx, &stream, out); + json_close(&stream); + } + + free(buffer.data); free(payload); free(url); diff --git a/src/github/pulls.c b/src/github/pulls.c index 61ae2342..c271cb47 100644 --- a/src/github/pulls.c +++ b/src/github/pulls.c @@ -107,10 +107,64 @@ github_fetch_pulls(struct gcli_ctx *ctx, char *url, return gcli_fetch_list(ctx, url, &fl); } -int -github_get_pulls(struct gcli_ctx *ctx, char const *owner, char const *repo, - struct gcli_pull_fetch_details const *const details, - int const max, struct gcli_pull_list *const list) +static int +search_pulls(struct gcli_ctx *ctx, char const *owner, char const *repo, + struct gcli_pull_fetch_details const *const details, + int const max, struct gcli_pull_list *const out) +{ + char *url = NULL, *query_string = NULL, *e_query_string = NULL, + *milestone = NULL, *author = NULL, *label = NULL; + int rc = 0; + struct gcli_fetch_buffer buffer = {0}; + struct json_stream stream = {0}; + + (void) max; + + if (details->milestone) + milestone = sn_asprintf("milestone:%s", details->milestone); + + if (details->author) + author = sn_asprintf("author:%s", details->author); + + if (details->label) + label = sn_asprintf("label:%s", details->label); + + query_string = sn_asprintf("repo:%s/%s is:pull-request%s %s %s %s %s", + owner, repo, + details->all ? "" : " is:open", + milestone ? milestone : "", author ? author : "", + label ? label : "", details->search_term); + e_query_string = gcli_urlencode(query_string); + + url = sn_asprintf("%s/search/issues?q=%s", gcli_get_apibase(ctx), + e_query_string); + + free(milestone); + free(author); + free(label); + free(query_string); + free(e_query_string); + + rc = gcli_fetch(ctx, url, NULL, &buffer); + if (rc < 0) + goto error_fetch; + + json_open_buffer(&stream, buffer.data, buffer.length); + rc = parse_github_pull_search_result(ctx, &stream, out); + + json_close(&stream); + free(buffer.data); + +error_fetch: + free(url); + + return rc; +} + +static int +list_pulls(struct gcli_ctx *ctx, char const *owner, char const *repo, + struct gcli_pull_fetch_details const *const details, + int const max, struct gcli_pull_list *const list) { char *url = NULL; char *e_owner = NULL; @@ -130,6 +184,17 @@ github_get_pulls(struct gcli_ctx *ctx, char const *owner, char const *repo, return github_fetch_pulls(ctx, url, details, max, list); } +int +github_search_pulls(struct gcli_ctx *ctx, char const *owner, char const *repo, + struct gcli_pull_fetch_details const *const details, + int const max, struct gcli_pull_list *const list) +{ + if (details->search_term) + return search_pulls(ctx, owner, repo, details, max, list); + else + return list_pulls(ctx, owner, repo, details, max, list); +} + int github_pull_get_patch(struct gcli_ctx *ctx, FILE *stream, char const *owner, char const *repo, gcli_id const pr_number) @@ -340,7 +405,8 @@ github_pull_set_automerge(struct gcli_ctx *const ctx, char const *const node_id) } int -github_perform_submit_pull(struct gcli_ctx *ctx, struct gcli_submit_pull_options opts) +github_perform_submit_pull(struct gcli_ctx *ctx, + struct gcli_submit_pull_options *opts) { char *url = NULL, *payload = NULL, *e_owner = NULL, *e_repo = NULL; struct gcli_fetch_buffer fetch_buffer = {0}; @@ -351,26 +417,26 @@ github_perform_submit_pull(struct gcli_ctx *ctx, struct gcli_submit_pull_options gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "head"); - gcli_jsongen_string(&gen, opts.from); + gcli_jsongen_string(&gen, opts->from); gcli_jsongen_objmember(&gen, "base"); - gcli_jsongen_string(&gen, opts.to); + gcli_jsongen_string(&gen, opts->to); gcli_jsongen_objmember(&gen, "title"); - gcli_jsongen_string(&gen, opts.title); + gcli_jsongen_string(&gen, opts->title); /* Body is optional and will be NULL if unset */ - if (opts.body) { + if (opts->body) { gcli_jsongen_objmember(&gen, "body"); - gcli_jsongen_string(&gen, opts.body); + gcli_jsongen_string(&gen, opts->body); } } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); - e_owner = gcli_urlencode(opts.owner); - e_repo = gcli_urlencode(opts.repo); + e_owner = gcli_urlencode(opts->owner); + e_repo = gcli_urlencode(opts->repo); url = sn_asprintf("%s/repos/%s/%s/pulls", gcli_get_apibase(ctx), e_owner, e_repo); @@ -382,20 +448,20 @@ github_perform_submit_pull(struct gcli_ctx *ctx, struct gcli_submit_pull_options /* Add labels if requested. GitHub doesn't allow us to do this all * with one request. */ - if (rc == 0 && (opts.labels_size || opts.automerge)) { + if (rc == 0 && (opts->labels_size || opts->automerge)) { struct json_stream json = {0}; struct gcli_pull pull = {0}; json_open_buffer(&json, fetch_buffer.data, fetch_buffer.length); 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); + if (opts->labels_size) { + rc = github_issue_add_labels(ctx, opts->owner, opts->repo, pull.id, + (char const *const *)opts->labels, + opts->labels_size); } - if (rc == 0 && opts.automerge) { + if (rc == 0 && opts->automerge) { /* pull.id is the global pull request ID */ rc = github_pull_set_automerge(ctx, pull.node_id); } diff --git a/src/gitlab/comments.c b/src/gitlab/comments.c index 009b684b..add5c91d 100644 --- a/src/gitlab/comments.c +++ b/src/gitlab/comments.c @@ -66,7 +66,7 @@ gitlab_perform_submit_comment(struct gcli_ctx *ctx, struct gcli_submit_comment_o payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); - url = sn_asprintf("%s/project/%s%%2F%s/%s/%"PRIid"/notes", + url = sn_asprintf("%s/projects/%s%%2F%s/%s/%"PRIid"/notes", gcli_get_apibase(ctx), e_owner, e_repo, type, opts.target_id); diff --git a/src/gitlab/issues.c b/src/gitlab/issues.c index fe6ea433..1e122e98 100644 --- a/src/gitlab/issues.c +++ b/src/gitlab/issues.c @@ -207,27 +207,29 @@ gitlab_issue_reopen(struct gcli_ctx *ctx, char const *owner, char const *repo, } int -gitlab_perform_submit_issue(struct gcli_ctx *ctx, struct gcli_submit_issue_options opts, - struct gcli_fetch_buffer *const out) +gitlab_perform_submit_issue(struct gcli_ctx *const ctx, + struct gcli_submit_issue_options *const opts, + struct gcli_issue *const out) { char *e_owner = NULL, *e_repo = NULL, *url = NULL, *payload = NULL; - struct gcli_jsongen gen = {0}; int rc = 0; + struct gcli_fetch_buffer buffer = {0}, *_buffer = NULL; + struct gcli_jsongen gen = {0}; - e_owner = gcli_urlencode(opts.owner); - e_repo = gcli_urlencode(opts.repo); + e_owner = gcli_urlencode(opts->owner); + e_repo = gcli_urlencode(opts->repo); gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "title"); - gcli_jsongen_string(&gen, opts.title); + gcli_jsongen_string(&gen, opts->title); /* The body may be NULL if empty. In this case we can omit the * body / description as it is not required by the API */ - if (opts.body) { + if (opts->body) { gcli_jsongen_objmember(&gen, "description"); - gcli_jsongen_string(&gen, opts.body); + gcli_jsongen_string(&gen, opts->body); } } gcli_jsongen_end_object(&gen); @@ -241,8 +243,19 @@ gitlab_perform_submit_issue(struct gcli_ctx *ctx, struct gcli_submit_issue_optio free(e_owner); free(e_repo); - rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, out); + if (out) + _buffer = &buffer; + + rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, _buffer); + if (rc == 0 && out) { + struct json_stream stream = {0}; + json_open_buffer(&stream, buffer.data, buffer.length); + rc = parse_gitlab_issue(ctx, &stream, out); + json_close(&stream); + } + + free(buffer.data); free(payload); free(url); diff --git a/src/gitlab/merge_requests.c b/src/gitlab/merge_requests.c index d95928be..e40feb88 100644 --- a/src/gitlab/merge_requests.c +++ b/src/gitlab/merge_requests.c @@ -84,6 +84,7 @@ gitlab_get_mrs(struct gcli_ctx *ctx, char const *owner, char const *repo, char *e_author = NULL; char *e_label = NULL; char *e_milestone = NULL; + char *e_search = NULL; e_owner = gcli_urlencode(owner); e_repo = gcli_urlencode(repo); @@ -109,14 +110,25 @@ gitlab_get_mrs(struct gcli_ctx *ctx, char const *owner, char const *repo, free(tmp); } - url = sn_asprintf("%s/projects/%s%%2F%s/merge_requests%s%s%s%s", + if (details->search_term) { + char *tmp = gcli_urlencode(details->search_term); + bool const need_qmark = details->all && !details->author && + !details->label && !details->milestone; + + e_search = sn_asprintf("%csearch=%s", need_qmark ? '?' : '&', tmp); + free(tmp); + } + + url = sn_asprintf("%s/projects/%s%%2F%s/merge_requests%s%s%s%s%s", gcli_get_apibase(ctx), e_owner, e_repo, details->all ? "" : "?state=opened", e_author ? e_author : "", e_label ? e_label : "", - e_milestone ? e_milestone : ""); + e_milestone ? e_milestone : "", + e_search ? e_search : ""); + free(e_search); free(e_milestone); free(e_label); free(e_author); @@ -503,7 +515,7 @@ gitlab_mr_wait_until_mergeable(struct gcli_ctx *ctx, char const *const e_owner, } int -gitlab_perform_submit_mr(struct gcli_ctx *ctx, struct gcli_submit_pull_options opts) +gitlab_perform_submit_mr(struct gcli_ctx *ctx, struct gcli_submit_pull_options *opts) { /* Note: this doesn't really allow merging into repos with * different names. We need to figure out a way to make this @@ -516,8 +528,8 @@ gitlab_perform_submit_mr(struct gcli_ctx *ctx, struct gcli_submit_pull_options o struct gcli_jsongen gen = {0}; struct gcli_repo target = {0}; - target_branch = opts.to; - source_owner = strdup(opts.from); + target_branch = opts->to; + source_owner = strdup(opts->from); source_branch = strchr(source_owner, ':'); if (source_branch == NULL) return gcli_error(ctx, "bad merge request source: expected 'owner:branch'"); @@ -525,7 +537,7 @@ gitlab_perform_submit_mr(struct gcli_ctx *ctx, struct gcli_submit_pull_options o *source_branch++ = '\0'; /* Figure out the project id */ - rc = gitlab_get_repo(ctx, opts.owner, opts.repo, &target); + rc = gitlab_get_repo(ctx, opts->owner, opts->repo, &target); if (rc < 0) return rc; @@ -540,23 +552,23 @@ gitlab_perform_submit_mr(struct gcli_ctx *ctx, struct gcli_submit_pull_options o gcli_jsongen_string(&gen, target_branch); gcli_jsongen_objmember(&gen, "title"); - gcli_jsongen_string(&gen, opts.title); + gcli_jsongen_string(&gen, opts->title); /* description is optional and will be NULL if unset */ - if (opts.body) { + if (opts->body) { gcli_jsongen_objmember(&gen, "description"); - gcli_jsongen_string(&gen, opts.body); + gcli_jsongen_string(&gen, opts->body); } gcli_jsongen_objmember(&gen, "target_project_id"); gcli_jsongen_number(&gen, target.id); - if (opts.labels_size) { + if (opts->labels_size) { gcli_jsongen_objmember(&gen, "labels"); gcli_jsongen_begin_array(&gen); - for (size_t i = 0; i < opts.labels_size; ++i) - gcli_jsongen_string(&gen, opts.labels[i]); + for (size_t i = 0; i < opts->labels_size; ++i) + gcli_jsongen_string(&gen, opts->labels[i]); gcli_jsongen_end_array(&gen); } } @@ -567,7 +579,7 @@ gitlab_perform_submit_mr(struct gcli_ctx *ctx, struct gcli_submit_pull_options o /* generate url */ e_owner = gcli_urlencode(source_owner); - e_repo = gcli_urlencode(opts.repo); + e_repo = gcli_urlencode(opts->repo); url = sn_asprintf("%s/projects/%s%%2F%s/merge_requests", gcli_get_apibase(ctx), e_owner, e_repo); @@ -577,7 +589,7 @@ gitlab_perform_submit_mr(struct gcli_ctx *ctx, struct gcli_submit_pull_options o /* if that succeeded and the user wants automerge, parse the result and * set the automerge flag */ - if (rc == 0 && opts.automerge) { + if (rc == 0 && opts->automerge) { struct json_stream stream = {0}; struct gcli_pull pull = {0}; @@ -592,7 +604,7 @@ gitlab_perform_submit_mr(struct gcli_ctx *ctx, struct gcli_submit_pull_options o if (rc < 0) goto out; - rc = gitlab_mr_set_automerge(ctx, opts.owner, opts.repo, pull.number); + rc = gitlab_mr_set_automerge(ctx, opts->owner, opts->repo, pull.number); out: gcli_pull_free(&pull); diff --git a/src/issues.c b/src/issues.c index d367fb00..b10bf3d0 100644 --- a/src/issues.c +++ b/src/issues.c @@ -103,7 +103,7 @@ gcli_issue_reopen(struct gcli_ctx *ctx, char const *owner, char const *repo, } int -gcli_issue_submit(struct gcli_ctx *ctx, struct gcli_submit_issue_options opts) +gcli_issue_submit(struct gcli_ctx *ctx, struct gcli_submit_issue_options *opts) { gcli_null_check_call(perform_submit_issue, ctx, opts, NULL); } diff --git a/src/pulls.c b/src/pulls.c index 373c2e3d..867f86b8 100644 --- a/src/pulls.c +++ b/src/pulls.c @@ -50,11 +50,11 @@ gcli_pulls_free(struct gcli_pull_list *const it) } int -gcli_get_pulls(struct gcli_ctx *ctx, char const *owner, char const *repo, - struct gcli_pull_fetch_details const *const details, int const max, - struct gcli_pull_list *const out) +gcli_search_pulls(struct gcli_ctx *ctx, char const *owner, char const *repo, + struct gcli_pull_fetch_details const *const details, + int const max, struct gcli_pull_list *const out) { - gcli_null_check_call(get_pulls, ctx, owner, repo, details, max, out); + gcli_null_check_call(search_pulls, ctx, owner, repo, details, max, out); } int @@ -143,9 +143,9 @@ gcli_pull_checks_free(struct gcli_pull_checks_list *list) } int -gcli_pull_submit(struct gcli_ctx *ctx, struct gcli_submit_pull_options opts) +gcli_pull_submit(struct gcli_ctx *ctx, struct gcli_submit_pull_options *opts) { - if (opts.automerge) { + if (opts->automerge) { int const q = gcli_forge(ctx)->pull_summary_quirks; if (q & GCLI_PRS_QUIRK_AUTOMERGE) return gcli_error(ctx, "forge does not support auto-merge"); diff --git a/templates/bugzilla/bugs.t b/templates/bugzilla/bugs.t index 02790287..d10891d7 100644 --- a/templates/bugzilla/bugs.t +++ b/templates/bugzilla/bugs.t @@ -77,3 +77,6 @@ array of struct gcli_attachment use parse_bugzilla_bug_attachment; parser bugzilla_attachment_content is object of struct gcli_attachment with ("attachments" => use parse_bugzilla_attachment_content_only_first); + +parser bugzilla_bug_creation_result is +object of gcli_id select "id" as id; diff --git a/templates/github/pulls.t b/templates/github/pulls.t index 8f2e467b..c745c884 100644 --- a/templates/github/pulls.t +++ b/templates/github/pulls.t @@ -63,5 +63,9 @@ object of struct gcli_pull with parser github_pr_merge_message is object of char* select "message" as string; -parser github_pulls is array of struct gcli_pull - use parse_github_pull; +parser github_pulls is +array of struct gcli_pull use parse_github_pull; + +parser github_pull_search_result is +object of struct gcli_pull_list with + ("items" => pulls as array of gcli_pull use parse_github_pull);