pax_global_header00006660000000000000000000000064150701720750014516gustar00rootroot0000000000000052 comment=e6c678fe70d621547765437ba9d07dd85bb1f849 gcli-2.9.1/000077500000000000000000000000001507017207500124455ustar00rootroot00000000000000gcli-2.9.1/.builds/000077500000000000000000000000001507017207500140055ustar00rootroot00000000000000gcli-2.9.1/.builds/alpine.yml000066400000000000000000000005311507017207500157770ustar00rootroot00000000000000image: alpine/edge packages: - libcurl - gcc - make - pkgconf - musl-dev - curl-dev - flex - bison - libbsd-dev - kyua - atf-dev - libedit-dev sources: - https://git.sr.ht/~herrhotzenplotz/gcli tasks: - build: | cd gcli ./configure --debug make -j all - check: | cd gcli make -j check gcli-2.9.1/.builds/debian-stable.yml000066400000000000000000000006021507017207500172200ustar00rootroot00000000000000image: debian/stable packages: - build-essential - libssl-dev - libcurl4-openssl-dev - pkgconf - bison - flex - make - libtool - libbsd-dev - libatf-dev kyua sources: - https://git.sr.ht/~herrhotzenplotz/gcli tasks: - build: | cd gcli ./configure --debug make -j 4 all - check: | cd gcli ./configure --debug make -j 4 check gcli-2.9.1/.builds/freebsd.yml000066400000000000000000000005571507017207500161510ustar00rootroot00000000000000image: freebsd/14.x packages: - atf - ca_root_nss - curl - kyua - libedit - libssh2 - libtool - libunistring - pkg - pkgconf - readline sources: - https://git.sr.ht/~herrhotzenplotz/gcli tasks: - build: | cd gcli ./configure --debug make -j 4 all - check: | cd gcli ./configure --debug make -j 4 check gcli-2.9.1/.editorconfig000066400000000000000000000002301507017207500151150ustar00rootroot00000000000000root = true [{*.{c,h,sh},Makefile,*.mk}] end_of_line = lf insert_final_newline = true indent_style = tab tab_width = 8 trim_trailing_whitespace = true gcli-2.9.1/.gcli000066400000000000000000000001451507017207500133640ustar00rootroot00000000000000pr.base=trunk pr.upstream=herrhotzenplotz/gcli pr.inhibit-delete-source-branch=yes forge-type=gitlab gcli-2.9.1/.gitignore000066400000000000000000000012331507017207500144340ustar00rootroot00000000000000/gcli /templates/*/*.c /templates/*/*.t~ /templates/*/*.h /Makefile /aclocal.m4 /autom4te.cache/ /compile /config.guess /config.h /config.h.in /config.h.in~ /config.log /config.status /config.sub /configure~ /depcomp # build artifacts /gcli-*.tar.bz2 /gcli-*.tar.gz /gcli-*.tar.xz /install-sh /libtool /ltmain.sh /missing /**/.deps/ /**/.dirstamp /src/pgen/parser.h /stamp-h1 /ylwrap /**/*.o /**/*.lo /**/*.la /**/*.*~ /**/.libs/ /ar-lib /configure.ac~ /test-driver /tests/pgen-simple.log /tests/pgen-simple.trs /**/*.a /src/pgen/lexer.c /src/pgen/parser.c /pgen /m4/libtool.m4 /m4/ltoptions.m4 /m4/ltsugar.m4 /m4/ltversion.m4 /m4/lt~obsolete.m4 /libgcli.pc build/** gcli-2.9.1/.gitlab-ci.yml000066400000000000000000000023601507017207500151020ustar00rootroot00000000000000stages: - testing - dist alpine-amd64: stage: testing image: alpine:3.17 script: - apk add libcurl gcc make pkgconf musl-dev curl-dev flex bison libbsd-dev kyua atf-dev libedit-dev git - ./configure --debug - make -j all - make -j distcheck freebsd-arm64: stage: testing tags: - freebsd - arm64 script: - ./configure --debug - make -j 4 all - make -j 4 distcheck freebsd-riscv64: stage: testing tags: - freebsd - riscv script: - ./configure --debug - make -j 4 all - make -j 4 distcheck dist: stage: dist image: alpine:3.18 script: - apk add libcurl gcc make pkgconf musl-dev curl-dev flex bison xz gzip bzip2 libbsd-dev kyua atf-dev cmark git - ./configure --debug - (cd docs/website && ./deploy.sh) - tools/gentarball.sh artifacts: name: "Dist Tarballs" paths: - dist/gcli-*/* - docs/website/website_dist.tar.xz debian-amd64: stage: testing image: debian:bookworm script: - apt-get update - apt-get install -y --no-install-recommends build-essential libcurl4-openssl-dev pkgconf bison flex make libbsd-dev libatf-dev kyua libreadline-dev libssl-dev git - ./configure --debug - make -j all - make -j distcheck gcli-2.9.1/.hotzrc000066400000000000000000000006761507017207500137700ustar00rootroot00000000000000# local kakrc for gcli set-option global ctagscmd "uctags -R --fields=+S" set-option global ctagspaths "include src thirdparty" evaluate-commands %sh{ if [ $(uname -s) = FreeBSD ]; then N=$(nproc) echo "set-option global makecmd \"idprio 0 make -j${N} -C build\"" else echo "set-option global makecmd \"nice make -C build\"" fi } hook global BufSetOption filetype=c %{ add-highlighter buffer/ show-whitespaces -spc " " smarttab } gcli-2.9.1/Changelog.md000066400000000000000000000545431507017207500146710ustar00rootroot00000000000000# Changelog This changelog does not follow semantic versioning. ## 2.9.1 (04-Oct-2025) ### Fixed - A double-free has been fixed that occered right after issue creation. In some cases this may have caused crashes of gcli. Submitted-by: Artyom Sinyugin ## 2.9.0 (26-Aug-2025) ### Added - A `-S` / `--assignee` option has been added to the issues subcommand. This flag allows you to search for issues that are assigned to the given user. See the `gcli-issues(1)` manual page for more details. - An experimental `discussions` action was added to the pulls subcommand. It prints reviews and their comments in a threaded view. See the `gcli-pulls(1)` manual page for more details. - The online version of the gcli tutorial is now compiled into a manual page `gcli-tutorial(1)` which is installed by default. It is included pre-generated in the release tarball but can be rebuilt by setting the newly added `--enable-maintainer` option of the configure script. - Two actions `approve` and `unapprove` have been added to the `pulls` subcommand. These allow giving approval or rejecting a pull request, offering to enter a commant about why a given PR was approved or rejected. Because this reuses code from the reviews subsystem this currently only works on GitHub and Gitlab, however support for Gitea will be added in the next release. ### Fixed - A bug in the autodetection of remotes and forge types from configured git remotes has been fixed. In cases where the git remote was pointing at an ssh-URL with a scheme but without a port the remote was not properly detected and lead to an incorrectly recognised owner/repo combination. - A confusion about the automerge feature has been fixed causing pull requests with automerge to not work on Github forges. Note that this does not fix the general bug on Github which doesn't correctly report whether a pull request has been marked as auto-merge. - Running the pulls checkout action as the last action used to result in an error 'not enough arguments'. ### Changed - Compiler output file extensions are now guessed by the configure script This is done for Windows compatibility. People have reported that it is now possible again to build gcli on MSYS2. If the guessed extensions are wrong, one may override the guessed extensions using the environment variables `EXEEXT`, `OBJEXT`, `LIBEXT`, `EXEEXT_FOR_BUILD`, `OBJEXT_FOR_BUILD` and `LIBEXT_FOR_BUILD`. - When a pull/merge request or issue submission has failed, gcli will now store the entered message in `$PWD/gcli_message`. When the `pulls|issue create` subcommand is later re-run and this file is found, gcli will ask you whether you wish to recall its contents for the new message. Suggested-by: Bence Ferdinandy - The pdjson dependency is now unbundled by default. In case it is not found via pkg-config the vendored copy is used instead. ## 2.8.0 (25-May-2025) ### Added - The pulls subcommand now supports assigning pull requests to users. A new assign action has been added. - The releases create subcommand now has an option `-T`/`--template`. This option allows you to pass a path to a file that is used for the release notes. If combined with `--yes` the entire command is non-interactive allowing you to use it from scripts. If not combined with `--yes` an editor is opened with a copy of the passed file, allowing you to use it as a template for further editing. See the manual page `gcli-releases(1)` for details and usage examples. Suggested by: xaizek ### Fixed - Parsing timestamps with timezone offsets now works properly. This was an issue on Gitea forges where sometimes timestamps with explicit timezone offsets were returned. - Building on macOS has been fixed Darwin doesn't have a separate library for the POSIX real-time extensions. Trying to link with -lrt thus results in errors. The configure script now explicitly checks for a host platform apple-darwin and disables `-lrt` linkage in this case. Reported by: botantony - Fixed a crash in jemalloc caused by a double-free in the patch action on Gitlab This only occured in the case where multiple patches are in a merge request. - Fixed incorrect argument parsing in the pipelines subcommand leading to a nonsensical error message - Fixed broken automerge feature when creating merge requests on Gitlab. ## 2.7.0 (04-Mar-2025) ### Added - A `open` action has been added to various subcommand actions. This action allows you to open the item in a web browser. The URL to open is passed to the configured program in `url-open-program` in the global section. If this option is not set it defaults to `xdg-open` which uses your default browser. This feature currently works for Github, Gitlab and Bugzilla. I'm planning on implementing this for Gitea as well however some considerations have to be taken into account first. Requested by: Baptiste Daroussin - The interactive status prompt now has a command `done` that allows you to mark a notification as done. When run on an action it will return you to the list of notifications. - An `edit` action has been added to the issues subcommand. This action allows you to edit the original post / body / message of an issue, e.g. to update a TODO list checkmark. - Comment submission has been implemented for Bugzilla. ### Fixed - Fixed a printf formatting bug on 32-bit platforms in review tool. Submitted by: Artyom Sinyugin - Fixed Bugzilla support Bugzilla support was broken due to the path refactoring in 2.6.0. This caused the backend to not properly recognise the options passed to it. Compatibility has now been restored. Reported by: Baptiste Daroussin - The interactive status command now properly handles comments and CI checks for GitHub PR notifications. ### Changed - The status subcommand provides an interactive mode for going through notifications. This mode has been changed to reuse the actions of their respective subcommands. You can pass the same options as documented in the manual pages to the notification items. These features are available for issues and pull requests as of now. This means that if you select an issue notification you get a prompt for actions on the respective issue. - The labels subcommand now uses the action-style argument parser to perform tasks on specific labels. With this change a few new actions have been added for changing the title, the colour or the description of a label. Refer to the manual page `gcli-labels(1)` for more information. ### Removed ## 2.6.1 (2025-Jan-19) ### Fixed - Fix rendering bug caused by liblowdown 1.4 API breakage. - Fixed missing documentation in `gcli-releases(1)` Reported by: xaizek - Fix configure to fail when the compiler in `CC` does not exist - Fix manual page compatibility with `groff_mdoc` Submitted by: remph ## 2.6.0 (2025-Jan-04) ### Added - Added a `checkout` action to the `pulls` subcommand that allows quickly checking out the target branch of a pull request. - Added a `-R` command line option to the `pulls create` subcommand that allows specifying reviewers directly when creating the pull request. gcli now also asks for reviewers when creating a pull request interactively. See `gcli-pulls(1)`. - Support for pull requests has been added to the interactive `status` subcommand. ### Fixed - Fixed bad owner/repo when inferred repository information from a git remote with an ssh-url contained a port number - gcli now handles nested projects on Gitlab correctly - The documentation in `gcli(5)` mentioned the forge option `api-base` however it didn't work because of a typo in the responsible code. Parts of the other documentation however was using the incorrect option name without the option. The config option and related documentation has been fixed to spell `api-base`. In order not to break existing configurations an alias has been added such that `apibase` is also a valid name, however `api-base` is preferred. Reported by: Jiří Štefka - The previously added Markdown rendering functionality through liblowdown can now be disabled at runtime by providing a command line flag, setting an environment variable or by setting a configuration variable. Reported-by: Gavin-John Noonan - The interactive status subcommand now doesn't crash anymore for release notifications on Github. - Compatibility with the newly released liblowdown 1.4.0 has been restored. The new minor version broke the API causing compilation failures in gcli. Submitted by: Hoang Nguyen - Fix a segfault when no default account is declared in gcli config Submitted by: remph - Fix a segfault when listing repositories Submitted by: remph - Fix github API error 422 on `gcli forks create` This is caused by the distinction between users and organisations on Github. Submitted by: remph ### Changed - gcli now uses `time_t` internally to represent timestamps. This is visible to the user in that all timestamps are now printed in the consistent format `YYYY-mmm-dd HH:MM:SS` instead of the default format that each forge uses. - When searching for a usable editor gcli now consults the following places in this order: - gcli config file - `$GIT_EDITOR` - `$VISUAL` - `$EDITOR` Submitted by: remph - gcli now uses a dedicated type for paths to objects on forges internally. This change is a quite intrusive in the code base and has been tested extensively over the past month in real scenarios. If you encounter bugs where things can't be found, command line option parsing seems to not work correctly anymore or stuff is submitted incorrectly otherwise please report a bug. ## 2.5.0 (2024-Aug-26) ### Added - Added a `-R` flag to the comment subcommand that allows you to reply to a comment with the given ID. ### Fixed - In various configuration places and environment variables where boolean values are accepted you can now specify `true` as a truthy value. Submitted by: Gavin-John Noonan - The configure script now exits gracefully whenever a required program couldn't be found. Reported by: Alexey Ugnichev - A bug genereting invalid JSON when adding labels to a GitHub issue was fixed. - The reviews cache directory is now automatically created if it doesn't exist avoiding a 'No such file or directory' error when invoking the review action for the first time. Reported by: Bence Ferdinandy - A few bugs in the patch parser have been fixed: - Under rare conditions hunk ranges were incorrectly parsed - Parser errors when a diff included lines starting with a backslash (e.g. when there is no newline at the end of file) were fixed Reported by: Bence Ferdinandy - The installation location of the manual pages of gcli has been fixed. The latest release accidentially installed manual pages to `${DESTDIR}${PREFIX}/share/man` instead of `${DESTDIR}${PREFIX}/share/man/manX`. Reported by: Bence Ferdinandy ### Changed - The pipelines subcommand has been refactored to accept actions for pipelines. This allows cases where a pipeline triggers child pipelines to be handled properly. See `gcli-pipelines(1)` for documentation. Reported by: Bence Ferdinandy ### Removed ## 2.4.0 (2024-June-28) ### Added - Added a convenience flag `-V` or `--version` that prints the version of gcli. - Added an option to disable the spinner to indicate network activity You can now either set `disable-spinner=yes` in your default section in the config file, specify `--no-spinner` as a command line flag or set the environment variable `GCLI_NOSPINNER` to disable the spinner. This is useful for dumb terminal environments like acme where the `\r` used by the spinner is not interpreted. Submitted by: Gavin-John Noonan - Added an interactive status feature Running `gcli status` will now drop you into a prompt-based interactive TODO/notification menu. Currently this only supports issues however this will be extended in future releases. The old behaviour of `gcli status` showing a list of notifications can be achieved by supplying the `-l` or `--list` option. See also https://gitlab.com/herrhotzenplotz/gcli/-/issues/224 - Experimental support for code review has been added When enabling experimental features through either: - setting `GCLI_ENABLE_EXPERIMENTAL` in the environment to `yes` - setting `enable-experimental` in the default section of the gcli config file to yes you will get a action `review` on the pull request subcommand. This subcommand will pull down the diff of the given pull request and loads it into your editor. You can then annotate the diff, save and quit. gcli will then parse the diff, extract comments and allows you to submit them as a review. Please refer to the new `gcli-pulls-review(1)` manual page that has been added for this feature. Also note that this is experimental and may be buggy. However, testing is very much appreciated and I will gladly discuss ideas and usability issues. Please check in on IRC (#gcli on Libera.Chat) for these matters and/or post to our mailing list at https://lists.sr.ht/~herrhotzenplotz/gcli-discuss. ### Fixed - Fixed resolving true .git directory when working inside a git worktree. This used to work before, however newer versions of git changed how these worktrees pointing to the true git directory. Reported by: Robert Clausecker - Fixed bad error message extraction when API errors occured on GitLab. We now check for common error message fields in the returned payload from the GitLab API and choose the (probably) most useful one. - Fixed segmentation fault when displaying certain pull requests on Gitea. - Fixed some spelling mistakes in various manual pages ### Changed - The GitLab API returns the list of comments in reverse chronological order. We now reverse this list to print the comments in the correct order such that a logical timeline of a conversation can be seen. - The build system of gcli has been migrated away from Autotools. The GNU autotools have been complicating and slowing down the gcli builds by a huge amount. Using autotools prevented easy cross-compilation of gcli because it would have required the use of GNU make. Instead of autotools we now use a hand-written configure script and a hand-written Makefile that gets pre-processed by the configure script. You can still invoke the configure script as expected however some options aren't available anymore. Packagers are likely interested in the `--release` flag of the configure script. Most other compilation environment flags are controlled by environment variables. Please refer to `./configure --help` for more information. The `HACKING.md` file also includes valuable information including examples. Out-of-tree builds are still fully supported. If you encounter bugs / other difficulties due to these changes please report back! ## 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 - Preliminary (and thus experimental) support for Bugzilla has been added. For this a new yet undocumented `attachments` subcommand has been introduced. Currently if no account has been specified it will default to the FreeBSD Bugzilla - this may however change in the future. - A search feature has been added to the issues subcommand. You can now optionally provide trailing text to the issues subcommand which will be used as a search term: ```console $ gcli issues -A herrhotzenplotz Segfault ``` This will search for tickets authored by herrhotzenplotz containing "Segfault". - Added partial support for auto-merge. When creating a pull request on Gitlab and Github you can set an automerge flag. Whenever this automerge flag is set a pull request will be merged once all the pipelines/checks on the pull request pass. This feature is not fully documented yet as there are bugs in it, especially on Gitlab there are flaws. Please consider this feature unstable and experimental. ### Fixed - Fixed a segmentation fault when getting a 404 on Gitlab. This bug occured on Debian Linux when querying pipelines at the KiCad project. The returned 404 contained unparsable data which then lead to the error message to be improperly initialised. Reported by: Simon Richter - Fixed missing URL-encode calls in Gitlab Pipelines causing 404 errors when using subprojects on Gitlab. You're now not forced anymore to manually urlencode slashes as %2F in the repos. Reported by: Simon Richter - Fixed the patch generator for Gitlab Merge Requests to produce patches that can be applied with `git am`. Previously the patches were invalid when new files were created or deleted. - Fixed Segmentation fault when the editor was opened and closed without changing the file. Several subcommands have been updated to also account for empty user messages. - Fixed incorrect colour when creating labels. In any forge the provided colour code was converted incorrectly and always producing the wrong colour. - Fixed a segmentation fault when listing Github gists - Fixed possible JSON escape bug when creating a Github Gist - Fixed gcli reporting incorrect libcurl version in the User-Agent header when performing HTTP requests. - Fixed possible segmentation fault when no token was configured in gcli configuration file. ### Changed - Internally a lot of code was using string views. Maintaining this was a bit cumbersome and required frequent reallocations. A lot of these uses have been refactored to use plain C-Strings now. This also involved changing some code to use the new `gcli_jsongen` set of routines. Due to these changes there may be regressions that are only visible during use. If you encounter such regressions where previously working commands suddenly fail due to malformed requests please report immediately. ### Removed ## 2.1.0 (2023-Dec-08) ### Added - Added a little spinner to indicate network activity - Added Windows 10 MSYS2 to list of confirmed-to-work platforms - Added a new action `set-visibility` to the repos subcommand that allows updating the visibility level of a repository. - Added a new action `request-review` to the pulls subcommand that allows requesting a review of a pull request from a given user. - One can now define custom aliases in the alias section of the config file. Aliases are very primitive as of now. This means they are just different names for subcommands. Aliases may reference other aliases. - Added a new `-M` flag to both the pulls and the issues subcommand to allow filtering by milestones. - Added a new `patch` action to the pulls subcommand. This allows you to print the entire patch series for a given pull request. Also added the missing implementations for this feature for Github and Gitea. - Added a new `title` action to both the issues and the pulls subcommand that allows updating their titles. ### Fixed - Fixed incorrect internal help message of the `repos` subcommand. - Worked around ICE with xlC 16 on ppc64le Debian Linux, gcli now compiles using xlC and works too. - Fixed various memory leaks. - Spelling fixes in manual pages (submitted by Jakub Wilk https://github.com/herrhotzenplotz/gcli/pull/121) - Wired up alread existing implementation for forking on Gitea to gcli command. - The `status` subcommand now works properly on Gitea. ### Changed - Subcommands can now be abbreviated by providing an unambiguous prefix that matches the subcommand. ### Removed ## 2.0.0 (2023-Sep-21) ### Added - This changelog has been added - gcli is now built as a shared or static library which the gcli tool links against This implied so many changes that the major version number was bumped. - Added a package-config file for libgcli - Added a `-L` flag to the `issues` and `pulls` subcommand to allow filtering by label - A work-in-progress tutorial has been added and is available at [the GCLI directory](https://herrhotzenplotz.de/gcli/tutorial) on my website. - Gitlab jobs now show coverage information ### Fixed - Parallel builds in autotools have been re-enabled - Improved error messages in various places - Bad roff syntax in manual pages has been fixed ### Changed - the `gcli pulls create` subcommand does not print the URL to the created release anymore. - The test suite is now using [atf-c](https://github.com/jmmv/atf) and [kyua](https://github.com/jmmv/kyua). These are dependencies if you want to run the tests. These tools are installed out of the box on most BSDs. - A newly introduced dependency is the `sys/queue.h` header. On GNU/Linux systems you might need to install it as part of libbsd. ### Removed - The reviews subcommand has been removed because it was generally useless This feature will be reimplemented as a WIP of [#189](https://gitlab.com/herrhotzenplotz/gcli/-/issues/189) gcli-2.9.1/HACKING.md000066400000000000000000000250661507017207500140440ustar00rootroot00000000000000# Hacking on GCLI This document gives you hints to get started working with on source code of [gcli](https://herrhotzenplotz.de/gcli/). Please note that this document only captures the state of the code at the some points in time and may be out of date. If you feel like this is the case please submit bug reports or, even better, provide patches. ## Building GCLI We use handwritten Makefiles to build GCLI. This has a few advantages: - Portability across many platforms, even many older ones - I (Nico) know Makefiles fairly well - Cross-Compilation can easily be done - High flexibility - Few to no dependencies - Short compilation times A few caveats are: - The Makefile must work with at least 3 implementations of Make: - BSD Make (bmake) - Schily SMake (smake) - GNU Make (gmake) Some of these make implementations are very buggy (most notably GNU make) - Getting target dependencies just right is not easy For that reason I highly suggest testing with all three make implementations. ### General workflow A hand written shell script called `configure` is run inside a directory where to place build files. This script checks the environment for various properties such as: - The compiler to use - Compiler options - Target system properties - Dependencies and Libraries - Additional tooling that can be used The script allows you to configure multiple build directories from a single source directory (so called "out of tree builds"). The following example shows you how to configure a default build directory: $ mkdir build $ cd build/ $ ../configure Once the configure script has run you can run make to build gcli: $ make To install gcli to the default prefix (`/usr/local`) you can run: $ make install To run the test suite: $ make check Note that running the test suite requires ATF and Kyua. More details can be found below. To run the compiler's built-in static code analyser: $ make analyse If you wish to change the compiler to be used you can set these in the environment: $ env CC=/usr/local/bin/clang17 ../configure To build a default release build with optimisations you can run: $ ../configure --release Check the built-in help of the configure script for more details: $ ../configure --help #### Full Debug build The configure script comes with a `--debug` flag that configures a directory for a build with no optimisations and full debug info. I suggest you use it for development purposes: $ ../configure --debug --enable-maintainer You can proceed as usual with make. #### Sanitized Builds TBD #### Cross-Compilation gcli supports cross compilation. A cross-compilation setup can be achieved by setting at least the following environment variables: - `CC` to the target system (aka. host) compiler - `CC_FOR_BUILD` to the build system (aka. build) compiler - `PKG_CONFIG_PATH` to the path where pkgconfig should look for `.pc` files e.g. to compile from FreeBSD amd64 to a armv7l Linux system: $ CC=/opt/armv7-linux-gnueabihf-gcc/bin/armv7-linux-gnueabihf-gcc \ > CC_FOR_BUILD=cc \ > PKG_CONFIG_PATH=/opt/armv7-linux-gnueabihf-gcc/root/usr/lib/pkgconfig \ > ../configure --debug Configuring gcli 2.4.0-devel Checking for realpath ... realpath Checking host compiler ... /opt/armv7-linux-gnueabihf-gcc/bin/armv7-linux-gnueabihf-gcc Checking host compiler type ... gcc Checking host compiler target ... armv7-linux-gnueabihf Checking for cross-compilation setup ... yes Checking build compiler type... clang Checking build compiler target ... amd64-unknown-freebsd14.1 Checking for pkg-config ... pkg-config Checking for libcurl ... found Checking for atf-c ... found Checking for libedit ... found Checking for kyua ... kyua Checking for ccache ... ccache Checking for install ... install Writing config.h Configuration Summary: Build system type: amd64-unknown-freebsd14.1 Host system type: armv7-linux-gnueabihf optimise for: debug CC: /opt/armv7-linux-gnueabihf-gcc/bin/armv7-linux-gnueabihf-gcc CC_FOR_BUILD: cc CFLAGS: CFLAGS_FOR_BUILD: LIBCURL_CFLAGS: LIBCURL_LIBS: -lcurl LIBATFC_CFLAGS: -I/usr/local/include LIBATFC_LIBS: -L/usr/local/lib -latf-c Using libedit: LIBEDIT_CFLAGS: -I/usr/include/editline LIBEDIT_LIBS: -ledit Configuration done. You may now run make. When you now run make the compilers will be chosen appropriately. The test suite will not work when cross-compiling. If needed you may have to set the values of - `EXEEXT` - `OBJEXT` - `LIBEXT` This is the case on undetected Windows systems. It should be possible to build gcli under cygwin this way. ## Tests The test suite depends on [Kyua](https://github.com/jmmv/kyua) and [libatf-c](https://github.com/jmmv/atf). To run the test suite in a configured directory `build` run: $ make -C build check # Code Style Please use the BSD Style conventions for formatting your code. This means: - Functions return type and name go on separate lines, no mixed code and declarations (except in for loops): void foo(int bar) { int x, y, z; x = bar; for (int i = 0; i < 10; ++i) z += i; return x; } This allows to search for the implementation of a function through a simple `grep -rn '^foo' .`. - Use struct tags for structs, do not typedef them struct foo { int bar; char const *baz; }; static void foodoo(struct foo const *const bar) { } - Indent with tabs, align with spaces `»` denotes a TAB character, `.` indicates a whitespace: void foo(struct foo const *thefoo) { » if (thefoo) » » printf("%s: %d\n" » » .......thefoo->wat, » » .......thefoo->count); } - Try to have a max of 80 characters per line I know we're not using punchcards anymore, however it makes the code way more readable. - Use C11 Please don't use C17 or even more modern features. Reason being that I want gcli to be portable to older platforms where either no modern compilers are available or where we have to rely on old gcc versions and/or buggy vendor compilers. This also means that GNU extensions are forbidden. If you use the compiler flags I mentioned above you should get notified by the compiler. There is a `.editorconfig` included in the source code that should automatically provide you with all needed options. [Editorconfig](https://editorconfig.org/#pre-installed) is a plugin that is available for almost all notable editors out there. I highly recommend you use it. # Adding support for new forges The starting point for adding forges is [include/gcli/forges.h](include/gcli/forges.h). This file contains the dispatch table for fetching data from various kinds of forges. A pointer to the current dispatch table can be retrieved through a call to `gcli_forge()`. You may have to adjust the routines called by it to allow for automagic detection as well as overrides on the command line for your new forge type. You should likely never call `gcli_forge()` directly when adding a new forge type as there are various frontend functions available that will do dispatching for the caller. ## Parsing JSON When you need to parse JSON Objects into C structs you likely want to generate that code. Please see the [templates/](templates/) directory for examples on how to do that. Currently the [PR Parser for Github](templates/github/pulls.t) can act as an example for all features available in the code generator. The code generator is fully documented in [pgen.org](docs/pgen.org). ## Generating JSON We not only need to parse JSON often, we also need to generate it on the fly when submitting data to forge APIs. For this the `gcli_jsongen_` family of functions exist. Since these have been introduced quite late in the project their use is not particularly wide-spread. However this may change in the future. To use these, take a look at the header [include/gcli/json_gen.h](include/gcli/json_gen.h) and also the use in [src/gitlab/merge_requests.c](src/gitlab/merge_requests.c). # User Frontend Features The gcli command line tool links against libgcli. Through a context structure it passes information like the forge type and user credentials into the library. All code for the command line frontend tool is found in the [src/cmd/](src/cmd/) directory. [src/cmd/gcli.c](src/cmd/gcli.c) is the entry point for the command line tool. In this file you can find the dispatch table for all subcommands of gcli. ## Subcommands Subcommand implementations are found in separate C files in the `src/cmd` subdirectory. When parsing command line options please use `getopt_long`. Do not forget to prefix your getopt string with a `+` as we do multiple calls to `getopt_long` so it needs to reset some internal state. ## Output formatting Output is usually formatted as a dictionary or a table. For these cases gcli provides a few convenience functions and data structures. The relevant header is [gcli/cmd/table.h](include/gcli/cmd/table.h). Do not use these functions in the library code. It's only supposed to be used from the command line tool. ### Tables You can print tables by defining a list of columns first: ```C gcli_tblcoldef cols[] = { { .... }, { .... }, }; ``` For a complete definition look at the header or uses of that interface in e.g. [src/cmd/issues.c](src/cmd/issues.c). You can then start adding rows to your table: ```C gcli_tbl table = gcli_tbl_begin(cols, ARRAY_SIZE(cols)); for (int i = 0; i < list.whatever_size; ++i) { gcli_tbl_add_row(table, ...); } ``` The variadic arguments you need to provide depends on the columns defined. Most relevant is the flags and type field. Make sure you get data type sizes correct. To dump the table to stdout use the following call: ```C gcli_tbl_end(table); ``` This will print the table and free all resources acquired by calls to the tbl routines. You may no reuse the handle returned by `gcli_tbl_begin()` after this call. Instead, call the begin routine again to obtain a new handle. ### Dictionaries The dictionary routines act almost the same way as tables except that you don't define the columns. Instead you obtain a handle through `gcli_dict_begin` and add entries to the dictionary by calling `gcli_dict_add` or one of the specialized functions for strings. `gcli_dict_add` is the most generic of them all and provides a printf-like format and variadic argument list. You can dump the dictionary and free resources through a call to `gcli_dict_end`. gcli-2.9.1/LICENSE000066400000000000000000000024231507017207500134530ustar00rootroot00000000000000Copyright 2021, 2022 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. gcli-2.9.1/Makefile.in000066400000000000000000000471301507017207500145170ustar00rootroot00000000000000GCLI_VERSION = @PACKAGE_VERSION@ # Sort of compatibility for GNU make and BSD make .PATH: @SRCDIR@ VPATH=@SRCDIR@ ################ Enviroment and values saved by the configure script ########################## AR = @AR@ CC_FOR_BUILD = @CC_FOR_BUILD@ CC = @CC@ CCACHE = @CCACHE@ CCOM_FOR_BUILD = @CCOM_FOR_BUILD@ CCOM = @CCOM@ OPTIMISE = @OPTIMISE@ ENV_PKG_CONFIG = @ENV_PKG_CONFIG@ ENV_PKG_CONFIG_PATH = @ENV_PKG_CONFIG_PATH@ INSTALL = @INSTALL@ KYUA = @KYUA@ RANLIB = @RANLIB@ RM = @RM@ PREFIX = @PREFIX@ SHELL = @SHELL@ EXEEXT = @EXEEXT@ LIBEXT = @LIBEXT@ OBJEXT = @OBJEXT@ EXEEXT_FOR_BUILD = @EXEEXT_FOR_BUILD@ LIBEXT_FOR_BUILD = @LIBEXT_FOR_BUILD@ OBJEXT_FOR_BUILD = @OBJEXT_FOR_BUILD@ ENABLE_MAINTAINER = @ENABLE_MAINTAINER@ ################################################################################################ ############## Optimiser / Warning flags ##### COPTFLAGS_gcc_debug= -O0 -g3 -Wall -Wextra -Werror COPTFLAGS_gcc_release= -O2 -DNDEBUG COPTFLAGS_clang_debug= -O0 -g3 -Wall -Wextra -Wno-gnu-zero-variadic-macro-arguments -Werror COPTFLAGS_clang_release= -O2 -DNDEBUG COPTFLAGS_xlc_debug= -qpic -qcheck=all -qfulldebug -qnoopt -g -qhalt=w COPTFLAGS_xlc_release= -qpic -qoptimize=4 -DNDEBUG COPTFLAGS_sunstudio_debug= -xO0 -g -xvector=%none COPTFLAGS_sunstudio_release= -xO5 -DNDEBUG COPTFLAGS= $(COPTFLAGS_$(CCOM)_$(OPTIMISE)) COPTFLAGS_FOR_BUILD= $(COPTFLAGS_$(CCOM_FOR_BUILD)_$(OPTIMISE)) CSTDFLAGS_gcc= -std=c11 -pedantic -D_XOPEN_SOURCE=700 CSTDFLAGS_clang= -std=c11 -pedantic -D_XOPEN_SOURCE=700 CSTDFLAGS_xlc= -qlanglvl=stdc11 -D_XOPEN_SOURCE=700 CSTDFLAGS_sunstudio= -std=c11 -D_XOPEN_SOURCE=600 -pedantic=yes # Oracle Compiler on Solaris doesn't support 700 CSTDFLAGS= $(CSTDFLAGS_$(CCOM)) CSTDFLAGS_FOR_BUILD= $(CSTDFLAGS_$(CCOM_FOR_BUILD)) ############## CFLAGS ######################## ENV_CFLAGS= @ENV_CFLAGS@ CFLAGS_clang= -Wno-gnu-zero-variadic-macro-arguments -fPIC -pipe CFLAGS_gcc= -fPIC -pipe CFLAGS_sunstudio= -m64 -errtags=yes -errfmt=error -errwarn=%all -xcode=pic32 -DPIC -xregs=no%appl CFLAGS_sunstudio += -erroff=E_TOKENLESS_MACRO # Causes errors with TAILQ_HEAD(, ...) CFLAGS_sunstudio += -erroff=E_STATEMENT_NOT_REACHED # Causes errors in pdjson.c CFLAGS= $(CSTDFLAGS) $(COPTFLAGS) $(CFLAGS_$(CCOM)) -I@SRCDIR@/include -I. $(ENV_CFLAGS) ENV_CFLAGS_FOR_BUILD= @ENV_CFLAGS_FOR_BUILD@ CFLAGS_FOR_BUILD= $(CSTDFLAGS_FOR_BUILD) $(COPTFLAGS_FOR_BUILD) $(CFLAGS_$(CCOM)) -I@SRCDIR@/include -I@SRCDIR@/thirdparty -I. $(ENV_CFLAGS_FOR_BUILD) ############## LDFLAGS ###################### ENV_LDFLAGS= @ENV_LDFLAGS@ LDFLAGS= $(ENV_LDFLAGS) ENV_LDFLAGS_FOR_BUILD= @ENV_LDFLAGS_FOR_BUILD@ LDFLAGS_FOR_BUILD= $(ENV_LDFLAGS_FOR_BUILD) ############## CPPFLAGS ###################### ENV_CPPFLAGS= @ENV_CPPFLAGS@ CPPFLAGS= -DHAVE_CONFIG_H=1 $(ENV_CPPFLAGS) ENV_CPPFLAGS_FOR_BUILD= @ENV_CPPFLAGS_FOR_BUILD@ CPPFLAGS_FOR_BUILD= -DHAVE_CONFIG_H -DYY_NO_UNPUT -DYY_NO_INPUT $(ENV_CPPFLAGS_FOR_BUILD) # CFLAGS for dependency tracking for various compilers CCDEPFLAGS_gcc= -MD -MF ${@:$(OBJEXT)=.d} CCDEPFLAGS_clang= -MD -MF ${@:$(OBJEXT)=.d} CCDEPFLAGS_xlc= -qmakedep=gcc -MF ${@:$(OBJEXT)=.d} CCDEPFLAGS_sunstudio= -xMD -xMF ${@:$(OBJEXT)=.d} CCDEPFLAGS_unknown= CCDEPFLAGS= $(CCDEPFLAGS_$(CCOM)) CCDEPFLAGS_FOR_BUILD= $(CCDEPFLAGS_$(CCOM_FOR_BUILD)) ########## Static Analysis Flags ############################# CANALFLAGS_clang= --analyze -Werror CANALFLAGS_gcc= -fanalyzer -c -o /dev/null CANALFLAGS= $(CANALFLAGS_$(CCOM)) LIBCURL_CFLAGS= @LIBCURL_CFLAGS@ LIBCURL_LIBS= @LIBCURL_LIBS@ LIBATFC_CFLAGS= @LIBATFC_CFLAGS@ LIBATFC_LIBS= @LIBATFC_LIBS@ ############## LIBEDIT ########################################## LIBEDIT_FOUND= @LIBEDIT_FOUND@ LIBEDIT_CFLAGS_1= @LIBEDIT_CFLAGS@ LIBEDIT_LIBS_1= @LIBEDIT_LIBS@ LIBEDIT_CFLAGS= $(LIBEDIT_CFLAGS_$(LIBEDIT_FOUND)) LIBEDIT_LIBS= $(LIBEDIT_LIBS_$(LIBEDIT_FOUND)) LIBEDIT_CPPFLAGS_1= -DHAVE_LIBEDIT=1 LIBEDIT_CPPFLAGS= $(LIBEDIT_CPPFLAGS_$(LIBEDIT_FOUND)) ############## READLINE ######################################### LIBREADLINE_FOUND= @LIBREADLINE_FOUND@ LIBREADLINE_CFLAGS_1= @LIBREADLINE_CFLAGS@ LIBREADLINE_LIBS_1= @LIBREADLINE_LIBS@ LIBREADLINE_CFLAGS= $(LIBREADLINE_CFLAGS_$(LIBREADLINE_FOUND)) LIBREADLINE_LIBS= $(LIBREADLINE_LIBS_$(LIBREADLINE_FOUND)) LIBREADLINE_CPPFLAGS_1= -DHAVE_LIBREADLINE=1 LIBREADLINE_CPPFLAGS= $(LIBREADLINE_CPPFLAGS_$(LIBREADLINE_FOUND)) ############## LOWDOWN ######################################### LIBLOWDOWN_FOUND= @LIBLOWDOWN_FOUND@ LIBLOWDOWN_MAJOR= @LIBLOWDOWN_MAJOR@ LIBLOWDOWN_MINOR= @LIBLOWDOWN_MINOR@ LIBLOWDOWN_CFLAGS_1= @LIBLOWDOWN_CFLAGS@ \ -DLIBLOWDOWN_MAJOR=$(LIBLOWDOWN_MAJOR) \ -DLIBLOWDOWN_MINOR=$(LIBLOWDOWN_MINOR) LIBLOWDOWN_LIBS_1= @LIBLOWDOWN_LIBS@ LIBLOWDOWN_CFLAGS= $(LIBLOWDOWN_CFLAGS_$(LIBLOWDOWN_FOUND)) LIBLOWDOWN_LIBS= $(LIBLOWDOWN_LIBS_$(LIBLOWDOWN_FOUND)) LIBLOWDOWN_CPPFLAGS_1= -DHAVE_LIBLOWDOWN=1 LIBLOWDOWN_CPPFLAGS= $(LIBLOWDOWN_CPPFLAGS_$(LIBLOWDOWN_FOUND)) # Only used in maintainer mode LOWDOWN= @LOWDOWN@ ############## LIBCRYPTO ######################################## LIBCRYPTO_CFLAGS= @LIBCRYPTO_CFLAGS@ LIBCRYPTO_LIBS= @LIBCRYPTO_LIBS@ ############### -lrt ############################################ ENABLE_LIBRT= @ENABLE_LIBRT@ LIBRT_LIBS_1= -lrt LIBRT_LIBS= $(LIBRT_LIBS_$(ENABLE_LIBRT)) ############## PDJSON ########################################### # system version PDJSON_CFLAGS_1= @PDJSON_CFLAGS@ PDJSON_LIBS_1= @PDJSON_LIBS@ PDJSON_SRCS_1= # vendored version PDJSON_CFLAGS_0= -I@SRCDIR@/thirdparty/pdjson PDJSON_LIBS_0= PDJSON_SRCS_0= thirdparty/pdjson/pdjson.c # dispatch PDJSON_FOUND= @PDJSON_FOUND@ PDJSON_CFLAGS= $(PDJSON_CFLAGS_$(PDJSON_FOUND)) PDJSON_LIBS= $(PDJSON_LIBS_$(PDJSON_FOUND)) PDJSON_SRCS= $(PDJSON_SRCS_$(PDJSON_FOUND)) ################################################################# .PHONY: all all-auto clean clean-auto check check-auto install install-auto analyse analyse-auto all: Makefile @$(MAKE) all-auto clean: Makefile @$(MAKE) clean-auto check: Makefile @$(MAKE) check-auto install: Makefile @$(MAKE) install-auto analyse: Makefile @$(MAKE) analyse-auto all-auto: gcli$(EXEEXT) manpages LIBGCLI_SRCS = \ src/bugzilla/api.c \ src/bugzilla/attachments.c \ src/bugzilla/bugs-parser.c \ src/bugzilla/bugs.c \ src/bugzilla/comment.c \ src/bugzilla/config.c \ src/gitea/comments.c \ src/gitea/config.c \ src/gitea/forks.c \ src/gitea/issues.c \ src/gitea/labels.c \ src/gitea/milestones.c \ src/gitea/pulls.c \ src/gitea/releases.c \ src/gitea/repos.c \ src/gitea/sshkeys.c \ src/gitea/status.c \ src/github/api.c \ src/github/checks.c \ src/github/checkout.c \ src/github/comments.c \ src/github/config.c \ src/github/forks.c \ src/github/gists.c \ src/github/issues.c \ src/github/labels.c \ src/github/milestones.c \ src/github/path.c \ src/github/pulls.c \ src/github/releases.c \ src/github/repos.c \ src/github/sshkeys.c \ src/github/status.c \ src/gitlab/api.c \ src/gitlab/checkout.c \ src/gitlab/comments.c \ src/gitlab/config.c \ src/gitlab/forks.c \ src/gitlab/issues.c \ src/gitlab/labels.c \ src/gitlab/merge_requests.c \ src/gitlab/milestones.c \ src/gitlab/pipelines.c \ src/gitlab/releases.c \ src/gitlab/repos.c \ src/gitlab/snippets.c \ src/gitlab/sshkeys.c \ src/gitlab/status.c \ src/attachments.c \ src/base64.c \ src/comments.c \ src/ctx.c \ src/curl.c \ src/date_time.c \ src/diffutil.c \ src/forges.c \ src/forks.c \ src/gcli.c \ src/issues.c \ src/json_gen.c \ src/json_util.c \ src/labels.c \ src/milestones.c \ src/nvlist.c \ src/path.c \ src/pulls.c \ src/releases.c \ src/repos.c \ src/sshkeys.c \ src/status.c \ src/url.c \ src/waitproc.c \ src/port/err.c \ src/port/string.c \ src/port/sv.c \ src/port/util.c \ $(PDJSON_SRCS) GCLI_SRCS= \ src/cmd/actions.c \ src/cmd/api.c \ src/cmd/attachments.c \ src/cmd/ci.c \ src/cmd/cmd.c \ src/cmd/cmdconfig.c \ src/cmd/colour.c \ src/cmd/comment.c \ src/cmd/config.c \ src/cmd/editor.c \ src/cmd/forks.c \ src/cmd/gcli.c \ src/cmd/gists.c \ src/cmd/gitconfig.c \ src/cmd/interactive.c \ src/cmd/issues.c \ src/cmd/labels.c \ src/cmd/milestones.c \ src/cmd/open.c \ src/cmd/pipelines.c \ src/cmd/pull_reviews.c \ src/cmd/pulls.c \ src/cmd/releases.c \ src/cmd/repos.c \ src/cmd/snippets.c \ src/cmd/status.c \ src/cmd/status_interactive.c \ src/cmd/table.c TEMPLATES= \ templates/bugzilla/api.t \ templates/bugzilla/bugs.t \ templates/gitea/milestones.t \ templates/gitea/status.t \ templates/github/api.t \ templates/github/checks.t \ templates/github/comments.t \ templates/github/forks.t \ templates/github/gists.t \ templates/github/issues.t \ templates/github/labels.t \ templates/github/milestones.t \ templates/github/pulls.t \ templates/github/releases.t \ templates/github/repos.t \ templates/github/status.t \ templates/gitlab/api.t \ templates/gitlab/comments.t \ templates/gitlab/forks.t \ templates/gitlab/issues.t \ templates/gitlab/labels.t \ templates/gitlab/merge_requests.t \ templates/gitlab/milestones.t \ templates/gitlab/pipelines.t \ templates/gitlab/releases.t \ templates/gitlab/repos.t \ templates/gitlab/snippets.t \ templates/gitlab/sshkeys.t \ templates/gitlab/status.t PGEN_SRCS= \ src/port/err.c \ src/pgen/dump_c.c \ src/pgen/dump_h.c \ src/pgen/dump_plain.c \ lex.yy.c \ y.tab.c TEMPLATE_HEADERS= ${TEMPLATES:.t=.h} TEMPLATE_SRCS= ${TEMPLATES:.t=.c} Makefile: @SRCDIR@/configure @SRCDIR@/Makefile.in @echo "Reconfiguring Makefile" env CC="${CC}" YACC="${YACC}" LEX="${LEX}" RM="${RM}" RANLIB="${RANLIB}" \ KYUA="${KYUA}" LOWDOWN="${LOWDOWN}" SHELL="${SHELL}" CFLAGS="${ENV_CFLAGS}" CPPFLAGS="${ENV_CPPFLAGS}" \ INSTALL="${INSTALL}" CC_FOR_BUILD="${CC_FOR_BUILD}" ENABLE_MAINTAINER="${ENABLE_MAINTAINER}" \ LDFLAGS="${ENV_LDFLAGS}" LDFLAGS_FOR_BUILD="${LDFLAGS_FOR_BUILD}" \ CFLAGS_FOR_BUILD="${ENV_CFLAGS_FOR_BUILD}" CPPFLAGS_FOR_BUILD="${ENV_CPPFLAGS_FOR_BUILD}" \ PKG_CONFIG="${ENV_PKG_CONFIG}" PKG_CONFIG_PATH="${ENV_PKG_CONFIG_PATH}" CCACHE="${CCACHE}" \ EXEEXT="${EXEEXT}" OBJEXT="${OBJEXT}" LIBEXT="${LIBEXT}" \ EXEEXT_FOR_BUILD="${EXEEXT_FOR_BUILD}" OBJEXT_FOR_BUILD="${OBJEXT_FOR_BUILD}" LIBEXT_FOR_BUILD="${LIBEXT_FOR_BUILD}" \ sh @SRCDIR@/configure @CONFIGURE_CMD_ARGS@ LIBGCLI_OBJS= ${LIBGCLI_SRCS:.c=.libgcli$(OBJEXT)} ${TEMPLATE_SRCS:.c=.libgcli$(OBJEXT)} GCLI_OBJS= ${GCLI_SRCS:.c=.gcli$(OBJEXT)} PGEN_OBJS= ${PGEN_SRCS:.c=.pgen$(OBJEXT_FOR_BUILD)} # Dependencies DEPS= ${LIBGCLI_OBJS:$(OBJEXT)=.d} ${GCLI_OBJS:$(OBJEXT)=.d} ${PGEN_OBJS:$(OBJEXT_FOR_BUILD)=.d} -include $(DEPS) pgen$(EXEEXT_FOR_BUILD): $(PGEN_OBJS) $(CCACHE) $(CC_FOR_BUILD) $(CFLAGS_FOR_BUILD) $(CPPFLAGS_FOR_BUILD) \ $(LDFLAGS_FOR_BUILD) -o pgen$(EXEEXT_FOR_BUILD) $(PGEN_OBJS) y.tab.c y.tab.h: @SRCDIR@/src/pgen/parser.y $(YACC) -d @SRCDIR@/src/pgen/parser.y lex.yy.c: y.tab.h @SRCDIR@/src/pgen/lexer.l $(LEX) @SRCDIR@/src/pgen/lexer.l .SUFFIXES: .h .t .c # Hack to make GNU make shut up $(TEMPLATE_HEADERS) $(TEMPLATE_SRCS): pgen$(EXEEXT_FOR_BUILD) .t.h: @mkdir -p $$(dirname $@) ./pgen$(EXEEXT_FOR_BUILD) -th -o $@ $< .t.c: @mkdir -p $$(dirname $@) ./pgen$(EXEEXT_FOR_BUILD) -tc -o $@ $< gcli$(EXEEXT): libgcli$(LIBEXT) $(GCLI_OBJS) $(CCACHE) $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) -o gcli$(EXEEXT) \ $(GCLI_OBJS) libgcli$(LIBEXT) $(LIBCURL_LIBS) $(LIBEDIT_LIBS) \ $(LIBREADLINE_LIBS) $(LIBCRYPTO_LIBS) \ $(LIBLOWDOWN_LIBS) $(PDJSON_LIBS) \ $(LIBRT_LIBS) libgcli$(LIBEXT): $(LIBGCLI_OBJS) $(CCACHE) $(AR) -rc libgcli$(LIBEXT) $(LIBGCLI_OBJS) $(CCACHE) $(RANLIB) libgcli$(LIBEXT) $(LIBGCLI_OBJS): $(TEMPLATE_HEADERS) $(TEMPLATE_SRCS) .SUFFIXES: .c .libgcli$(OBJEXT) .gcli$(OBJEXT) .pgen$(OBJEXT_FOR_BUILD) .tests$(OBJEXT) .c.libgcli$(OBJEXT): @mkdir -p $$(dirname $@) $(CCACHE) $(CC) $(CFLAGS) $(CCDEPFLAGS) $(CPPFLAGS) \ $(LIBCURL_CFLAGS) $(LIBCRYPTO_CFLAGS) \ $(LIBLOWDOWN_CFLAGS) $(LIBLOWDOWN_CPPFLAGS) \ $(PDJSON_CFLAGS) \ -DIN_LIBGCLI=1 -c -o $@ $< .c.gcli$(OBJEXT): @mkdir -p $$(dirname $@) $(CCACHE) $(CC) $(CFLAGS) $(CCDEPFLAGS) $(CPPFLAGS) \ $(LIBEDIT_CPPFLAGS) $(LIBREADLINE_CPPFLAGS) \ $(LIBCURL_CFLAGS) $(LIBEDIT_CFLAGS) \ $(LIBREADLINE_CFLAGS) $(LIBCRYPTO_CFLAGS) \ $(LIBLOWDOWN_CFLAGS) $(LIBLOWDOWN_CPPFLAGS) \ $(PDJSON_CFLAGS) \ -c -o $@ $< .c.pgen$(OBJEXT_FOR_BUILD): @mkdir -p $$(dirname $@) $(CCACHE) $(CC_FOR_BUILD) $(CFLAGS_FOR_BUILD) \ $(CCDEPFLAGS_FOR_BUILD) $(CPPFLAGS_FOR_BUILD) \ -c -o $@ $< clean-auto: $(RM) -f pgen$(EXEEXT_FOR_BUILD) gcli$(EXEEXT) \ libgcli$(LIBEXT) \ $(LIBGCLI_OBJS) $(GCLI_OBJS) \ $(TEMPLATE_HEADERS) $(TEMPLATE_SRCS) \ $(PGEN_OBJS) $(DEPS) \ lex.yy.c y.tab.c y.tab.h \ $(TEST_PROGRAMS:=.tests$(OBJEXT)) \ $(TEST_PROGRAMS) \ tests/Kyuafile \ $(MANPAGES) ###### TEST SUITE ##################################### TEST_PROGRAMS = \ tests/append-url$(EXEEXT) \ tests/base64$(EXEEXT) \ tests/bugzilla-parse$(EXEEXT) \ tests/difftests$(EXEEXT) \ tests/gitea-parse$(EXEEXT) \ tests/github-parse$(EXEEXT) \ tests/gitlab-parse$(EXEEXT) \ tests/json-escape$(EXEEXT) \ tests/jsongen$(EXEEXT) \ tests/parse-url$(EXEEXT) \ tests/url-encode$(EXEEXT) tests/json-escape: tests/json-escape.tests$(OBJEXT) tests/github-parse: tests/github-parse.tests$(OBJEXT) tests/gitlab-parse: tests/gitlab-parse.tests$(OBJEXT) tests/gitea-parse: tests/gitea-parse.tests$(OBJEXT) tests/bugzilla-parse: tests/bugzilla-parse.tests$(OBJEXT) tests/url-encode: tests/url-encode.tests$(OBJEXT) tests/jsongen: tests/jsongen.tests$(OBJEXT) tests/base64: tests/base64.tests$(OBJEXT) tests/difftests: tests/difftests.tests$(OBJEXT) tests/parse-url: tests/parse-url.tests$(OBJEXT) tests/append-url: tests/append-url.tests$(OBJEXT) $(TEST_PROGRAMS): libgcli$(LIBEXT) $(CCACHE) $(CC) $(CFLAGS) $(CCDEPFLAGS) $(CPPFLAGS) \ $(LIBATFC_CFLAGS) $(LIBCURL_CFLAGS) $(LIBCRYPTO_CFLAGS) \ $(LDFLAGS) -o $@ $(@:=.tests$(OBJEXT)) libgcli$(LIBEXT) \ $(LIBATFC_LIBS) $(LIBCURL_LIBS) $(LIBCRYPTO_LIBS) \ $(PDJSON_LIBS) .c.tests$(OBJEXT): @mkdir -p $$(dirname $@) $(CCACHE) $(CC) $(CFLAGS) $(CCDEPFLAGS) $(CPPFLAGS) \ $(LIBATFC_CFLAGS) $(LIBCURL_CFLAGS) $(PDJSON_CFLAGS) \ -DTESTSRCDIR="\"@SRCDIR@/tests/\"" -c -o $@ $< $(TEST_PROGRAMS:$(EXEEXT)=.c): $(TEMPLATE_HEADERS) tests/Kyuafile: Makefile @mkdir -p tests echo "syntax(2)" > tests/Kyuafile echo "test_suite('gcli')" >> tests/Kyuafile for t in $(TEST_PROGRAMS); do printf "atf_test_program{name='%s', timeout = 1}\n" "$${t##tests/}" >> tests/Kyuafile; done check-auto: tests/Kyuafile $(TEST_PROGRAMS) $(KYUA) test --build-root ./tests --kyuafile tests/Kyuafile #################### static analysis ####################################### LIBGCLI_ANALYSE= ${LIBGCLI_SRCS:.c=.libgcli.analyse} GCLI_ANALYSE= ${GCLI_SRCS:.c=.gcli.analyse} ANALYSE_TARGETS= $(LIBGCLI_ANALYSE) $(GCLI_ANALYSE) analyse-auto: $(ANALYSE_TARGETS) $(ANALYSE_TARGETS): $(TEMPLATE_HEADERS) .SUFFIXES: .c .libgcli.analyse .gcli.analyse .c.libgcli.analyse: @mkdir -p $$(dirname $@) $(CC) $(CFLAGS) $(CPPFLAGS) \ $(LIBCURL_CFLAGS) $(LIBCRYPTO_CFLAGS) \ $(LIBLOWDOWN_CFLAGS) $(LIBLOWDOWN_CPPFLAGS) \ $(PDJSON_CFLAGS) \ -DIN_LIBGCLI=1 $(CANALFLAGS) $< .c.gcli.analyse: @mkdir -p $$(dirname $@) $(CC) $(CFLAGS) $(CPPFLAGS) \ $(LIBEDIT_CPPFLAGS) $(LIBREADLINE_CPPFLAGS) \ $(LIBCURL_CFLAGS) $(LIBEDIT_CFLAGS) \ $(LIBREADLINE_CFLAGS) $(LIBCRYPTO_CFLAGS) \ $(LIBLOWDOWN_CFLAGS) $(LIBLOWDOWN_CPPFLAGS) \ $(PDJSON_CFLAGS) $(CANALFLAGS) $< #################### install target ######################################## MANPAGES= \ docs/gcli-api.1 \ docs/gcli-comment.1 \ docs/gcli-config.1 \ docs/gcli-forks.1 \ docs/gcli-gists.1 \ docs/gcli-issues.1 \ docs/gcli-labels.1 \ docs/gcli-milestones.1 \ docs/gcli-pipelines.1 \ docs/gcli-pulls.1 \ docs/gcli-pulls-review.1 \ docs/gcli-releases.1 \ docs/gcli-repos.1 \ docs/gcli-snippets.1 \ docs/gcli-status.1 \ docs/gcli.1 \ docs/gcli.5 \ docs/gcli-tutorial.1 bindir= /bin mandir= /share/man BINDIR= $(DESTDIR)$(PREFIX)$(bindir) MANDIR= $(DESTDIR)$(PREFIX)$(mandir) .PHONY: manpages manpages-auto # phony target to ensure the makefile is updated with the correct # sed substitions. # # The -auto target here makes make not print "foo is up-to-date" # when they are up-to-date. manpages-auto: Makefile @SRCDIR@/configure $(MANPAGES) manpages: Makefile @SRCDIR@/configure @$(MAKE) manpages-auto $(MANPAGES): @SRCDIR@/configure Makefile .SUFFIXES: .1.in .1 .5.in .5 .md ################################################# # Maintainer target for regenerating the tutorial TUTORIAL_PAGES:sh= find @SRCDIR@/docs/website/tutorial/ -type f -name \*.md | grep -v old_index.md | sort TUTORIAL_PAGES!= find @SRCDIR@/docs/website/tutorial/ -type f -name \*.md | grep -v old_index.md | sort # Dispatch between maintainer/non-maintainer mode TUTORIAL_SRC_MAINTAINER_0= TUTORIAL_SRC_MAINTAINER_1= @SRCDIR@/docs/gcli-tutorial.1.in $(TUTORIAL_SRC_MAINTAINER_$(ENABLE_MAINTAINER)): Makefile $(TUTORIAL_PAGES) cat $(TUTORIAL_PAGES) \ | $(LOWDOWN) -s -tman \ -m section=1 \ -m title=gcli-tutorial \ -m manheader="Tutorial of GCLI - Generated Offline Version of https://herrhotzenplotz.de/gcli/tutorial" \ -m date="@PACKAGE_DATE@" \ > @SRCDIR@/docs/gcli-tutorial.1.in docs/gcli-tutorial.1: $(TUTORIAL_SRC_MAINTAINER_$(ENABLE_MAINTAINER)) ################################################# # Manual page preprocessing rules .1.in.1 .5.in.5: @mkdir -p $$(dirname $@) sed \ -e 's|\@PACKAGE_DATE\@|@PACKAGE_DATE@|g' \ -e 's|\@PACKAGE_STRING\@|@PACKAGE_STRING@|g' \ -e 's|\@PACKAGE_BUGREPORT\@|@PACKAGE_BUGREPORT@|g' \ -e 's|\@PACKAGE_URL\@|@PACKAGE_URL@|g' \ -e 's|\@PACKAGE_VERSION\@|@PACKAGE_VERSION@|g' \ < ${<} > $@ install-auto: manpages gcli $(INSTALL) -d $(BINDIR) $(INSTALL) -m 0755 gcli $(BINDIR) $(INSTALL) -d $(MANDIR) $(INSTALL) -d $(MANDIR)/man5 $(INSTALL) -d $(MANDIR)/man1 # TODO: compress manual pages for PAGE in $(MANPAGES); do \ case $$PAGE in \ *.1) \ $(INSTALL) -m 644 $$PAGE $(MANDIR)/man1 \ ;; \ *.5) \ $(INSTALL) -m 644 $$PAGE $(MANDIR)/man5 \ ;; \ *) \ echo error installing man page $$PAGE >&2 \ ;; \ esac \ done ############################################################### # Distcheck target distcheck: Makefile @SRCDIR@/configure @$(MAKE) distcheck-auto distcheck-auto: (cd @SRCDIR@ && @SRCDIR@/tools/gentarball.sh) mkdir -p dist/.build-gcli-$(GCLI_VERSION) (cd dist && tar -x -f @SRCDIR@/dist/gcli-$(GCLI_VERSION)/gcli-$(GCLI_VERSION).tar.gz && chmod -R a-w gcli-$(GCLI_VERSION)) (cd dist/.build-gcli-$(GCLI_VERSION) && ../gcli-$(GCLI_VERSION)/configure && $(MAKE) && $(MAKE) check) (chmod -R a+w dist/gcli-$(GCLI_VERSION) && rm -fr dist/.build-gcli-$(GCLI_VERSION) dist/gcli-$(GCLI_VERSION)) ############################################################### # Help Target .PHONY: help help-auto help: Makefile @$(MAKE) help-auto help-auto: @echo "Available targets:" @echo " all - Do a full build (this is the default target)" @echo " check - Run the test suite" @echo " dist - Build distribution tarballs" @echo " distcheck - Run the test suite on the built distribution tarball." @echo " clean - Clean the build directory" @echo " analyse - Run static code analysis" @echo " install - Install build artifacts" @echo " manpages - Only build manual pages" gcli-2.9.1/README.md000066400000000000000000000067621507017207500137370ustar00rootroot00000000000000# GCLI Portable CLI tool for interacting with Git(Hub|Lab|Tea), Forgejo and Bugzilla from the command line. ![](docs/screenshot.png) ## Why? The official GitHub CLI tool only supports GitHub. I wanted a simple unified tool for various git forges such as GitHub and GitLab because every forge does things differently yet all build on Git and purposefully break with its philosophy. ## Building ### Download Recent tarballs can be downloaded here: [https://herrhotzenplotz.de/gcli/releases/](https://herrhotzenplotz.de/gcli/releases/) There are official packages available: Packaging status ### Dependencies Required dependencies: - libcurl - yacc (System V yacc, Berkeley Yacc or Bison should suffice) - lex (flex is preferred) - C11 Compiler and linker - make - pkgconf or pkg-config Optional dependencies: - liblowdown - libedit - libreadline The test suite requires: - [Kyua](https://github.com/jmmv/kyua) - [ATF](https://github.com/jmmv/atf) ### Compile In order to perform a build, do: ```console $ ./configure [--prefix=/usr/local] $ make # make [DESTDIR=/] install ``` You may leave out `DESTDIR` and `--prefix=`. The above is the default value. The final installation destination is `$DESTDIR/$PREFIX/...`. If you are unsure, consult the builtin configure help by running `./configure --help`. In case any of the above does not work, please either report a bug, or submit a patch in case you managed to fix it. Details on cross-compilation can be found in [HACKING.md](HACKING.md). ### Testing To run the test suite first make sure you have all the necessary dependencies installed (see above). Then you can run `make check` in the build directory to run the test suite. For more details also see [HACKING.md](HACKING.md). ### (Previously) known to work platforms Incomplete list of tested operating systems: - FreeBSD 13.0-RELEASE amd64 and arm64 - Solaris 10 and 11, sparc64 - SunOS 5.11 i86pc (OmniOS) - Devuan GNU/Linux Chimaera x86_64 - Debian GNU/Linux ppc64, ppc64le - Gentoo Linux sparc64, ia64 - Fedora 34 x86_64 - Haiku x86_64 - Minix 3.4.0 (GENERIC) i386 - OpenBSD 7.0 GENERIC amd64 - Alpine Linux 3.16 x86_64 - Darwin 22.2.0 arm64 - Windows 10 (MSYS2 mingw32-w64) - NetBSD 9.3 amd64, sparc64 and VAX Tested Compilers so far: - LLVM Clang (various versions) - GCC (various versions) - Oracle DeveloperStudio 12.6 - IBM XL C/C++ V16.1.1 (Community Edition) ## Support / Community Please refer to the manual pages that come with gcli. You may want to start at `gcli(1)`. For further questions refer to the issues on Github and Gitlab. Also, there's an IRC channel #gcli on [Libera.Chat](https://libera.chat/). Alternatively you may also use the mailing list at [https://lists.sr.ht/~herrhotzenplotz/gcli-discuss](https://lists.sr.ht/~herrhotzenplotz/gcli-discuss). ## Bugs and contributions Please report bugs, issues and questions to [~herrhotzenplotz/gcli-discuss@lists.sr.ht](mailto:~herrhotzenplotz/gcli-discuss@lists.sr.ht) or on [GitLab](https://gitlab.com/herrhotzenplotz/gcli). You can also submit patches using git-send-email or Mercurial patchbomb to [~herrhotzenplotz/gcli-devel@lists.sr.ht](mailto:~herrhotzenplotz/gcli-devl@lists.sr.ht). ## License BSD-2 CLAUSE (aka. FreeBSD License). Please see the LICENSE file attached. ## Credits This program makes heavy use of both [libcurl](https://curl.haxx.se/) and [pdjson](https://github.com/skeeto/pdjson). gcli-2.9.1/configure000077500000000000000000000413251507017207500143610ustar00rootroot00000000000000#!/usr/bin/env sh # set -o pipefail CONFIGURE_CMD_ARGS="${*}" PACKAGE_VERSION="2.9.1" PACKAGE_DATE="04-Oct-2025" PACKAGE_STRING="gcli $PACKAGE_VERSION" PACKAGE_BUGREPORT="https://lists.sr.ht/~herrhotzenplotz/gcli-discuss" PACKAGE_URL="https://sr.ht/~herrhotzenplotz/gcli" find_program() { varname=$1 shift printf "Checking for $varname ..." >&2 for x in $*; do if command -v $x >/dev/null 2>&1 && [ -x $(command -v $x) ]; then binary="$(command -v $x)" printf " $binary\n" >&2 echo "${binary}" exit 0 fi done printf " not found\n" >&2 exit 1 } die() { printf "%s\n" "${*}" exit 1 } tolower() { tr '[:upper:]' '[:lower:]' } check_compiler() { command -v "${1}" > /dev/null 2>&1 || die " '${1}' not found" } compiler_type() { if ${1} -v 2>&1 | grep clang > /dev/null; then echo "clang" elif ${1} -v 2>&1 | grep '^gcc' > /dev/null; then echo "gcc" elif ${1} -qversion 2>&1 | grep 'IBM XL C' >/dev/null; then echo "xlc" elif ${1} -V 2>&1 | grep 'Studio' > /dev/null; then echo "sunstudio" else echo "unknown" fi } normalise_compiler_target() { tolower | sed -e 's|x86_64|amd64|g' -e 's| ||g' } compiler_target() { ccom=$1 cc=$2 case $ccom in gcc|clang) $cc -v 2>&1 \ | grep '^Target' \ | cut -d: -f2 ;; xlc) # for xlc we can't easily get its target. instead we ask its # configured assembler for the target. Right now I assume it # is a GNU assembler. If this doesn't work for you please fix! # I don't have access to any equipment running anything else # and I will not beg IBM for giving me access. xlc_config=$($cc -v 2>&1 | grep XL_CONFIG | sed 's|.*XL_CONFIG=\([^:]*\):.*|\1|') [ -z "${xlc_config}" ] && die "xlc config file not detected" xlc_as=$(cat $xlc_config | grep -E 'as[\t ]+.*=[ ]+' | sed 1q | cut -d= -f2 | sed 's| ||g') [ -z "${xlc_as}" ] && die "failed to figure out assembler used by XL C" $xlc_as --version 2>&1 | grep 'GNU assembler' >/dev/null || die "Can only derive compiler target from GNU assembler, please submit fix for your assembler" $xlc_as --version 2>&1 | grep target | sed "s|.*\`\([^']*\)'.*|\1|" ;; sunstudio) # Only tested on Solaris 10 ... if this messes up on Linux or SunOS # i86pc feel free to send a patch... if $cc -V 2>&1 | grep SunOS_sparc > /dev/null; then echo "sparc64-unknown-solaris" else echo "unknown-unknown-unknown" fi ;; *) echo "unknown-unknown-unknown" ;; esac | normalise_compiler_target } # args= pkgconfig-name variable-name is-mandatory find_package() { printf "Checking for $1 ..." >&2 if ! $PKG_CONFIG --exists $1; then if [ "${3}" = optional ]; then printf " not found\n" export ${2}_FOUND=0 return else # $3 = required die "not found" fi fi # readline and possibly other dependencies in Debian (yikes) # return -D_XOPEN_SOURCE which breaks builds. Rip it out - # we know what we're doing... ha! export ${2}_CFLAGS="$($PKG_CONFIG --cflags $1 | sed 's|-D_XOPEN_SOURCE=[[:digit:]]*||g')" export ${2}_LIBS="$($PKG_CONFIG --libs $1)" export ${2}_FOUND=1 export ${2}_VERSION="$($PKG_CONFIG --modversion $1)" printf " found\n" >&2 } usage() { cat >&2 < Makefile echo "Writing config.h" cat > config.h </tmp/foo .Ed .Sh SEE ALSO .Xr git 1 , .Xr gcli 1 .Sh AUTHORS .An Nico Sonack aka. herrhotzenplotz Aq Mt nsonack@herrhotzenplotz.de and contributors. .Sh BUGS Please report bugs via E-Mail to .Mt @PACKAGE_BUGREPORT@ . .Pp Alternatively you can report them on any of the forges linked at .Lk @PACKAGE_URL@ . However, the preferred and quickest method is to use the mailing list. gcli-2.9.1/docs/gcli-comment.1.in000066400000000000000000000042111507017207500164400ustar00rootroot00000000000000.Dd @PACKAGE_DATE@ .Dt GCLI-COMMENT 1 .Os @PACKAGE_STRING@ .Dd $Mdocdate$ .Sh NAME .Nm gcli\ comment .Nd Comment on tickets in git forges .Sh SYNOPSIS .Nm .Op Fl y .Op Fl o Ar owner Fl r Ar repo .Op Fl i Ar issue | Fl p Ar PR .Op Fl R Ar comment .Sh DESCRIPTION .Nm can be used to add comments in the discussion under issues and pull requests on .Xr git 1 forges such as GitHub, GitLab and Gitea. Note that PRs are treated as issues on GitHub and Gitea, making the .Fl i and .Fl p flags exchangeable without changing the overall effect of creating the comment. .Nm will open an editor, either specified in your environment through .Ev EDITOR or the one set in your global config file to enter the comment. See .Xr gcli 1 . .Sh OPTIONS .Bl -tag -width indent .It Fl o , -owner Ar owner Comment in the repository of the given owner. This option can only be used in combination with .Fl r . .It Fl r , -repo Ar repo Comment in the given repository. This option can only be used in combination with .Fl r . .It Fl y , -yes Do not ask for confirmation before submitting the comment. Assume yes. .It Fl i , -issue Ar issue Create the comment under issue .Ar #issue . .It Fl p , -pull Ar pr Create the comment under PR .Ar #pr . .It Fl R , -in-reply-to Ar comment Create a reply to the comment with ID .Ar comment . This will put the refenced comment prefixed with .Dq "> " into the response file for you to edit. .El .Sh EXAMPLES Comment under PR #11 in the upstream repository: .Bd -literal -offset indent $ gcli comment -p 11 .Ed .Pp Comment under issue 1 in herrhotzenplotz/gcli: .Bd -literal -offset indent $ gcli comment -o herrhotzenplotz -r gcli -i 1 .Ed .Sh SEE ALSO .Xr git 1 , .Xr gcli 1 , .Xr gcli-issues 1 , .Xr gcli-pulls 1 .Sh AUTHORS .An Nico Sonack aka. herrhotzenplotz Aq Mt nsonack@herrhotzenplotz.de and contributors. .Sh BUGS There is no way to preview the markdown markup, however you can input markdown which will be rendered on the remote site. .Pp Please report bugs via E-Mail to .Mt @PACKAGE_BUGREPORT@ . .Pp Alternatively you can report them on any of the forges linked at .Lk @PACKAGE_URL@ . However, the preferred and quickest method is to use the mailing list. gcli-2.9.1/docs/gcli-config.1.in000066400000000000000000000032641507017207500162520ustar00rootroot00000000000000.Dd @PACKAGE_DATE@ .Dt GCLI-ISSUES 1 .Os @PACKAGE_STRING@ .Sh NAME .Nm gcli\ config .Nd Git Forge Configuration .Sh SYNOPSIS .Nm .Cm ssh .Nm .Cm ssh add .Fl t Ar title .Fl k Ar keypath .Nm .Cm ssh delete .Fl i Ar id .Sh DESCRIPTION .Nm is used to change the settings of the Git Forge Account. You can use it to e.g. add or delete SSH Public Keys used to push to forges. .Sh OPTIONS .Bl -tag -width xxxxxxxxxxxxxxxxx .It Fl t , -title Ar title Set the title of the SSH Key to be added. This is a short description of the key. .It Fl k , -key Pa key-path Path to the file containing the SSH public key. .It Fl i , -id Ar id ID of the public key to delete. .El . .Sh SUBCOMMANDS .Bl -tag -width xxxxxxxxxxx .It Cm ssh List SSH public keys for the current user. .It Cm ssh add Add an SSH public key for the current user. .It Cm ssh delete Delete an SSH public key for the current user. .El .Sh EXAMPLES Print a list of registered SSH public keys: .Bd -literal -offset indent $ gcli config ssh .Ed .Pp Register ~/.ssh/id_rsa.pub on the default forge: .Bd -literal -offset indent $ gcli config ssh add \\ -t "Key for $(hostname)" \\ -k ~/.ssh/id_rsa.pub .Ed .Pp .Sh SEE ALSO .Xr git 1 , .Xr gcli 1 .Sh AUTHORS .An Nico Sonack aka. herrhotzenplotz Aq Mt nsonack@herrhotzenplotz.de and contributors. .Sh BUGS When using this feature to manage SSH keys on GitHub be aware that you need the .Dq read:public_key scope enabled on your access token. You will receive HTTP 404 errors otherwise. .Pp Please report bugs via E-Mail to .Mt @PACKAGE_BUGREPORT@ . .Pp Alternatively you can report them on any of the forges linked at .Lk @PACKAGE_URL@ . However, the preferred and quickest method is to use the mailing list. gcli-2.9.1/docs/gcli-forks.1.in000066400000000000000000000051261507017207500161300ustar00rootroot00000000000000.Dd @PACKAGE_DATE@ .Dt GCLI-FORKS 1 .Os @PACKAGE_STRING@ .Sh NAME .Nm gcli\ forks .Nd Manage repository forks in git forges .Sh SYNOPSIS .Nm .Op Fl n Ar n .Op Fl y .Op Fl s .Op Fl o Ar owner Fl r Ar repo .Ar actions... .Nm .Cm create .Op Fl o Ar owner Fl r Ar repo .Op Fl i Ar target-owner .Sh DESCRIPTION Use .Nm to manage forks of other repositories in various .Xr git 1 forges such as GitHub, GitLab and Gitea. .Sh OPTIONS .Bl -tag -width indent .It Fl o , -owner Ar owner Operate on the repository of the given owner. This option can only be used in combination with .Fl r . .It Fl r , -repo Ar repo Operate on the given repository. This option can only be used in combination with .Fl o . .It Fl y , -yes Do not ask for confirmation. Assume yes. .It Fl s , -sorted Reverse the output such that most recent items appear at the bottom. .It Fl i , -into Ar target-owner When forking a repository, this is the organisation or user the repository is forked into. .It Fl n , -count Ar n Fetch at least .Ar n forks. Setting .Ar n to -1 will fetch all forks. Default: 30. Note that on repositories with many forks fetching all forks can take a considerable amount of time and may result in rate limiting by the respective API. .El .Sh ACTIONS .Ar actions... may be one or more of the following: .Bl -tag -width indent .It Cm delete Delete the fork. You will be asked for confirmation unless you set .Fl y . .Pp The following flags can be specified: .Bl -tag -width indent .It Fl r , -repo See .Sx OPTIONS .It Fl o , -owner See .Sx OPTIONS .It Fl y , -yes Do not ask for confirmation before deleting the fork. See .Sx OPTIONS . .El .El .Sh EXAMPLES Clone vim/vim and fork it into your account: .Bd -literal -offset indent $ git clone git@github.com:vim/vim $ cd vim $ gcli forks create --into .Ed .Pp This will ask you if you want to add a remote to your fork. In case you accept the offer, the origin remote will be renamed to upstream and a new origin will be pointed at your newly created fork. You may also want to setup a .gcli file at the same time: .Bd -literal -offset indent $ printf -- "pr.upstream=vim/vim\\npr.base=trunk\\n" >> .gcli .Ed .Pp Delete your fork of the current repository without confirmation: .Bd -literal -offset indent $ gcli forks -y delete .Ed .Sh SEE ALSO .Xr git 1 , .Xr gcli 1 .Sh AUTHORS .An Nico Sonack aka. herrhotzenplotz Aq Mt nsonack@herrhotzenplotz.de and contributors. .Sh BUGS Please report bugs via E-Mail to .Mt @PACKAGE_BUGREPORT@ . .Pp Alternatively you can report them on any of the forges linked at .Lk @PACKAGE_URL@ . However, the preferred and quickest method is to use the mailing list. gcli-2.9.1/docs/gcli-gists.1.in000066400000000000000000000050531507017207500161340ustar00rootroot00000000000000.Dd @PACKAGE_DATE@ .Dt GCLI-GISTS 1 .Os @PACKAGE_STRING@ .Sh NAME .Nm gcli\ gists .Nd manage GitHub Gists .Sh SYNOPSIS .Nm .Op Fl s .Op Fl l .Op Fl n Ar n .Op Fl u Ar user .Nm .Cm create .Op Fl d Ar description .Op Fl f Pa path .Ar gist-file-name .Nm .Cm delete .Op Fl y .Ar gist-id .Nm .Cm get .Ar gist-id .Ar file-name .Sh DESCRIPTION Use .Nm to list, create, download or delete GitHub Gists. Without a subcommand specified, .Nm will list Gists of the given or auto-detected user account. .Sh OPTIONS .Bl -tag -width indent .It Fl l , -long Print a long list instead of a short table. .It Fl s , -sorted Reverse the output such that most recent items appear at the bottom. .It Fl u , -user Ar owner List Gists of the given user. .It Fl n , -count Ar n Fetch at least .Ar n gists. Setting .Ar n to -1 will fetch all gists. Default: 30. Note that on users with many gists fetching all gists can take a considerable amount of time and may result in rate limiting by the GitHub API. .El .Sh SUBCOMMANDS .Bl -tag -width indent .It Cm create Paste a new Gist. The following flags can be specified: .Bl -tag -width indent .It Fl f , -file Pa file Read the content from the specified file instead of standard input. .It Fl d , -description Ar description The description of the Gist to be created. .El .It Cm delete Delete a Gist. The following options can be specified: .Bl -tag -width indent .It Fl y , -yes Do not ask for confirmation before deleting the Gist. Assume yes. .El .It Cm get Download a file from a Gist. There are no options to this subcommand. .El .Sh EXAMPLES List neutaaaaan's Gists: .Bd -literal -offset indent $ gcli gists -u neutaaaaan .Ed .Pp Paste a new gist named foobar and read from foobar.txt: .Bd -literal -offset indent $ gcli gists create foobar < foobar.txt .Ed .Pp Delete gist with id 3b546069d2856e6051bbe3c1080f1b5d: .Bd -literal -offset indent $ gcli gists delete 3b546069d2856e6051bbe3c1080f1b5d .Ed .Pp Print foobar.txt from Gist with id 3b546069d2856e6051bbe3c1080f1b5d into your pager: .Bd -literal -offset indent $ gcli gists get 3b546069d2856e6051bbe3c1080f1b5d foobar.txt | $PAGER .Ed .Pp .Sh SEE ALSO .Xr git 1 , .Xr gcli 1 .Sh AUTHORS .An Nico Sonack aka. herrhotzenplotz Aq Mt nsonack@herrhotzenplotz.de and contributors. .Sh BUGS This subcommand only works on GitHub. It is not implemented for GitLab, as GitLab snippets work differently. .Pp Please report bugs via E-Mail to .Mt @PACKAGE_BUGREPORT@ . .Pp Alternatively you can report them on any of the forges linked at .Lk @PACKAGE_URL@ . However, the preferred and quickest method is to use the mailing list. gcli-2.9.1/docs/gcli-issues.1.in000066400000000000000000000140611507017207500163150ustar00rootroot00000000000000.Dd @PACKAGE_DATE@ .Dt GCLI-ISSUES 1 .Os @PACKAGE_STRING@ .Sh NAME .Nm gcli\ issues .Nd Manage issues in various git forges .Sh SYNOPSIS .Nm .Op Fl n Ar n .Op Fl a .Op Fl s .Op Fl A Ar author .Op Fl L Ar label .Op Fl M Ar milestone .Op Fl S Ar assignee .Op Fl o Ar owner Fl r Ar repo .Op Ar "search-query" .Nm .Fl i Ar issue .Op Fl o Ar owner Fl r Ar repo .Ar actions... .Nm .Cm create .Op Fl o Ar owner Fl r Ar repo .Op Fl y .Op Ar issue-title .Sh DESCRIPTION Use .Nm to search, list, create, edit or delete issues in repositories in various .Xr git 1 forges such as GitHub, GitLab and Gitea. Without any action specified, .Nm will list issues in the given or auto-detected repository. .Pp On Bugzilla one can use the quick-search syntax in the .Ar search-query to make more fine-grained searches. See .Sx "SEE ALSO" and .Sx "EXAMPLES" for more information. .Sh OPTIONS .Bl -tag -width indent .It Fl s , -sorted Reverse the output such that most recent items appear at the bottom. .It Fl o , -owner Ar owner List issues in the repository of the given owner. This option can only be used in combination with .Fl r . .It Fl r , -repo Ar repo List issues in the given repository. This option can only be used in combination with .Fl o . .It Fl a List issues disregarding their state. This will list closed issues as well. Cannot be combined with actions. This does not affect the .Fl n 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 specified once. .It Fl M , Fl -milestone Ar milestone Filter issues by the given milestone. This option may only be specified once. .It Fl S , Fl -assignee Ar assignee Filter issues by the given assignee. This option may only be specified once. .It Fl n , -count Ar n Fetch at least .Ar n issues. Setting .Ar n to -1 will fetch all issues. Default: 30. Note that on large repositories fetching all issues can take a considerable amount of time and may result in rate limiting by the respective API. See .Sx CAVEATS . .It Fl i , -id Ar issue execute the given .Ar actions for the specified .Ar issue . .El . .Sh SUBCOMMANDS .Bl -tag -width indent .It Cm create Create a new issue in the given or auto-detected 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 Specify in which repository the issue is to be created. .It Fl y , -yes Do not ask for confirmation before creating the issue. Assume yes. .El .El .Sh ACTIONS .Ar actions... may be one or more of the following: .Bl -tag -width "labels [options]" .It Cm all Display both a summary and the original post of the issue. .It Cm comments Print a list of comments under the issue. .It Cm status Print a short summary of the issue. .It Cm op Print the original post of the issue. .It Cm close Close the issue. .It Cm reopen Reopen a closed issue. .It Cm assign Ar assignee Assign the issue to the given .Ar assignee (user name). .It Cm labels Op Ar options The following options can be specified more than once: .Bl -tag -width indent .It add Ar label Add the given label to the issue. .It remove Ar label Remove the given label from the issue. .El .It Cm milestone Ar id Assign the issue to a milestone with the given .Ar id . .It Cm milestone Fl d Clear associated milestone of the given issue. .It Cm notes Alias for the .Cm comments action that prints the list of comments associated with the issue. .It Cm title Ar new-title Change the title of the issue to .Ar new-title . .It Cm attachments List bug attachments. This action is only available on Bugzilla. .It Cm open Open the issue in a web browser using .Xr xdg-open 1 . .It Cm edit Open the original post in an editor for editing. After saving and exiting the editor the original post is updated when confirmed by the user. .El .Sh EXAMPLES Print a list of issues in the current project: .Bd -literal -offset indent $ gcli issues .Ed .Pp Search for issues containing .Dq crash in contour-terminal/contour on GitHub including closed issues: .Bd -literal -offset indent $ gcli -t github issues -o contour-terminal -r contour -a crash .Ed .Pp 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 .Pp Print both a summary and comments of issue 1 in herrhotzenplotz/gcli: .Bd -literal -offset indent $ gcli issues -o herrhotzenplotz -r gcli -i 1 status comments .Ed .Pp Add the labels .Sq foo and .Sq bar to the issue with id 420: .Bd -literal -offset indent $ gcli issues -i420 labels add foo add bar .Ed .Pp List issues with the label .Dq bug : .Bd -literal -offset indent $ gcli issues -L bug .Ed .Pp Search for Bugzilla Bugs in product .Dq foo with .Dq RandomUser set in CC: .Bd -literal -offset indent $ gcli -t bugzilla issues product:foo cc:RandomUser .Ed .Sh SEE ALSO .Xr git 1 , .Xr gcli 1 , .Lk "https://bugzilla.mozilla.org/page.cgi?id=quicksearch.html" "Bugzilla QuickSearch Syntax" .Sh AUTHORS .An Nico Sonack aka. herrhotzenplotz Aq Mt nsonack@herrhotzenplotz.de and contributors. .Sh CAVEATS GitHub and Gitea treat Pull Requests as Issues. Due to the semantics of .Nm those issues that are actually PRs are dropped from the output. In this case a note will be printed indicating how many issues were dropped. You can suppress this warning using the .Fl q program option. .Sh BUGS GitHub only supports removing labels from issues one by one. If you still want to remove multiple issues with a single gcli call, you may do something like: . .Bd -literal -offset indent $ gcli issues -i42 \\ labels remove bug \\ labels remove foo .Ed .Pp Please report bugs via E-Mail to .Mt @PACKAGE_BUGREPORT@ . .Pp Alternatively you can report them on any of the forges linked at .Lk @PACKAGE_URL@ . However, the preferred and quickest method is to use the mailing list. gcli-2.9.1/docs/gcli-labels.1.in000066400000000000000000000061661507017207500162530ustar00rootroot00000000000000.Dd @PACKAGE_DATE@ .Dt GCLI-LABELS 1 .Os @PACKAGE_STRING@ .Sh NAME .Nm gcli\ labels .Nd Manage ticket labels in git forges .Sh SYNOPSIS .Nm .Op Fl o Ar owner Fl r Ar repo .Nm .Cm create .Op Fl o Ar owner Fl r Ar repo .Fl d Ar description .Fl n Ar name .Fl c Ar colour .Nm .Op Fl o Ar owner Fl r Ar repo .Fl i Ar name .Ar actions... .Sh DESCRIPTION Use .Nm to list, create, edit or delete labels for Pull Requests/Merge Requests and issues in repositories in various git forges such as GitHub, GitLab and Gitea. Without any action specified, .Nm will list all defined labels in the given or auto-detected repository. .Sh OPTIONS .Bl -tag -width indent .It Fl o , -owner Ar owner Work in the repository of the given owner. This option can only be used in combination with .Fl r . .It Fl r , -repo Ar repo Work in the given repository. This option can only be used in combination with .Fl o . .It Fl i , -name Ar name Execute actions on the label identified by .Ar name . .El .Pp .Sh SUBCOMMANDS .Bl -tag -width indent .It Cm create Create a new label in the given or auto-detected repository. .Pp The following flags must be specified: .Bl -tag -width indent .It Fl n , -name Ar name Set the short name of the label to the given .Ar name . .It Fl d , -description Ar description Set the description of the label to the given .Ar text . Note that on GitHub this field may only consist of up to 150 characters. .It Fl c , -colour Ar code Set the colour of the label to the given .Ar code . .Ar code is expected to be a 6 digit hexadecimal RGB colour code. .El .El .Sh ACTIONS When given a specific label with .Fl i you can execute one or more of the following actions: .Bl -tag -width indent .It Cm status Print a short summary about the label. .It Cm name Ar new-name Change the name of the to the given .Ar new-name . .It Cm description Ar new-description Change the description of the label to .Ar new-description . You should wrap the description in quotes in case it contains spaces. .It Cm colour Ar hexcolour Change the colour of the label to the six-digit hexadecmial RGB colour code .Ar hexcolour . .It Cm delete Delete the given label. .El .Sh EXAMPLES Print a list of all labels in the current project: .Bd -literal -offset indent $ gcli labels .Ed .Pp Create a new label called .Sq bug with a description .Sq Something is not working as expected and give it a red colour: .Bd -literal -offset indent $ gcli labels create \\ --name bug \\ --description "Something is not working as expected" \\ --colour FF0000 .Ed .Pp Delete the label .Sq foobar in herrhotzenplotz/gcli and use the configured account .Sq gitlab : .Bd -literal -offset indent $ gcli -a gitlab labels -o herrhotzenplotz -r gcli -i foobar delete .Ed .Sh SEE ALSO .Xr git 1 , .Xr gcli 1 .Sh AUTHORS .An Nico Sonack aka. herrhotzenplotz Aq Mt nsonack@herrhotzenplotz.de and contributors. .Sh BUGS The delete subcommand should ask for confirmation and have a flag to override this behaviour. .Pp Please report bugs via E-Mail to .Mt @PACKAGE_BUGREPORT@ . .Pp Alternatively you can report them on any of the forges linked at .Lk @PACKAGE_URL@ . However, the preferred and quickest method is to use the mailing list. gcli-2.9.1/docs/gcli-milestones.1.in000066400000000000000000000044521507017207500171670ustar00rootroot00000000000000.Dd @PACKAGE_DATE@ .Dt GCLI-MILESTONES 1 .Os @PACKAGE_STRING@ .Sh NAME .Nm gcli\ milestones .Nd List and manage milestones in various Git Forges .Sh SYNOPSIS .Nm .Op Fl o Ar owner Fl r Ar repo .Nm .Op Fl o Ar owner Fl r Ar repo .Op Fl i Ar id .Nm .Cm create .Op Fl o Ar owner Fl r Ar repo .Fl t Ar title .Op Fl d Ar description .Sh DESCRIPTION The .Nm command can be used to work with milestones on various .Xr git 1 forges. You can list, create, modify or delete milestones. .Sh OPTIONS .Bl -tag -width indent .It Fl i , -id Ar id Operate on a milestone with the given .Ar id . .It Fl o , -owner Ar owner Work with milestones on a repository of the given .Ar owner . .It Fl r , -repo Ar repo Work with milestones on the given .Ar repository . .It Fl t , -title Ar title Create a milestone with the given .Ar title . This option is mandatory when creating a milestone. .It Fl d , -description Ar description Create the milestone with the given .Ar description . .El .Sh ACTIONS When operating on a single milestone you may use one or more of the following actions: .Bl -tag -width indent .It Cm all Print both general status info and a list of issues related to the given milestone. .It Cm status Print general metadata and information about the milestone. .It Cm issues Print a list of issues attached to the milestone. .It Cm delete Delete this milestone. .It Cm open Open this milestone in a web browser. .El .Sh EXAMPLES Print a list of milestones for the current auto-detected forge: .Bd -literal -offset indent $ gcli milestones .Ed .Pp Print details about the milestone with the ID 42: .Bd -literal -offset indent $ gcli milestones -i 42 status .Ed .Pp Create a new milestone with the title foobar: .Bd -literal -offset indent $ gcli milestones create -t foobar .Ed .Pp Delete milestone number 420 in vim/vim on GitHub: .Bd -literal -offset indent $ gcli -t github milestones -i 420 delete .Ed .Sh SEE ALSO .Xr git 1 , .Xr gcli 1 .Sh AUTHORS .An Nico Sonack aka. herrhotzenplotz Aq Mt nsonack@herrhotzenplotz.de and contributors. .Sh BUGS The delete subcommand deletes the milestone without asking for confirmation. .Pp Please report bugs via E-Mail to .Mt @PACKAGE_BUGREPORT@ . .Pp Alternatively you can report them on any of the forges linked at .Lk @PACKAGE_URL@ . However, the preferred and quickest method is to use the mailing list. gcli-2.9.1/docs/gcli-pipelines.1.in000066400000000000000000000057241507017207500170000ustar00rootroot00000000000000.Dd @PACKAGE_DATE@ .Dt GCLI-PIPELINES 1 .Os @PACKAGE_STRING@ .Sh NAME .Nm gcli\ pipelines .Nd Inspect and manage GitLab Pipelines .Sh SYNOPSIS .Nm .Op Fl n Ar n .Op Fl o Ar owner Fl r Ar repo .Nm .Fl p Ar pipeline-id .Op Fl o Ar owner Fl r Ar repo .Ar pipeline-actions... .Nm .Fl j Ar job-id .Op Fl o Ar owner Fl r Ar repo .Ar job-actions... .Sh DESCRIPTION .Nm is used to display data about the Continuous Integration (CI) service of GitLab. You can list pipelines of a given repository, list jobs in a given pipeline or perform actions such as restarting jobs or fetching their logs. .Sh OPTIONS .Bl -tag -width indent .It Fl o , -owner Ar owner Operate on the repository of the given owner. This option can only be used in combination with .Fl r . .It Fl r , -repo Ar repo Operate on the given repository. This option can only be used in combination with .Fl o . .It Fl n , -count Ar n Fetch at least .Ar n items. Setting .Ar n to -1 will fetch all items. Default: 30. Note that on large repositories fetching all items can take a considerable amount of time and may result in rate limiting by the GitLab API. .It Fl p , -pipeline Ar pipeline-id Execute the given actions for the specified .Ar pipeline-id . .It Fl j , -job Ar job execute the given .Ar actions for the specified .Ar job . .El .Sh ACTIONS .Ar job-actions... may be one or more of the following: .Bl -tag -width artifacts .It Cm log Dump the log of the job. .It Cm status Print a short summary of the job. .It Cm cancel Cancel the job. .It Cm retry Retry the job. .It Cm artifacts Op Fl o Ar outfile Download the artifacts archive as a zip to disk. The default output file is .Pa artifacts.zip but it can be overridden by using the .Fl o flag. .El .Pp .Pp .Ar pipeline-actions... may be one or more of the following: .Bl -tag -width children .It Cm all Print a summary with all information about the pipeline. .It Cm status Print a short status summary of the pipeline. .It Cm jobs Print a list of jobs running in the pipeline. .It Cm children Print a list of child pipelines triggered by this pipeline. .It Cm open Opens the pipeline in a web browser. .El .Sh EXAMPLES Print a list of the last 30 pipelines in the current project: .Bd -literal -offset indent $ gcli pipelines .Ed .Pp Print a summary of pipeline 420: .Bd -literal -offset indent $ gcli pipelines -p 420 all .Ed .Pp List only jobs of pipeline #3316: .Bd -literal -offset indent $ gcli pipelines -p3316 jobs .Ed .Pp Dump the log of Job #423141 in herrhotzenplotz/gcli: .Bd -literal -offset indent $ gcli pipelines -o herrhotzenplotz -r gcli -j 423141 log .Ed .Sh SEE ALSO .Xr git 1 , .Xr gcli 1 .Sh AUTHORS .An Nico Sonack aka. herrhotzenplotz Aq Mt nsonack@herrhotzenplotz.de and contributors. .Sh BUGS We are missing a .Fl a flag. This is the current implied behaviour. .Pp Please report bugs via E-Mail to .Mt @PACKAGE_BUGREPORT@ . .Pp Alternatively you can report them on any of the forges linked at .Lk @PACKAGE_URL@ . However, the preferred and quickest method is to use the mailing list. gcli-2.9.1/docs/gcli-pulls-review.1.in000066400000000000000000000055421507017207500174440ustar00rootroot00000000000000.Dd @PACKAGE_DATE@ .Dt GCLI-PULLS-REVIEW 1 .Os @PACKAGE_STRING@ .Sh NAME .Nm gcli\ pulls\ review .Nd GCLI Integrated Code Review Tool .Sh SYNOPSIS .Nm .Op Fl o Ar owner Fl r Ar repo .Fl i Ar id .Cm review .Sh DESCRIPTION .Sy WARNING! This feature is rather new. Expect bugs and broken features. Please report bugs if you hit any. The same also applies to this manual page. Please suggest improvements that can be made to this feature too. To enable this experimental action you must either set .Dq enable-experimental in your gcli config file (see .Xr gcli 5 ) to .Dq yes or set the environment variable .Ev GCLI_ENABLE_EXPERIMENTAL to .Dq yes . .Pp The .Nm action can be used to perform code review tasks for pull requests on Git forges supported by gcli. .Pp Running this action will drop you into your editor and lets you annotate the diff of the PR. Comments are always above the change they refer to. A change is either a single diff line or multiple lines wrapped in curly braces on the lines surrounding the change. Comments may be prefixed with .Dq > : .Bd -literal -offset indent @@ foo.md -0,0 +0,0 @@ hunk starts here No change This is a single line comment +Foo > This is is a multiline comment on a multiline diff. > > Comment continues here. { not a change -Removed this line +Added this line +Added another line. } this is also not a change .Ed .Pp The above example produces two comments, the first referring only to the addition of .Dq Foo and the second referring to 4 lines surrounded by the curly braces. .Pp Everything leading up to the first diff will be attached as a top-level comment for your review. .Pp After you saved and exited the editor you will be asked what actions to take on your review. This allows you to request changes, approve the changes, leave a plain comment or to postpone the review locally. .Sh OPTIONS .Bl -tag -width indent .It Fl o , -owner Ar owner List PRs in the repository of the given owner. This option can only be used in combination with .Fl r . .It Fl r , -repo Ar repo List PRs in the given repository. This option can only be used in combination with .Fl o . .It Fl i , -id Ar PR Review pull request .Ar PR . .El .Sh EXAMPLES Review PR #69 in the current inferred forge and remote: .Bd -literal -offset indent $ gcli pulls -i 69 review .Ed .Pp Review PR #12 in curl/curl on Github: .Bd -literal -offset indent $ gcli -t github pulls -o curl -r curl -i 12 review .Ed .Sh SEE ALSO .Xr git 1 , .Xr git-merge 1 , .Xr git-branch 1 , .Xr gcli 1 , .Xr patch 1 .Sh AUTHORS .An Nico Sonack aka. herrhotzenplotz Aq Mt nsonack@herrhotzenplotz.de and contributors. .Sh BUGS At the moment the diff cache directory is not created automagically. You have to manually create it in case gcli complains. .Pp This feature is not implemented (but in active development) on Gitea. .Pp Please report bugs at @PACKAGE_URL@, via E-Mail to @PACKAGE_BUGREPORT@ or on Github. gcli-2.9.1/docs/gcli-pulls.1.in000066400000000000000000000212521507017207500161410ustar00rootroot00000000000000.Dd @PACKAGE_DATE@ .Dt GCLI-PULLS 1 .Os @PACKAGE_STRING@ .Sh NAME .Nm gcli\ pulls .Nd Manage Pull Requests on Git Forges .Sh SYNOPSIS .Nm .Op Fl a .Op Fl A Ar author .Op Fl L Ar label .Op Fl M Ar milestone .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 .Ar actions... .Nm .Cm create .Op Fl o Ar owner Fl r Ar repo .Op Fl t Ar branch .Op Fl f Ar owner:branch .Op Fl y .Op Fl R Ar reviewer .Op Fl T Ar template .Op Ar "PR title..." .Sh DESCRIPTION Use .Nm to list, create, edit or delete Pull Requests (PRs) in repositories on various .Xr git 1 forges such as GitLab, Gitea or GitHub. Without any action specified, .Nm will list open PRs in the given or auto-detected repository. .Pp For the .Nm .Cm review subcommand please refer to .Xr gcli-pulls-review 1 . .Sh OPTIONS .Bl -tag -width indent .It Fl s , -sorted Reverse the output such that most recent items appear at the bottom. .It Fl o , -owner Ar owner List PRs in the repository of the given owner. This option can only be used in combination with .Fl r . .It Fl r , -repo Ar repo List PRs in the given repository. This option can only be used in combination with .Fl o . .It Fl A , -author Ar author Filter pull requests by the given author. .Pp Note that the implementation is somewhat limited on GitHub and Gitea since the respective API does not allow off-loading the filtering to its side. Due to this fact using this option may take an increased amount of time because .Nm needs to iterate all the fetched data and filter out the requested information. .It Fl L , -label Ar label Filter pull requests by the given label. See the notes about the .Fl A option above - the same reasoning applies to this option. .It Fl M , -milestone Ar milestone Filter pull requests by the given milestone. See the notes about the .Fl A option above - the same reasoning applies to this option. .It Fl a List all PRs, including closed and merged ones. Cannot be combined with actions. This does not affect the .Fl n option. Note that this flag has a different meaning in the .Cm create subcommand. See .Sx SUBCOMMANDS for more information. .It Fl n , -count Ar n Fetch at least .Ar n pull requests. Default: 30. If .Ar n is set to -1 this will fetch all pull requests. Note that on large repositories fetching all pull requests can take a considerable amount of time and may result in rate limiting by the respective API. .It Fl i , -id Ar PR execute the given .Ar actions on the specified .Ar PR . .El .Pp .Sh SUBCOMMANDS .Bl -tag -width create .It Cm create Create a new PR in the given or auto-detected repository. The editor will come up and ask you to enter the PR message. .Pp If the pull request submission has failed, gcli will store the entered message in a file called .Pa gcli_message . Whenever this file exists in the current working directory, gcli will ask you whether you wish to recall its contents for the pull request 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 Specify the owner of the repository where the PR is to be created. .It Fl r , -repo Ar repository Specify the name of the repository where the PR is to be created. .It Fl t , -to Ar branch The target (base) branch of the PR. This is the branch the commits are to be merged into. You may omit this flag if you have set pr.base in your .gcli config file. .It Fl f , -from Ar owner:branch The source (head) branch of the PR. This is the branch that contains the commits that are to be merged into the target repository. You may omit this flag and gcli will try to infer this information. .It Fl y , -yes 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 Fl T , -template Ar template Use the provided .Ar template as a template for the PR message. The template file is going to be loaded into the editor to let you edit it further. .It Ar "PR Title..." The title of the Pull Request or Merge Request. .El .It Cm review See .Xr gcli-pulls-review 1 . .El .Sh ACTIONS .Ar actions... may be one or more of the following: .Bl -tag -width comments .It Cm all Get all the relevant information about a PR. The following actions are implied: .Cm status , .Cm op , .Cm commits and .Cm ci . .It Cm checkout Do a git checkout of the head branch associated with this pull request. This requires that .Xr git 1 is available in the .Ev PATH and that the current working directory resides within a clone of the target repository. .It Cm commits Print the list of commits associated with the Pull Requests. .It Cm comments Print a list of comments under the PR. .It Cm status Print metadata of the commit such as the ID, head and base branch etc. .It Cm op Print the original post of the Pull Request. .It Cm ci Print a list of checks that ran on the PR (GitLab Pipelines and GitHub CI). .It Cm diff Print a diff of the changes attached to the PR. This can be piped into .Xr patch 1 or .Xr git-apply 1 . .It Cm close Close the PR. .It Cm reopen Reopen a closed PR. .It Cm merge Op Ar options Merge the PR. The source branch is deleted by default unless you set the .Dq pr.inhibit-delete-source-branch option to yes in your .Pa .gcli file. You may supply the following options: .Bl -tag -width indent .It Fl -squash , s Squash the commits before merging. .It Fl -inhibit-delete , D Delete the source branch after merging. .El .It Cm milestone Ar milestone-id Assign the pull request to the given .Ar milestone-id . .It Cm milestone Fl d Clear a set milestone on the pull request. .It Cm notes Alias for the .Cm comments action that prints a list of comments associated with the PR. .It Cm labels Op Ar options The following options can be specified more than once: .Bl -tag -width indent .It add Ar label Add the given label to the pull request. .It remove Ar label Remove the given label from the pull request. .El .It Cm title Ar new-title Change the title of the pull request to .Ar new-title . .It Cm review Review the pull request. See .Xr gcli-pulls-review 1 for more details on this action. .It Cm assign Ar user Assign the pull request to the given .Ar user . .It Cm open Open the PR in a web browser using .Xr xdg-open 1 . .It Cm reviews List reviews of this pull request. .It Cm discussions Print the review discussions in a threaded form. This is currently only implemented for GitHub. .It Cm approve Give your approval on the pull request. .It Cm unapprove Revoke your approval on the pull request. .El .Sh EXAMPLES Print a list of open PRs in the current project: .Bd -literal -offset indent $ 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 The base will be what pr.base in .gcli is set to. .Bd -literal -offset indent $ gcli pulls create "summary here" .Ed .Pp Print both a summary and comments of PR 11 in herrhotzenplotz/gcli: .Bd -literal -offset indent $ gcli pulls -o herrhotzenplotz -r gcli -i 11 all comments .Ed .Pp Merge PR 42 in the upstream repository: .Bd -literal -offset indent $ gcli pulls -i 42 merge .Ed .Pp Note that you could also pull the PR head and merge it manually into the base branch. Assuming trunk is the base branch: .Bd -literal -offset indent $ git fetch upstream pull/42/head:42-review $ git checkout 42-review $ $ git checkout trunk $ git merge --no-ff 42-review .Ed .Pp List pull requests that have the .Dq bug label: .Bd -literal -offset indent $ gcli pulls -L bug .Ed .Pp List pull requests that are associated with the milestone .Dq version420 : .Bd -literal -offset indent $ gcli pulls -M version420 .Ed .Pp Change the title of pull request #42 on GitHub to .Dq "This is the new title" : .Bd -literal -offset indent $ gcli -t github pulls -i 42 title "This is the new title" .Ed .Pp Same command as above, but with abbreviated pulls subcommand: .Bd -literal -offset indent $ gcli -t github pu -i 42 title "This is the new title" .Ed .Sh SEE ALSO .Xr git 1 , .Xr git-merge 1 , .Xr git-branch 1 , .Xr gcli 1 , .Xr patch 1 .Sh AUTHORS .An Nico Sonack aka. herrhotzenplotz Aq Mt nsonack@herrhotzenplotz.de and contributors. .Sh BUGS Please report bugs via E-Mail to .Mt @PACKAGE_BUGREPORT@ . .Pp Alternatively you can report them on any of the forges linked at .Lk @PACKAGE_URL@ . However, the preferred and quickest method is to use the mailing list. gcli-2.9.1/docs/gcli-releases.1.in000066400000000000000000000125051507017207500166060ustar00rootroot00000000000000.Dd @PACKAGE_DATE@ .Dt GCLI-RELEASES 1 .Os @PACKAGE_STRING@ .Sh NAME .Nm gcli\ releases .Nd manage releases on git forges .Sh SYNOPSIS .Nm .Op Fl sl .Op Fl n Ar n .Op Fl o Ar owner Fl r Ar repo .Nm .Cm create .Fl t Ar tagname .Op Fl n Ar name .Op Fl c Ar commitish .Op Fl a Pa asset .Op Fl o Ar owner Fl r Ar repo .Op Fl d .Op Fl p .Op Fl T Pa template-file.md .Nm .Cm delete .Op Fl o Ar owner Fl r Ar repo .Op Fl y .Ar release-id .Sh DESCRIPTION Use .Nm to list, create or delete releases for repositories on .Xr git 1 forges such as GitLab, Gitea or GitHub. Without a subcommand specified, .Nm will list releases in the given or auto-detected repository. If you are the owner of that repo, you will also see draft releases. You will not see those if you are not the owner of that particular repository. .Sh OPTIONS .Bl -tag -width indent .It Fl s , -sorted Reverse the output such that most recent items appear at the bottom. .It Fl l , -long Print a long list instead of a short table. .It Fl o , -owner Ar owner List releases in the repo of the given owner. This option can only be used in combination with .Fl r . .It Fl r , -repo Ar repo List releases in the given repo. This option can only be used in combination with .Fl o . .It Fl n , -count Ar n Fetch at least .Ar n releases. Setting .Ar n to -1 will fetch all releases. Default: 30. Note that on repositories with many releases fetching all releases can take a considerable amount of time and may result in rate limiting by the GitHub API. .El .Sh SUBCOMMANDS .Bl -tag -width indent .It Cm create Create a new release on the given or auto-detected repository. The editor will come up and ask you to enter a message for the release. .Pp The following flags can be specified: .Pp .Bl -tag -width indent .It Fl t , -tag Ar tagname Specify a tag to be used or to be created for the release. This option is mandatory. See .Fl c for how to specify from what the tag should be created. .It Fl n , -name Ar name Name of the release. .It Fl c , -commitish Ar commitish When a new tag is to be created, this specifies what the tag is based on. It can be either a branch or a commit hash. Unused if the tag already exists. Otherwise this defaults to the default branch from .Xr git 1 . .It Fl o , -owner Ar owner Operate on the repository of the specified owner. This option can only be used in combination with .Fl r . Use this if you want to e.g. create the release in an organisation and not your own account. .It Fl r , -repo Ar repo Create the release in the given repository. This option can only be used in combination with .Fl o . .It Fl a , -asset Pa asset Attach the given asset to the release. It will be uploaded to GitHub and be made available for download. You can specify this option multiple times to attach more than one asset to the release. .It Fl y , -yes Do not ask for confirmation before creating the release. Assume yes. .It Fl d , -draft Mark this release as a draft. .It Fl p , -prerelease Mark this release as a prerelease. .It Fl T , -template Pa template-file.md Use the provided .Pa template-file.md as a template for the release message. Unless .Fl y has been specified, an editor will be opened to edit a copy of this file. Using this option together with .Fl y allows for completely automated creation of releases. .El .It Cm delete Delete a release. .Pp The following options can be specified: .Bl -tag -width indent .It Fl r , -repo Ar repo Delete the release in the given repository. This option can only be used in combination with .Fl o . .It Fl o , -owner Ar owner Delete the release in the repository of the given owner. This option can only be used in combination with .Fl r . Use this if you want to delete a release in a given organisation and not your own account. .It Fl y , -yes Do not ask for confirmation before deleting the repository. Assume yes. .El .El .Sh EXAMPLES Delete release with ID 54656866 in herrhotzenplotz/gcli-playground without asking for confirmation: .Pp .Bd -literal -offset indent $ gcli releases delete --owner herrhotzenplotz \\ --repo gcli-playground --yes 54656866 .Ed .Pp Create a new release named Foobar in herrhotzenplotz/gcli-playground. Create a new tag called banana based on the commit with the hash 0fed3c9 and upload .Pa foobar.tar.xz , barfoo.tar.gz and .Pa CHANGELOG as assets to the release. .Pp .Bd -literal -offset indent $ gcli releases create --owner herrhotzenplotz \\ --repo gcli-playground --tag banana --name Foobar \\ --commitish 0fed3c9 --asset foobar.tar.xz \\ --asset barfoo.tar.gz --asset CHANGELOG .Ed .Pp Create a release called .Dq v2.8.0 from a pre-existing tag. Read the release notes from the file .Pa relnotes.md and do not ask for confirmation. .Pp .Bd -literal -offset indent $ gcli releases create --tag v2.8.0 --name 'Version 2.8.0' \\ --commitish v2.8.0 --template relnotes.md --yes .Ed .Sh SEE ALSO .Xr git 1 , .Xr gcli 1 .Sh AUTHORS .An Nico Sonack aka. herrhotzenplotz Aq Mt nsonack@herrhotzenplotz.de and contributors. .Sh BUGS Currently uploading release assets to GitLab doesn't work. Prereleases and draft releases are unsupported by GitLab. Using those flags in a GitLab forge type remote will produce warnings but still create the release. .Pp Please report bugs via E-Mail to .Mt @PACKAGE_BUGREPORT@ . .Pp Alternatively you can report them on any of the forges linked at .Lk @PACKAGE_URL@ . However, the preferred and quickest method is to use the mailing list. gcli-2.9.1/docs/gcli-repos.1.in000066400000000000000000000056761507017207500161460ustar00rootroot00000000000000.Dd @PACKAGE_DATE@ .Dt GCLI-REPOS 1 .Os @PACKAGE_STRING@ .Sh NAME .Nm gcli\ repos .Nd Manage remote repositories on various git forges .Sh SYNOPSIS .Nm .Op Fl s .Op Fl n Ar n .Op Fl o Ar owner .Nm .Cm create .Fl r Ar name .Op Fl d Ar description .Op Fl p .Nm .Op Fl o Ar owner Fl r Ar repo .Ar actions... .Sh DESCRIPTION .Nm can be used to list or manage your own or an organisation's repositories on .Xr git 1 forges such as GitHub, Gitea and GitLab. With no actions given, .Nm will list repositories, either of the through .Fl o specified owner or, if omitted, your own. Otherwise the given actions are executed on the specified or auto-detected repository. See .Sx ACTIONS . .Sh OPTIONS .Bl -tag -width indent .It Fl s , -sorted Reverse the output such that most recent items appear at the bottom. .It Fl o , -owner Ar owner Operate on the repository of the given owner. This option can only be used in combination with .Fl r . .It Fl r , -repo Ar repo Operate on the given repository. This option can only be used in combination with .Fl o unless you are creating a repository. .It Fl y , -yes Do not ask for confirmation. Assume yes. Applies only to the .Cm delete action. .It Fl n , -count Ar n Fetch at least .Ar n repositories. Setting .Ar n to -1 will fetch all repositories. Default: 30. Note that on owners with many repositories fetching all of them can take a considerable amount of time and may result in rate limiting by the GitHub/GitLab API. .It Fl d , -description Ar description Set the description of a repo to be created. .It Fl p , -private Create a private repo. .El .Sh ACTIONS .Ar actions... may be one or more of the following: .Bl -tag -width "set-visibility level" .It Cm delete Op Fl y Delete the repository. You will be asked for confirmation unless you set .Fl y . .It Cm set-visibility Ar level Change the visibility level of the repository. .Ar level may be one of: .Bl -tag -width "private" .It private Make the repository private. .It public Make the repository public. .El .El .Sh EXAMPLES List your own repos: .Bd -literal -offset indent $ gcli repos .Ed .Pp List neutaaaaan's repositories: .Bd -literal -offset indent $ gcli repos -o neutaaaaan .Ed .Pp Delete vim/vim without confirmation: .Bd -literal -offset indent $ gcli repos -o vim -r vim -y delete .Ed .Pp Create a repository called emacs with a description and make it public: .Bd -literal -offset indent $ gcli repos create -r emacs -d "welcome to the holy church of emacs." .Ed .Sh SEE ALSO .Xr git 1 , .Xr gcli 1 , .Xr emacs 1 .Sh AUTHORS .An Nico Sonack aka. herrhotzenplotz Aq Mt nsonack@herrhotzenplotz.de and contributors. .Sh BUGS Currently it is only possible to create repositories for authenticated users thus it is impossible to create a repository in another organisations. .Pp Please report bugs via E-Mail to .Mt @PACKAGE_BUGREPORT@ . .Pp Alternatively you can report them on any of the forges linked at .Lk @PACKAGE_URL@ . However, the preferred and quickest method is to use the mailing list. gcli-2.9.1/docs/gcli-snippets.1.in000066400000000000000000000035601507017207500166510ustar00rootroot00000000000000.Dd @PACKAGE_DATE@ .Dt GCLI-SNIPPETS 1 .Os @PACKAGE_STRING@ .Sh NAME .Nm gcli\ snippets .Nd manage GitLab snippets .Sh SYNOPSIS .Nm .Op Fl l .Op Fl s .Op Fl n Ar n .Nm .Cm delete .Ar snippet-id .Nm .Cm get .Ar snippet-id .Sh DESCRIPTION Use .Nm to list, create, download or delete GitLab snippets. Without a subcommand specified, .Nm will list all of your own snippets. .Sh OPTIONS .Bl -tag -width indent .It Fl l , -long Print a long list instead of a short table. .It Fl s , -sorted Reverse the output such that most recent items appear at the bottom. .It Fl n , -count Ar n Fetch at least .Ar n snippets. Setting .Ar n to -1 will fetch all snippets. Default: 30. Note that on users with many snippets fetching all snippets can take a considerable amount of time and may result in rate limiting by the GitLab API. .El .Sh SUBCOMMANDS .Bl -tag -width indent .It Cm delete Delete a snippet. .It Cm get Fetch the raw contents of the snippet. .El .Sh EXAMPLES List all of your snippets: .Bd -literal -offset indent $ gcli -t gitlab snippets .Ed .Pp Delete snippet with id 69420: .Bd -literal -offset indent $ gcli -t gitlab snippets delete 69420 .Ed .Pp Print snippet with id 69420 into your pager: .Bd -literal -offset indent $ gcli -t gitlab snippets get 69420 | $PAGER .Ed .Sh SEE ALSO .Xr git 1 , .Xr gcli 1 .Sh AUTHORS .An Nico Sonack aka. herrhotzenplotz Aq Mt nsonack@herrhotzenplotz.de and contributors. .Sh BUGS .Bl -dash .It This subcommand only works on GitLab. It is not implemented for GitHub, as GitHub Gists work differently. .It Creating snippets is currently unimplemented. .It There is no .Fl y flag to ask the user whether he is sure about deleting a snippet. .El .Pp Please report bugs via E-Mail to .Mt @PACKAGE_BUGREPORT@ . .Pp Alternatively you can report them on any of the forges linked at .Lk @PACKAGE_URL@ . However, the preferred and quickest method is to use the mailing list. gcli-2.9.1/docs/gcli-status.1.in000066400000000000000000000017021507017207500163230ustar00rootroot00000000000000.Dd @PACKAGE_DATE@ .Dt GCLI-STATUS 1 .Os @PACKAGE_STRING@ .Sh NAME .Nm gcli\ status .Nd Print a list of notifications and/or TODOs .Sh SYNOPSIS .Nm .Op Fl n Ar number-of-items .Sh DESCRIPTION .Nm prints a list of TODOs and notifications on the given account. .Sh OPTIONS .Bl -tag -width indent .It Fl n Ar number-of-items Fetch at most .Ar number-of-items items and print them. If given a negative number, all notifications are fetched. The default is 30. .El .Sh EXAMPLES Print a TODO list for my-account: .Bd -literal -offset indent $ gcli -a my-account status .Ed .Pp .Sh SEE ALSO .Xr gcli 1 , .Xr gcli-issues 1 , .Xr gcli-pulls 1 .Sh AUTHORS .An Nico Sonack aka. herrhotzenplotz Aq Mt nsonack@herrhotzenplotz.de and contributors. .Sh BUGS Please report bugs via E-Mail to .Mt @PACKAGE_BUGREPORT@ . .Pp Alternatively you can report them on any of the forges linked at .Lk @PACKAGE_URL@ . However, the preferred and quickest method is to use the mailing list. gcli-2.9.1/docs/gcli-tutorial.1.in000066400000000000000000000522721507017207500166530ustar00rootroot00000000000000.\" -*- mode: troff; coding: utf-8 -*- Tutorial of GCLI - Generated Offline Version of https://herrhotzenplotz.de/gcli/tutorial .TH "gcli-tutorial" "1" "04-Oct-2025" .SH Installing GCLI .SS Through package manager .LP If you\(cqre on FreeBSD you can just install gcli by running the following command: .LP .EX # pkg install gcli .EE .PP On NetBSD you can run: .LP .EX # pkgin install gcli .EE .PP On Ubuntu, Debian, Devuan and the like you can run: .LP .EX # apt install gcli .EE .PP On ArchLinux you can either use the AUR manually or use your favourite AUR helper: .LP .EX # yay -S gcli .EE .SS Compile the source code .LP Other operating systems may currently require manual compilation and installation. .SS Windows NT Notes .LP It is entirely possible to build gcli on Windows using .UR https://msys2.org MSYS2 .UE \c \&. Please follow their instructions on how to set up a development environment. .SS Generic build instructions .LP For this purpose go to .UR https://herrhotzenplotz.de/gcli/releases https://herrhotzenplotz.de/gcli/releases .UE and choose the latest release. Then download one of the tarballs. .PP For version 1.1.0 this would be: .PP .UR https://herrhotzenplotz.de/gcli/releases/gcli-1.1.0/gcli-1.1.0.tar.xz .UE .PP Now that you have a link, you can download it, extract it, compile the code and install it: .LP .EX $ mkdir \(ti/build $ cd \(ti/build $ curl -4LO https://herrhotzenplotz.de/gcli/releases/gcli-1.1.0/gcli-1.1.0.tar.xz % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 342k 100 342k 0 0 2739k 0 --:--:-- --:--:-- --:--:-- 2736k $ ls gcli-1.1.0.tar.xz $ .EE .PP Install the dependencies for building gcli: .PP e.g. on Debian systems: .LP .EX # apt install libcurl4-openssl-dev pkgconf build-essential .EE .PP or on MSYS2: .LP .EX $ pacman -S libcurl-devel pkgconf .EE .PP Extract the tarball: .LP .EX $ tar xf gcli-1.1.0.tar.xz $ cd gcli-1.1.0 .EE .PP Configure, build and install gcli: .LP .EX $ ./configure \&... $ make \&... $ make install .EE .PP Check that the shell finds gcli: .LP .EX $ which gcli /usr/local/bin/gcli $ $ gcli version gcli 1.1.0 (amd64-unknown-freebsd13.2) Using libcurl/8.1.2 OpenSSL/1.1.1t zlib/1.2.13 libpsl/0.21.2 (+libidn2/2.3.4) libssh2/1.11.0 nghttp2/1.53.0 Using vendored pdjson library Report bugs at https://gitlab.com/herrhotzenplotz/gcli/. Copyright 2021, 2022, 2023 Nico Sonack and contributors. $ .EE .SS Advanced Windows Environment Setup .LP In case you want to use the installed gcli from outside MSYS2 (e.g. in cmd.exe) you may wish to update the \f(CRPath\fR environment variable and add the \f(CRC:\emsys2\eusr\ebin\fR directory to it. Make sure you have the library paths set up correctly. .SH First steps .SS Listing issues .LP Let\(cqs start off by listing some issues - here for the curl project which is hosted on GitHub under \f(CRcurl/curl\fR. To list issues for it one would run: .LP .EX $ gcli -t github issues -o curl -r curl .EE .PP You will see the list of the 30 most recent open issue tickets. The command above does the following: .IP "\(bu" 3 invoke gcli .if n \ .sp -1 .if t \ .sp -0.25v .IP "\(bu" 3 as a global option we switch it into GitHub-Mode .if n \ .sp -1 .if t \ .sp -0.25v .IP "\(bu" 3 invoke the issues subcommand .if n \ .sp -1 .if t \ .sp -0.25v .IP "\(bu" 3 operate on the repository owner curl (\f(CR-o curl\fR) .if n \ .sp -1 .if t \ .sp -0.25v .IP "\(bu" 3 operate on the repository curl (\f(CR-r curl\fR) .LP Note that the \f(CR-t github\fR option goes before the issues subcommand because it is a global option for gcli that affects how all the following things like subcommands operate. .PP However, now I also want to see closed issues: .LP .EX $ gcli -t github issues -o curl -r curl -a .EE .PP The \f(CR-a\fR option will disregard the status of the issue. .PP Oh and the screen is a bit cluttered by all these tickets - let\(cqs only fetch the first 10 issues: .LP .EX $ gcli -t github issues -o curl -r curl -n10 .EE .SS Searching for issues .LP Before reporting a bug or looking for solutions to a problem that you found in a program you may want to search for issues. .PP Let\(cqs search for \f(CRavast\fR in \f(CRcurl/curl\fR: .LP .EX $ gcli -t github issues -o curl -r curl -a avast NUMBER NOTES STATE TITLE 11383 9 open Issue with FileZilla server (GnuTLS) and close_notify 10551 7 closed Unable to use curl 7.87 for SSL connections on Windows 10 Pro 8848 2 closed CURL SSL certificate problem when AVAST HTTPS scanning enabled $ .EE .PP As you can see searching for something is just a matter of appending keywords to the \f(CRissues\fR subcommand. You can specify any amount of search terms - they will all be used in the query. Again, \f(CR-a\fR ignores the status of the issue such that we also see closed tickets. .SS Examining issues .LP As of now we only produced lists of issues. However, we may also want to look at the details of an issue such as: .IP "\(bu" 3 the original post .if n \ .sp -1 .if t \ .sp -0.25v .IP "\(bu" 3 labels .if n \ .sp -1 .if t \ .sp -0.25v .IP "\(bu" 3 comments .if n \ .sp -1 .if t \ .sp -0.25v .IP "\(bu" 3 assignees of the issue (that is someone who is working on the bug) .LP Let\(cqs get a good summary of issue \f(CR#11268\fR in the curl project: .LP .EX $ gcli -t github issues -o curl -r curl -i 11268 all .EE .PP As you can see most of the options are the same, however now we tell gcli with the \f(CR-i 11268\fR option that we want to work with a single issue. Then we tell gcli what actions to perform on the issue. Another important action is \f(CRcomments\fR. Guess what it does: .LP .EX $ gcli -t github issues -o curl -r curl -i 11268 comments .EE .PP I know a person that likes to post long verbose traces. Let\(cqs search for an issue authored by them on the OpenSSL GitHub page: .LP .EX $ gcli -t github issues -o openssl -r openssl -A blastwave -a NUMBER STATE TITLE 20379 open test \(dq80-test_ssl_new.t\(dq fails on Solaris 10 SPARCv9 10547 open Strict C90 CFLAGS results in sha.h:91 ISO C90 does not support long long 8048 closed OPENSSL_strnlen SIGSEGV in o_str.c line 76 $ .EE .PP The \f(CR-A\fR option lets you filter for specific authors. .PP Let\(cqs look at the issue state of \f(CR#10547\fR: .LP .EX $ gcli -t github issues -o openssl -r openssl -i 10547 status NAME : 10547 TITLE : Strict C90 CFLAGS results in sha.h:91 ISO C90 does not support long long CREATED : 2019-12-01T04:35:23Z AUTHOR : blastwave STATE : open COMMENTS : 9 LOCKED : no LABELS : triaged: bug ASSIGNEES : none $ .EE .PP That\(cqs nine comments - let\(cqs read the original post and the comments in our favourite pager \f(CRless\fR: .LP .EX $ gcli -t github issues -o openssl -r openssl -i 10547 op comments | less .EE .PP As you can see gcli will accept multiple actions for an issue and executes them sequentially. .SH How to find documentation .LP When using gcli one may not always remember all the options and flags for every subcommand. gcli has lots of integrated help to guide you through its commands. .SS Subcommand help .LP You can list all available options for the issues subcommand by doing: .LP .EX $ gcli issues --help .EE .PP With your current knowledge you can also explore the \f(CRgcli pulls\fR subcommand. .SS General usage .LP Run the following command: .LP .EX $ gcli --help usage: gcli [options] subcommand OPTIONS: -a account Use the configured account instead of inferring it -r remote Infer account from the given git remote -t type Force the account type: - github (default: github.com) - gitlab (default: gitlab.com) - gitea (default: codeberg.org) -c Force colour and text formatting. -q Be quiet. (Not implemented yet) -v Be verbose. SUBCOMMANDS: ci Github CI status info comment Comment under issues and PRs config Configure forges forks Create, delete and list repository forks gists Create, fetch and list Github Gists issues Manage issues labels Manage issue and PR labels milestones Milestone handling pipelines Gitlab CI management pulls Create, view and manage PRs releases Manage releases of repositories repos Remote Repository management snippets Fetch and list Gitlab snippets status General user status and notifications api Fetch plain JSON info from an API (for debugging purposes) version Print version gcli 1.2.0 (amd64-unknown-freebsd13.2) Using libcurl/8.1.2 OpenSSL/1.1.1t zlib/1.2.13 libpsl/0.21.2 (+libidn2/2.3.4) libssh2/1.11.0 nghttp2/1.53.0 Using vendored pdjson library Report bugs at https://gitlab.com/herrhotzenplotz/gcli/. Copyright 2021, 2022, 2023 Nico Sonack and contributors. .EE .PP This gives you an overview over all the available subcommands. Each subcommand in turn allows you to get its usage by supplying the \f(CR--help\fR option to it. .SS Manual pages .LP Furthermore I recommend reading into the manual page \f(CRgcli-issues(1)\fR and \f(CRgcli-pulls(1)\fR: .LP .EX $ man gcli-issues $ man gcli-pulls .EE .SH Setting up gcli for use with an account .LP Creating issues on GitHub requires an account which we need to generate an authentication token for gcli. .PP If you want to test this with a different forge than GitHub look at \c .UR ./08-Other-forges.html the tutorial for other forges .UE \c \&. .PP Log into your GitHub account and click on your account icon in the top right corner. Then choose the \f(CRSettings\fR option. Scroll down and choose \f(CRDeveloper settings\fR on the bottom of the left column. Under \f(CRPersonal access tokens\fR choose \f(CRTokens (classic)\fR. .PP Click on \f(CRGenerate new token (classic)\fR. .PP Set a useful name such as \f(CRgcli\fR in the Note field, set the expiration to \f(CRNo expiration\fR and allow the following \f(CRScopes\fR: .IP "\(bu" 3 \f(CRrepo\fR .if n \ .sp -1 .if t \ .sp -0.25v .IP "\(bu" 3 \f(CRworkflow\fR .if n \ .sp -1 .if t \ .sp -0.25v .IP "\(bu" 3 \f(CRadmin:public_key\fR .if n \ .sp -1 .if t \ .sp -0.25v .IP "\(bu" 3 \f(CRgist\fR .LP Then create the token. It\(cqll be printed in green. Do not share it! .PP Now we need to tell gcli about this new token. To do this, create a configuration file for gcli - on Windows you need to do this from the MSYS2 Shell: .LP .EX $ mkdir -p ${HOME}/.config/gcli $ vi ${HOME}/.config/gcli/config .EE .PP Obviously, you can choose any other editor of your choice. Put the following into this file: .LP .EX defaults { editor=vi github-default-account=my-github-account } my-github-account { token= account= forge-type=github } .EE .PP Replace the \f(CR\fR with the previously generated token and the \f(CR\fR with your account name. .PP If you now run .LP .EX $ gcli -t github repos .EE .PP you should get a list of your repos. If not, check again that you did all the steps above correctly. .SH Creating an issue .LP \fBNote\fR: This assumes you have \c .UR ./04-Account-Setup.html configured gcli with an account .UE for GitHub already. .SS Preparation .LP For this case I have a playground repository that you may as well use for testing with gcli. It is available at \f(CRherrhotzenplotz/ghcli-playground\fR. .PP To see a list of issues, we can run: .LP .EX $ gcli -t github issues -o herrhotzenplotz -r ghcli-playground -a NUMBER NOTES STATE TITLE 13 0 open yet another issue 12 0 closed wat 11 0 closed blaaaaaaaaaaaaaaaaaaaah 10 0 closed \(dqthis is the quoted\(dq issue title? anyone?\(dq 9 0 closed test 8 0 closed foobar 7 0 closed foobar 5 0 closed test2 4 0 closed test $ .EE .SS Invoke gcli .LP Let\(cqs create a bug report where we complain about things not working: .LP .EX $ gcli -t github issues create -o herrhotzenplotz -r ghcli-playground \e \(dqBug: Doesn't work on my machine\(dq .EE .PP The message \(lqBug: doesn\(cqt work on my machine\(rq is the title of the issue. .SS Original Post .LP You will see the default editor come up and instruct you to type in a message. This message is the \(lqoriginal post\(rq or the body of the issue ticket that you\(cqre about to submit. You can use Markdown Syntax: .LP .EX I tried building this code on my machine but unfortunately it errors out with the following message: \(ga\(ga\(gaconsole $ make love make: don't know how to make love. Stop make: stopped in /tmp/wat $ \(ga\(ga\(ga What am I doing wrong? ! ISSUE TITLE : Bug: Doesn't work on my machine ! Enter issue description above. ! All lines starting with '!' will be discarded. .EE .SS Submit the issue .LP After you save and exit the editor gcli gives you a chance to check back and finally submit the issue. Type \(oqy\(cq and hit enter. .PP You can check back if the issue was created and also view details about it as you learned earlier. .SH Commenting .LP Discussions on GitHub and the like are done through comments. You can comment on issues and pull requests. .SS Reviewing a discussion .LP Say you were looking at an issue in curl/curl: .LP .EX $ gcli issues -o curl -r curl -i 11461 comments .EE .SS Create the comment .LP And now you wish to respond to this thread: .LP .EX $ gcli comment -o curl -r curl -i 11461 .EE .PP This will now open the editor and lets you type in your message. After saving and exiting gcli will ask you to confirm. Type \(oqy\(cq and hit enter: .LP .EX $ gcli -t github comment -o curl -r curl -i 11461 You will be commenting the following in curl/curl #11461: Is this okay? [yN] y $ .EE .SS Commenting on pull requests .LP When you want to comment under a pull request use the \f(CR-p\fR flag instead of the \f(CR-i\fR flag to indicate the PR number. .SH Creating a pull request .LP Creating a pull request with gcli is usually as simple as running .LP .EX $ gcli pulls create .EE .SS Preparation .LP Suppose you have a git repository forked and cloned: .LP .EX $ git clone git@github.com:contour-terminal/contour $ cd contour $ gcli forks create --into herrhotzenplotz .EE .PP Then you do some work on whatever feature you\(cqre planning to submit: .LP .EX $ git checkout -b my-amazing-feature $ git add -p $ git commit .EE .SS Push your changes .LP You then push your changes to your fork: .LP .EX $ git push origin my-amazing-feature .EE .SS Create the pull request .LP Now you can run the command to create the pull request: .LP .EX $ gcli pulls create From (owner:branch) [herrhotzenplotz:my-amazing-feature]: Owner [contour-terminal]: Repository [contour]: To Branch [master]: Title: Add new amazing feature Enable automerge? [yN]: .EE .PP Most of the defaults you should be able to simply accept. This assumes that you have the source branch checked out locally and remotes are configured appropriately. .PP Otherwise you can just change the defaults by entering the correct values. .SS Enter original post .LP After you entered all the meta data of the pull request gcli will drop you into your editor and lets you enter a message for the pull request. .SS Submit .LP After you saved and exit type \f(CRy\fR and hit enter to submit the pull request. .SH Other forges and bugtrackers .LP gcli is capable of not only interacting with Github. It also currently supports: .IP "\(bu" 3 GitLab .if n \ .sp -1 .if t \ .sp -0.25v .IP "\(bu" 3 Gitea .if n \ .sp -1 .if t \ .sp -0.25v .IP "\(bu" 3 Bugzilla .SS Bugzilla .SS Notes .LP Bugzilla is commonly used as a bug tracker in various large open-source projects such as FreeBSD, the Linux Kernel, Mozilla and Gentoo. .SS Searching .LP Suppose you want to search for bug reports containing \f(CRsparc\fR in the Gentoo Bugzilla. .PP In this case you need to configure an account that points at the correct URL in \f(CR$HOME/.config/gcli/config\fR by adding: .LP .EX gentoo { forge-type=bugzilla api-base=https://bugs.gentoo.org/ } .EE .PP Now you can search the Gentoo Bugs: .LP .EX $ gcli -a gentoo issues sparc NUMBER NOTES STATE TITLE 924443 0 UNCONFIRMED Add keyword \(tisparc for app-misc/fastfetch 924430 0 RESOLVED media-libs/assimp-5.3.1 fails tests on sparc 924215 0 CONFIRMED dev-libs/libbson dev-libs/mongo-c-driver: alpha arm ia64 mips ppc ppc64 s390 sparc keyword req 924191 0 CONFIRMED media-libs/exempi: unaligned access causes dev-python/python-xmp-toolkit-2.0.2 to fails tests on sparc (test_file_to_dict (test.test_core_unit.UtilsTestCase.test_file_to_dict) ... Bus error) 924180 0 CONFIRMED dev-python/psycopg-3.1.17[native-extensions] fails tests on sparc: tests/test_copy_async.py::test_read_rows[asyncio-names-1] Fatal Python error: Bus error 924031 0 IN_PROGRESS sys-apps/bfs: \(tiarm \(tiarm64 \(tippc \(tippc64 \(tisparc keywording 923968 0 CONFIRMED dev-python/pyarrow-15.0.0 fails to configure on sparc: CMake Error at cmake_modules/SetupCxxFlags.cmake:42 (message): Unknown system processor 921245 0 CONFIRMED media-video/rav1e-0.6.6 fails to compile on sparc: Assertion \(gaDT.dominates(RHead, LHead) && \(dqNo dominance between recurrences used by one SCEV?\(dq' failed. 920956 0 CONFIRMED dev-python/pygame-2.5.2: pygame.tests.font_test SIGBUS on sparc 920737 0 CONFIRMED sparc64-solaris Prefix no longer supported .EE .SS Issue details .LP Furthermore we can look at single issues: .LP .EX $ gcli -a gentoo issues -i 920737 all comments NUMBER : 920737 TITLE : sparc64-solaris Prefix no longer supported CREATED : 2023-12-26T19:20:58Z PRODUCT : Gentoo Linux COMPONENT : Profiles AUTHOR : Tom Williams STATE : CONFIRMED LABELS : none ASSIGNEES : prefix ORIGINAL POST Resurrecting a Prefix install on Solaris 11.4 SPARC. It was working rather well for me; after a hiatus I had hoped to use it again but my first emerge --sync has removed the profile needed to merge any updates or new packages. I note commit 8e006b67e06a19fae10c6059c7fc5ede88834601 in May 2023 removed the profile and keywording for prefixed installs. There is no associated comment. There doesn't seem to be a bug report in regards to the change (I'm quite sure almost nobody uses it, so probably fair enough) Any easy way to restore the profile for now? Eventually Solaris/SPARC and thus Prefix will be gone anyway, but useful for now. Thanks for your continued efforts. AUTHOR : sam DATE : 2023-12-26T19:21:33Z I think at the very least, when removing Prefix support in future, a 'deprecated' file should be added to the relevant profiles asking if anyone is using it to step forward. AUTHOR : grobian DATE : 2023-12-26T22:58:12Z Solaris 11.4 itself is a problem. I doubt you ever had it \(dqworking\(dq. AUTHOR : grobian DATE : 2023-12-26T22:59:57Z Linux sparc team is not relevant here $ .EE .SS GitLab .SS Configuring an account for use with a token .LP First you need to generate a token: .IP "1." 3 Click on your avatar in the top left corner .if n \ .sp -1 .if t \ .sp -0.25v .IP "2." 3 Choose Preferences in the popup menu .if n \ .sp -1 .if t \ .sp -0.25v .IP "3." 3 Select \f(CRAccess tokens\fR in the preference menu .if n \ .sp -1 .if t \ .sp -0.25v .IP "4." 3 Click the \f(CRAdd new token\fR button .if n \ .sp -1 .if t \ .sp -0.25v .IP "5." 3 Choose some reasonable values .RS .IP "\(bu" 3 The token name can be your hostname e.g. \f(CRgcli $(hostname)\fR .if n \ .sp -1 .if t \ .sp -0.25v .IP "\(bu" 3 Clear the expiration date. It will be defaulted to some high value by GitLab. .if n \ .sp -1 .if t \ .sp -0.25v .IP "\(bu" 3 Select the \f(CRapi\fR scope .RE .LP Now click \f(CRCreate personal access token\fR. Save this token - \fBdo not share it with anyone else\fR. .PP You can now update your gcli config in \f(CR$HOME/.config/gcli/config\fR: .LP .EX defaults { gitlab-default-account=gitlab-com ... } gitlab-com { account= token= forge-type=gitlab } .EE .PP After that you should be able to run the following command: .LP .EX $ gcli -t gitlab issues -o herrhotzenplotz -r gcli .EE .PP If this process errors out check the above steps. If you believe this is a bug, please report it at our issue tracker! .SS Gitea .LP The steps here are roughly the same as with GitLab. .PP To generate a token: .IP "1." 3 Click your avatar in the top-right corner .if n \ .sp -1 .if t \ .sp -0.25v .IP "2." 3 Choose \f(CRSettings\fR in the popup menu .if n \ .sp -1 .if t \ .sp -0.25v .IP "3." 3 Select \f(CRApplications\fR in the menu on the left .if n \ .sp -1 .if t \ .sp -0.25v .IP "4." 3 Under \f(CRGenerate new token\fR enter a reasonable token name .if n \ .sp -1 .if t \ .sp -0.25v .IP "5." 3 Click the \f(CRGenerate token\fR button .if n \ .sp -1 .if t \ .sp -0.25v .IP "6." 3 Save the token - \fBdo not share it with anyone else\fR. .LP You can now update your gcli config file in \f(CR$HOME/.config/gcli/config\fR: .LP .EX defaults { gitea-default-account=codeberg-org ... } codeberg-org { account= token= forge-type=gitea api-base=https://codeberg.org/api/v1 } .EE .PP The example here uses Codeberg. Update these fields as needed for your own use case. gcli-2.9.1/docs/gcli.1.in000066400000000000000000000220501507017207500150010ustar00rootroot00000000000000.Dd @PACKAGE_DATE@ .Dt GCLI 1 .Os @PACKAGE_STRING@ .Sh NAME .Nm gcli .Nd interact with git forges without using a web-browser .Sh SYNOPSIS .Pp .Nm .Op overrides .Cm subcommand .Op options .Sh DESCRIPTION .Nm can be used to interact with .Xr git 1 forges like GitHub, GitLab and Gitea from the command line in order to make many tasks like managing issues and pull requests easier. .Pp If you're looking for a guided tutorial either go to .Lk https://herrhotzenplotz.de/gcli/tutorial or read the offline version .Xr gcli-tutorial 1 . .Pp Calls to .Nm usually consist of either only the subcommand to list requested data or the subcommand plus further subcommands or options to perform various tasks. Some commands may also take an item to operate on and accept multiple actions that will be performed on the item (e.g. PRs may be summarised, comments fetched and a diff printed all in one command). .Pp The default behaviour of .Nm 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. .Bl -tag -width milestones .It Cm issues Issues in repositories. See .Xr gcli-issues 1 . .It Cm pulls Pull Requests on repositories. See .Xr gcli-pulls 1 . .It Cm labels Manage labels for issues and pull/merge requests on repositories. See .Xr gcli-labels 1 . .It Cm forks Forking repositories. See .Xr gcli-forks 1 . .It Cm gists GitHub Gists are like paste bins to where you can dump code snippets etc. See .Xr gcli-gists 1 . .It Cm snippets Support for GitLab snippets. See .Xr gcli-snippets 1 . .It Cm repos Manage your own or other repositories. See .Xr gcli-repos 1 . .It Cm comment Submit comments under issues and PRs. See .Xr gcli-comment 1 . .It Cm status Print a list of TODOs and/or notifications. See .Xr gcli-status 1 . .It Cm pipelines Inspect and manage GitLab Pipelines. See .Xr gcli-pipelines 1 . .It Cm releases Create and manage releases. See .Xr gcli-releases 1 . .It Cm milestones List and manage milestones. See .Xr gcli-milestones 1 . .It Cm config Change user settings for the forge. Allows you to e.g. upload or delete ssh keys. See .Xr gcli-config 1 . .It Cm api Perform direct queries to the API and dump the JSON response to stdout. This is primarily intended to assist debugging gcli. See .Xr gcli-api 1 . .It Cm version Print version and exit. .El .Sh OPTIONS .Nm overrides are: .Bl -tag -width indent .It Fl a , -account Ar override-account Manually override the default account. .Ar override-account must name a config section for an account in the global config file. See .Sx FILES . .It Fl r , -remote Ar override-remote Use .Ar override-remote as the remote when trying to infer repository data. .It Fl c , -colours Ignore .Ev NO_COLOR as well as whether the output is not tty and print ANSI escape sequences for changing text formatting. Default is to output colours unless stdout is not a tty. See .Xr isatty 3 . This is useful in combination with modern pagers such as .Xr less 1 . .It Fl -no-spinner Disable the animated spinner that is displayed when .Nm is making network requests. This is useful inside dumb terminals or in editors such as acme. See also .Ev GCLI_NOSPINNER in .Sx ENVIRONMENT . .It Fl -no-markdown Disable automatic markdown rendering. This is useful in case improperly formatted markdown is causing unreadable output or if messages aren't markdown at all. See also .Ev GCLI_RENDER_MARKDOWN in .Sx ENVIRONMENT . .It Fl q , -quiet Suppresses most output of .Nm . .It Fl v , -verbose Be very verbose. This means that warnings about missing config files and request steps are printed to stderr. .It Fl t , -type Ar forge-type Forcefully override the forge type. Set .Ar forge-type to .Sq github , .Sq gitlab .Sq gitea , or .Sq bugzilla to connect to the corresponding services. .El .Pp Common options across almost all of the subcommands are: .Bl -tag -width indent .It Fl s , -sorted Reverse the output such that most recent items appear at the bottom. .It Fl n , -count Ar n Fetch multiple items of data. The default is usually 30 items, but this parameter allows to fetch more than that. Setting .Ar n to -1 will result in all pages being queried and all items being read. However, be careful with that, since if there is a lot of data to be fetched, it may result in rate limiting by the GitHub API, aside from the fact that it may also take a considerable amount of time to process. .It Fl a , -all Fetch all data, including closed issues and closed/merged PRs. .It Fl y , -yes Do not ask for confirmation when performing destructive operations or performing submissions. Always assume yes. .It Fl o , -owner Ar owner Operate on the given owner (organisation or user). Can only be used in combination with .Fl r . .It Fl r , -repo Ar repo Operate on the given repository. Can only be used in combination with .Fl o . .It Fl i Ar id Operate on the given numeric identifier. .El .Pp Other options specific to the context are documented in the respective man pages. .Sh ENVIRONMENT .Bl -tag -width XDG_CONFIG_HOME .It Ev GIT_EDITOR, VISUAL, EDITOR If the gcli config file does not name an editor, .Nm will search the named environment variables in this order and use the first that is set as the editor. .It Ev XDG_CONFIG_HOME There should be a subdirectory called gcli in the directory this environment variable points to where .Nm will go looking for its configuration file. See .Sx FILES . .It Ev GCLI_ACCOUNT Specifies an account name that should be used instead of an inferred one. The value of .Ev GCLI_ACCOUNT can be overridden again by using .Fl a Ar account-name . This is helpful in cases where you have multiple accounts of the same forge-type configured and you don't want to use the default. .It Ev NO_COLOR If set to .Sq 1 , .Sq y or .Sq yes (capitalisation ignored) this will suppress output of ANSI colour escape sequences. See .Sx OPTIONS (--colours). .It Ev GCLI_NOSPINNER If set to .Sq 1 , .Sq y or .Sq yes (capitalisation ignored) this will disable the animated spinner when gcli is making requests. See .Sx OPTIONS (--disable-spinner) .It Ev GCLI_RENDER_MARKDOWN If this is set to .Sq 0 , .Sq n or .Sq no (capitalisation ignored) this will disable markdown rendering in gcli's output. See .Sx OPTIONS (--no-markdown). .El .Sh FILES .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 . .Pp .It Pa .gcli A repo-specific config file intended to be committed into the repo so that users don't have to manually specify all the options like .Fl -in , .Fl -from , .Fl -base etc. when creating pull requests. See .Xr gcli 5 for details about this file. .Pp .El .Sh EXAMPLES List recently opened issues in the current upstream repository: .Bd -literal -offset indent $ gcli issues .Ed .Pp Merge upstream PR #22: .Bd -literal -offset indent $ gcli pulls -p 22 merge .Ed .Pp Get a summary and comments of upstream PR #22: .Bd -literal -offset indent $ gcli pulls -p 22 summary comments .Ed .Pp Establish a connection to GitHub and print the last 10 pull requests in contour-terminal/contour regardless of their state. .Bd -literal -offset indent $ gcli -t github pulls -o contour-terminal -r contour -a -n10 .Ed .Pp This can be useful if neither your config file nor the directory you're working from contain the relevant forge and repository information. .Sh SEE ALSO .Xr git 1 , .Xr gcli-issues 1 , .Xr gcli-pulls 1 , .Xr gcli-labels 1 , .Xr gcli-comment 1 , .Xr gcli-review 1 , .Xr gcli-forks 1 , .Xr gcli-repos 1 , .Xr gcli-gists 1 , .Xr gcli-releases 1 , .Xr gcli-comment 1 .Xr gcli-pipelines 1 .Xr gcli-config 1 .Sh HISTORY The idea for .Nm appeared during a long rant on IRC where the issue with the official tool written by GitHub became clear to be the manual dialling and DNS resolving by the Go runtime, circumventing almost the entirety of the IP and DNS services of the operating system and leaking sensitive information when using Tor. .Pp Implementation started in October 2021 with the goal of having a decent, sufficiently portable and secure version of a cli utility to interact with the GitHub world without using the inconvenient web interface. .Pp Later, support for GitLab and Gitea (Codeberg) were added. .Sh AUTHORS .An Nico Sonack aka. herrhotzenplotz Aq Mt nsonack@herrhotzenplotz.de and contributors. .Sh CAVEATS Not all features that are available from the web version are available in .Nm . However, it is a non-goal of the project to provide all this functionality. .Sh BUGS There is an undocumented .Cm ci subcommand available for GitHub CI services. The subcommand is undocumented as it is not well tested and likely subject to changes. .Pp Please report bugs via E-Mail to .Mt @PACKAGE_BUGREPORT@ . .Pp Alternatively you can report them on any of the forges linked at .Lk @PACKAGE_URL@ . However, the preferred and quickest method is to use the mailing list. .Pp You may also report an issue like so: .Bd -literal -offset indent $ gcli -a some-gitlab-account \\ issues create \\ -o herrhotzenplotz -r gcli \\ "BUG : ..." .Ed gcli-2.9.1/docs/gcli.5.in000066400000000000000000000133471507017207500150160ustar00rootroot00000000000000.Dd @PACKAGE_DATE@ .Dt GCLI 5 .Os @PACKAGE_STRING@ .Sh NAME .Nm gcli .Nd gcli configuration file formats .Sh DESCRIPTION .Nm gcli has two different configuration files. A user configuration file that contains default values for .Nm gcli and a repository-local configuration that contains sensible default values for a given repository. The latter is meant to be checked 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_HOME}/gcli/config . On most systems this equal to .Pa ${HOME}/.config/gcli/config . .Pp The user configuration file contains definitions for accounts as well as sensible default values for things like an editor. .Pp The file is structured in sections, each section has a name and consists of a collection of key-value pairs. E.g.: .Pp .Bd -literal -offset indent section-name { key1 = value 1 key2 = value 2 } .Ed .Pp There must be a section named .Dq defaults which may contain the following keys: .Bl -tag .It editor Path to a default editor. This might be overridden by the environment variable .Ev EDITOR . .It pager Name of the default pager when interactively displaying large output. May be overridden by the environment variable .Ev PAGER . If none of the above are specified, .Xr less 1 is used as a sane default pager. .It github-default-account Section name of a default GitHub account to use whenever the account is unspecified on the command line or in the environment. See .Ev GCLI_ACCOUNT in .Xr gcli 1 . .It gitlab-default-account Section name of a default GitLab account to use whenever the account is unspecified on the command line or in the environment. See .Ev GCLI_ACCOUNT in .Xr gcli 1 . .It gitea-default-account Section name of a default Gitea account to use whenever the account is unspecified on the command line or in the environment. See .Ev GCLI_ACCOUNT in .Xr gcli 1 . .It disable-spinner Disable the progress spinner in the ui. .Nm uses this when making requests. This can also be set, and overridden with the .Ev GCLI_NOSPINNER environment variable. .It url-open-program Name of a program in the .Ev PATH to open URLs with. Defaults to .Cm xdg-open which uses your default web browser. Note that the program name may not contain any arguments. If you need to pass any arguments to the program you must write a wrapper script. .El .Pp All other sections define accounts for forges. Each of these account definitions have the account name as their section name and may have one or more of the following keys defined: .Bl -tag -width forge-type .It forge-type The type of the forge. May be one of: .Bl -bullet -compact .It github .It gitlab .It gitea .It bugzilla .El .It api-base (optional) Used to override the API base URL of the forge. This is useful for self-hosted instances. Depending on the .Dq forge-type the default values are: .Bl -column forge-type "default value" .It Em forge-type Ta Em "default value" .It github Ta Lk "https://api.github.com" .It gitlab Ta Lk "https://gitlab.com/api/v4" .It gitea Ta Lk "https://codeberg.org/api/v1" .It bugzilla Ta Lk "https://bugs.freebsd.org/bugzilla" .El .It account (optional) The username used to authenticate at the API. .It token (optional) A generated application token to use with this account. The gcli tutorial at .Lk https://herrhotzenplotz.de/gcli/tutorial has some documentation on how to generate thes tokens. .El .Ss Repository Local Configuration File For repository-local configuration you can use a special configuration file. It contains definitions for gcli that are specific to the repository. .Pp The Repository-local configuration file is located in the root directory of the repository and should be named .Pa .gcli . .Pp It contains a list of key-value pairs. Allowed keys are: .Bl -tag -width pr.upstream .It pr.base Name of a branch that the changes should be merged into by default. Usually this is one of .Em master , .Em main or .Em trunk . .It pr.upstream Name of the upstream repository to submit the pull request to by default. This is a pair of the format .Dq owner/repository . .It pr.inhibit-delete-source-branch If defined and set to .Dq yes this will prevent the pull request source branch to get deleted when merging a pull request by default. .It forge-type When hosting on multiple forges this can be set to a type that will be used as a default when other overrides are unspecified. For possible values see the equivalent definition in .Sx "User Configuration File" . .El .Sh EXAMPLES .Ss User Configuration File An example for the user configuration file consisting of both a GitHub and a GitLab account: .Bd -literal defaults { editor=/path/to/ganoooo/emacs pager=less github-default-account=herrhotzenplotz-gh gitlab-default-account=herrhotzenplotz-gitlab } herrhotzenplotz-gh { account=herrhotzenplotz token=foobar api-base=https://api.github.com forge-type=github } herrhotzenplotz-gl { account=herrhotzenplotz token= api-base=https://gitlab.com/api/v4 forge-type=gitlab } .Ed .Pp Notice that this allows you to run gcli and force it to use a specific GitLab account. E.g.: .Bd -literal $ gcli -a herrhotzenplotz-gl issues -a .Ed .Pp .Ss Repository-Local Configuration file The .Pa .gcli file for the gcli project itself looks like this: .Bd -literal pr.upstream=herrhotzenplotz/gcli pr.base=trunk pr.inhibit-delete-source-branch=yes .Ed .Sh SEE ALSO .Xr git 1 , .Xr gcli 1 , .Xr less 1 , .Xr xdg-open 1 .Sh AUTHORS .An Nico Sonack aka. herrhotzenplotz Aq Mt nsonack@herrhotzenplotz.de and contributors. .Sh BUGS Please report bugs via E-Mail to .Mt @PACKAGE_BUGREPORT@ . .Pp Alternatively you can report them on any of the forges linked at .Lk @PACKAGE_URL@ . However, the preferred and quickest method is to use the mailing list. gcli-2.9.1/docs/pgen.org000066400000000000000000000151421507017207500150420ustar00rootroot00000000000000# -*- org-confirm-babel-evaluate: nil; -*- #+TITLE: PGEN #+SUBTITLE: JSON Parser Generator for C #+AUTHOR: Nico Sonack #+EMAIL: nsonack@herrhotzenplotz.de #+OPTIONS: toc:nil * Motivation GCLI needs to traverse and parse lots of JSON data because of the nature of the web APIs it communicates with. Initially most of this JSON traversal code was written by hand. It contained lots of boilerplate that could be generated. A language was invented to describe these parsers and generate C Code and C headers from them. ** Example Suppose you have the following JSON object: #+begin_src javascript { "title": "foo", "things": [ { "id": 42, "name": "Alice" }, { "id": 69, "name": "Bob" } ] } #+end_src You want to parse this into a C struct like the following: #+begin_src C typedef struct thing { int id; const char *name; } thing; typedef struct thinglist { const char *title; struct thing *things; size_t things_size; } thinglist; #+end_src You can now use the following /PGEN/ code to describe the parser that reads in JSON and dumps out C structs: #+name: parser-def #+begin_src prog parser thing is object of thing with ("id" => id as int, "name" => name as string); parser thinglist is object of thinglist with ("title" => title as string, "things" => things as array of thing use parse_thing); #+end_src This will generate two functions with the following signatures: #+begin_src shell :exports results :results output ../pgen -th < id as int, "name" => name as string); parser thinglist is object of thinglist with ("title" => title as string, "things" => things as array of thing use parse_thing); EOF #+end_src #+RESULTS: : #ifndef : #define : : #include : void parse_thing(struct json_stream *, thing *); : void parse_thinglist(struct json_stream *, thinglist *); : : #endif /* */ Whenever you call into the generated parsers, make sure the pointers to your output variables point to zeroed memory. ** Simple Object Parser Use the following syntax to parse a simple object: #+name: jsonin #+begin_example javascript { "id": 42, "title": "test", "users": [ {"name":"user1"} ] } #+end_example #+begin_src C typedef struct user { char *name; } user; typedef struct thing { int id; char *title; user *users; size_t users_size; } thing; #+end_src #+name: parser_def_code #+begin_example prog parser user is object of user with ("name" => name as string); parser thing is object of thing with ("id" => id as int, "title" => title as string, "users" => users as array of user use parse_user); #+end_example Then you can parse the object like so: #+begin_src C :stdin so-testdata #include #include #include #include int main(int argc, char *argv[]) { struct json_stream input = {0}; thing the_thing = {0}; json_open_stream(&input, stdin); parse_thing(&input, &the_thing); json_close(&input); printf("id = %d\n", the_thing.id); printf("title = %s\n", the_thing.title); printf("users:\n"); for (size_t i = 0; i < the_thing.users_size; ++i) { printf(" - name = %s\n", the_thing.users[i].name); } return 0; } #+end_src #+RESULTS: ** Corner Cases Suppose you have the following JSON data: #+begin_src javascript { "foo": 420, "bar": { "id": 3.14, "deep": { "info": true } } } #+end_src Which you want to parse into the following struct: #+begin_src C typedef struct whatever { int foo; float id; int info; } whatever; #+end_src You will have to dig into the layers of objects but output the data into a flat struct. To do this, you can use the *continuation-style* parsers: #+begin_src prog parser whatever_deep is object of whatever with ("info" => info as bool); parser whatever_bar is object of whatever with ("deep" => use parse_whatever_deep, "id" => id as bool); parser whatever is object of whatever with ("foo" => foo as int, "bar" => use parse_whatever_bar); #+end_src As you can see, the parsers =whatever= and =whatever_bar= use other object parsers to continue parsing the into the same struct in a nested JSON object. The code above generates the following header: #+begin_src sh :exports results :results output ../pgen -th < info as bool); parser whatever_bar is object of whatever with ("deep" => use parse_whatever_deep, "id" => id as bool); parser whatever is object of whatever with ("foo" => foo as int, "bar" => use parse_whatever_bar); EOF #+end_src #+RESULTS: : #ifndef : #define : : #include : void parse_whatever_deep(struct json_stream *, whatever *); : void parse_whatever_bar(struct json_stream *, whatever *); : void parse_whatever(struct json_stream *, whatever *); : : #endif /* */ * Notes and experiments ** Github Checks #+name: jsondata #+begin_src sh :results output verbatim :exports both curl -4 -L "https://api.github.com/repos/quick-lint/quick-lint-js/commits/b4cc317fab45960888d708edb41c1ccbc4a4dd21/check-runs" \ | jq '.check_runs | .[].name' #+end_src #+RESULTS: jsondata #+begin_example "test npm package on Ubuntu with Yarn" "test npm package on Ubuntu with npm (--global)" "test npm package on Ubuntu with npm" "test npm package on macOS 12 with Yarn" "test npm package on macOS 12 with npm (--global)" "test npm package on macOS 12 with npm" "test npm package on macOS 11 with Yarn" "test npm package on macOS 11 with npm (--global)" "test npm package on macOS 11 with npm" "test npm package on Windows with Yarn" "test npm package on Windows with npm (--global)" "test npm package on Windows with npm" "npm package" "winget manifests" "test Chocolatey package" "MSIX installer" "test on Ubuntu 20.04 LTS Focal" "test on Ubuntu 18.04 LTS Bionic" "test on Fedora 36" "test on Fedora 35" "test on Debian 10 Buster" "test on Debian 11 Bullseye" "test on Arch Linux" "test on macOS 12" "test on macOS 11" "test on Windows" "Scoop package" "Chocolatey package" "test on Ubuntu 20.04 LTS Focal" "test on Ubuntu 18.04 LTS Bionic" #+end_example gcli-2.9.1/docs/screenshot.png000066400000000000000000005621641507017207500162760ustar00rootroot00000000000000PNG  IHDR 7c?eXIfII*  7(1 2iGIMP 2.10.342023:10:20 16:58:37iCCPICC profilex}=H@_SRfqP"ũVBЪ4iHR\ׂUg]\AIEJ_Rhq?{ܽfUѬ鶙I%\~U" cVR,cN_.Ʋ9Ղ8MAXeX֕= #}e4G" B:*FV OG\H.\(X@ $S^R$ 8Z >v |⟤7:Z.;\OdJ}Sk^o}>Y*}%^yw_woi%r XiTXtXML:com.adobe.xmp ybKGDZZZau pHYs+tIME :%6m~ IDATxunbanQQl@QEP%Uk[?vYX|ޯ*;w39ϜRĕ,.`ͺ࿿E/nB!BM r b{(J}mV v8XX 6?\H!B!Ŀ Eb ,o;T84 '$B!eZ6\,†e*%B!W vQB!B4M.e@nWB!BιxÆe#2,XU|P/~I²~įJ#B!QpQ|,JFkKa=6 L2VlB(FLd (wfΌ :4+%ba2l@*7xĕ*Ǯ\\ׯKB !ĿPP"MSyTP ӲvkY2. $4D7 :]'TZ,FrC,z}re_4-"p9y~gqlu$$35ԪRpZ4FJZ%&*׷3{-ڣi*gPx0LҡU]5?B?!<,nwbYfQ@зkS$eeǑPQ#дA5v=Ƥ0 0Lzjf-I*T,_9 ^:Y_ vaEec><݉p6)1d`k,|'bՆCbY%~T[x*#uHgv=oJ\l nZԫYʏl6^QwelvWJ,߬Oք!ğރxЩM]m׈3kgݓ9}6o;n溙&<1JJ3ntü}Эޤ ^q"e 2<>ƽ ET!Bp9m=nb.z5CH ''%0LQ!sd2zEUVof¬y?0o;3x}"TUc뮣{|\ۮv@  QC)Nd&EB?H|@/<~3K~Nms]}0|6 w3//bZg=B\ GO$]ӬKt, 6ɠe8&BBRBxu2(S6?RK6n?O1K,"ƒp턇b>?çek8" ˠt!up` prs5 ]3 i-2Q\n)EQkT+ɸW'oϖ}ɋ[̂ucl{eg+(E7 ^ O<؏`6m=/qi^ Tq{t+؉D>{;˄nƲ,^yg>xYjecyIHg?)B!~UUx*f}cCraqlBo/`۞c|%a7GSy_ҥ-MS0B!W+pzDmoD%@B!W))|x/o8͡+:a"$B!WU0\GuY=H6!- B!B t- ֽ{PBI,x&s6e2D!B)Q1%,f#4, O~>D{#4 >B!N4(JdT0+yXB!BEsZ1$B!W3,X®I !B!*|,X!+ѿeejoHja2 B! #LgU>d4DVoNϠ:I%!B!UQl HFugGYfw,"T.B!I޳F-4Cɘ|!B!$< w +Q;fR"B! Ȉ?8[\vQJB!BM2^qG%B!W?1 Z_X{|S @B!ci6axx8__B)q t!B!ğSXΛ$B!B\Um%dB!B\U6˃_}$fIb^uWsr<$fIs¹soB!BeW+SQ :Չ4R\uq1Gr[u/|ݟ_pE3c~~g+ɧK,Bg{we}wGҷeYB!oWUE* |(:؛|Z%,mlW`ŜE/+FNF&Ot4D5ݬq3ߧDs} GLgMQ|9g 1/G˨ hР<[g|aC,?|njHbMw%{fv'{d3!BsU| H55(^EidrhCϻsg l;Jvk򄗒} .Cu7sneF>_yejŕnǚ85iYd޴|>NЍzv-!f~zY98eCםAߛsCD`ߚu<k֮&牥 z!BG.X;w'UjE"h*e" ɷc̈́־v-׊>DY,?H ,[so .Em1^Ŝm^;:Qv"JRl8*ƕvukS 27+~f.7ѩ] ~jZdM`WXw8|dJ -6(vmjĐyP-OO3*j[to9JEyؒ΄Gx *ScH)B!b- #k"zgEX>bo.+ƾ9$\3}]N紩^H0KvT|ʐ/bHONbHmkǩ4 "$8~vgZ{\ C9ty5٩8@RlRgXCRǟAz>&4ܷdbg'3r4Y>Ѐ]Yر&a1ƤM?;v}/B! /59薵w]˔Ui}CUt9,nWL ð@Q-VϏ񰬂Ca` vM)ֺ Vapy+^R53{[ӡq%ׅ{Omc9GrMo#<t HG8} MM"˘{0sDZ@Q2 t4E,?>fK&B9(gNP!B&Ц-ÛDrP &(.tr|$}{s6{_@L/ӤJ87lfՍuqDDӹC=`ɜ.Ѹ=הN#).[İjK {Pa o[oڮ([ӱ=JnhlGR/FaAU(,_3Ժ6F"oLˑļ+J %YxkoZ1ߎ賛X iڥ;-ڟLL᝕ '~ kӘ1(vBҫ.V}˸OK,!B787 ci6axn<?ev%ro|d䛴A>*w{3 >r&]#2Q42 2XFx𓔣_ЅI!*̉]5:ЧkpCXh_G"43}.Nffh+6LOFn)8ml^OXAV6B i>BBЮ8~)N~S> B pR _5 rI+!B̩S([ޤ >pt[4M#$ɺ?4EEQ t)FDpR}򊢠 6_t,wȀbi6QaNTO'j_wCDZ;57w\t]51J!BCx;UPH?s93?JS+PY|NUobQl\ ˤ5߱:VF_}m!< D:1M EUPGtrطx1SלRu؊WX$[^ЍJçfç wqKé;'aظhv'^Dѣ\];?fH嵇>{i=L|>hڽ#VNcx@rJq3LONP$+g^Q潻pC*h,r1[BJg=2lqˬL+M)\,y/(nh*ܮc7-2h E[^"n(/d޴UV-ǠGҶZD^}j.BYQѕӫW[zuoLZr ;N`bkզ5UqxM<)NulA!TlX[pM*՘.[RB8UҡUy2RZfzU>ǑIbr&IYy *6nDa$dUoNK+ѝsSq;|>b&IYMȣj3-UtjX7"5DE')%|ji-?Lru/FI' E矃O/hR@NrsWUƛZУShк%Mg5MpIʶ26flI`ȘD&\=^z6*Ҽ]3yEIJE'BV>!U{cU4mSmՇP3jMEFqmZ22kcݿ$ΐ:^%5o$5ݍX]Qik4=$&gbE}(2IEWGӭF8^h,êD@vHH\ޚAmAFM~hgzU Txmc"4uOC.#Q JM IH ǯSas *Vx-mnTiߍwevѤ]}|$&go")EE:S3IYfв]mSHL$ߟMV[> OiҦ9QxW!_tgL2d4!_s&#=j='& qd_<0(coఇsmv -]j_29v:[Zpx\$]޸A-b8v:0Qqj*R0ͦ`jEX|17kbѻ|AT8\5MEU Vpep߶ ӊ:\gš5vhc֬`rc߶ єR![tTB"WnO\7IOngnꖍ 3o[vi@| +ӡ=8CR-m"*ӷd㰫T7z.))$:^2=#p' ud@߶ hWp9zVaKŞ[Yn躝Ko]Eàl8gˉKsD"*NlB"%OS;8];]^098g4'HNqҠ hjQԢ)CCK\ZӨ Usxj"4gwеZgS94B wZQUbS OLTyTBZ ۖ(]Bʯ–;syv#, Nru6ٳ8JWz (LY*:?:XGj|_\.52r <aaB֫<%HfPLB7ldnR$t4-ޱC< *}EX%Aq8U²B2& ڿWL`(C:Ɛ?G6?x IDAT 54~y.v)6,&?6 .n}kE |}-;:mbC/Ӯ\aCyr#0 }0}ٱ<Ř71Wi 3eྴ1-HԾl=;B@ԙH9' N LˁmWo~ 3o/:s'}IG^}ۻeRҳ[`sqipyu;l<;j<\zϜ1.gёrbgPNl'*cSmq{|)vE!eC/[Qӷx*u~k|/AJP\6VgZT:5lI'߼!Gt`2(jSzqhS\ѭ$&_A+ gO/ۈA*r(x}lT?6qUEIat;FqFJ`R4 ĭ#n*2yV|J4@XJ:nBR 94b1WٶaC4F@qSF%X Hp$9_b\w,EcS"~IZwnSǿk|z>-;N!#@BQM{;]Nޓ1%TƶN%]]Sewj-}ۡDsvC )m@{<(ͻ vmϸ=7Qؓ⿂5#=4-SشNj ]/=ʠWC*l_4a%5"bm@z˶_ݪF%r5F )IeݬIzJ"0rPC?ow^q2ۍ$j `eJVփ,V.OAMҷ{gհ. Xo0o~(ĩ\KL0um;Ɖ_3|X&eF' t!({s2Βhz¯88]v@Tll۱{oqlJ k|ҏ4˞G١b\, 'v F:3P}}m +MEy%[h}43NX&VßU]aTj؜JrѲC ʝ<¢%2g 4sYՏ`iTY;CʣÏv-aB ~wi~ U{^oy`#'I̲:srH3pJNfn?-UFEٶ+\09 != 3Mgl[?P D"уZpP2O'?!)iN"S0# }i$qb@rD>[li&$+#|q.C\ռ`36U5AVz`Cjz..3٫; +~cvEWGu:G߅eV$5e$Hho$;(}Hz Ȣi`ᮂWRWyzx9!`𓞞_Ue$ңq=%-Ma7uy?fĔťo7bAK撖*)ƭ#CxvTw `^_Hn0] ٻbʏ}ٹ +pggtCn"o-ZG4Y=ucm7?#?&{禭Pu#<԰`5 뷒|*J}[ѡG 2fl?ZHƫ[76}so`n`k\ '==eG;t' p{@Zn{OrtN !wJ|<~p9TL>F~(8lZ6ċju 2/`𰆽`jHN 4Y*Š^x+dYuU-p!tj P ]1t6춢n).?K!*v}:, P1t~sE0~,X/4L| hZA~)RE% kIM74lg,2 ˮ/譥iBԂBuo_)v}~Rp8|sAæ e-M2 :^sJXY0o]&S`XrV88ix.\n+9i.UVLݏWWۥC![ xH!B!DB!B]$JԢ*Q!g dt%CpI麪lQ_;~@p|@PEV5"JDP2:2)E_qZo''aQVPJF/B!{b#T Nnx㕇2n0-JhXhf3O-1<%%_k2+4-;KcKmƒl9eRu_fMBwWxyj5-CneS1f=16<۪]Fsk`~5yۍޜ7m|; /ʤZrn]/M- nb o .{E_c^~鬒/.Rh>~w t;e,`V-zkeмk /aU rmd<) ьwF31|azWs\f4hׅWnoNWL asp&B/ͣߘ]Nurw5)5>u4;S ʻ45M\v=Ύ=iTkV?9; L6,1LxKoߟڟ}c[aZx~<~yAǏ~[eYx~zt<:IJwq ,1,,ò0L ˲p2, W/\GP٪\ӼN\Ys~S{ {Io xnWex<~^5o~עm&yX`E.8ݕLʷ{9Fnͧ:󇲨iPwj/A 1*y'ߣc^21,K(C+Ъe= OC 41 S?M&~}|ͧcby*(niO$YPM|Y߯˷ y*?F7ps9, |sL}^?,bi8Ɔ_ b2o&n\4>D0s2ؽ$Tg r²b~W,;WCɸKNK/0x~ݔ|!.񯨈nr>w#iOk a:g\bصxrod[~'7p:Mu4412TV,q&ʴi]B!?lc РN45@v:hV/q$  iR"/Ier7؃-) ;0b[D^GqfNa aԘ8dkI>-E5`Gݽ'݄͘x?FϿY)ߴɓ}*W⻏gIz=ޞ#>cSX7tVZr|bFMPa;݇J^Sb #^ݝKa),ZibD&R g y{|Wn}31V:9ks ۚYrcjl)f 73^yc<rsr~uϵ,hӧ?cnoG`~N ZԈDsDqX3 xԻy)ٗע=8F-508}?;pggCWϯU./s m*X"&L_KdaCՍYF7ۈ{xxn4|>4oLɟ2,U>K ̌ɝyDԯʲwb1dEޛ=%C̡ݬ:]uy;nm,ʞ3ycZ1};yݕBx~L,˶Уa$K?BU;HOHѓ+tjZ0=bf#qn]}i Cz'O:ō7~ErlUϑ\e])z7L0M%s0|vdXf,_%(6{7{r//L ^ސYTޗf؈!С&o:xi[)zXjc=Ƞn6vH͌8#/9>#v=k' ؒCVxfޛd/z)_,{͙7zH_ۓ IgxZǔfѳA{Vϸ7 ?A I$#oi±U_{-lʂcx׺d=f3{L,tWB- `d&XЦfit#\ E%?_Ō!%h.}~tGӮ_CH5n-L UМbMlzJ.:*jӦبqkҽ[gڝʚbcEk L7Ӵ"lYIN&~?4姴zFJ&?k:A!tk]|[5 mFO*:s>[MxLֈ _nYTԜtϤqFy'0^^CVi~>B mL/a#u|TYH#}pC5HN̥ykAwP#8=u#h>Z}?˧Dͼ$ 22nf'""@jd3snhOή/74B &"< @Dxh{L7뙱p[vwߟϐƾ8]\[A^y#>,ᡁ.xFJ5x.B Az%8xp0\AG`ʬY^2Ss؝áynL&}`|nj+)ۚF"K|ڟ[Q OtP\6L rx|+_O|K3xm>J*^z~+k(պ/ '$w@Hx7SvZ N*+Y΄W:ZIoelF}%!gs765Ύyq0O|n/gR(Y]i7pE]\?|\$'Ec*˗PLB gg:p`FeQ>6KQP]NU VpLvHa À*5-KQ25*%V}T6P1?̬[1* Ǔn|=goOq/lQRWi?kYqq6̖m{IM5hحY'v1sSN+_{{#w0 \4m 9={_Z@zE*\16xinyf6tiO򲳨4| <:2p:3Og ymhڬAkv~~L%Z+ִuV>،ij.Q}~L{7`QR%<, ;WĖ+OMNjqSZ*dc[^f㬗+xxDp'{yۺel`RmjQ KQҩ^$zq耛\Uk^ ؏5i\bެ|=Kl r/c-:J*Q1u1%|vN(TY|8K=Jn'%EӴZ8{u |HSN/ cX1qӷ/Z}bmflJ'ЖAa>\AG]λsBbK 5#P&:#I*ڶg2fѷ?ǝMNKbC2ƍyG>Re悕aܫ)VF}e!3&d6l#93Or<,-cë*?9^H$E:(MPQDTAP@REtPz{s~4Qߗ>OݳggvvfvwƖXx /hѱx} |(nHK䇭'XzOO4*Œ ̊{Fs!;b8"[vc$$pN-̬{(_;7sH,`XLLni8ޘ\"7=%t=+^A*&]ܖ|j/"Wغ?sh.Dx '0@ê2ocd~yׯ2gRZƭUV_z2h8J:V)n\3J_ KpXZngenEbI7K#`[*[j#"E1D^v>8H~'jD3gs28l_;PPy@6X)0);?Rrw'?'YvG(Ъo$+, z`^K QZEZ^L|~=EP `dQGtT`+,{Q%Pk=h>. yxkQ㷒72W$;;.H(ҐWy/?άoIWk OP,c^17Cq2dPQ#k5aɜQؗV5#*%^s |I皟JQ$$"WΥPF0䤓dUGɘmy-(%V :* hԎ*A@tUhΝPx 681jq卑0DV0e"D): FAU%cP@h~D/. 6 W!{u VSE )2*,bcA.񙦒"1t)eQS!nDZz{%4*[I߾"{W?NO3wO(QШշJIW<AAPd6;cnkױUQd]YQHEPl\OrT0v(uC=AP ́rigO'p.:?Qiϲݶ/a|ޔW9j͖{ IDATߺ%n2 Ɔdi֧9"C-(liEMSЗe(;#Ӻ[OI0p\E*sl&ភ/E\:)RWHJX Tv+PD@XyEcX9s(!{a YZZ789O0\dQTn#Zb%;K./^sw?K|-ĝFzޣ'ҨH[N"9ΐ~FT`u:7:Nãa{foÎEزiZP\0J$tϒ魡E"klzQ ;#3WY`!~̈1S \ :&\޽+~G3q2>x><;2u2bs폏ٹ}+.DuL]dDLLlk`ƭM#v۝eWxn@ol4bNKKyq`/ h폒w5n)dдŠٱ1ս\Af͈Уש фvn>vw$bޮ".n:S3zF<K[Kս >9&ac׭[F'. GHKR@n:f|5W0kU=D|N'r]Afs 7=w@Y|4D RptуvBk/SO3x@kBLMHy9P)|8nrD]:WR̈GOgG 'j%1LjwPd-"Tj jԠs:ë{V3V:ԍϵ@x/&;1vW6s%ZORW^}[`knRˆkH@5wpuׯ=5۴&R}H^/u5x'i INpLZj&&W/?j5y}Ѵ9`y3 pqnx͛ӫK}ѭ[ZVRo=ۤt=H۸/Ӭeub~>@N˚sWѧCM];,bdoEv rS֨oI؂"4 u#m >}-s7ͥl3 \Ljz.,W,-;-1aBY2~|oAF`*Gڈr 'p_/M2{tgQ|;Y*D,2MueQ,2-B57$Bj5`rl:# *a] fΒLjuY|~HN mՇ @b&cG}tAO3mQR0Iͱ ˲J˒O om{S_s6ާKS8V!HOtJzI1wKVyEQ/=x|=U;“}^dٰȒf<3&\:>YE7s)v9%7-\ܳAɬݹ#"K6NIY"`-m`k$ 'p '6NZɋ\N5TFΌD#ɤk/oE݉1id~Kq#.ldI-{jr!APSs" P2\$Cd+;uoS S1Kdv5iERFWs1f9ɦB;Fp͍cg 9j7Q#^sbgԍ$=&>IC Gl6eݩ cz&Gن]98;ؓû=+c\6eTc.bp1ß)ϖ߱䪣~*8u {[(Dc̻OnfLCǐu;O.Yf̱LowD vf:ΫyK7>~ JKg}mT֏Co7c開T#kW#D~ïgXk-5c/6+VIam?)4~UXm JhOdxĩ/ ؟n-=Ij14sd>m6R^M%`| #/?ꡈ!X|AYjZ1R;Y9@FUN oNV~E`6 {a> E=_vr 'p?rK6s9qu0llr eLe2y<ͬ_s(й{<3dO`ބޏt~ޫ6uRsͤLg;Zj2 W~\~;~"ԳѠ"ڭrBQQ(Si -I27Q ߥ!ިС OvmV^^ee4`<)NEZeX2eLa\2 F^|M:1{Qr=mSQhY&./^.N$טԭ ٩ql?Oh0Ws>[KQgB¢y 04hBO'R:{cdGo<"k1{ҋffSy2%;uaN\+ƔOn.ΤH)9$dH6mǬ1=b9wy *o/T9}Jߒ>~^4-'ۙ,k]#8CC@FO~Va~?ɫEl~Oc%7$~úkQ#X(0?^4,ŘSYLC).\b QLDϖ|h;᭺1+2v|xEe0D?O`h/*nO+Ƴvd33$l?b/I0![ħ}& V<'+!Q1q@|3ifBZvⳎ䧜"K]~յ(*o'6뱫Tx2crs<{uG-1AeYKnpꅇ&w8>siӾ˹8 N8Ŀ ߽G-#$.]^SA/fD~ 4y?}BBAȑ3"{UG3@$&I9By`) MC)9/pUAe__IPΈƊ8+"= si<ɳxbr%^0mg}#>; @ƎU +*PP5gNēIAjOĐhŬFbu]dùBtB=137RP%veޟ y e#PjN'H um]w3̘.bՉ̩טJΙyI#eGuᢲs%T]\/v;}χKO!*x;[mX//4*ʇc"ТU1s '+8|9?ao/Vq1J F*y  ?jECo#IFc̙ddI VVo3=>`9EQJ*jU(*;M[~dx6m=G4dD,L^8ÑiW1\|C1{wd\\«u|vd0U$[\]b&" KL=ZC q1.xQI7LQ~&:܊m f7x b2 ^u"f[+N8h^LZ|1i `EBi/JR9Y9W8dy} t#%GAA BGZ%H8o/ߑTj<FyKQ.)dtJRaV(h1MJWWTj"$YKj"MqQ2.gَ)%I7둌6-AE/ Ix l`UO<,c~~|r5~|j-eBd6m є`F7n&@$!. >xyZ0l B6oO"ԤUȒkoƌS1T,պ) g _OBxd94<Ν|㟚 \s|aGGp0&І%2`EU~p}" nE*A殆^l{ƚŧÿ I~.)`CR%q"¯ qEoW㥱m*mDʲE&/>LE rb)iЪ1xB^KVLZ0թD<1MΕ 'p‰VSY=ggEo.JP.L֍^xeSVIZ[j];cus93"nAZJfǜ)rD&:UTÅ*\dlv\<7L"WAbPbRƓ H4Q| ϼҗ dPyWOIĹ+EEQ-ժ'q=ݟBݛcѯ7d~.<0zo Hɷ{D}oW6@DrΝ I(ش|^{tcj6.}a^gI2DEGUa\i08JE̴f|4?|mز<w>>MZatTjWb'tGNַ| ~SK\ufON64.{"TTμѽ߱5^XVEDtD Z_T&j+J'h]܊R9,a@ynw%D[~}hmQ<#X! y{#xѺH-) ;FK.mt4M;нC Lѭ."z/qzQ9<$Gxs1Wԙ#(FUN8ĿQErфDһG+zumBrHF3Qٱ.#;|_2싽_^}sGS|RDuwpQ S*n)R(&2ǩXM:@扺A@$',qAź*\"%g:2SI.FH^vWS,9ٻӨ0Sd?flp:9Da71r+"jW4zWܔ|bqpU' ZnR1I9Rj(K&W&i’C31;qC'I CF-LYbJ$ x{厛V{Mbۙ  \aJ\"2rQWwD V~x,Μm))/OXé\r6a8/ l闎3txAPc?c IEvv+$]'G펷T\ٹqvzб?g7@T. iiQVѾX'~O''3J-й7A8nQ[<0՛N^!zQ$?-)DjL|=l*ҵk[$.ZAx*3 IDAT^: )ě +ׯ%ShQ;߮;R)U]l|DOIVЀ?pg/'pӚ#ջѩs?] jjMqV18[LۧSu))a., 6IΈ(=-4Z+TCF4͚_N8cL ka]5N8WPkҐɃ`9s@p 'p 'U&tAgl'^QN8N8ԨJ *V9 S+>FN8N8sƇZtI;5v )_wCEZ4E:OjԨHs4(,:!lWgtԩdoPDѴAE|o)+ԯ]e=Dԩdq Ope?CCZ>ʾZN+U]>X:E?O.ЦOo? }cq9Y7%+yP)[ѵ1*eV &c{SdwѺ~[3aKV7;Y2|HOj{Ztfu0,P=B*38j4kN@*>wpۭ )ޮO[>fꪲx9#9Ŀ@'^1CiQ7g|/=? 3٫;VS&|sؓ!^:#v̅Y|6َ,VEUt,ز"cR$D\DBIy*Vَ Q,*R\R$̊ (-v pЏzj2(ʭ! E&r%]T-&3V!^:G;EL~ D2VZ zWb,$W*DL$%p W )i&ʄ"],;"ʀ eR;3RlrE%"~ndˎbfͣ]4tkQ r""֮al^m^yk x},c/vG[bnbE?K+QSح!.*l&36(ܒ&3968܏lfRTx(PHO(VrmMF d\ɷQ_HdH1\-u+qYٴ3Ŋ2~z%;%Q3_.LRry u#S,II62棏ek:ߛo5k괄-Bv`w<4E9E|ъ&OЀUOvEOYo-Po@'+zB%4W0-u(& fY pCeÒBDPI C:p_T82z<7|+SGy L_ns(쒛JRB >t g[bþ|PS{|Q> hن/zCVtiؗ_)HF=$X.O<ƽRODT*Wґi^flVT;<8. ߗ{vv]*9CJ>hɱKYI;h&fO@=ɺ0(/7N5ӌdXFY`5^ZT m;sMo_kR,f=V`$^Yƅ>@ԓY+Hn~Әev.=WmDNgކd'a!xً_DKf g[Cٰk)IYԸ5w'l& lVѮ3גҁøUA?ąrBK-f:kK9]:UAV@E/:u -SxcD3|TMoAPQjUNYӕ]YGƓu#j.Rk|=KY5ʯ|ƅo1$n5]Ϗ㟥f0sNGkD[ra4fބ4"i /s92&>y-'}7/& ~[ZTId%Wȷ٩޲k8LQTI]o _N=_ϱ\y#c)UΌvq9i1Bu["|6kM1z`/]M#x;ʼs57'-hQU3Iu#@'UPzRΎøv~G4B9^>LY5ly- 'dX 9BvhՖAV  5?Ɨcg`Acl8WUxvOQA|V4=UZZKH4/āuat et+,tOEE.߆Z.V]b=="kaմn1u/N͎$5-U,٨]ۙ\(Dct"ⵕ'(6k^/1rbV|?2f{~s0*UEvx+:jGsQ@ҼM8vS]=ЊLٓ~8g d|<#rt?M 8A7_ӮA ^G'<ٱ2fx{Ԣvê2b Ը8J`zךy8%nXMQT MaR@B#i~ҕBzyuƌY9K4!&pio||wj0^MB𼼟.&Ė9xsWZq@dD0* l3]}#2!apx`U 0az|vdr dPZ$l8oyݲ* >׏2iw|R~>g(J]K58'삊}8jمyLk|+ڙ tDDZ j[Q3xs4f/@δ^~l WxqG1r*E+6$ѥGW|\t߆єa֛qw =]):MCPCK *KYTvÈɺ|q4mE՟N2t(tڟ 9i#P.Z6&%jB|#9B~fDrnLp#X->nFo`Ig+ԥV R),%F nWT)o~3,""WٗFBF5GXR,w޷RzjÆ}ɯ'2ѨEv/e|5i)W5V[RM2j +0gLbhdރ\HWPdOYq @є2Jg~/L} Ɨd>;s}4#:>D弓Ll5FL܎WL{]ҵ hŅ|=a&o[SKy 'p N*ouę@0+NRQF/\;U"ϩ Ua%GKvIb7bT>(v;U+PN+sG/@! +Mbu,QQMCB9b 9 . T @Z& EeW+\ıDGx*vVK2S^;Ƣ7]·㥕U,V˱VJѴZiNaE3*eݳbȷY7{v3~RK-h;ٹ6B_gddoYeViX>rk py &/?ѠTԬY=ҾA94:O+zϬ_'ZC|k|b.voc U?ʝv"Q-*k͛&-P e}" ,SzCc8T(N+l`) AU)bņؿg?c#ݗJZ.Φm{v:5[TEk!ocΓ̙-o9\O`t܄u\+(IT2 Xrx:$ˡc1\/`JaǏRzQ9v&Mœȡ1BR˱c1nЅXwl+2/ưd,.$-$\EW2ݺ4y 2 =%`Ke8tiE}q),KDu">Gj~66m+IjQV߶ iu$cx2 ,KVt‰!D "ud˜~kK^Aj+'1{[6gj:݀ f/:NI#۠CX 6\OC.d0)Jc)v}ŮJ8Vk ]l+B~2 v@-AJ#3T) oݛа̜+V)~M&[E}?Qy>,t)J 6ECP9O =qt^=**6^]!Kw;* dnW#T"wD l緋 [.ȴiӞ.XR+5UK`0?e'fޜi\Jӈdݘ՘2 캐lODz{+T''W$V\HXDYѕx~uO6YQphLV삃ʸ$EZEϧ*4xW.'$0)tԘ_W-ືҫV;(V;2߸ӥ("jZAyIWڑU"RWnG ipDf (:%lv$+{F+,^,> >bib z7Ƞ4"(ugor{qONw#"vACetz7>M$AtIB͆K8ݟƍ[i1i',$4릡](xdNo5 nj]$'.^7js26jҾSJoNk@` w)](4VTΕ8j>naí:,Dvo>3wE޼U"$w;~fl /OPPu;p<"Q/rOͦB|l|!r.13nw7Ⱥ?H͘KBfYAnDT jFNȢG~U]1>8{H+sQXY끷@fĖxܑv=#X/ON`L=G3VVu=k,%둫@ĉ1SJ!fB2pΛIĶ5 gvPƏ[YMI0ٙ6'uPts5<=Aڞ¢xIu⩗Htqct؛YKM^>9CzICO+/|w%{fˆ]EAݯ-j(5#=fʪ m4."ea\+uL!l ]C.;-[E\8=~IbLxn^0JNZ=y야['EuJ>"{,1#y< _<_@Cn~<0ƽ- ȯENzV/孡xfIH dv==@dc?c3z[\&so4卯 zȍ+sfcj^/D+慅;Hݟ1jBC<״fڃAlb)pτ;yOט9×~]/؇I`xQ K7Bnu$P¯[WmIEec9L5ȲD;F2;:dCV{-Vr(t42h8XZCE-\sN7N[ʌ !dvfrrWǵ fFFYvBǸhӡC3QJZvFDhI۷g>W?'e"XET'7^(NOEѼIهR ЪIErVݓݷ:q!(?֣ ^]y4$u~t3ZN_m4ecz!Xb<7{xŷ<-UX̙G9^h%uI|i$ h%=uRWHFZ5#JaxVL9wBMFnlr%omBbRxҾM"I88_Kh"/EPxnhVֆzU:'ϘmH}8̴iK[ xyٷ5ZNeиy"Q>TeDFL&4+:naT"1~?zj CXQnVNb="ӻC[6PB^⛝-g*hޢQ N&zęI m~;FLPЭ]sݎ.e@G .\G*QnB''wȶ!hNʆyqlQJnRyÿuw9OKr~ 5ѴSX/ܰ IDATڄ+jAb[_yW1(.XH|z=`8j$sia}vp6*-e8MR+O ƛdl>w"G< E-8Eˡ2{$ZɂG8ol8ȁ3iKV߽BF 4)rYgJWM$cWz&G'3e~ BʼnsF$UDSpb/]qq'#e牗6b] ~N`v8&,q6-_ۛDW5GC'RNJY\,v #c`&V3 Ѐ#tz=NZHiWD1/1t<^j=-ˆoӝw3WȶUgn?yvr'V?>ѵ 1qLؗ̍9~ 'Qog}]}fn0ްg>PY"YrƢ\p%,4s=P |k*wbt(zL|sb,"y_--+[{+?#q~yfy yЀ%tBW5,O`"BHt䢬& ƳxjI|ޝׇc6o(xf ,ZZQy2 $8yp7ni0>@\ۿZJAc ^π۟Ms.;Mj/LmfN.e{_??{rP9!рprPvQ-I*R}#B/_=ע* 4Z h/7/F) vKۀ4wXɫRE촑oT03s(==1[HWuy5OnCֻahR5[\Cr@]eSeYFot%7 2*Jt %_9҃-_ÂCVQjލZ%*ŵj=:oS<սBJ/%BDԢEZGXOCż7;EAqs?4QZ[K1 c%V z6 \Ok2ykCS^J J5^J^u͎,ۈf6~)k=ҟKW&&W䷲T/@5^3Pd¿|Z͏-t H{$rgH q0G=چR#^; h@!ڮ!_c;h̆=0^eçSN\ *|o>qY%B|2q3,Z/fΞu(s2j*shy=s'a߾8]SEWvוBű)5U#}},76?<։Bǵ=3"_XȌz2N [pzt?ы_%K5μ'cڒʸO`b쿍^%/5ժy MCȳ?Lg?ldGy>wt7 ͢iJM f+wSl8ЀC[ô~a~;Y,`Ø'sʟߘFXx)tJDCHoJrsHNH%Z,R#:īY щEr`ID%/?|s/ɿ,b2V_UdIdѓ1ט1+)u|]?ݒϩ$#A\\TXMe7,00q9l9e$Y?^lea2`(L}xE0k^ޛ9T:Zvݿ ҕ-paʩĔSb"mvL*/_r֏۬6L*m 3qt?2O{bH.'&c<3WGKx;E4 Շh?İo„>#y^q/`~J XeeP9kSX)xaAU8׍.~~YQZ߯rK\zzBFѦdTfLGLRL66+&c55kUZc^B㥥yJ"%%?N,RT\SDLF V Rx}U9CtE&soZц֌X:+Rk卑b2VP㐨.g¢R^p`2"SNuk _*ʭkc <}|TRa*T`yE 0m>z)Z(vRRX)#wUy-*ȒB3Lmsz]]fQOkkgOa2bh<hmZ3&c.'j-7VR\55X]~j*irQIpHzV{TfCr09d򬪼\<N[~ Jhgӈm`H:(,em\ɭ$$Aǐqj/|ۡeGij^?|L)WޙCy7lzm(zЪo"b`ԔG1次^ή;>$U\l{|ΐ49Z7QvZ\6%݅M^z}=M Tv V} !;m}E%;ޔ9qW}I9 Ht<')];Pg<~!.ܼ',@ġdgŒd_%DĪMSxi߼4@u=;̯8v:.7=7`<6)'R= BA󞷱eק\g]APs]lݾU/absjz}7;v~HA*r.桤WZ{SSmo;o&Y=d̊HvIi9߿$ϻ0r^/)Kt KEq6o9eQpCHKܓo3sX"J{se߱Xv90C7AƆ xvT4w'9/BgֲwM_ĜMP `c_.,w]WG.[3| k"JCL.%|1Ytsjw~L7xp@'-7}D^uBe@$]֒>}9WX/g?zlRH֏0-⣧f(IGak߬~GƂl MGiGF&Y[| +)cZPw9!,\ /=:Sq$if,_>`{crWbuԞK4 c"awQT)¿i[>Z2jBrlХSS$[Ee9G@ gq*bJb#zC3[TdhAV e[nE&pe8#dYY4V쯀q-Jz'uL9C7IgfX]N;z "O@1=0 f3y]>ߓ˟ڻIlæu)ã1D# :˒I2#;6B["Zx`Ĝ|Z2>Őw৩/KMi{SԚӆ)8ͷ{r41{Ǣ Y( Y |뎖S{o^fKfݗ9zStd*Gf\gAK"V5}pn^'d%x]o̘!Ik4t$z3{vPͤiskWVh9bzkh-˯wVSᢶŵ-E\@Mn--{drmf=y*A@A'mLTȜiopm*N߂B@|p-_B##AA@|j&F7N ۜo-b֢ +ס T} Yx[3yEZQP'PxjT j3Iv.ZQgEDh+l~$9niœm)?]EL:E gP$wJ^NRm@o{jw&b"w %I@?.Zӗ{6ߊ|p;rˆHm[s0?W LhJcwgs%:í+8{p//P3ٴ?޿T7!{iGri3ۗs|V$Uc+c"\$5K%Uok*D2T׏Uq ThnˠtDH6nϣ3C{.D-qLvvf[-Ndq]S38ngC U8 tѝq>N91݉Z2v8Ƿ#9C"m8@:iz GNP~3g`4ki=G>f4Y֊2:+Aw!G]$ўb"g+]N D'pO?h="Ԩ:DF* dOW;֒{9=YM9͊ >.$|(,rh~*jo;8-/ >LeCg4 #\An=y!Zs +2Յ|vqa4 o`Jr@Pk %]nwp-?.ht.Q ʟ+&HuYF\ e  :O$'yKr}(ёtQW+%!(4J*?4?j#]oW#IؐtHRT::>YXRSB+>Fwb?~L]](-.ϾB *3UPpb@Q!^&% 5Ƹs2:Sn. be=8k-WDq'tni(? u$j% e}|[ycJ7/ZGMVZJH IDAT ;$2(UWQ(/ WG}GQWWgx u:{h:^RS@@tPjJQrM&;îŸq-(qkI`wH9wxFG&a%γ\(pKG9dE,\sݳpaޜߩK3hL@ي^Z3=CШ15+(1<EDy!RB9fLcRSzj.ٜ9Zne`\7Dr :(<ϢZ^9vɎ.@dy'P%`#i`& b&8VSvē6"K hw!΋wXvor3w="*|}YPPzKd>r[/b΄h:^ye>(#ܵ#v>K*Mz|NVLC'J38m='Y,(O h^A.؉BJ-3Ӱ  p8jy81'`+.b_Ugp]_ӂhԫ+g¢Qk"<[ѷ9e*'dl{Hծt {Nxlu#-?_th5BE=rTi@ze}MtN wa~tmpק\i+w%֨6lqɆ^ѳeLxm&NһN:IMn>[0{ ݉S xĶՁqh:nF9*f%mXJ.>+M Y;wBwס ƠI`xEmp4k"C'O#\Z?[h cno?^ǀ{u${%^̻1+?Aк8,>oM/~x^֤ٙ\MPqt =~ jMN[zU+$Nt<'rH,bƒ#Yj#kXQ]ncHs7š֘W(ՆU+zw9sFą`ȒHbAm!6ʛֳ̛sf̹ua\ASfdBn:(Ӷ2BڶX}u<=1^@Ywm^0j+w{ b" a;B+PY<:no_D "L{( 3L$/z$]dwy4ׇԍyj7%t} 1xuPj~#YMM bY|>F7t ZHq'}DIŝšO㢒-O'@֕|wֆ(Bp>[0#F #[X`":NϜ廝9adE:OӯJX=w7?Maylٻ8e=NUq|.͠Z%+=$^NoݸNvc u'9vZJJ))JC ;5U uXj(TטnVRl+lenq٫0[!;+9^SJŠ,Jo5,4P(3A X-\Ԓ_ j_BNfUWC^Pdb]It1_բP(H? h4/ 29iQVI RwnO򧥀JbC=BJPJ2OT1MҢ\q i ;Z;Nz }mWO9NfFG\gpLIbN+7f熹8~VTg 159u $()LMM量pb+ל@a@p(1qTxGȶ,Xő(hư>Y>-y(bM3* tSPþ=0;öE(8ܧ? f<{364PYZDS+ISTQ[? ;I/"Tsڴ kVfyܢcU1 *fQ1QJXq+Xi R%rY|Tɦt 2.awsL]PSt8%78dAqxH>xcD=SA)^~e# l|E@0$KE'V= 9ccW0J}(>sRD'VOf{h8{0.=mڵ >\K`hzڶoN\Z5.czINvIĄW͇`Ona+Si*#Q= Ѿ Y]LZ`SԠ%)G`s#Ŏ4McT|cchl':IHSMFds?o Pyf% qaxi?F‡(M9fDOX-{wgK7K$)Pf}LJYvu(H=KdI\n +_pԛa0u9DzpqqB6{<ƃ8?[e/$IhEv %WS)q#mN?ϞJhᇢig9ZFMt}p\:\ E&1\9\8}S"IM|&ǍUķhErb ~ NsKoa9+Aʲuԝvh4@7'OrԁZ/rY"" \cED+Kel\vi ԁ!K >9TwTs?J%DDP(TUT{კ7sRӹm${sF]]=hAӧ8]Drpb1jɻp / @ ||Z&63ᡸ*rY0u.0D$0f@k| QQHJZbNZ ']A4.0Wд3~.  JN7<֨P*utht+uy?4BzйM,m0;#FA,_Fgcx呉,?lXdŢopaRoH J"5}(_>2%%JN6ؾ`O0{Dh@`DмYtjZ{ȳ[7 ԊICr]wCx0t`Wj߾ABowE&`-EAE " ^B !;e"gDˁbF2+%/ٖƁH"%DGǞ ԭ/!W_I`Ơ `@gc\X &\$<$pޟ&?[ڱ4gD6kŌ7Ӹ{g-2nюVqq` ^>Ͽ1'j'Ҍ %2}'/Qd>> 5/0MO" 8Fq&&߰NJ[AF=1Gy/ǰxeSDy:ٿ}'s՛ys"p`ޛ;IĞA(&I7j^aث۷%J ;7me'G?SdDZDzŽ;ykvBI-l*[U">fDZ)Ƈf gT&lz˷Cq]jmаx^-UlJJlxxpL&oꇄʒMKBQ}x}P;BtNQ[oο]#F6dDz}1s'爃M"~t/o-MO9f.Mf$]o2 e(CFTUdܒP#Bl҂gn{sܲ-lyOxxu D\ώ8Z&i]1H8aګG7=3sPcMVߗLycc^Sͺ+D6*džS5U+oLOi<@t^,R[qY9a*8WgJص ,ݖFhrT<ź VuD4(NSu'^NHL )w^6?n1JGs)a쏘g\sL)+mu`+/͹_ 3 ?+DKtR"|m)KzQj^OLc^};p0a$׏3~22}P32ޢjՈr۸n,d-`Z̻.އ nSQ^(7_I՜2>u PO*ˉ>y9~yk/oN|7y%*ONsο~Drzd/nC5oF/G5lPa&L#45rj>Z j ;+W6ӡ ^y[ 4  UELG0bދ>kh!!VP2וUd4v\4Jd}6];2s_T $%7i׵#eīxFEv!* BRTA>xzTo hՁ0 n-$){׫HK$ak7jbj :uj/W vXI3Ҿ[5^;'Ú4uWmtDR/͙Y)Zoj#@{&}0VS2mo {3+7йWW"%$D ?-.m$k!˕\'jK[vlI+빑*υ\mGيZ P5#yWr⛏ヂVڀHrU y4r{n9dy3Pʴ+ЧF(:L@ZfԬGxpSfjˏZpd qrHԫ\ndսQW"Р"@#_Hd%X53lAApppc4YSEHM^6%oT<ȻJK>ƠW_֖o9e0Q9o'o1v.YǙ"D֌Ӑƺ1\֨µ0\̩CW8/ +*B"Bp:z}e(CP_e-ugƤN\ݵysPT KfecGNKm{*aRL32q{cRATR;. ÎԔ >‹Vd@EDAġNぬP"L?ffZLe;Z48kH5'c)xe =E޿D$P,DIK:qfsGs98]j[.g8s>%QV]6GaNjJGD$QαBkmNk'ZA4YAݎAd^7$n }Ǐgӡ|ʛlPb5EalE.΂ꥹ'NzZEpD*GtDQB!jmX(ϣDDQS.2C@q8\T*$!8v^\0+ 8K 3-!&?J苈wt%o:zQ!.Zol@OO,.^^v2tB?y,VL偻1\;|1!UX ِīܐѹE~~V@;9da[Tw 5޵Yn&^( 26 j9Ō^ŝV2Thrqx.) (I2w2 IQSlLd'=Ǝ#=:br̸ܔ1j4I$v'YQ\(Hs{*3Y$РEPJr'MF~d(`Eث]Vɧ e(COP@ rl Ş՘;5_%V1T Ӭm3ȽARʽF0 7Zk+*iy?i421ɱoܒ>t\:p8H:U?FtUؓZ6s"Đm(c IDATC?Ũs)iYϩH1YҹiL!}D>9ZԩQr>Ņ!Hb3;l ~OT"x gE'C^B0ڑv!XE_tkڊ!$N'UP|Tr .[Š<5z: V/Kdp\< fz6%*΢*#ւw>_ Qkw؏iCޒX)h$*N׷o,AC'3O_A/@J6:'X_)=( 9JBed͎Hn3g,L=]Ga!ӾX[EyL Yb{=Y3U7¾oqG'Us~y պ'MQRىEӴŇ]$F77%n904C`_o8^%%.٩-5K'OnK\pR#!h+ }:4ipo.k#?MdYTx*l~#})} 9sQQ"83coF؃`: _A2;OX[W3\H"oYzl,56Vk{{zDٓ'Q\ e(C2_DHچE&1.S(g}C[QR(>*.=ČٛȕxhtdP`Jlj y\.KKVQLNH,++jr 1+DN@vvGq|j1 H[(LƁg"۹ayǂJ!Fm͕5ْ`ERq|,2TA[ gZ)_{޻w6[ t>NCI|~z|fVr*gO]%SAD rȍ\! זܹ~EnTj\>|(h B,2ټ*D>ZNԂ˜ñTdY$7+gt+CvvYY\:s뷲ٴ5 EܼS +k py~V@hh!^ WNs3S1QLXX A2O\܍Kĥ:W渣r@o._fǹR(U> /78f!,,kg/f 81ST"K"R2 e(CP2<< 2* * $KY˜/A]t"#Nx?e1e(CP2 _= I$8en>ǯm^-K&'JVVUJ/](i>ZG^)ͪt U]$ x 7H)&"W,AIs'yK#Z6FutYtus%QDBN/ZANgRI| vAAHPP6Zv(~w@tXhuT(ЖM1far)8vىFA s[C#G瞤!C#Sdva3YD< ͉|nwRl;\N7.5(2& M%%BR-q`X"A39鴨8@)#FTh+:&1CZ٭o^Oҭ]4#i&(0tL7:Cte/ggv44p-y\YjnTtў*nw*MNTZ {*QL߉j+p#w zk!;L€()ǒ͠KhGjh߭:'Xq=Ѡ$D0rŘ`u:\Q˰]ܢ*y- 2_s@G^:]jA|c1>Ү: ?f11kbvab B*887 @ >/2#Gkmx2JT;co7:wؑ8WcJVvƿmCqsz2Wtj5?FǏԇ\]|(D[錜9{WERR^c+jN=KY/%g{YX)_ԑ4  :̏k_v=ZP"H Zƀ<5쁥4 }ܞ/ [e%=o/x:|O_?M5<>M9NJхrp#f-YW7su=ƘLhICI=>ȪS+% EѼ%_}>g?g#i.G_aÞZ00فբwfE#0$M0b`;4n,3x(oGmzRi|$CЋÆ5&YSdj[DУM ~9]&FdS~/IyU€x} ࣩiP"3RyaDN _\DfĠ~U 1*cGV JӶ ߲g( U:? 2_.HeBP6t"Ӎ|w^5>D>\P{e"aEDE&eE*ELCvc<"H"a}uf1r'@%-V;[&mCQYK0XĿ+(ddɵ#L+2lM',bL-X`Gr0fZ tzĘi!C0f)rHy~^cνN Z{ cj fQ) u3#cZɝCS%2mnƘata0cq{݌h4SI/ӓ%-Ȓ1ӄ1~9gfԒcǘQJywsmj "uv| YVo1PE%JO> @"A!Ӧ@n0+n;% KV»lmn'_3|x;bžhǺ5ifw{9ZJPw6?=,^Qg(]7(,E&;kHY&ſ-41`,rܡnQe@[0S,!Nn! Vk4x{QP鉧T{ _K]مڬd*t|j#^UZ>zԒn}"lb %)L8_No/ZXRA"3|l$_Ԯ@zvdY]8N;h4S:i1%OelZZƘnX3{c,oX7.Y&LJyRd)*`̱A+۫X?߷$nqxP$]xxy`PJy &z&FL♥ՙXqKy[7"2ޞh>~ 6 LF8i/cO,vxɼm=gD)O7\:<'Ӊ1r:K~+{Ӌ\L 6QAr;I6J^$Ėkf"享(2LG)0c/\M3F gdrl_8?`KtI.ن1ίZo{ Erava50fYqޞbT^zda̲a^tL38ΧAfSYGccV&+c{h";r;wNgٜ,=CY$ز.KtSժ#)G!CG X3K}lW ĕb3 }>CEI:|}=O_%\$B|LrKt T.f5 Èx$dGǟ^=G7vL=ل!Makہo@Př,)&wOS)O5 Ͽ1ǧVw2RH̬W?xC2iT G1}[g}SXdlrߺ5|AoIlLѭ8;*]a,4")0 Pű={*~:#p>'%v-ۉ`| P* 7j0+.3i0=/?`Β?7bdĕC{ysW_nšF:6ɣ̜tq; `껣ܥ>z{>?nƒutOT!S\㦓h<7'7-;!r1:Ո+1lAjҳV;e!|TdÄuhUMDt7S+([\>8A 9а=OEMVQ|W!Kj׭>wO0wWS0quR ԂVㅅD"ɪ~i9ǯReM<]/6]w1䕉{6t~;V‰k2~Vs Y_׉Ԉ@?=_m_sy}{!~- $?komrD8= Fl+Ete٧SCO_}1(@nwԺC|t*I`bʿ+/pXPzUq{1p^Ş'A`RֆTnY%o(V/LW/G>bsK.%x dևѨGفO2ydN[?k 8`8Ԛ@??N/M9"HDE"W)aOبoK{]$r'ӴiۚY_f+̞s<=Heޛŋmڗ.IhDXʹrZ#/Sޑ̮XNMH=W錪Sc~`cc)pGpmv߬¢M|W|p3}sYbE 񦤨k7pW"!$DG(yHN2ob1tk11?v:*-:;nRࡥ y5v5Ҡ-}4{ C^, ϿJk~UbG'UlP?m}ғz!kr*B[HTNScBHǶnbW1y}p16C܇rXM.m@Td+aKL׉<C \Y|Z6I3sm@|l:YWTeG|um1&[ek-%LW)h]OC%"*jV@5M `}rm+7Ar^ndz2DoZбV3%W/CRE~' qeaszXeP˿1 7C[v$:cgc0f2zLؘt.w^^hoߪ|(GmfL[LL]#Zvc`7Jԍ|x06vY Z˻x:!@Wډ9<ɇQq{(GX&6)LۋћuGe<S*_Y˜W/]>I>~ ;/`˺+"uK}NgSAZV_oHjUp?>F׳Q64 ksotFX]ŢyKY{0)NXĘ7Wp,BO>v>$1AvҊY˗JQ`pytbcv1sv]F&'(ȿs9nb8J GՆ\JG T́wXʇRQj@.kl|wu>O_PtQc5K3P}r"&-yJ޴PiNB^ao_Y~f=yF2k|xKCdYMM)N:GDAT`(׈wJOdHپȺj{yޘ{Zo GpjT]*}Pr*pȟCZo|uUaI*~3댯OK i֋x!ڃ/dbƎ@/|#6`x0}eƌ1dBfm/Qp"&-Ka &^O'wROc;tKyJAw8Wٺ3 L?ūս6 {5SR,v-[@ߡyiBp2t`cw1db,RG _}9/[Avw!0-dw7큔u!3O *M[S-H<|'G(}&<"ٿlaD?њGpE5`l! n**UԻ,vo-'o E'7WD _]e1t ֔ 2K$E+G%THS? Iv%ЙMDsxӶ XV@d|9{4dYMCϳ!>xg&j ̙֭<$ 7X;e7nylQ>fNxI}\HіrJ awW8GVO- qDKiD=4TCzk9o(*=)BؖOBjLd]motrO5$EEѨA͑RJy$ᖤ;Kp{dn_\"V<|<9QjZ+6*MvBInuWx{l) ׉-\AsB&;Dž9Ȭ;+ Hnv~oy"Q,*TL#b(A,FQᡒwB."ADCmvR.vpE4E19e #'٤$;޼[5ޤJ4nwUg8lڑ*RX>Ç/ArOQ@)A%$ҲAS^pSlJ#ݚl3w@XHVqG*HE=eBΧ8'V )&k{Lj)x'4Mi @ =Ryi;&6Vr·5FVCTl\N Y:|u9k] Ę8Ⲋ9ye֯bP5ygs3>]TEU}ȟ4Ā`(ylm;Nlִڅ fBC)K͓/@?6;1g/a +-b ]^cmZ :ee(*yYqr {ՂDP"RJSQDQDp *HCIH%  |^^G>9gN?3{9$ipw*r>&,'˙ʡ Fyg9ƛsg[je۩<|>ge@Ol %P/ h ̞=[y*՞с&0+ܚPMu47F@UݴxpzoP{R]Ifa 8o _\t4mۆz e&0w![2. ,Luؙ)O5qE)y\O,SNᖹYVxs*;ERŷ2ίȄE̜ч~yeB?4 D76ЖT>n‹  (i>~[.hd-;HOQddM(mzLQ@?Z!!X#og 1nKa1rF;&4%Om*ЮG#*krcꙈm{aLOM"$G$yv oz]“|Clj2v-k(tw67᳖UZZxrgK- 4ZN8xʼ9?$oԉ ,|YSJuta,hQo )>O<x3<6{竧+>?4ani֙:?#10G ٺNӬmڷ n Wj10s's`v{`qp= L0ѦSGw鉤2<vM'O⇝~:jQ16fCnO us0U8) XR^ hOq#ao=b5\D8}MȯDدCoa=.Vv~׾br|A!?r 21n v߂T’;8>ӵ=Gw2|Y-iнa4@JDgFΪQߎ9 2.JNjD?wRi­m0fSjEMA@TUM;e-EX= 9 bˊE>mU8 M YAÁF(ʤLUVGsqʈ(C+0 r]2h0gt[ɶ[[ "F7;͎(q.Zjd2SI-s\.u4x՜=onW?_=ɑ1ёGxZ!BeI)I) K#TʅL3򈌌'ƒ7WIzz. VyPU_LtX,) q,a 8/&/!_B#d+DDfLEs1F$%=|228OVQ);vSeS\PDBl"g"3(s\&ΞMx 2w>^%%+6-GTk%XHĉSIsx:npzKRlSK-\UATΜM!(#9->Ӳ9} ,Ҫ[חΧ7Q"LyA^jEa("2BcM1GNh TPKUPUU\I]|S6!\,?ۿ׷^@#BcLe"SK,bN(A2fNn gZ=/oG1 [FSLҊ4oDƉ˴#cS(I(zLM%.ûOz6.D%S*ה Fʙ\<:DrlƒSbA# TVQ["".R!8Ѐ*c8u!E @l%ma.'xȈK z{ b X]sgO6`?-ё;|rE c"ϲ\i1/aNO$J$;.cg.,rx:]Ą];<~r%":r**N}4>ګ |L2ryǓ~w?&:8H ;˚ {KsmlQIq@A2I iDg ҐId|>u:vS= ydUъnN'Q^(RdZtI9eDI̙ >^L$ԁFQKtF5z=zl"ˉ@I&@g㻯w婼ߏ .RJ$^H".D^ݩ+ISE,DD_KQr,o5QƐVO\dȬVUV"Ѽq @I̱ѭ@BZxy XĺL\/V}IlmZ3{`/) ΄cV!_CRDN 4ė9_j#=GNx n}X*JΡ(rt5 8VM`xK-+҈Ͷ!7=Ģzxq9 I׀-}h~m}U$JO[;)~u}ow]-~(?ۧ'*^n_cR}9gO,Re3ۚpr7WlHvSŐxs{atUj5oww1`uS‹ ?OBzWux\dG5 )mڍ'(>!{$=cYaoqtNK4qjb?%:C9v`wDz/7r uB :AN1UZ"OCi!E:~L&;x/d'[4AZ~졠ĆYA'hP 46fP .ח!hBScc֓JBo݆<3k剤T "C_OO(Un10iT bɶ7v -UX<.7%v\ zZ˱ 0UB7Ie.йmLnhrqz_}>VdiyF_-fNc^y.2 e.jmSh0 V&E [PJ~EGW$ @tRT`.?ubo 2_iY%]jLtRow23dMH^I"{g?* c; UxZKXl*A`09!ػ},cʨx U9p$ Z z j JRkx W$FnIn sUNoĀ]N[dD<Ҙp8h:0jx/ӕpIlNɃK;evIX5K+W/Y}ZDrLS8etz=:QdscBvʞqO}//‹"_rv@^i3 rJbSy=TRypdwŧ˓dZ+9֍HK2HGoC}{1uH["R hɹ*530 Djm#Pp=fy>ա3INeEG>t=bh NUWӠVwVvPlw#^QPVR?ƺy clYԍ !܃4.W>Xj冷A8^؝>[q&ܕ)yh#?Av|!pkdw IDAT<3idG_Ā)kp+`)otDZ^·O 3thf5h*,{ F^8}!vmU[XwM&_$]3E11i{,Y@9 >=VF]Jvډ{$2SX~&샇 f/[{+hb@UVD~!4 AT%"oc֧{KN‹<~_'lTrk4Kbي)qkhH'?~7ץԮ':RUUmJ"\E&Z-ZƿIj J|wLLP_'ۥw뎥k$TUUN홬͖r6]aPnEA$_t7૮;# M(<ة(ac]X d㮡CPٯ1~k}@Uk˾Amܕc[Р&m Hz!8&:eջC1 zb[) OyaV_6^hML%FO~ >΄ >5VcΎX=FcCpOX)ۆdڐ:*hb)lC;7TVs)oyフz;DY^-/2sW=B Yȋ:2m(-%m4*·h]KL(Id%<4mj⡇{ W]h=E}s!Ϙp]xbW[3~rb댟˿=5> oJe4,7/^@R@&'~<7^g%-qɸrܮQd@Ԛй**hiC Ohk\aHX)nr.r*qFPWh=[#jSV%$߹N؜>X6;s6y:4ǖ*/u rA5razLO/$.2~F=}'-L)@p@ -,i-UqvmƝFCA0Wt{Ij#Ԃ)#Vd2 ߿K|= ~x`'b܌@F}~6U3oVVyJ ^V=T꧸QNLnV Nttujj5"'NqPg$ @vKğ ],fuq(V>u iu`ΠkD6T .ly{2ڂ(݊I AqK3^"^T$:tptVanXAk.RQUMFR*Ra3Z\ODfJ:flȊ(8p;]TW+Ccl@AR ^$qfyOG@^B ^݊գ;ֹzKerֵ moa{ҫ\ CkF~q) ch'=1Gv.[hb!NC;QL|y8I+Si(t$Yʶ,+M>ZNJJuՆǷOfP:t*|PlCVTTD^3 ×@`U"{U.5P)*DOS].67ţiWt`TEDQUTIyBi-`6:|O 7?t;s / $2lD߅6X9Fn ʨ*-c{fQm ѱӑmV> N+mQxFڝGiɗ,|ɿfvۑUAc@RÃJW _dUB#ŭ BSX eSC*Kۇs0F'|>u|6O%/?p$+:98"D=,h0t;q*$c1Y[d ۃڂ"5G.rG5Ŏ,A!1:WY. %)CzccD%ͥgΒygʹ|uEhJ@pNT(r>DUAd0\^Z [5ƴ"V$yPdhѣ?]mTSR&hӶ17@5xz-xo G e4uxLڪy<=kiTחXŅ NFϸ{Wh>u4+v}1QgYVY TK`h)" QK`zk̊vnGonX<6μ5 hdKy,tkQ<=!949 '1S^P_ϟqw zD8jPA$0h(AQ<v "Z|zʝh4 uټ eȞ*K}MpI\V^5K2t2Kɣ"5ة.]^+|`-w!iwQJf("FW-@lv7>Pa'|y7YmF rM ]{u–KR9b{Y; LjsAdF."1E7j41Ev‰ o~˨mEŒ(DeDrKώG):\;iz\LW0cRnkmH"WJeEF3],+"*gee ծ|b92X=` |k|x7iqDU0Emyl4UQq>`%ϡpt燼S넯b盘'{غ| +v lLnwL x|.D?A¬J+ 4wOU'?XY ̉Td 6;6 ?:MftA;%<Dn!4iM'hڻ#vmB <(mڛe>~b9&IwgSuTOrP $ϡf-yNh\ˉdkYbylV.cfoҡYKFwUaЈ2Z;u#, VˬgfҺ_=}'s`XRGmD"Pw.e]E0gb&"Qh]{2xo'kxѱ~IT~㯐VoشS7XSma,?U0qcӿ> u=t[)wmV5}݁YSSVK?ҍaE˗å&>+"sm8y9' xǾÇ˂9cT#*U4 ⛞1sN&!=?U,(09]jކяt+v}nAQ]`\@`K{8;/Ϻ&mI ;͆}1"c:=::%*ޑ^x? #*- Ԅbb1I^a5Z0qQV>@,+8q2GGZiV{r}vQd*%ZBA<ZAA=/%" J]NDA:| f"Z\ .A8)SvQRICBuUr D~AOa#3@v|"'"tr%t>>pRUV̑yoo-C=ȒRJ( \1sxYrK&*grȵH9G%s!.d **!,* iE^,#Kn1 KډTKlL&Vfx#`-\\!z¹Y%:wo-4쪀dt+shEv3*Xb%>2R'JRl gPHVBlDG`2[]]Ĺ"lV/Yjj$&&ZAEICQvPQTop.(ҬEd+옫ˉqN[ALH#>/"PM]1Gf{,H Ddrm5$ɍ%/iv*#9 FPLBj݁n$䑔Y*DEzfT@S cSn"732Ue1r!>2~>C?aWEDEAf.xTiSH,xTϧeƳ=Yk`l&" nҒӈOIh}p8dQ(%g#I)JrrXJIN:G25dNLe:t G6 /!yw5Ӏvzt4녧< )kPGFWtU0y+޴ }o~%x6[nAM:vay_$@=ܵ]m˘[^x$toצujwa|xvQglU0믙 "A@.C%ط9 !R}}5 AA#35OΤLd/7lKM^Y, enB*9sGDz:Lzt<3郐N鏿iRT AfU낖@ZTII s"`E6m aSө_Az/4+c劉ԓ$NAAf -+?].&n ώ]p$$eLTax`pg#c?_6?~gq퉎BRF>2&b蠶d;O5L۴MY*(+7 r /.\n_&=e3ɇ^;#AWHLx[]@WwY:SI%Ч=4o>9hCn~usjLI/"eM3JIб=փF0g^ e޹ʗNLO3u_Y&^L)JU; Qs[:6U }deA~a%zc=.7aw_TI}iX-v Mt߇6AaJ_ u}RUZq7op4Sd04\ai7[ /'-j~pt7k8RfٴSչ ^{i-j`&}~r c|S>(^ puԕQ+?D@u O `0Mh5 Ztf]Ѡf2F&i1DxAAAE z-Rduɠ(ګ>+j"ZΠǠٟKsK:WxA(X|i]si]S3lWk4%j~: Z>KQs=oИ,m4ƶeKcIc^; :U2;F[^ci45爫?ĕm Wל#suDk&_T5;j+kEWZ:w gR1hZ%ϷU10ۋ0'!>|<6ӻ^ DgaoQ%u d$Т~+vG%Ƿ;۷G)5[h8' B" q*G+iڏm[ "׽g{k:E{3/"!L_E,͛ԑi)10=xԜ9yy+VZiлL{:G[ѷYmJfT0w"PQq1i ĖR]&Ey䵧xsT a3i"r>Oc_8;2EU!f?F@a*h5⯌'_X:q}" ̜j_u~OQrX]\`,#Cۯ!m7Ϣ_7y9%AY"7oA"/&*ck˦>|/z,A^xOߣjE@]U'S"hDD茩2j11ƌchkT{PUwuIv եbuVGbmR=t 9wL!E# ]m'*Jx'jR~h59"vEz!t1Mϱ*;zt7~qvr-]0\jߑܟ"9v|Ͱ t=(0ٸ宻p;Ĵ9i)o*?ڞYذWfw%=lK"Im ˅}8[!rP IDAT-®OX].;u P' "/C@<2{{{v8'og%L|/muGcgyyG>QG:I< WSv{3rcooˏϓ!L>"jqpV]2o-PU4(</~X{9p(Bֵ~N[GzdT\>eɟ U%"'`XS7vHΒ PIk&6[6a7'EE5ZÄiK`Ҡ֔Hd<8-o垎ujZ >ĸG_cyL^4zH<2N\Ńr6Nlw.q7SfWY {@W3eWy/w*eYь~8u4 O>BO^xa3xۮ ezW}ޛSסtg j6+Hۙ{* "+ ,|eF-;)ج{h)&褗YrKԽm7 Wa줗zƇ^x?kbmxn`.bUSi^ aݳP1ﻆҭK`w(19n:RP/#5)Z 5bA@ UȠRv9amrMU\!(Ɓk:bNH\FOCs1*S\UeD/dڼtx':kʏ_lِ&m|*GQA5}ty/[AeV2K_B/UG9c| [=c1떾j0=J0#na+yk&?JO o.\N>eg:MVNKMײ,(xϴxd Aք[".ln\'Yw霉׃BЈ^m @EX`u)9&:HzDUWԗAL߬D`:3Ol!Fp<حUXm"C|1Rrӎ#u{ioҨ$&qڰK %Xpܗ z+ZM5jeȂɜmݢxsgjLo3/=0ջ$b̔V[z莭l:ZPm!HQ($6#/ ˃W=詥 Rq<n/ 6݊]9A,_ 91DE+ISrpCqЉ+9zغa5c޴i3th=(/spj|rB3Y(wˇX{5 _(df|}$a$QQigB89zpvy0TT;ШTkZb:eC+!dhfHIRԉ2d='zZ: ۅ׫#ɸ$ՅUC572_sg.2w_t"k !4j&(KjpתqgѨYiml]YFD ji5bnpPҝ[Z1 QY,IB$ÃRjjˍǫ@) x$ dfc#jzUN7x!tKp"&>Ċعo9}_r>| 0z"F!qnRc$ iα}In[.k3) eUF/?M)21"K1kKԎ? =_yKP Vh\F14p!2rХm7kC)uy=%(fHRoLuheWMEѐ+_{pnP_柟ndcn,Jm^];r5Yz%xp!5ı0< UWyA<]{Lۏ܅e쳼52Tzh}'>KÛ m͙[~e ,L}L?KcV,?,ػ}%xE/׼ɓ?`dtkV3Kɜ]w.n7sXbVYf^SPrz<4C <|#nLeK m_9RÎ ̝,pKP\6#+|TSΨϰclj[KdǪD"Z&U 7 H+WgGGïxe>hEa|[LzeSlK**El<5 Ժ֕/C9[IR{zDnfL܄e-e>f~n\aO盎ēOz4a]vnG:Rm Ǧӻ_/z' ;N+q`R:CS7"hR,(\"LQP+M0(٫8a!0d:8eX Q*e*U qd; NsfA$=jl$O+r!S&I]2" q)tJ@9 hѩ  ^7ElߟOʠ.*ZGdKč}-GЩk R(oq7%";pCdÁ9QFm>(ܽznێcE ^sbbIY8X/Z{#==C[ٶi ˁw+FrfݵיE3` 48O^z !dcc OTvnͤeY$e RNvn=D9;^bZذ_m !.-.aXJ ؼ,?8ze 'GPO:AKAtJ]ZDaPC־ɫGbӒ^ 'vd߹:^E~&ILO'C[Ś$%smFsZ tL &sIhP'9SfJ t >XMl6DEڧӶYjþMȪ< Uqs$4Y^yxEVd$S{ gP-G5Wod;elID+]|`i;ܦ1{4'@%H&Ȣ[aDvZn=g=pn6/иJ0LEB)7 Ɉtz4'`.-:SrT=Qh:!293u 禮V~qZ7IйIئԚ⸵k,:OS}}oO}> j1Ȳܫzkf}ء5q||m,ϹJAz\gcUX,@KYÇ>|I; ?[bCHvrJdܫn|qEYʥ#?ǟEd/j·x$J_Ç>U;(̯ΟW94Շl^bOe(gR|!Jo+>:3sLgr3Wcی< o |U)pIV<2A>xs*1;6RXeiL]ƒ* +)o;IҝE/Lis2 TdLT_'S_$CY(̷&S_OBy%JK(̯~'G0eogYe%; U؅OK&^7Po ,yLz+NcْĊ!:Ň>|\%%(5Ͽ qg?iՖ%o̩ #oF$U(O>$N~ę}yjp;߱:e`ۅR}d#ؕL.mIP?J4: o"g"##IIIk2H m FvۘMlFͺL`JG^~k5_d+yqzQ6_,JX .CiVԨ9){C!!|P"7(-w="4l8F%  R mC ";wHD%77={WP|z%߽1VF@rqt%)ɖci/'Ҳ TS I7\nێ҈RO>*R0EJN5^NgDL .s^N]ץUÇDm=ɋy #?BAFJX!wxd$ v}̢'3\HZ?WF'0*TE5F#. AQP^xiBYA) e2)(%QJLJ z򲀟ZBsydQIDleA$"̀&+Lz" 3cRT j&x"* 2f¨Q&2" *& jzxR!5W^H%Af"CMTMug2cR+D0k#_REH?!Fgj ah3c:ˢZKd?afu @f ~S*5aDQ+EDQA\Jѡ&T"2FlDчS]%|bo3Uh  :܈JjWyA>B@<4g.}iIOj QDg":"0DA5VzS:뗽؍ HmN(d?ZI(DnK6č6'Ռ)eҥ<:y9Qru7Y*.-,ٌ:G@jד)c7&6VZ- 1 403dhl삀Hd JEh>|*K]_hE |^O kFPs 5gRaUrK2xbT(rhd_rN edFwBD.UһE/?"_<}Ƥ$asZh9{~8m=Jm|[YN\h7F$aqpyEDY߭?G{i>€s.x݅FٯH97 ;ӅfyQplO9g  efCcmߺE6$M,bwsoi>nǖͩ=ͬO> 1G:j2}O<~$wKEәu9_O@DDˢa҉(^}m4:4C'9;Gi|6sAL"fBwNVJqqfO]ĶhcxmȞmw7蔨Bcػ/4V3dedmQT`//e: Rm~c^Zsȳ~o? Wt|i&YQ&ndXĬ{SeSK9VᠨCp5 {)*s={>D;k2?a[2]nL>ͯ3_We/Lhs08C3e3'!k?\2 džMGLT:09xHF5[ <:- J{1VbQ\usv{>El8bPl8&>7[[r(M6L/Rn5/w#=ÌH$^f'8R)0g#4͈G?bYA}ci{3-=|1=LjC}M- /p/OT*?M ,6]sQfgõ: ,6?{~:=h A9Z!^0s(O#FӨY,"/qڼ/@TY1UT6罋Ç?iL5UJ5""hM]U&Eu2?V>}GTwGeg}Y6ڊT'D!:&{> cٴlFtݱM7zH1{Xv+;I2p}RsJluhM6놻8ek຤dr vuhP]Hhŏ|~][Qp6g??+ޡYwF]#r]32A6FzC"nO;Sq} !Ou }ܟ菲:C`B_էIx>yGMD'C;%?djq ͙sǢ(r(S>)zFFd'~wXֽCmyH·KhAz8?=Rѝ IDATȲ` >:w3#'h::ezdÇ?u2:^zlpaBOv c(Y8z55^kǏgh[&<Ӝ'p3̷]R6bQC9s ֡Eh:!T H))PP TԖ 45Dī\H sT WdZ*/e4۷n':nZwf;D63~Xjr;] vg$N])^Ώ?/ q871͘L擞z?p&ǢO" bm~{?VPT㦕,#TWuf)ǷJ392ip<uퟮfCM19d/ T_ $%ϏHJ5&j%7?2~(`jz$N;e,kphVp;!_%ӛ[lrOOD Y-#{% Q)/c!*eb;3ch@̓,@޽H n=8 ݍ0{ Du*i~Ǡa?unUm7v"[ԉzd${4{=߲z*uthP_JlR @\lYUd f쭐'#iJxa-gDc o*8āz+!+-s,3aO?X/V3{nl/]MAXUjh";s=ӃZF]CURDђbPa?/%;ORHoWsZn/#cDAp&Y`>>‡>Lqx`H z Zi͟w.Z?Z +F&ξ@|aF]Y@24V,#+;15DY]yx=7 A!k Z#No&C*zLNs+θ6fwS^`cזx|M6z(W[t.=1?P=r6K x֗ٸl.;vIbojUp! 9m%<ulN9.%W z$s3k+P njl~\8k:udbLU4;WTw,tl,GhQ$lN!悗mz4F tWRv6sENרiDPz28%,( \V Bds>y.Dd2bdVW &K"y%/u.R]ue /PWSkRg kf`˒WXI!.ӵ;e¡-Y&9.fx`x=G'XZ}dLnOkS*8jYl6(@ÆB۹{dl؍Jt 2H7n -zp, e6w9ˈR6£Ha*Y?,=/cפeG@ɤ9Ȩa{QnRd<\['pi|>˅zr@$v蒭$xA^`SoRH@tv@|O Ldް֜ߵErQŢknDDfو;c٨4PgfyB^~8 X:{mpϋo>۳ Ǹ^ᥪ$wB4Tm6YF)d6y' n^X9/x<|Tf=oiuE*6 OܽS?y3H$4$ 8i,+aC(-LA%]V^<3듘<ٲAaw@560gؕ0wxyt.tGF3wzM<H?e* &9B-DŽ|9w&qG]_ZKQN}[Xw:=@k6䘩ƝA~@IV؍)ƱnXѧzq뀶|F?#|{L:ՍH*?f?;ury{d9働/[&9/UPܑ NF7UiSgIOAmس~)&@~Fϑ82F&͸㓣19x bK/h(?⩷Љ`Nk#z1c7ܱ~;:Oq2 Iʴ9WT&'JlA)6xga.L'lN7,e)fUӿu{F<JTVAR:Y(LUR>Qtyyo +39) O$B̷Ʋf2uKPռ$<k{Rhʾ|mƫP@&@z\4"e7Cx|,- BѸ)8+1Bce4J=1~&tH<bq9r8M$RLBƮo0GvTFbhZG N; ZI8gQpxEPX>zw2⹝ WchBtfvZ KԔWp8tRP(ڭ-#ҥsk4U9l;mEդH >̀/M35rz%#MP|s|NDO64kBrDEpx|52D!5Pť#> x8(u)o`Wf 6mlw/JZdc kyN-zV>|r~JD˄3˱&C&?*Ҩ,;yWlϤ{ѹ} '{|_,-=qY8j4+}iʄj[3c:T:&:+wTF ?ڀPzU[V[|X>|;W.@xzDŽ)? zZǏdW^_˹2>s(Py0ϟ8 Un/}㯢B lށofa}Ç[%rϔr {Y>ȇ_瀦oÇO; WmtRXXOa2X߯l]SK.\ 164#2k00IaDxQ||ܟ]qmV}ݸN' YW\ǿuǐ`G=?))nՋ1\xuY]dKK7KFK7v2w_Fj6?Ld6)QCyn5$&f8+*.?hmQ3/毼^δ1\SXbX^Aat)-ntt1z {ƚ,3mnŇ>7j4Cg^_}@ΦL' Q =93DD>Z$6kNW,@jTdY^T<=0pUfD˖?oBiC\ߟ<-zQJ%.ڐHZ \=JߛCD%3s(q!umъNiX^lϱӲEe쟔_^& ŧg8Kpm>ZK:_JPj☐%7I~IjDI;Ç?U9""`'0v7fPkD=~76dٸt|-|L'`TP*Q Qr YvQr !`0 PW.NdL*=zjNJ&4 Iy~0/+\ ~H l v Dxgq Â`d B6KUq9%ϟ"(IIBj':A~Av B3k(>W68lA쁝:=ƴ\=&D+7of%%\"i,BD3ӾE,‰s5Ȁ1 P=:\^HT2gT F7HM doJdL< QyhS ƥ FQ@o'1ĻAeYdkRƄ6 6.,:WD%iQ`k$}cER{G&"2)z/)HJ~߇>Q)45'7 jb^7?#.#OtF*d c+v"bɫ(Eh %ypFNrTk8]OΕ9H)#jr ]KC?k=N4j5l˲R#{1"1*F!:$tcW%oϊ1 iCcX->%>_mb,8]|>s7=.B b㭐jKXß5>LNni-iqAT=ӓ)}/ꕉz=Qddѫ/йIfRGs2ck&9qW(/e'8R!cP< $9x'\*D/^X]k`G~s:C>.Aᖹu0E{U:ky&?'3s&OC5ܱ̖wȷ!tD^gcȣ-務ew{;`wcTU3mXŹJ[Dz,9#`n I`p<{6m<:EP,g]Q@n FOՎ6[/o@1͆Wi)COPZƚYzL}ҷ{*Aacp1E#Vcw8ū8s3}MA0.fɄ%,߿5Vt:d3{( &*9N8H{|5?I63qҚu;Z8p摹oKgi/ε+Z6wN#NK&6\ǫ#*g>J̴ǯ%&:UI FnwESU!"9Zߍcg2NdRܣ7cnCfZ?Cv"~tB\eZL@ضfگyd2-(W,[;{p[K:A%{jG!w҈*&6uj;ٹ=w[3so2K*iЬ90f쫴k23 X̅ :B]zw4̺3>όm2p l6J>|KDс YpZ{G 8 by ?Ru3zJК yi[0v|ޙ4fBSPP_FMk>3ؕl'VNV/NKSBQPmYI NS PDAh2s1ҭ cu/ղWU2Z:_gIJv$2}i3|); |n3A(4ӱO0 (b'2cpmb H zIݯDo ;VnFg^[u-2t g}ƘON?2C n9G.5( k2un;GTy IDAT1=pKW;& i4t~CE[!/ށRzO썘{^7 ާȕcm8M^8aCQ8`\Z>&,!G'p^8hz[{^}[+O̡S\ [}-@f:ޒo? 54iвS]r#zҠZVdEYQwRxl/Fveg|:[дCrGMs۩׮ |==mmML$ Vv.X-q<5Y;g$T0<üOH(`ĸ)) ~LA * w{Z'nC 仜it!<2NQ?vk\ E@~nеmơA]2vw)“聟֯#q|Rm$ԟF?"wcKb ms|:PYR^e)pNgΖ |N*]- ODƝ{2WG}I-aXsiWS\A"b q1(JDpSCvh=p^[X6|y.! "E98-ȊP:~ 3d"HT8\QF)Ag⧝;ylJ[ڏ&mY,`Bjꩧd?&)I4'+5>VIYf 3/d;6Dm8ZIińF eK^N8$ PR}/X t'vϧY@n$o‰s18<-lh QG]kG$wx=x/ b-8etHEAg_=?uuŜVeY uXUNvr/jx=iQ8h. Dd[쭁藷W\UK|erZ}^'&҈mP,> onޠI)iRIQDs)UFˆ_ppW$.Ċ٘%Apr\&ȑR> t` f-,ɣC|AFG.ca;: x۪ p98YBTnރxqL@XgaHz%ŧf0AeϚOYeB'(Y^@)*trEl$bq".ٍS9ѣU$QN|(/^Հ(WFD#])"#ةkʝ\85:}.Vd'Ȩ`p#I)@,=&3~t羥 ԄF  \ͻ8z@rg%>f: Trd)fHz EPW***j50q n X{NBV846s`JB?y|nhXD2'Ş8T)J2H9O5L%ygN" (^$@"k]T?%}Sތ~-m:E +c^7I2V,}r+Q19~r78KR%+^ʊJ\n'.dLEyND B/﹦ J8utH `Qy!Y,,cKࡻy<23/}4nܾb!~T:JP$ Vd󚟹S}n"BgV~*ߒɽ6pSXY@&>:]Fo%gqHPSn6x ondEV(//QZ)ϳt[쪤N^ ie∩XbݞKX8=\M\%G|./\!(U1֭Eb^% j{1,xKqUyALCDYY1rTiמU:% Dz9٬1N/x=E…UPd/7(qxQ)M'Cx 2.粵"k5@É Cy٠APnl1!8+I*N =(OAd/e />eeS5@zqy<(((LˋY;k>/sg @C%ًŠT8Dh9a} (,5G,_2Я}-Jl$UTJEEE̿c n=cÊgE?s42||2w5CPChd$wLKRZ[#);F1i75Q (Gv!l$VWFDR@өٞzr/8/Lم˒M2tBΎLvfb֖B2wLpGh,_GL!{I{c<[tV1řq=MF~؟N'Vt^ Pq՛'V48MY6_!(/Y̴!(*zLףBkݖLtU•;Ħh=1>7#,0"y{ciN19лi$:KϮ~r Ӳ|02:R1¥^)+B||7>lvgS9Mp a YɘW3_"Gfۯw\hbꢷ8y}ۅd`?:ov+lt;tg=Ϛ0>FODv0)^wnxf-7B:+IΑL!ce|=Gr*.][>}g{ߒnn>{y[Š:I2Y֯zz~e7AmzlL; zl;/ȢchouRR';q85X0gߍbW2kbE+ T!DvxD3O&ApTvlm'Vt̜5m?&40}/En'iokP];C.IBְKr|n7RocW^"{gOߕmOv~Y1 oޚO\u}8SgǯԱhNBkl^40Ȧy mW'0ihGh8",,?{ .I1ϰӗi,gOp3UZo&9e>oU%NG9Cy=M|=KFS_ta2lnɆpy.?[ϖ}vk1ljY<>'>BC4MA4\=(hi$ Q?/Z]Yo(V3D*=\Dmh.>hݞ2]Hނ ZH)B:V΄NrW9"+fA&$@+tlTzh-z_H |K|R!1 #*2ŜM+-{tQ~:rD^Xt%r!:&P?#"z*2ԺI|2(E.֝K:On7и^4gIwhiZ? )p#L$Ƈv.+fLZ$TCzaW@sdˈZubC#:yɏf;B`ubigS 9U082QMJΕ#z?cnΝˢƒjn@$Og(u])wDP+Z͙hg2U\L^F/$p+"NOQERLrnŕy({u ɑ$!er؉_PqN$XGS' GQ.AF|`+ƌg9 /sWb#V9_LNEC^fy7^k#('B>%/ZZ&\@eRO]yDDL(&Dc(Gᯧ8;?|%|D$/q2&k(S &H*h(󩤕'䳉#W&j '$G@p eN./40YDƆ-%-$0+tJ$j9r, 'z6 ©TJ] +qHJdSh|pOi1&,m4cL\(E*6]L1e &!Ҋ\UƱ\ (D #߄(Щ< |mԉ)tV-GT٪ fTr(!1Xj8ڨX\yͭl'1@f~ EҒbS S?*#VGQQNx(pJ)񱸳ɮJ8άBc4SNbW,I>ysd-m]ȥRPQQ1 Я' YikDyhw4ߚXGvmD_?3d5* `ո6h>=V;q%SLfu(>~SE.+ 1wwޟN߂/{h2vѻjt QQQQQ[;*2:1=U9okYh/`^\ZՇ?\NI%BߊF׋x˫] !PO ݻ |{55:T'Z@o~)P{HEEEEEEEEEE76E~mw'݅/\Tjh*ţwKAE6 ܝL馴um~\N7%N*/wBe^?\Vj_aӚ<׿xX6 nXBEEEEEEEEEp SY6@1xE+ʼ}\KPj_oT+ؼa'cm!EQٴGRg-.I dc3a8oOp]`+SF2x^ZȞJ`^ nYA7>U+2r&ٗ`+^@ x|j{#<ЂVWkў!XqRYΧ?BF:0*mq.4imHATdêxa^LķxMCbh0uتzl&"-6 "d'[ȗGV#UlZJ~{A $2ѩ]=Sk e]}Tu).pytvf(Sϥ :i%(/fֳrߠgY9XyƱ#~Qatj߀[sq,V-YR+|d sܪ/_52\Z=mM+agkDMjNʳ)-VѼ?j"4:ɩuokVy`=E]xa!;0eWj91ht=qwpY=GTbrq~~/tC,^@3dHk^>>Snx]NG4* r45iq ?oی,ބQ ۿL'0ͺU8ZH9Fذx5H7wh0Gxr5ZEEEEEEEEߤ`yydG=ȤOU uWl`w]JQABQv23l=S;?U|ÕtcE'Xp˯.Bi(FH3\C0zY,aVl B 3|6ʐ77"i14Sv&S';O`zpWݽ-c5 lx%gAl;jb¡Q@ľI7 'M"<@c~fG/P$RY̹<[#~ZĿn[z-s JDjɺsOƐ#_.f]pJY` onHnRrx0Uܤ8uJ婵h0B3r{*w gNfi:gB 1|hKs:Q&1_GW\UMDTTTTTTTTTf&:< {ɔ4H%kF2qZ?v!-Ȋ|%t#(&BBE䫲lU[/˿rIī'"KzrdCTDڴnR@#`5 ^YFbvgI&ӟNy&߉OxR'.'RU}Q׸f|] ZGR~|NJ8**$0 ne{9 k`T 3W\QUl£p{D EIR^pKD Q>nΟeoqGRzF4ddSu,o=6^X4F򋫐+$oas7" 쬢3xh|*9Waץb;|-qd.Q@vޥGrw/H}H(7߽᝞(76!y<|feW}|kfhͺew`+8y= Fm9y#S+?ƥ: ( .@_Bn{a1nRl%̦ ;tؚ&#jb1hQN.d]5lFH.A6bZ|'7=yN\^'P[DC3Bf+yydUV+FdG\jK eh|lDg|;]W %jMjJ_X(+½R~~􈲗;veˎ5R;D Md`UnL';#2HH?VHEI Ynw( R^RBvK=*********rEwF56icwEcn!({Fo&V@(Q T( }X;f)"ڛ|i۲<n8$ %>`l: ^D KY3SXDKFZߡ,>UG$Ѩ(pY IDATm@AQdE,˗:>nl 2h=ݻeAV3NS|TȜڷ>0p*~ eǦDjV CQuxgw݆S N;h2onpg&] ysoɼ(e'2:Uk(?֖܃w_z!62 nK "P7岒tq8TV:%WQW8(8CKMS%fK8[cP r)~FYE%ZA$OlܗlvVv;DHd _. (кg3~$ERG,acgɉhN ]e$7m9|R;6oĭ QŪY>Vߞnd%K]5&3):& QDJtywlXݍ>m1'D&:M_-و T{7`Pk ߀>.wdȀx"wϡ1~C?B0/2L*{H˨' Tځj˂`{dT7 ϧ4̩CI,!e/8;iciܻ l]Z:TTTTTTTTTg[Ek(Vޖ@[`;.+,%y䗸ӎ+vPDQzIJK+(*J/_H~!fTc7BEiޡ-Q7 Wj_<#RIC/jq@еQN~=gEUPQQQQQQQQI+(gmjF!{}ݏg"iE竈K(ݚEЙ0<1qjeYO~q5gΐC7~-%UPS>Q~9YߜE{C}O?m!Yӌw>lc!qTƴ5.Vr@k{y j8K!BQ Slw#hu5+ iX:CYyud$(.epVT"k TH,SUEZWTeUWYM -\7)С5:CPX@5ὒyKj);.qZ ]EEEEEEEEEE_E)TTTTTTTTTTTe _Tynwxnrߋ'~)8MEN}_9d?9n}sEXs{UH f!ti_2V[yKd_]Pl{? :H%>|/^3—--*)2&O`S-)t P|~o}6_*4xi|ˇ.[\W5,^:sp$B|`_2储lm]32k~{_eMdkʖՌOE/jH'o z} tqڲ96ܪ/_Vɣi@@T=|]KXGRGNjOO_gƐKP*=l&Z<ū-O{'_^faO( V9IueVpQ8I+ާ콄hڮ3rhW,f7<+q"n3x ݲYqQg( Ӆ"oNޡkbų!:"cC)M,lLuA~8kKK>\[s>Uϡ[7d #;dY=[W7lt<#kQ4{iYZFO.mmr4Qg.2k"1Y}xOߐA$p(5Zӵu G9s0Pd/mtqĊsxeY49鴃k؉( (ǙG`WX4b԰Fd|k94І|I4qt&n_{C *pS@ MڲˣdgF2E|HΞq:,g"$[3&뽒FLp~J!G#ϳl74eg㱕QLMҎmFF[;M vIiٵ'b0 h3 *r͙8;;/J*tȸ3u.h"1D4v,J\B-Bh&'lit"T Sâ<Q{ƒfuѷ[=YJe=|EɩdWQ`aO'x&bD&SӬ pX`0Iim?g?W݋sS˃>PC4$bhcH09)wбW_~vO3^Nj+W1h< B=?JfpuciӮ%J~aGz) a4h@PT ctgYs******ޏW5)a}/SQ AaqX~(CnwjF,

E2DM>8't{f=zi=3a ^M-W@vmu^u#~[/ZhXw58fPQQQQQ77{]{ObʺAHf^7Jph$]1i@BPBYf.$gŞ]fI!#^?cE4m*\A15<BA>z<^O`堠C:m*"db Rˡ3=J62Yt.{^gPS~P>Oρ/2uĬ Ds)\udx29샾Bϟ_Vr2$|/:©<:c `ohY$Id" j4@NJg"8w S?ًtM҉""8,d&u ZAA> :kG Mvk6T"%UeX"hh߷_ VDtԧ^/MlOAa z-A3f& D 4NN+&.$Mf N2~m4h(j@p9z _@@o)IUMlacQrLWMrGW0jX}t3:a}NAV* k&ئSTTTTTTf-.X( \aloIL'ݠT+(SV}*>9 QNN^J}Un(ʵx욕QPo&tS{֟"ZyhXO ?Aj׍"tO:qP25ked3pOǵTytk,=AY<}טj!b(~gY$O0{338c@AJX/yo(He'jT(9[Ss+[H'ygDu 2Ǔ\qhղ}L ʟҥB]^Ug^1A1 F#>N-^  [Rc$JvAƌZHQuhP% _cl݂PU2ò'jE2 {F$gV^Ziww7vA~8k4ΡЫy$G+KF7 a S͊U{sl2xp|z~>v;С^'r\^/o4mv,dYOiKdVn?OMڠpF h'T#}uíL$i׳=bN)AkԍGؿf=;2<$  \_&Dug2$ICp?QwVTZz !>(ui2)-h5~˟K⡇пe 5Die@Za޴7qUmgy*Tgzىw8Lw=W:D-!1 yU#&nghT_xť(-V6[{Hʼn|/q<8 A6B 0#yU hHx l{9غX_o˳bd}+ &?‚#h{wbP>)?؛z‰%̢"RAAC8 }XMWm˧6 /~qK.ᑒ \:wԀ7neW CHF Fea6f@ 96JBM>CņV-99Lē^O]ƨ}xS49 ӧ㌝:q@.04%'Xh!_Zy} s;!|Lt1zP$RRX&BH >zn}_}C[%$W#uu$ktPTXLK3ՂTR!~0c48rNE=:u>=4BNg*1  7 @[,kN#LQoIwK6T>7Aƪ=$tJS8nG[Cث;3i{vr<9 }).錽奔i%,6M*7j$+Tc# IDATKKbvDt䁡K'+$wҢQ# o%&1pIgPd'Dk'+0 KН_ϔRI&Ϳ֕JFQ :<$)t-{'q`Mm£Hۻ IqTz%Vg1wIYEpd0Ɍ$J/[X'L0V1׾Oͩdzy/9cgp%KP$ $Xx9×^LOOc|3cɇCHpӷ |~"Ź |  jZl~8/'u(j7Wė:0[H^+U@#NVoܝ'nI݋4kZPeو:)EF)D(@5xd_د #ӁKcU$LPU*T f %n0VZ. ,>*+ȵ_d[MNvSTƥ@ |+V2'n?J_'* E?UKdy\n 3aJTV8(T@׈O  P#_2RS   ,BAA?@BeeTxĎ  ?_" MV@Sg(Pњ"{v6S.ts|&v/>5ānT^"a<N؝=k_gNGŮ  @P*ey?y XIr>cI56s1ziϢ=/f7ҶS9 uTԋ?2r*j3ByfQ bfo&׈^wSqO{_:;>޷y0s>Oc05;3iԭ?>.$ (ڴT'm'W ۴w7]`3AALV5ѱGKHqY/-f]  @#y`DZ9u8F'1)u" _s qG7­P v'*2PRA%*n"6;| ލMz]$6kͭ(Ө}7t$n͆ gm(^up{зN(iq  U|j3zL֥(CGh.e" ڕT[͓3U1[9*PU3%RdF 8Ƽ Mq:YIBZԏґdQbł ׽])Ŋ5kڷ^_Ǹ`&5Tn վw L`U   L0kr_|4 g)&?$֯P}6|5?ƳtTN'oUH?t$ss4uD~ϐ% { \O#<؇g_$q{s_[\yn=9Ga}c5D^]ܞwf(TAAA V5Z-6_?P\ddVVf(YFH Cu2܂J\1hB'QNf몚5^gO ŕ߳XJYqN!:XGڥZO:QU˨ ;ӎ-*΂Av鐉j()ka(7N",h}*  @AAA@D     eM 2S#)k%91 pVqi.{0G'һq T/x+I^MTUgaM)` $YK ^I8 uQrU=nn='mMx=n2SR9x2$ݸ֒4h\Zf2,wOJٓ4z5ClTȫz\aDj)f4ʵ+i$$S?ľȮޜPECb:ԋ!-D6[iѴ6fs9z:sEKuI 6rG. F2S ?/%v:ض'*t>4lX`QZ&&Ö)NkV6MNRqUyIoAXb$v}{Y}̴iDI-q!>!anEvs& \jYU݌]GBzٳe/9.Ʉa o~|2&Տ mytѵIxqT9bGIAA:q;< ܖrlqkP7{cr<5<7{49'+OI}@NK %;a e~42!c.CiRש7p#GQ~8>+_&a[M “Y7]r $8z9|Z;_JQӖdyQ②x{" 1Ҹm$ Uvs$s?A()'.Ê7Q0I ROȰhՁO_ ((sX5f=<0_D;˥B'^HK07)a; D4n{/&r}b=jFO׆A3|HAAo6]<4f2TV KˇP{;9v-{gJѤCBVqm^ -;V)Sߡ0Kǻ߸G;ɨs~% p1M\8oa;o؈߻Pӿ _MH)r:.e0G 66q+Ә-&݈.6s؍4;H΁b3`6Xɧ8owuOxAφ9utmsg+  2>-b+Eu^| 3[-hT5u3{?%6fd e~Q#{'FG}e:I{OS~UZ ^/?{[QJ|bZߏ{y@XǢtr W/AVl^xfk>{ AAJjI鎵l`3fQX\~^*Z$ҮE"Mh%P\.ԉaTab( 6,c5UOh4n@ 7UmX}\DѾ6rs3F@ 45mp4R"\ܽ-',fz  6 ąӢDĎMjqyh$CJE:Q'1$qqr<2Wh̋V-q{X'T#:DQ7!nJfA j alc9E@zx$,[¤1c! ƫխxs,7n)UytЉDjdbh2WG}$X4c{7Aө]DFCe9Û(lNw" { {Kۅ [Wk^Qd]XwW5'NQ8y"BcPoaxuzWE3.us0k^ Pej5kޭ7yP% kL zm"UM 6x olBДw>OUeKUS,2ɓ1ȵ1h<73KH>f ܠIBXC*Zs6wQP =;T5ޠGv:h֯ *s4+F.!Ϡu#[4|")7Dcw=(9!y&[JZ"!=_安B["}lpٛDgAAn@TTеE=,x W+Qsy%3hPV,XFHesi\D M2C$؟)5X| \Ʊk%J?g4A#ʘLr%H;Bi`|s4{VڗљLI550^N(x"adm2t2H^v.Yʺ70<>s\NZCu@#q܈Po(\67wڃ_Ea_6n ]r QHHفuac 7͍6J.9@wzeYFd1e\5p>O4d* Ly_`7g"Tc҈Ck^?0}ky[EB[QyRp9Ѵ ~<3HF? FubV]~wf2L-Ws~#WWt⿌  KgkNI;ѢA Z%!"oiq8L#_VkKȨ\f#y=2 cj\I5ѻoKmIfW%[iҬ.w<L8a>[ #XVH($uľeb:7-;/{ٯ;G.r*ѬmAU(W!~c6Mh$+U%0"h52bice֧&jz|U?s/jHnmGȠz$?H$[+`dYCX:$ACb Y蝏QlݽA3{} 7. .}^r Pܜ?]@ߗ Pj*Dn[VS[WQIj=u9ɴiq[5Tlt=pkuvQ;@GP!|c[̞)'>[\: OMvZP'8VO࡛&}o;QF-gĄCoNďY>&Ѽ7~dQ q$a0g64rz<?hP^VW+L:VXLz^`'45`1yi4.mx!/@Tbcpo> W_{p>f-3Lw)f.CcQLńVMԸEp䙷ٱxb뗋xgs(j)/*bݢEV L{EK.lxnA3zq*|u Yvmjc!~S8O>c\J2ÌV i"DAUVנ*)/uHhQIVЪ'مxIzfU/ 0_~WjLDlYE~X4ed8~G!S,ZtcJAfP+Lr[|u@S5IV(ߪV_z8Qx*+ήB|4kk<ϊ" f#:HK09ȥ}lDURZXNIK 0LzHeXVVr $ڪ!=}AAD[CEfD"IK-eH fUQ ^ k?ÕT:Ȫ=Y-mŨ@U)DX rѯ gי?!P>E h6¯ҬY~AAȍР9OWnw3!6ecYz:I%]3Sɮu0!C0e dRz0%v,yR;·?aȔSsۃQ1A$A*HR+<'d̩?ndm.~*;2d?Jb1  @ntgKF=eI9XfUU#]Vܼ>J2u}vه1aIp ?׍c*nc܃4'kc0}㚝^£Gqrl>Y;6#qt _r AoHObHuG;Y9w%G}rO#K,v{R>W=H9}/ه1ҽa eZq041O[yp%ǐ;yrtK8S& ĄӶGmQU-_HD&/=g_zͫcZOy%>y76aUT"xA4 ӲsݷnC|}Sg!rtÖy3!AAHq8hФ.wҎIJrkuT-]+2:}iޢ_::9bЊdghgRu}^"mHGyCNu%c%?D-▇`} +E&iQ|接 IDATh^;caDj4|iI !ܔ=Q;| jO#/obILV ڵkJɓF%C*8s V-Fb/f3bL?򊝴iט`<:MXߌh#wK0~U5"JD`p+0pN~+J>|p"MzةAL\ UG5nmH,Aćj9O  ?^lH>ݹ{s4ɻsb$U5$Ҳż.RtɱGqρBW u*6n`gpgy}cIqAg3ptC.;Og֊X}f ye ȗxd%<3y)E*K;sǫh j a(8I^;M c\f9z?;vkKE/twQz<(?3'Of}cxs=>dWzNZT@V|=gj[8 {?W.f?O@- g{33UNk3+>A ;KxitwGA}_܈(ysAACF@vW;itZ$TT^I,AρDySdƳPϹrn$ 0%JH)-lR)*עU(Kv]$g=F&ǽͥ VN#jPSi7_K͢Bqym! Cˉ$Y0@vqzp"ѣCAzEz7وFWFEUo#p^HAAd#ctOWOz,?GK9KYh T;y5v%Y&!.r/Q 8 }bGZuN+,uRЁߩ1 ݩĤZM' ߐU'lIg˖|trl!*rO''*nA'akƘ|+90]׀o| Fdɞx}2]UjBxx${^Uaì|'_}7}! \x=<^$Gct#+5 kBl덕(n P^/:Uo;S<7@6 6fؾ$CbwݽqyU%nOՔ˿x<֞awf}{&kyݏ[6'X$t s8O<<C>^f+y<|Fs*xZ9aHe"N¹_C'>> @xGt#ݛ^]kck Gh(>  pR`pjt8nHyeXTer8SuD4xn2`4lB{y%xl6lʻd{DjrB"h$p;dgU # \%ef­U+@c|M8sIXbrT3~zB|(6AAD+B-מ95>AAD 3. ?@;  DUKπnu1Jand)v31a> ,эXs9'@c,z#z`DUT$IBĘV~{̒d'9TgtUhMW?55AADrM~܎CC3Km/xz9>:T cg{ p??45s._LNֵP?,YUA.  w܆<&<=귊 7ΞwSe枦סzvm~N69$^1:>8Ԕmk4FANq&> a4jk\k9b-yDĦ^ZU)ϒytI(#s Z{iR)y :yK$)[j(v24maJA̖=2NtVy H\*7  ;D&)/Q?tZKJ2f [Q|(w^`On^פE$IǨM[зGsZƘ{eԏv?T_G/e9u< Ue /%gd[{MMR$W*` *_tAP/=[xРTS$y\.[B4"rAAy;1,}=Π؜(seQ@\\cƫ1 oֲwNpAAMB I<<; b_gYЛsL&".\l}U]^YV\+U<ު@y՛*U9Z-h*a ϥʗuyvWUdPUY`*s&]ƤSѨt^"  w۫'7> s߂#Eqt|GoKH+' a4qۈNT=:p,r0^xe0Ǘ`k H9+ѼB$mŞ! M$ ^LnL'e!嬆JpJW"_z2mi NpI*N&< :LE7   p$w8 X{XYv:qUկ*qm2frl>=[pZ Nܞ+$<Ң*hyX|⚁&˦ydA:~uө55hWɗHj& m2٣Ì6^W둪FR nOU]/z1i6e^2ɄJboAAoI V5Z-6_?z0UQ(+wRb u]8Kxf: P+|97Q5W@ebX*\9O zʋ+1،wJӀwiXAFڪUU\U,*PU 5Q^Gunwc: +AAߟ?w $l&l~suiͿxh2bo/&~wYYW%HRU 5Weկe0[t++  MiEANmsIA,+  "   7.Q5fNx S=n̝arj+h6;dk(ݞz7t+-^CekDz%kɈģ="s0$l\Ԅ]Ofud1abgx3Ox UQAe=_|S4jׁQKXVNzn`sI4ԁj:©  {޳[Q-ѢQػ*ߙS{!"].AA6Dk/ JG[(i$!>3*[Y˵̜93{ٜ'.‘؁".-:1gR]++][$pgvۅ 4lߚnkxxva|01MZs[^W2hDz6YA;S-ĂGqܑڑv^ ][nxgfuIc֋2tuW%ϟ~<ڢɦB!B\kʀtuDfiDo.&$y=?*mnTE MW[\jB)*IhS:J~DVA,&&[/,3Q/apƼEJ}Fq[_xEN2YMTJpplϻrn΃*<{Ǣ[aLvɂ!Bq-ܶV %[~d]6حf4o%.hƙ(ZwC%ԎAE*ĄWx4nd%L:Lh ё4qҊB! vLIՄ^}> {msN_ !Bqͮiv3P>cJީ,t>1 7Oި}&Ꙭ*fC`P8.$Y^SNBჸ90'sIhVǃ*ˈ)wq`Jƕ`@w]u{oMK;B!?G73|l>>Зq[Qɂ'XbR=z L;|{'xAŤ,-p;;@~e>[`}VkOQR73ii_E@ZQZ˭Q&sKյ 04o4Pn#ZZI}7vI+B!L2Lf3!]d0}ytL&? & ]EWU+^w=.7& |^n݆{+.Bh š>/.@@t͊bƺ8>^L|q2{eNfws\Q԰[M(҆B!02RSD!B!D !B!2!B!2MB<Щ.kX~<ٻV-g> lF?lu۵.fUkSNL54إb}|[~ޟ48MDT`^i&M^o2f?pޛg9WVykg ˗a |2u(3V%gPD7t%0mjlسbcfo$ O!Bwm͕K`ƪ~I =]pgGnoqιJ5FNMHLdsi ײZBoGّ CPͩӰnes~w54FC ٱa=EZI>з7%\u/`FFe{q6ӹW'4CZ$<kƊ֒ٔWA-p_h~ZeCR:6u#B!$ae #o;6j<=<=5:a L]EgΊMJC`u>/yo] 8cGaT@aHk%VvΧZ.Wd 4)x-ԮUZaJKUIhG؁ hv 62n[,ؚ^鮌f/uͯǿC.7r)`ZSIG(5|{5)tAYA!-eߩ״׈B!ϭިC*)8JqWW8gA 0+oe̳CI>Lǹ":tioV~e<fSI7d`SEN `⤧1T3~<c>|o1ygOedXV@lנ`Z\KJuaL47wbS tE!$"HPgP-?iް]`@hj-!B @.kfI nDt:>G浅{ᕁ!f}/~wA&J!XqtV_?^mf lpzZ bR'J"EгM PIwE)_ekݜ)%ٳaۖQi[ƒ<`H罵XCT6~s[&bMZL%"EIQ %nG4;u%%䗡+ +ytlY侂<~mmՆ|}&xyd@yxX(aշljOghQ7-%E ~ OUB!b bM)YydB!WQ àzB! c0n3Zi=tCQ(;/? <(@ihڇ~۰*vbFY g?b#1tTUQ)9༸ B!/Pn]T38zVw ecdjzԫt45eŔOhڱ%kӽKSTGtDV&I$dO)qv=K@ϳ')ž'T>/}0,ͧQfB!R,gIZJMg IDAT5X˂/_WXҁL e+YWcOyniϾU{*@iQ>~%yފ + 4=w ,?(7$D:S 07jI/7[Ahy V}`0 r摞G@j>WyJ 14҉a`& Cwfx1C5a+ .By8c]S\fw-@ 4/iEg`V+v.1@7d1!B!~/π8vf-x)ޛ蚓3I1UC5HDDVƂTAtGO>Oz a 09M~kYy'C~ҥzDq2sKPPpU/f7ɴڜxH!B!ҝ P)q 㒟 (d(q-W\^^k&=xcu0;g.bI' f]dlc6aS 6jθ7S3La߆Md'^Q:VO #x-iB!5e@a+6yOan_RjPſuhkU4 75VV>=,!YlܓM{rK B_sޫ>+j]Tթ.j!^\_EcPQ"̋I|sBˉ˫QT SYlNTHPbd.QbYn4ME6D[t\B!B\S}}S.19+x5:XLSۚ,۞M{o^[3Zҙ}" 5\X: b p&U;cڏhX-$]'o?47 RsF< p:hU.W(s6y[+ ]QMTVnʃ%4s??-2qM5=/bpgr>uylhYF偙P'gH5=)ob%Y%(8/|g!g2[?zҼқ4p|a ITz!M!Bu FPe Kʏ. c) 6k>;)CD;ooGw&GlEqװޓ~7]rО:AnrK]nQkqwԠ`f=.M'#yx+2!6'[W]%'%2\D=~21qt0y6z,%:i{Jcy]5D}#U;6ei!V[0Tc+3^Oa>lY)㟝˟%rzХemܚMLb]D `ՂSg'ixU !BH>T4ͨ0((('=Mn8GMqM׻68\)Q_]#{6(۞JX)J΄ GB8y oFWoݞuħbJFX 9K@]BNq9iܼ"j%T UKv0a@#97D͍JwtJҏC8]_-ѣw34 bͦDn')idgS6`ҥU !Bkp(';;ɔ}ArY1nmA nO5ꆒr"hGwWD<|>P CO8|gpsۙSTq^m(߁!<àXK99r얎NF;ʫS_e9oˆ`]ۅVI=@`}R^MzՃwKG~q ܥ:B 7db7)֩ՌfjAU |x0XjMx~hF8 Myf4 T@IB!zn?bY<{OSр[cyj)&+{cq%|}qE= (>o@=dŏwh(>:|4Xu.mLϞƧD6%:|!6:(.ȑb@zl8;b꺳X*|.G&i>֬G7o`_,/Uj6U;&=h#.iǤaCFDXJ1q>=!R(&wla{kYl(B!L/nG$4oW"(.Z#a߾ǽt%j.1g](BNNǛs87FjѾN4u֠~4K#dW%ޛͩ;Dx9K|;g%&BB,TKʢ[v8F-u8%bji_'zuSnu֏/]snrhئvT:& G20L&2&q@]i7+hwsmKt ;NᬟbوҠ9s2=:wA"4]y|)TAjıy3Y|GNYn=؅B!ߛϏ"(d6咚iBh1|?wlvH괺½"#(B!o ##57"9\c"~'6֙LyoB!v_X2 =Ӂl'r- kxoUB!I/W,{-!p?4!B?O @ʤ;|<2z1C\auX0e[Q:`ZF֙|"vfh0>P׾bS8t(%r =`1oR~MOc"F6~GV euRq]"*B__~zL9 Uu,^şoR#R#.w%iVNB݄Z6 (~C|R >r.B-sEdx/>QJGVV1i..$\Y^ iB!aT 1!X"i&;}Y'ˣ+pΦ;W9XG.OEtxYZRK~f7T޽/qŲdd\q7t⎉3kĩL{c&_'a7FҧM<'ڛ~2agҹ*bę|at G3E$e!BK /fo 4lTc;jbDVmʲx\9'41 ^&>}1'\:&kĩ PV}4O_' cljn (vkڀ_b۔L4{Nklf;CJ"u:d/P"nu_5m/21EZB!gxYUUlv; >"ciJ\| 5cAnr2!QUX=fCS˜͖7أo~_4˶fo휃j80g(ioW/'2U]Ä'OBiN_9(=6A7ץQ&.T?#<307-'ς*'viIB!B\Rq]ng;mIAXOY.L`pHlɟ%26_7Kt~gSjkpCpvo൷̉m?G_s<׋5 xhQR3!aAX-Eu]8MBZDgOA]1[@yyk2SoeJG3KK>Xޙ<CAwS$>-)>wkChq .海q^W|R^{g!2lҊB!Fd@tzuj;W2uؽ^BnGvsNWyYmOUqtH<*)ZC1ҀIx ߬NCP/eƃ(dfOGlv:M҃[~CѩA5'i]}YZ = 2p~Ϥ} ,|vPz'dBdB!$aP= cfM#d.U#0 P3ubܒ#; L ІU3⣩Y# [ wi*טNb8km vWoּ rV~v!oxi[4Ǜ]JdNDXKZN+i-8 I|e$& AQ@MS(>}ã}c^;_Y!B!B{~M((*ٺ 1.elIjyTEőoOמV0`xŀ14riz{wBJO2,Wa`: v'ZS+~}׍L p[XQ4>Wyв lݖĢcLsrҨײ)kH<40TlBLC i!B!MKK#IY8E]oٶ` l0:mbɅݐFvxe|_~1,[Xuhhe׷[k4܊qG^W XWVIc=:uF-pGtOg(_Y+fX1U*բ1,|z85&U+J`C wTڵ&;yIڋB!@˪ba%VA?{/~̖L ?L6>yg0GX|z43"vwin?0Fqz]ҰDn!4:64=B9k9NpHow":ԏom%cB5&4䚈mVVN>¹s`S:kDQ=i^j&'^{9%J^f~DH޵7W~edU5C;S$B!5QRTefCBq\R3B!B02RSEB!!B!91P?aPon:ڷ^]$4ťU~^Q~WV6 F8߸OU1`D/ni=Mv⩱jg0]eIFmyppK Fs67'OFw&\dN{oªUj6S_[;ha-;Ӊã_V._ ÈahO|uﺶ?42'n n79eW䷬B~z=GwcӇ#3fN;PH[/}ඁxr d: ڽ 9œx:W\m1 z{T6Ȏ6"9~zqKÀ3^N}7NwЛ T޹iށO NKE%08Z> L? l1 zw'-F7Fuֳ97.[޴'1L>}c |c*]֤cFgk)>{ңI uHZ|EYVw;믐}t9'w|1G?zR#B\ΠE:gGu*:XGn d%:Fe!]+tT"/yd0ۈ IǂH7Q5~qS[kd1~i~T42ruo..%ߣR|e(&J a"d{؉ 0?aPTzkj۱(nҊQxf#3Kp i9Nb=aq8rk/r]oJ @8J]kFdÆ% .<ˠ{ݜ/VaEIaAD7QRꦰ&̺ 47!U 6_Tإݬ`e 5*։fR}^0"ӑ[F[b'*+QevBW~f|0?B~QXU0 bp;6kXɦ͹kX?80ϵJEL3^Ҋ4(јNM>-s[QoSFA@p+MRt0\s9pbpPT hkAq"6庩Dž<716EŅ"~J U,d'wN3b/( QWRG`EڨI@<v®Pg}GDD4"K| V ."iK>-da&ɛ嵑ףjͻug՚usnJM &uD, p󯐾v,R|7ӆQU1@50֯N=God|;,X 񝑠9b([6&u|>?3_Ǚ[;c I/zh %Z^v77O#\V΄jzK֛2Se҅|Z+~ZX>~݇Xmм&<<>!~Z0 QM&Tcˑ ʘL|*gb }|]D Y[#7B'c\V\bƪ*D5hogpnt޽>A\9sb\}7obG kȾH3U.㮷>ODziR]רvlܡZm?-ϦCf<> k7%BT'Q3-FƑZ6)NeɱŔeNrd\ z V Ym^|;8}VJ5v+~EhP,vVoIOٶ W@TΝ'N41z|9L^cTF4)\I/Yd4T / =Ϲsٽ`A}\b;KK,Ѕ?yŽhp3X<̘geYωSS]ITp;^:7ke9y^]Mw&E~ !o b?6cV'֘.ѳQ.Gc. qI&7}LeI.N!U&</.wb>6}|akđsvlc<}l#C=`;*Z*<Λb\Բ v| jbMQБx}1cDzj8#'ơUK߾1!cgb3J-z{9t&Zq}6uG3s1iU zs VmC` [k*X:Iv%5w6Roߓiꏦ2x[,_bg~G~FWKfSn΍#0 .~``WQO`\ǨgaNbuvPyЩs[Zy45pU%ÖrSTc0dhkt TyU&}ݽڑȧzsnr?.3?qlΉ!(GpRӒ1'6[_s {54zmw<0u?mvoEE_Đ'Sޜ7ƣF~foaCcҴXHPWzz<<وJsb Fu͸k0>2O ņlS;{dF<GyAB|44,cXl#FOdc>z䔛Kxb\BOK`]hԑ6U&MgjoĮl=S /=00; W182fkR.ѓL6nkMffDDɰ~DUUBQea’w^N}y6_^IYY1n CR|^_{Bϛ9O,b鋠蚁0woOޜ KI"*"&Q0Y0!fQPQ̢H l<ӡ?{{} =UH{t^|*&b;'Bӝm!/+u60 @LE -]T8OTTGuP =OfҕlBt!S]ǏO\0|6SZY[oF& 5{*WpW߂f7R[@M00λ ;vHI*3j.p]GDُVSWR<[ 4>z 0tpG(!BѫłI9VU]ן]|A%+ʶ"J$-:c5k ['٨YF=|0(&~^O٣y6ֲ,u;H O}]Ap,&j" $(F" 4d즪;u`)ڀBr2[zәIiA58pqRi tAٶ穌ՙcظtmgrn(7s՗٣ǣO!)J"U>> kehOa44Ψ ZٝŐ~Ʉj/  Ud ,-l&1.ҫK$޲J5,۔y/;V- v|zoM|J$I+N&:Ht^z;\7e"/>x7$N=Z *!(:!"{$aB{Sjр7*;s"t]g߶T=`` :j!Iu.C_S_`5b5Ddl^w iݔ3T ,:oa2cB~I9kNIU|p3iMB#ұ=xNk 6YW^̊i&v 0eN?ٌ.CbmB ~g2)SōbЬf,*&χzP_s#a2N "M ؒco u d ξrfLɹp&NP5l6>\N pH;z%ؖ Q 5:Xm}ʫ5@ښ`!"̏ŎnìCTUGW̿Q~1 %& U(-ҿ{B}d!q0 i 6J_!<\Uħ T9U^W&.ݘ8Vf?p +{Qz^{"6~ãops~09zm~\DdB ^OX9;j6ѳ ;T6ЌW tK|t"zs9v@MMhF4MEbPc7:*0 BQX vy'ߖ$h򅗀Ft+-*QPΣ2 ^Ah hhZnTU㗮a`rQBA?5RZ4>_Cω*\t2[ishrti㮈 Lffn\;AlGگ)u4N.K&Yk-gK$b1Dv} 7|Tۜ;m)4}z4=溴]Bt͜7bݢ瓛rDMzt:) A^ՃHL~*?ۅolbMkg73þˏnEpsm Qk |} ws;g45A'cZ;\AKnAu|f7LuW2kY97ps&M?cK^˘~yT_j@L~nlcVX""}D#Ijνd5dGU<4:wϹ[rP ˞ar=H{ȪY94z?V(Z¢?d0"[ǎVS״L8◗r՟Lܘ=. S uf7Xw9,AERom7_rkmZ__sJ/"mn"#O&I`UY>u\'UYm:MCJ#tVK?S}Ԃ\7ytT!=FSOiÇ˸V6̺!ohg>y펳;9.oAFR#g^[}3{錸o$?!s:ctc e">8=ǵq`;s'lf9lڳM[j{co\ "+HNar[TGkjeSg9@A.;-bJ.=#/SQ#ܛ%8Mmu%>m([j&~'ݱ/_IM&v]͡LDaN|AlZj5:%j}'hs 4z]Hٲh+ٛٗ dAo(bwN=a&ض C -U =1 Dkm%>BYF()m*a[ , +ؿ?}OEelȪ",*0AEA1^I+aؽ,XENǨ+fA 50Ame Z3zÚneۚL*}<%%Ը5tC"vmdkp#Hu~!U>n7q1{l5n/7ϾAf`-7 y >9po>?5b2ɢcWَYQTP~\ !Ɓqww.F|&/PURuWCL]RFo+g&-QЬ4)-ӇQͼrǮ X_BB9ln.|&LtIB|f |:v*=bF:%5!B]fͤ'&HF5h(*䀚ĉ[jɩhF|*=*m &BCe;S"B'Ds=et5c}ؾy|G1cY/ :%Ss)}G֢<.m*jpgM`~IV~=ʽUKS>߬Ft cO)Qyu~"~[[Ua dgUB1Y9dGB4Ll8\N~=sn֟P3p-||Ąnp 3f66T elRHXxMkl48]+hs7kO9ή4]xO.{ XHY?\ݬX͎,eޜ%lk6A"-`GY @~(EXHNh4T[>1@`5 ؃l߶Rg0Jj8Bס4TB=;[o^IaaPb!:&` SF]9N?-u b.l AqxI,{"~iJ:c9f1Vm0H$I$b⨮քA3aT6]ÓoFG` ,`>nCx걱:U9Pڢ$I$I"xNL W WC!<*DY &?B@D@$I$_K_|ꥲ?m syD^zYL=/(4&Uq;l^U.nţʍg1YHP4S]/cp{Oׁ'y~xHf#.† z!)尢Q:q Kq'=Ϝ$?;~|U/gάI<=2:+G:21Q/sp& w;ܸv˯U+B@70K`Y)v7jϯĝ/cHu)Ӯfܠ0v$Iu%/>{/h/qV}ɂ"cFHQ2BNh SƜ+G BߩWέ1|]>6?r~kg6Ώc8 oP%k)r.}H3\dk0W\z2 χ8&9B!ϥO']9ZϞΌtR~[u#0eŔ3yxkc0lI,ߺ;JkcKh '͇s'3[,pԹE|왇nl}0 8wȟO#8s8O|I$I9 zy'!2KNjWCŘ/lNP|pC7ϟzL$Zmل|XJ:&՚B:,& ]k:!u` XV P,_'zyusX,c6l6+SM i&l-%:.~ *XmVLf} Ռhb6cUjfblRA(Ʉ !M`9vSn)o_&VNH&bʠ\00P0 $`3PCً_cĦh#36րզ`Z(CaP ʉ0 Bݪ5Ռ@V$h`vxE:3M7 TS̚0H0v0^m40,AUb`5?a[L!PpL&6S{&_˿0 Th,fL 6+8pmb2i:(' 4@p{T :|$-Vp_nfkOgeBA a2a[d2MJBAHxyCNe\<#c1+l1zO !PU$!5@;eP?=n^>I IDATE9:jKnx"U/?=-of6:s81Ve+tA/4~ KrXSƣ<8-9wB^<_;;?)n:u{@PgsԺ]l.v&|U-*ۮIǯpQ; בБopٌLZd9ĹSyޛ"Gsr[,[OE=;9=&o%6&?Y@5wlck>g#\4n&W0%{L {?{1~Ca#8ow{,F +^׾LS!0  (B ! Yau3?1-\Ln3C Sn,Vyy&s/NE`s3{,y)J}>7LyM }:]=)H7/2yeӏ׹g 0!wj~Fx+C"7;2PJrM/wƗa%cR8:Ky3aYk,M'S2AGw78˳4n7?\p>Ur듌/Pq;o2(3_Sp|I]Jwr OQm7B}DLԣńKDPӜƩ}14'.gˆ[?W9t # cקL5A8"CNS<0a>H,g|_~q-& y4 we#;ҵs2No}쁬yqmkd!I$o@#4nC7k6Kyo]-CmK JU>;htM`@4޺ߐ 2s+i/q?:?G:V,v.w-\2pAZ~ퟨ)&ܭ$GF^~ztW <.U#h~!!u-^-if4gW F - D)Z!׳o#ζ+GGV !7sϔ;ZE iPuZã~DӋ Lɺdr90Ag=O5c0 VC Pe3ML07|7хżIl+vފsoQnFB /&>>aqvMŢi,:=ܵwlWշYK5!>z;j<;= WW8 .ߌq]c-ֈ4bmd3_2!w!?aQhuwĩ$&DhK">-UA߇ )%oS)PN{ДLb\$gEJxuﺏzI!%I7J0t|P{1OE`a%z<;Jh1+x8p' APooM ≌hj ЬCTT?Q]:` Ͼyb:n>/[`1L ױF vc'?DŽ[;?SM+8̛8ʧYߔKFR?vK)­""ii <G$fښlx}<](=ĈcgkVp[ii  Cg!,6kn h?!M=_M B !`H i=|fțOΝ|IK@`M7 4x"&ji+=i.!U諸EA Dqyơ^Ó ϹC*Pp'I=+׺/P] Itl;B0-y=~ -hن&kL-vzv3\X){Ly'Μ0R/$IWUSp >ǧfїO/I٫޻'Φb|hAVL}<|QO\F}3sT PRwb>G6j]n潮#U \E гGq9L;o7~j͵̿r:_(F43iRV|]R Qm3o.w>U |~%Q0뱫6?7JkY"wA v,&1'ݦ`̇rE#ZfϤ gTNM2, 2 ![339_Zf,aݞJ&4K {I:n8snLXt$SgL8-:n(6.r'i|Nj)gQ5+Sg1vL:]D'Z%L !s[|/BSf,iӸф]x gaNs`2=%II#.׿lNDU;-XČ"n[.fϢun3M{wq"p;ď4C8W6~~Q 1<8u)Q9w]G rPdϦPO!~ .iM/ pHKzJ Ka~%g D$`U=U/੗ᴈ2muҔ$I}& ÁiT̂|_.%u%ʮŢ1S@h2vWv_B[%ޭxotc*b_m,'_ 6`C9}2|o "k8]a[F硢}e-M-4^JJJ_HlNĚ[نBre籷؉hfgXS)J0RKuk`0H]Yu>6&KS]5?,]k0sJ0:/zή{(sjf{_*- hjPOy۱Եb|T~MasdDCcCY+ ʪE3PC~r2 -o`"4]dOˮ(W"|@yY;Y@5+hS l6>\>|S :UN7{vYBw$Iopnk5y鲗+lؓ$I$I/;WQ d$I$I$U= BW fVڋ$I$I$'zilaA$I$I*_$I$I$H$I$I$I$I$I$H$I$I$I$I$I$H$I$I$I$I$Id"I$I$I$I$I$Id"I$I$I$I$I$I?'Bjp04*mt fRP~$bQI9৮)DJ5AcA=.Q50'AdD Z(DmvSñk$I$I7uDK.eQ^2.fwy~D)k^x`W{qg.z1BίLxiB 3܎y)TqA:ikZf){^_?o^JC7uUМgBWyy2$I$I#z@'c`9eXn݉R{?OEA&Z:wN9| ҅RU ԮV3řf|K_RZƬYڹG^_^NKMbyx"S'CL$I$IQpTēoCp#i[-n+'̜{CCI9J>?° /Og܅6/DySZ&cGٔ7eI$I$Iߔ$ 1};R_AM[D g_ u>]`iaC VHH?TFC.ɧіʈ;q!8L%>F"7F!sM$I$Ifb⨮D$I$I$?ݒ$I$I$ed"I$I$I_&pXPj{04*+ħGa>TtU9tydc|JgjDp=Jwg/q&ξ>6DWSKxt`4}R#DY3&oZʺS 4%'R4\Du

0ᗙ%I#z@!gr}{,vz9^4{t&[ߠnTbP-ߨK{QyRB IDATn=Ƞ?Rciþ%51qF2lP_^]mG&c+Z 3rE2KA{oO5phf{1} 4"=ƶV>]5D&7 L$`[ q@&Ag-=W噳\sݭ@;Sʦy8ĄiC-pwE\% r8a?|OB:>xwY2 \VusG}0 "X:4 JvF֓ "n=ٽy%r:uN^CnNGуpդ3U$uBG`kI]TRLLy'x~ip$p^_;f+⻅֏[Uj0Y$ ]đﮢ2K8!y5\B 2pqd+mpqbs^pZ3btV Z'%mnډ[}Ua!U4=qa-?DY56K] Q^ĮXBߨx+OZ#%*^|"6t\x.]lXA 0oJyQ#΀\bYw >7eZ‡%>8s[cBуTT7`:Tֶe)𵶐SRGAY=Uun{U9'B w)ҥ)E@:* R"EHKUHGJNB IHe9 Mz}Zsrس>~lGWw1=Qx69nv|:" F<2K(`7 Ž8'/X(>xƂ?*[_@Ժe.Wi7N쬃+Gv}BB cwsc~|߸#^1* ""A|+c D4,7F,vϹ͜%JhDxF V8hj401=i {Dri VƁ(ޏU*W\dڼˤi~*'.]G3 _iCUBFĵ-3T2D@d?m, `2cڻIRPB*x Pju[wsb |!r xpsHno~v;",};d4Aj# jT%!:"Ġ(ʽ"/oK(_9m27\J*a\_/A^:%a (* {F vբ|HH&DFEXz\ȭ;T, d@Oyb97s;PYD ͠|d(ΚGϜpHՊkV\KL|^7 a#ӂj!#-<3ރ!nŦcAԨ;YAP73y+v7ˊzPGN^ATP)ʏT4aTS>Ưlܾō074 LE$$br wEV`,,$!9‰ڑJ, (T1DRM^$ K*;rXaioN*a T҃J`ceIɏ]~~OpVR!K͏H6oNϒ51ن&Wj$>n5q֍e<6ŎSl8&ر)xVI0;}IJ˷ sj.d՛vb댎dX.?|_hu}qvCq#2o{D˥J퉏x8Վxq\z>$fWm˟ GJTX&W^?eYz?'lV;cqG~+~K ;1*4YIy7SV /',1 KRޥk dg?LMqnѥBd7 Oư#Q vO`cxԫQ)a֏{,sWvr0G3Q!7MR\çVn*<#F˸%cx/ ‰=[fIcoD;6ƛXNt,*z"JFnnҸ{%&fJ̲Iej:ҙ ;| KP9d9YR;J 늷ɍ?eӨ]+SQjvߎgX Zd$؋'CoaR Ov8{lj-'7MDL`Y _Y ==ax;dI3$ C/1sAdԋKn+"MЗ}g4w1*jNھ4Ԟln6Mu֯ 7_ʅ{ K)kC҅b UՉ==mGswCFp+`N`惼ܵ'~z/6Bo7R&L X xw-Btܠ;l . uX5d!_M;LyXj]-1:o`sp8uf< \ wHIëi]H Ȩi܏Xf}i᫩Ц=N&EIrQa)ڝ,l:g&Ύb.߼.H &RH4R"傩Pč䘟D(槳i8?%]yT8#+q%m)z*W ];qWI2ȠPZ9^l"Y&uCIg 9IH.D鬡8?1=a{jK,٠"{\#׷ 맮 EG ̎/۰ 4 %6RwvE+H3 N9o19"sF<_C=v\(Isw 7H,OjU De-ڵDPF55:7f¥X,!4`ܥ;K(_sW%Rh8Mh\#$ O1Ez;_`'&GVk^wP Uybʭ4L GWZ;(K{6 P*:Sɕۙڋ%`" -BF*u;5|0YҼ ( Y8}!7ҬX<m9om$p\])hkc,W:3O[o'-||szcȖTg,Cf2f*|bkneMf$nO?`m7֯nGo3]~ d Yt-f ḋܼGP@ | ӿF9M!]ҥ:'NebSl2ig5Zgf_Zm>, X1?{W  dYP]mqwُRW` {CEtf~84:SÙ|Hf|n_Nff;R\aסY^W7*ATsT:icHsƇҬ~E"*`5wƞ+ZoK7Qʹ"Z6BP{Ѻ7 ~( !՚V!Ha/T-'9zBuNA(KiˋՂFcIʳ{!CR7PԫKWqEdԽ[VyIٔTśidPQjjk©犩dr4^퍈@x*Ȏt(T@DdgT+ϷK1Qz8 $[q~2SIPGE\LO;Z;i8񍊠: oL9 &+Y K֙SBOMIR!1:<+B07ҟtj]ܩU5kȳBj!T)-le޶LC5 LErs#LVQ>“hKϓDy(ҝl^F͟N-Hx%IbHV )b@L7U5P~ }0m]z~!t.4j&).LƕV ˣQ>i,kf*oeG|`'{oƢz`|"!èDAF`$B+WA\ !7S~39QZkD%*!;9]P7]2r.ل_jy`).vL[D|vn)1x 55S=+1%?1PPWJPK!XKR(BF>8(1@g{ƒy;Xs^1r! / Ш 8 ؔ '"܇Qߚ| 5F(BvdV 6>뽋/v\PH62hԕRϮ_2eT* rծ)sX1]hڱm t Ex/)yqfZz,ԫg1lǪ̗R[ p+ǂ[`QbHn^{n9'ډԘspܦOS};Bb4j_OH>5_;1g;̱7{ J3//G *ҶEg8K5!B=lFA.%7w#'8Ws)ѓ)'Nf26PsRŗn]Z9Mos|Faޜ=Uú94#wkI9OgZһ+J/vqO<<]سf-3&ѢGxùQ=UgdԢDhO(`ʸ[%̪5r$ ^XsI,zDR)dBE1%^Ec^_{2fѿg=N]&fw*3 ={ $9Jw^U"k IDATNEՁZ'ap(DDI'Z E"ފD<[(z {0)"Ak)LDT0dPݙ>ߠ&nrXh}ք{S_w)̕pۂsH>TpaXVlŶ%=1'S 9(av>]?Jg0| ŴStF|zT݈IoemBY٤]Cٯˢ K{ FlU )?a oɖ]r3XHϭ5'xVT ǧ XS8w=SVO೟Z/Wx;,Yl Wꑚw+E7~fߵt*WNX>/$jli1勏|/xg4)g^3c.wVɐt\+&,"Y%:ٟ=X@pqPS~Р}[)G 8xZM^o KI1kɵЬmkSvFcG -3揧mUa98<6G" O jD2v BY=_/@ 03n6l-jHAx(uK ;(Ԃ\ APB0 rK2EwEDkw4x* WVIv@q3{$^]ͿMyWn//tFDTZ55{;h"#F3W0% ZCq+eѯy},F, d`e̓i?9hx͉U!##6m/Oj(Gn1e@EGVùC>߿BN-7-oY\͏ET*92Cp6&oi|ӧUlX˫}&ɢ HD۾i [͸eު.?tV(d8צAw~^vUoGkĕ[hAt$T*"zTpJ6:$3DzwM?&D׶23ͫ$J(DWQlq}lYvow*6c˯x(@Av\4q82|d}?, ogU<:V3jz%)7W-bnc S ڮ6̋]?LӶQ\\fb#?rr.=LȂV),3[m VEGOc䛔j5x+pdٸ %nL+돯g*%sq9m/{R06v)sIjyZw=z2%S7zC>kzr ُ"7]ۑ4ߛ /;ֈ$_=ދؗwtĐV HSuU34!SŎEx(jJMZy(I9V#ճ99o0jyAA{͔垓^`I=93d ~>UUor'Tc $?7 vctB,&*.יI^  98ə"Z; . #۝|ڎ܎[5tVςstm?Π.+zu)oTmJ:.$Pen*qr^n.5PI˕ӭd:{xSh8cǞ-?DjV' ЪDޘC^:/Ll)o ,7f4/5}74/t:ȸ3یQlJq+/LNQ-֎Uzs.o}z\d>ًʹ*)fa|]C##ێt>Mi E*֏ӭNXJx}mդYmUHpVC!dfk&L@-¾_rQԩ5!!eWtO=:E79fm)X*@jkb *.JX~Ҟl=6h~ Yv<*W`z P*dYA+.;tNHFR("R;ob9(. IIIAx;nfEb6|&7g^|2>?*1wz1]}d\%3)Tjchn&h?~J/Sp5G Y3teKj<JK &b8D0'->3>ݵ?w1knk]:sghs1/ 0sǂVYCdڤ<QلwWb:Gwq א}$/_@>OQq ߙN!!]yp $s'֎4Q{G=2 v\DA ;.xw΂V(dƦpϓ{yKjd&uRx&Fx(D եIF0mXP(ߨ sǼg3)Ş)\OL݅{X+ t4@P&F+ ^Al\8ݝ#9N""g_gɢ 2ܫ )V(.\-@Dpt+ŽnkU>|La7̢I2bT$%yz] ğgOFwfJ Ya~9WD6 v c- MHE{N%=Kh!ص[bT"6\՚wdk,4t!7&q)\Zt#ւ}+m&r(Q*{A/j9t'OrE/oGp6E?ݎǰ 6E 7KĘ Ğ N@Vfy8iU(pʏjYBDtm`|j8Dž+)L I~lăTbL܊+]^~ I8i Da3e+UqR8ɸ9 + e@T*KGFwGGv0Joz!4+ԜݾW ,S,Z|&uTgr氀Sh>v3~B2JQ0X YF4C>nI-RF1Pk HR>@-FIaO\AXOSrx3f^ 8$U|+\*~vs1IaL_u|VUC$/YSq OgB6maƋiUh,PE{hKA'ը׹ ]q!Qԑztc4n+v7XA,BIMG  xjt*P|I7LvN0zO0t *^N0ꊢ<5oNN5qQ 8 )+ \UI|/=} YW5m yU"O<$@aZsع~zeސv;J}%]AT^V`u;_AJ)i bRh<epswC(;KF"`+z5hQu"i_*^šmgvCdܸ̥D+Q~̽;6j6MyI8;i\F]n$ɇ}CaDDz=.I$Ś߻;_ۋzdYF=ȒMWФ_"t?GѴ|cٞUޣBOڵuCJM+Yq`VnQ(ظ.ldl !k(1;tU#t`ԖR؈$83 Y!"Y Fmj ?%!sL[jJ Iy8{d=gPI8gD}8FD 1 {V!,{!Kd;x) $tJ7$1K,ɎgFJg ᎉJavB@Gc x^WT5cŴ8 vqZ4qzUzw䀨ȣHC`Leo/Lə*dYKú~%hRٔNvZ:F7*@" ̇ޞA /?*bww#( C^1;gΤkhi ]{Y+(FeV yF 6BlZpBG-y!?}~:,_pB]CTSj3YPb.RA(_ꝄԊHGx:Q6܌|⓬8e!["'F|Gڬ4څ?5=ylyz/OӵggK"9v+іҷMI]9ݦ|DIPh0R8j3Nڒ; &ȵMxI|udI<#B%] HR;.c>}ȑy|C x6uDxn]FѪ|}3~$:%{|?P! |hD܍MK:MfU5E йܺȉ_f6'7pZX~}pv] 9 O6QNPX{4r<~JxkYFW.^IY7% " (ٞgZz .eбCF _>_^-ux=}O""qWYt7`~c 1?'EQxQ@:>x*dA^UȖXsl:zq$!nq[d%ALM ׵3%(c4j2=ǮewWT.!ÉU(8H:tn;9Z3p*sU` PߕQH<|um™)6a3g *L;#Ǝ+k8̓Q?'(!c% ztomfJ!(8{2w; O5(*9+35<־퍑 >c%gWXXyFc'7$˅ J+b=ϵEg'1rjNGph:6ctȀ-^kz|`gX\ô}-ƶQ3 \Rje>0M:L }EYFdۯ5S5+I' J`.bl sE5ZH@;pW2a4gOFP`-~gv0`e?ϤI3?̎N/>Μw;/S]Wvҕ#yώ μȯB>u՟/լf>?X N^xx(Yu'= 6|c5@Xw×.f$9GT|R2t(]9s˶"*0Mj-$8{jk<|z2{uR?f$Fټs6]fa^G!vSXn.N壻h +st?kiV-'n\ZNNl6 ݙ~ӧLKٖ?Вlu֣zpx+Xwg\K!|i/Se񛫼4v-]lĿ6mQ?.jZ`8^y0el`ƻzuT y33$a`_vPGAKy=qk7}3Ct E JSlԖ9R|_T̚?^Rj,wU2=WbӜX<;BBE2w\6nŽ܇u e$t󷞍jǞdɇ? ě71hV?qPnG=޿A}G۽ē莟$r.IƱ|i*z#phu̾8=4G/WgxJQy~lјffǏShhry{O֯Dkozwqy2]x µo9p]8C\}THPa#'hc+3lSQc9Oy>;Yj'O]B'fn:2R0]Vؕ z1?}6!ȼHuk- yI%w#e[3傂Jje UXC + k-$#{ 2g燌~>︚@XU-fEɃO{蔡%i{CJ f~a34odZn|v7Bd d fe jZ'hT'~ A(uUAu#tZpAR.\Vc  /~|%i& e~P5=^OI$`(DIs"z0&&zlJq$-u2񠟂 auHgh`7!PYlhْ$.;nOEmqgJ2p-uQn7I*C-fRf^/AL]KMe5ALӄ^#PT!)#@ˉ"S\\'חHIu㭨&+щ1XO_%1ɅݤAU|> Hٶy IDATz<H2iNKk.dO-Ap,Z|u^4V+yPql8zdUO\CVҢ:H8SܘJbyl*v dZ()%LZ@E hCjBQRTZ 0vKD8ZVG\ˉ;SPAQVNQT!L'եՄHMG(oҢ!a[Ir%euDVrMM:Q Kk*INa˄Jf;i8~TIGV2Μc%zTWy6i1Z(徆Տ]&b eU&iY™eѡF;d:OT#㽋ɩv<ҍ$'fMVꆀh12%j $M_G$n; >O=UT !%*E.n8:/u$$bk2%R_Kam$wAdDMV0HXuPUdB9NAE47J}U U!4׉bTOU5UsmL.mpt&1Hq+<c*HNIJMU +XJ&@z#ɉBuoDAch> 5ӧ-х!{i Rmȱ0u#IV)lrh"5>b*N&$9$n;n5̋*I$$:hL୨"2D;N%hm$9 ~k%-yYTdRlMZ(EGkPf-(A3uUԄzn.KIM1hIK6S^V\ [q'ر5}TׇQUV/fxXA) %ِ"Z?%ՙpeKkʶ$FLSK'vzd5Niq*1%`U bYKJViC FDS8ťՄBht; zƴTV@N|"Oz d).#L'1F@}M=Z _Y5htRmhhs"H^Zd J!TTlD:jq I(G"zcj*"Gح$:h$jђ {=F0۬$9^BYKiY $lMnK,ċX'%9%|u*}qյx yox8\.=9 B?j9#UW7`NRF^o\ AB!}?sWA'V\ Jl6{8J`}YW~k椂 j(DAAc=    o   "AAA   "AAA   "AAA   DAAA   DAAA   H#af L%NqeTp8zF2MެRW>GeN+Vp(Du]d%2 NbzQHӄQS#UAAro}a. _-[\GAP^dg9tF]8r 㣗[񉻹G"^o{,`sqyCR3u zaO|-ƒȖf AAj}(ޜ>kאz}P TEAQUT=\7) 5TEUrYL0ҼE[BGn?ҝI!>/#c8z8"=CA =PX/AAAi2Dy~_TPvt{ C_jKm͌j¢P% |UPTm^f?R_ .7l/w#F`  ?ωU%h"FTK~ *viXxi,Ib݋ nze Io& i<0w ts2k“EA{?ʤna!O+F  r3C Ij-*I_5CBemxw1n z <R &BlFtHZ \%n}-wGAAk9ysY+\8M0T72fӂiQYY>#wf6 N IIvz QU=޲`kiIΠU ί5갚E"  4'*]kfՓ>I%ѪU$IȒ$Ȓ$ٞV>R503z&y;H _$!ܥ mJS_f^ FFRlp   ?jZ'PÄsEQ+ Ie$Tb h5 AWGMkIh42rcry42SQTIF#*"I ĽAA Eѣ؈e$&Ǐ?GO>,7 \$4  ?,.   "AA!XFU%N"B'm*)(#/;93NmMgͧ&&NVnįhtirbR%7Pi)58zVZv,D@[OAbq9NUWRWh:!URPC$r4xo$ej+)dЙtsY`}U4oYxWǡZcUB?5"ȩ*$e!ՔR5iifj\Y94sɨBKAIJzRMhjWӘtj@!Ekk揩زrIewspU0eT>CRi`T" EUx w*қ`WDMI(=T@YPH%רNB7[+Źj^=Y<-{HS3>ԁֶ/>ur֔7{02:-gRSxHSxiΚ3ۥS`<΋<9-%Gyv_4ǹi9N}*NuR.yn+_3UŐyv`T_-qC[[ɓPx̬19c~{Cxb ﯫ}wNϽĨ>N޺o26{Lu]*)-ᑅyiĊS~Gɑ6lm$."0ﶉ\[z6ase˙i1iV ,_"{.u׳EeKF{UIl|g7'X5, _┓\Ϩ+%O2=.k,/L >]83kpm>eܶ$6&2}4$~/N}ףA04Z5݆)QƐu1l 0s mj+a/=g [3qE&OlTFvU8CGnzt*>~Ա,4l$)h5J':E;|߾˅_E WEF\L,C#%Z'rC7$Idw;ĤS)؜W^Ɯw1nE7wHǂzXO嫵o3ywtp.oι 5e/0s^|p*q!?O=QCXb+g7_BS=#~3k^͍=!vs!A|n>/@/5|RüY?i0f%!Xo_f*{?kR%䛯nfv͌*^!-h59J˟Xh4| ?H:PDm֠A^m[d ՇځnE&HNAWuZ `HM s∥=};9rq)bY77H6RY\J >P*+8R׵*h7vc2i*U|CW5Z=1T1b0]4L$'v|Y0?HRN;Pcq Y*jdr2IHǏ-޾c^pNFhqڵ_N:0jI[%rgG:dc!e|< & 3Isbԝ($Ydۛ0GW6 菥 IKZF"9$['֊CE#W`l.YOte&+Y8k.z~jO^hY;cњU{w F^UMsS|-Egn'r07/йp18c#ܨGa2ϣv[r݋:-j-*f^#W9_I׼!`dܫr8GGSEHJЭU=x[?2m2w󖪘[8Bm97O~j$8oOUIo=wVCYzK&*-eYQRr[jck<>D#D Eqd%Q]6JAXU{I7^+cKxmαW`N6"S 8 4LR+Dd{*xLiE2M9DQK7rRKo vX}pYoAk>#udXuF3ӓIȾU!-%qټw{k$;; Lz2,T ڴN'jߝ$) = *:]>Q<g3 jT k̼\~;j`+> R<>gS`+=z=]d4TbA]R}d9a oxh٘ #"mp5K_XoOcqp sZ":tB)\C"goάU; xQtL6K1+[-dx|\G5)|AώȀF):$aiı} \0O;K>HRg~"PitjI P]zjxmZ"A @ r`T.eUsybe]yjxk"Jt2z`2h=t5É(LHoO9L|3JHF0<5.,;k~O)>) gu2 j㱹K`R,[ o,PxE;aPSǃzgn{->`Gu~;@-{H?0`m gZ~rFL  & A +x7Iqj@X,BaD h&>>39祍?%f^?ϒyq%5i=PU3WsK, .~@ Qo'? Q`#H4u0{2$iqUY3X% K~ +plrF&5ɂ&*fe+')j ru9M 8EM3Ujjfq'UvB0U6ard<!݀Ӥ+HhJO*4qRR'#H}@ӊTFd̍1PR&c1JVO`5dP^'Кu$cE Tyc-憴 Lt':U Z:u1JEu˂{I.|z6tdx dԒd7bЊOUt$6cat2JʀDMx!rlޓPH\AAAk,.   [D" TUKQ/?UyO߾70UdJ8@7 Ba/Vϥ)A'Ә-eY`40;9LDixFכmt=ylDRH zqRZ^LFx 9!lVx'MY2#9PU;%$Сm685b1Zri.0>ZhK)hcAj,v:u̥U ).kmdHiNkFG6tlS6x=%Reѵ]&.} * knK)شQ*NO62 j(pcd9tiL]a5r*ih*.S1a8V:ϥuvXZҲ]Z$GR8Wsrd;e57Ѫm.[:(oF3mҾů~ &Bi՘tGf A?7fMLKf4KlIlNtʥEO}hxF;:5K!7'fԖUV$\iitCۈKD99OmE^RƴLw9~͚gTkˋ ǰ{#JqΣޑEf=X*=US5|ჇPl.:w̥Exs`wҹC3ZFҾOϖ>Q; W/UνqIᳲ#oGK~wܚ-g޻m%bYGh2xN^˓}:;M]W\t 6yu} ^'R׌69ʷz6NcܰP]R)9|oxwMd|̎^TUGy|a_!"x΀d)+Sn_e&<ϵ1x5]ІA%q,Ƅ+agbێb<5/S91`Ɍdrwӵ} 6C1v_XҺEF3¼ܧ%+ n-:uٟE\ u .\[8rx[V}*&RL5n[]W”$a!}Q{~&<2n eɨ?[Jdu/%g-qήpaL.4p SxgG4,NGUS4i~(uyD<9Jx'k77wæNiKLREyJ#G>G %iS;KAHoے聍@c"sXw(roz%NC)DW}E<7)v9Nd܋/dDn.Uֳ9j#z|㛳*٦ 97<]o|\DT?ރt In~w}GM.qOG$gEU'{Uʎ?p3oO+2v8X2=@s>?]!,LM̸/,UOo!!voePhbBQM{%P0WK>"n(þ :PD%v)k\')[cs)*sLy;i)DLdhV!;EE+A> ?Vms >>(tbbˇܕ/f=ygxv{}n{S<+= Mܷ/Ovq:lc>Q?,Ai|H-Yy\'o3| *(9eXuQ1_lVt{g2٣a+?Wƛsgr{^N-!E沿x^<~3WjX>m5W0 Ŏq'OUr#I~gǫqK?y#9Pdmd]]} <{|S$n^رJɽn޻ r`ŸxB/cX4r]R}.k gkhi|amL%`fhw?4[?B%\>9ٍGVcҥ}}kl;~x㹯JY|X {qӳy2>0.)?n|uJDF օi_щwǩ6U<|:~cJ#2y>ֳd\-XCK/c5I\:d.t&woS=oi|h|~>M/n% a%>5.Vwgf;L^\7QͨHt2e[$Q_1u6=oD.vkMnd7;7˹ٺsf֢FXSܺ(I]qa>xi;ӗ%8JGxK_Mtj6f-kVq Z8j?|HfY'{6R,@m(J};=^}SөG'TtGoܯ?Cm(e/ c{YuMG.{p.PVO%¶#=O> |h˃\zhb(VƌB\ _lHƌ:yh]}X= \Y18jm%fMQ}蘓 |ZEivZ#GZ.473w;y}~4y-CtoR 2v-(\1cIj$H^AØ=m~ªmeьO!Ț8#?236*'O{@Ĥl#'SAhѧ=z'/lidS^N V|>4Ū$b&sjOzfruD3$#Ɖ奠v $Jz fayIhԡ`棿YW-eS.jїԘgʣRWyȡ5>-O̻C[chumrRsHo:w F,֟>*m<#ne<;u8.ICyA$ 'B~grMI[}ӧ1ۚ`AcdmgcV?i"]ޅ%3'4ƨ_bȐ!ݪA)|@DY/`•,Ҭ Sn|HiDA.E2 aJrּ݊##o}GQy`kMxG(il OCm1a5hs /䵇VQ(m|7L.b=84*:CA B=k^&1yT~;)1[ dCxjNX0$W$vTyIܖzrz)Jޠ Mbs8OM ]%ʥC91Ou\rхtWTu|q5{q~|Px7U?}2 Z2mny+bS55YS/:41@αu; 5& &uN-z)I6Lr30 5XM <%)ϿO,Gz_x=0xm؋/6[\W Ƿ_?ձnj/yO~p?L2=\qPj"pN+fMg{G2p慧fѳ<giK -&%^-gOaqySnXQQJY<1n.眪kx^LL4w~䁧$珚vL棂5wr6,ؾJMz=E7rbJBh8Mfl_a{1>[Ybxf%wNI-%Fgcc-@U,uoGhgđWN kxƻq#鍧j?Rz[Ya4vRc+O .W&g>Cؾ1>:::hS.)]yu Ÿ @jƴX_ +s>H8<ڻ()D&]̰*  PTT\\PP *F@PTVL NL =cqѽU陞95MU[0qy|lp?{qԝz ⼏T m. pqNW/^#?v_:Q>+OHc77QT|TMw̘.t,,Hs ,kC!n3?U鵒 u lC[9O6k}CXrCM M-).7T l60"PRfe;D^vJ,8Z;3#:'Ĺ|; ؎yF eS;1~s46GnzON[JYMW>+؁@y1 E(>’dاT0T7Ҷ}<7mmc0#aL0ԣI/ȰǾ&p[a֔SlU\o?3Br 6'OMoѦ!ԍW72á nw8r:fbjŌdC(Z:/b1/|zWɒ{VsqW/:i{l ̨3utoNspl ᜁ|"V{3Yod߹Y@va@~ u"IƳ狝GƼEj+4Zm'0t|Q[@}^3g|t^z&Fvaf$Bˑ־6Qn†E47!dg&HMEj  5{yrV칂ڦPk/#r3} `Ղa~!g|zJzxo\R{톎emqLavٝxVV>:+F$=0z_N΍Lxl^ G&7[ȉ/).cyAIYωjxȣ &*\n'u407щ.N80`R maϯ:n4=Qxnޛ3*1Pٱ 7rpPXߪlњIq$q#wq}S< ㌄?zNY_T%3,Vn?ͽpxȒISX3`9VHl \W&Ƣ̤ Kz1YCL 0,ОhK3[_6ASlSX^4bzR:1Շ1 Vڝq6Oqkq8"84Pe*jL57Hr[XmBNA)㢌l>ގ=P+4M8-@4z(5W䧦3۵!a{RԬqgBFvZGUbSuTTnʳpZ-UKn]V5&#eÉ+)O:tʩ]<k~`ABj0:FP뇬hd6T  u G+50BePAnX74( 4VoC)ZzZS:f}[,jg=7yo7VMs <Cˌ|rxI~\mعCow`G5x.JJlp82$JrL**(˧<>^<45Nf=& -\ʣcwwwﰪD.3xmݶ_Ηv3Hq[XdM%RPJEAe"m*9%T6DqeҶn9@O&ejuоm 9[N;ͳ89#Ȋ`:pD6bC>5?0~ٜCjU1Ld}BzVT7@E\\|u}a]ĒG&>S&XOi_nK⌓+`ؼi=xnIlxuu0^F6ڧcD?Xo6#yLxlNuJ{3sibǜ?,JȮD eؾU漽rRL<5m.2XkKTRScZ,fgС|'ĕu+0]C^; Gͷ5y\F(Wf%*nΎ/Pk5>0h&X'VdU]EnEdcΙdVU[iӯcKb`Xde$e0t̠0Fja@j&z<6}P˶l)p ûx xH5 Ov#͌'eVLlwRO;'n7|};Gظ12'k,e}:qޙm9`f++ĻeUDۮ8%"lR07k -=BkZ ][ڬf^)IDAT$/z% ?Vp:lMX<ٮ4^ϣ_']A+9|!*/qv߂` %V]O @^e''eggp^zsAy ͳLXnȓgY)n<>:i툱iN{[:Lڝ͉\V݅krG9 g_AJ\;k_07v 9;㹴7W$ZQH)xTׅS01{ׅkX6>5Ҹ]V5F2;!LۛxC%Kl84ޙ+M;䵗"Ҽa)dR782! M~_ ri5Ly*>Xh\{U0Tgsxj}5g fN 1DZϿwlK[~o ˂8owFbqXfgW[_ cuvVo5;HM(l43:*>4pa6]cF1&\v̘:Ez_ʋ$SǵwZàt%Xޯ0_';Ә>}DGٙ#\1gd1a"|rT,s73^Lz`%ȗL1w.Ø|6q !*M#:̩-9œz۰h^g^^Si88{pN)7X&i /McŹy,z ^4݋-X˗ΆB,@w$|S@r+ O,_z Wq|v2x\0_GnMqj*=hp`xvKm#AډpuC<<}>Y<vùsx{6|W7mǰsƀskzXz|g5:rCw{o;xM N?b^5iKr9^:6x9dy}8F@+r2m=5{xO8WI^2iWnu÷ubjw6[_;ta1WoQ4Gl+'n0dYLOʗoWKY&yHa: <ؤ O}]‚3dsQ&D{Y9ww'c5 gu,X^|9s8.FNz&qݒ\,.7f?c/穙pY,s2q\0$aL갱lZ&p='|yF.Nn̬4Ytσ&E~E| !#O R\2p&yHڏS3&BX2V*ʪׄd/L8Ʊ`cYl8+~jCtiuݯp|w;wg eڈ[XYb`$LnN]q3 QMMT4$=; +&e~5&q^* Kf`}ExScݼD#?'gA󉈈;8<Մ~m z=WÝʉY^< ֖uF07?t?"r 1!5,DQRT;J\d$Џ9DiAՕEDD+zX)>뷲p66*ǰçdºާ%4#q PFBAhHc- g_΄ܻsN ;-ËVK\y'ɦg#pϛDDDDD?v1 N$_۟m3v]=;EDDDDDUw?.n=Vo,; +|@*q汧*B!ysqj*,#Ao^߭ԁe>%]ܥ RB0&?"8}":d% $j+eZGDDDDD~mkw """""?six h\;22]َfX:^K?Pc@DDDDDD@DDDDDDDDDDDDDDDDDDD@DDDDDDDDDDDDDDDDDDD@DDDDDD@DDDDDDDDDDDD@DDDDDD@DDDDDDDDDDDDDDDDDDD@DDDDDD@DDDDDDDDDDDD@DDDDDD@DDDDDDDDDDDDDDDDDDD@DDDDDDDDDDDDDDDDDDD@DDDDDD@DDDDDDDDDDDD@DDDDDD@DDDDDDDDDDDDDDDDDDD@DDDDDDDDDDDDDDDDDDD@DDDDDD@DDDDDDDDDDDD@DDDDDD@DDDDDDDDDDDDDDDDDDD  !"""""r Pc@@DDDDDQc@4 ȱc1!"""""*""""""34 KMDDDDDDg`C DDDDDD‡] """""ǂa""""""ǎ)|(1 """"""DŽixEDDDDffs0aavkHIK7M4MLL0J(Mpw8Qn/W 4-7Z %70ZGjX 8u~J{IENDB`gcli-2.9.1/docs/website/000077500000000000000000000000001507017207500150375ustar00rootroot00000000000000gcli-2.9.1/docs/website/.gitignore000066400000000000000000000004301507017207500170240ustar00rootroot00000000000000tutorial/01-Installation.html tutorial/02-First-Steps.html tutorial/03-Find-Documentation.html tutorial/04-Account-Setup.html tutorial/05-Creating-an-issue.html tutorial/06-Commenting.html tutorial/07-Creating-a-pull-request.html tutorial/08-Other-forges.html tutorial/index.html gcli-2.9.1/docs/website/build.sh000077500000000000000000000007001507017207500164720ustar00rootroot00000000000000#!/bin/sh # # This script builds the website into a temporary directory # cd $(dirname $0) # Build the tutorial ( cd tutorial ./gen.sh ) # Make a dist directory and copy over files DISTDIR=$(mktemp -d) mkdir -p ${DISTDIR}/tutorial mkdir -p ${DISTDIR}/assets cp -p index.html ${DISTDIR}/ cp -p \ tutorial/0*.html \ tutorial/index.html \ ${DISTDIR}/tutorial cp -p \ ../screenshot.png \ ${DISTDIR}/assets/screenshot.png echo "${DISTDIR}" gcli-2.9.1/docs/website/deploy.sh000077500000000000000000000003541507017207500166740ustar00rootroot00000000000000#!/bin/sh -xe # # This script builds the website and then creates a tarball that # is pulled regularly from the server # cd $(dirname $0) DISTDIR=$(./build.sh) tar -c -f - -C ${DISTDIR} \. | xz > website_dist.tar.xz rm -fr ${DISTDIR} gcli-2.9.1/docs/website/devl.sh000077500000000000000000000004511507017207500163300ustar00rootroot00000000000000#!/bin/sh cd $(dirname $0) find . -type f -a \( \ -name \*.md -o \( \ -name \*.html -a \ \! -path ./tutorial/index.html -a \ \! -path ./tutorial/0\*.html \ \) \ -o -name toc \ -o -name top.html \ -o -name footer.html \ \) \ | entr -rs "echo 'Regenerating ...' && ./serve.sh" gcli-2.9.1/docs/website/index.html000066400000000000000000000074261507017207500170450ustar00rootroot00000000000000 GCLI - A Git Forge CLI

GCLI - A Git Forge CLI

GCLI is a tool that lets you interact with Git forges such as GitLab, Gitea and GitHub with a consistent command line interface.
It allows you to create, inspect and interact with issues, pull- and merge requests, inspect CI and pipelines and much more.
Screenshot of gcli

General

There is not much so far on this page. However, you can go look at

GCLI is available in various distributions

Packaging status
If you want gcli to be available in your favourite operating system please submit them to the respective packaging tree.
Please also tell me such that I can link to them here.

Bug Reports and Development

Report bugs by sending an E-Mail to the mailing list. You can view the archives on the web on Sourcehut. We also accept bugs reported via our mirrors on GitLab and GitHub, however the mailing list is the preferred method.
I also happily accept patches and encourage sending to our development mailing list. Its archives can also be viewed on the web on Sourcehut. Alternatively you can send patches to our mirrors on GitLab and GitHub too, however the mailing list is the preferred method.
For sending patches via E-Mail please refer to git-send-email. If you have never done that, I can recommend git-send-email.io
If you are using Mercurial, you can refer to Sourcehut's documentation for patchbomb.
For questions or general discussions about patches and bugs you can join the IRC channel #gcli on Libera.Chat.

The CSS on this page is stolen and edited from the even better motherfucking website .

gcli-2.9.1/docs/website/serve.sh000077500000000000000000000002401507017207500165160ustar00rootroot00000000000000#!/bin/sh # cd $(dirname $0) DISTDIR=$(./build.sh) cleanup() { rm -fr ${DISTDIR} } trap cleanup EXIT TERM INT python3.11 -m http.server -d ${DISTDIR} 8080 gcli-2.9.1/docs/website/tutorial/000077500000000000000000000000001507017207500167025ustar00rootroot00000000000000gcli-2.9.1/docs/website/tutorial/01-Installation.md000066400000000000000000000050241507017207500221040ustar00rootroot00000000000000# Installing GCLI ## Through package manager If you're on FreeBSD you can just install gcli by running the following command: # pkg install gcli On NetBSD you can run: # pkgin install gcli On Ubuntu, Debian, Devuan and the like you can run: # apt install gcli On ArchLinux you can either use the AUR manually or use your favourite AUR helper: # yay -S gcli ## Compile the source code Other operating systems may currently require manual compilation and installation. ### Windows NT Notes It is entirely possible to build gcli on Windows using [MSYS2](https://msys2.org). Please follow their instructions on how to set up a development environment. ### Generic build instructions For this purpose go to [https://herrhotzenplotz.de/gcli/releases](https://herrhotzenplotz.de/gcli/releases) and choose the latest release. Then download one of the tarballs. For version 1.1.0 this would be: https://herrhotzenplotz.de/gcli/releases/gcli-1.1.0/gcli-1.1.0.tar.xz Now that you have a link, you can download it, extract it, compile the code and install it: $ mkdir ~/build $ cd ~/build $ curl -4LO https://herrhotzenplotz.de/gcli/releases/gcli-1.1.0/gcli-1.1.0.tar.xz % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 342k 100 342k 0 0 2739k 0 --:--:-- --:--:-- --:--:-- 2736k $ ls gcli-1.1.0.tar.xz $ Install the dependencies for building gcli: e.g. on Debian systems: # apt install libcurl4-openssl-dev pkgconf build-essential or on MSYS2: $ pacman -S libcurl-devel pkgconf Extract the tarball: $ tar xf gcli-1.1.0.tar.xz $ cd gcli-1.1.0 Configure, build and install gcli: $ ./configure ... $ make ... $ make install Check that the shell finds gcli: $ which gcli /usr/local/bin/gcli $ $ gcli version gcli 1.1.0 (amd64-unknown-freebsd13.2) Using libcurl/8.1.2 OpenSSL/1.1.1t zlib/1.2.13 libpsl/0.21.2 (+libidn2/2.3.4) libssh2/1.11.0 nghttp2/1.53.0 Using vendored pdjson library Report bugs at https://gitlab.com/herrhotzenplotz/gcli/. Copyright 2021, 2022, 2023 Nico Sonack and contributors. $ ### Advanced Windows Environment Setup In case you want to use the installed gcli from outside MSYS2 (e.g. in cmd.exe) you may wish to update the `Path` environment variable and add the `C:\msys2\usr\bin` directory to it. Make sure you have the library paths set up correctly. gcli-2.9.1/docs/website/tutorial/02-First-Steps.md000066400000000000000000000071551507017207500216360ustar00rootroot00000000000000# First steps ## Listing issues Let's start off by listing some issues - here for the curl project which is hosted on GitHub under `curl/curl`. To list issues for it one would run: $ gcli -t github issues -o curl -r curl You will see the list of the 30 most recent open issue tickets. The command above does the following: - invoke gcli - as a global option we switch it into GitHub-Mode - invoke the issues subcommand - operate on the repository owner curl (`-o curl`) - operate on the repository curl (`-r curl`) Note that the `-t github` option goes before the issues subcommand because it is a global option for gcli that affects how all the following things like subcommands operate. However, now I also want to see closed issues: $ gcli -t github issues -o curl -r curl -a The `-a` option will disregard the status of the issue. Oh and the screen is a bit cluttered by all these tickets - let's only fetch the first 10 issues: $ gcli -t github issues -o curl -r curl -n10 ## Searching for issues Before reporting a bug or looking for solutions to a problem that you found in a program you may want to search for issues. Let's search for `avast` in `curl/curl`: $ gcli -t github issues -o curl -r curl -a avast NUMBER NOTES STATE TITLE 11383 9 open Issue with FileZilla server (GnuTLS) and close_notify 10551 7 closed Unable to use curl 7.87 for SSL connections on Windows 10 Pro 8848 2 closed CURL SSL certificate problem when AVAST HTTPS scanning enabled $ As you can see searching for something is just a matter of appending keywords to the `issues` subcommand. You can specify any amount of search terms - they will all be used in the query. Again, `-a` ignores the status of the issue such that we also see closed tickets. ## Examining issues As of now we only produced lists of issues. However, we may also want to look at the details of an issue such as: - the original post - labels - comments - assignees of the issue (that is someone who is working on the bug) Let's get a good summary of issue `#11268` in the curl project: $ gcli -t github issues -o curl -r curl -i 11268 all As you can see most of the options are the same, however now we tell gcli with the `-i 11268` option that we want to work with a single issue. Then we tell gcli what actions to perform on the issue. Another important action is `comments`. Guess what it does: $ gcli -t github issues -o curl -r curl -i 11268 comments I know a person that likes to post long verbose traces. Let's search for an issue authored by them on the OpenSSL GitHub page: $ gcli -t github issues -o openssl -r openssl -A blastwave -a NUMBER STATE TITLE 20379 open test "80-test_ssl_new.t" fails on Solaris 10 SPARCv9 10547 open Strict C90 CFLAGS results in sha.h:91 ISO C90 does not support long long 8048 closed OPENSSL_strnlen SIGSEGV in o_str.c line 76 $ The `-A` option lets you filter for specific authors. Let's look at the issue state of `#10547`: $ gcli -t github issues -o openssl -r openssl -i 10547 status NAME : 10547 TITLE : Strict C90 CFLAGS results in sha.h:91 ISO C90 does not support long long CREATED : 2019-12-01T04:35:23Z AUTHOR : blastwave STATE : open COMMENTS : 9 LOCKED : no LABELS : triaged: bug ASSIGNEES : none $ That's nine comments - let's read the original post and the comments in our favourite pager `less`: $ gcli -t github issues -o openssl -r openssl -i 10547 op comments | less As you can see gcli will accept multiple actions for an issue and executes them sequentially. gcli-2.9.1/docs/website/tutorial/03-Find-Documentation.md000066400000000000000000000046221507017207500231370ustar00rootroot00000000000000# How to find documentation When using gcli one may not always remember all the options and flags for every subcommand. gcli has lots of integrated help to guide you through its commands. ## Subcommand help You can list all available options for the issues subcommand by doing: $ gcli issues --help With your current knowledge you can also explore the `gcli pulls` subcommand. ## General usage Run the following command: $ gcli --help usage: gcli [options] subcommand OPTIONS: -a account Use the configured account instead of inferring it -r remote Infer account from the given git remote -t type Force the account type: - github (default: github.com) - gitlab (default: gitlab.com) - gitea (default: codeberg.org) -c Force colour and text formatting. -q Be quiet. (Not implemented yet) -v Be verbose. SUBCOMMANDS: ci Github CI status info comment Comment under issues and PRs config Configure forges forks Create, delete and list repository forks gists Create, fetch and list Github Gists issues Manage issues labels Manage issue and PR labels milestones Milestone handling pipelines Gitlab CI management pulls Create, view and manage PRs releases Manage releases of repositories repos Remote Repository management snippets Fetch and list Gitlab snippets status General user status and notifications api Fetch plain JSON info from an API (for debugging purposes) version Print version gcli 1.2.0 (amd64-unknown-freebsd13.2) Using libcurl/8.1.2 OpenSSL/1.1.1t zlib/1.2.13 libpsl/0.21.2 (+libidn2/2.3.4) libssh2/1.11.0 nghttp2/1.53.0 Using vendored pdjson library Report bugs at https://gitlab.com/herrhotzenplotz/gcli/. Copyright 2021, 2022, 2023 Nico Sonack and contributors. This gives you an overview over all the available subcommands. Each subcommand in turn allows you to get its usage by supplying the `--help` option to it. ## Manual pages Furthermore I recommend reading into the manual page `gcli-issues(1)` and `gcli-pulls(1)`: $ man gcli-issues $ man gcli-pulls gcli-2.9.1/docs/website/tutorial/04-Account-Setup.md000066400000000000000000000031211507017207500221340ustar00rootroot00000000000000# Setting up gcli for use with an account Creating issues on GitHub requires an account which we need to generate an authentication token for gcli. If you want to test this with a different forge than GitHub look at [the tutorial for other forges](./08-Other-forges.html). Log into your GitHub account and click on your account icon in the top right corner. Then choose the `Settings` option. Scroll down and choose `Developer settings` on the bottom of the left column. Under `Personal access tokens` choose `Tokens (classic)`. Click on `Generate new token (classic)`. Set a useful name such as `gcli` in the Note field, set the expiration to `No expiration` and allow the following `Scopes`: - `repo` - `workflow` - `admin:public_key` - `gist` Then create the token. It'll be printed in green. Do not share it! Now we need to tell gcli about this new token. To do this, create a configuration file for gcli - on Windows you need to do this from the MSYS2 Shell: $ mkdir -p ${HOME}/.config/gcli $ vi ${HOME}/.config/gcli/config Obviously, you can choose any other editor of your choice. Put the following into this file: defaults { editor=vi github-default-account=my-github-account } my-github-account { token= account= forge-type=github } Replace the `` with the previously generated token and the `` with your account name. If you now run $ gcli -t github repos you should get a list of your repos. If not, check again that you did all the steps above correctly. gcli-2.9.1/docs/website/tutorial/05-Creating-an-issue.md000066400000000000000000000036651507017207500227360ustar00rootroot00000000000000# Creating an issue **Note**: This assumes you have [configured gcli with an account](./04-Account-Setup.html) for GitHub already. ## Preparation For this case I have a playground repository that you may as well use for testing with gcli. It is available at `herrhotzenplotz/ghcli-playground`. To see a list of issues, we can run: $ gcli -t github issues -o herrhotzenplotz -r ghcli-playground -a NUMBER NOTES STATE TITLE 13 0 open yet another issue 12 0 closed wat 11 0 closed blaaaaaaaaaaaaaaaaaaaah 10 0 closed "this is the quoted" issue title? anyone?" 9 0 closed test 8 0 closed foobar 7 0 closed foobar 5 0 closed test2 4 0 closed test $ ## Invoke gcli Let's create a bug report where we complain about things not working: $ gcli -t github issues create -o herrhotzenplotz -r ghcli-playground \ "Bug: Doesn't work on my machine" The message "Bug: doesn't work on my machine" is the title of the issue. ## Original Post You will see the default editor come up and instruct you to type in a message. This message is the "original post" or the body of the issue ticket that you're about to submit. You can use Markdown Syntax: I tried building this code on my machine but unfortunately it errors out with the following message: ```console $ make love make: don't know how to make love. Stop make: stopped in /tmp/wat $ ``` What am I doing wrong? ! ISSUE TITLE : Bug: Doesn't work on my machine ! Enter issue description above. ! All lines starting with '!' will be discarded. ## Submit the issue After you save and exit the editor gcli gives you a chance to check back and finally submit the issue. Type 'y' and hit enter. You can check back if the issue was created and also view details about it as you learned earlier. gcli-2.9.1/docs/website/tutorial/06-Commenting.md000066400000000000000000000014451507017207500215530ustar00rootroot00000000000000# Commenting Discussions on GitHub and the like are done through comments. You can comment on issues and pull requests. ## Reviewing a discussion Say you were looking at an issue in curl/curl: $ gcli issues -o curl -r curl -i 11461 comments ## Create the comment And now you wish to respond to this thread: $ gcli comment -o curl -r curl -i 11461 This will now open the editor and lets you type in your message. After saving and exiting gcli will ask you to confirm. Type 'y' and hit enter: $ gcli -t github comment -o curl -r curl -i 11461 You will be commenting the following in curl/curl #11461: Is this okay? [yN] y $ ## Commenting on pull requests When you want to comment under a pull request use the `-p` flag instead of the `-i` flag to indicate the PR number. gcli-2.9.1/docs/website/tutorial/07-Creating-a-pull-request.md000066400000000000000000000025011507017207500240600ustar00rootroot00000000000000# Creating a pull request Creating a pull request with gcli is usually as simple as running $ gcli pulls create ## Preparation Suppose you have a git repository forked and cloned: $ git clone git@github.com:contour-terminal/contour $ cd contour $ gcli forks create --into herrhotzenplotz Then you do some work on whatever feature you're planning to submit: $ git checkout -b my-amazing-feature $ git add -p $ git commit ## Push your changes You then push your changes to your fork: $ git push origin my-amazing-feature ## Create the pull request Now you can run the command to create the pull request: $ gcli pulls create From (owner:branch) [herrhotzenplotz:my-amazing-feature]: Owner [contour-terminal]: Repository [contour]: To Branch [master]: Title: Add new amazing feature Enable automerge? [yN]: Most of the defaults you should be able to simply accept. This assumes that you have the source branch checked out locally and remotes are configured appropriately. Otherwise you can just change the defaults by entering the correct values. ## Enter original post After you entered all the meta data of the pull request gcli will drop you into your editor and lets you enter a message for the pull request. ## Submit After you saved and exit type `y` and hit enter to submit the pull request. gcli-2.9.1/docs/website/tutorial/08-Other-forges.md000066400000000000000000000124031507017207500220150ustar00rootroot00000000000000# Other forges and bugtrackers gcli is capable of not only interacting with Github. It also currently supports: - GitLab - Gitea - Bugzilla ## Bugzilla ### Notes Bugzilla is commonly used as a bug tracker in various large open-source projects such as FreeBSD, the Linux Kernel, Mozilla and Gentoo. ### Searching Suppose you want to search for bug reports containing `sparc` in the Gentoo Bugzilla. In this case you need to configure an account that points at the correct URL in `$HOME/.config/gcli/config` by adding: gentoo { forge-type=bugzilla api-base=https://bugs.gentoo.org/ } Now you can search the Gentoo Bugs: $ gcli -a gentoo issues sparc NUMBER NOTES STATE TITLE 924443 0 UNCONFIRMED Add keyword ~sparc for app-misc/fastfetch 924430 0 RESOLVED media-libs/assimp-5.3.1 fails tests on sparc 924215 0 CONFIRMED dev-libs/libbson dev-libs/mongo-c-driver: alpha arm ia64 mips ppc ppc64 s390 sparc keyword req 924191 0 CONFIRMED media-libs/exempi: unaligned access causes dev-python/python-xmp-toolkit-2.0.2 to fails tests on sparc (test_file_to_dict (test.test_core_unit.UtilsTestCase.test_file_to_dict) ... Bus error) 924180 0 CONFIRMED dev-python/psycopg-3.1.17[native-extensions] fails tests on sparc: tests/test_copy_async.py::test_read_rows[asyncio-names-1] Fatal Python error: Bus error 924031 0 IN_PROGRESS sys-apps/bfs: ~arm ~arm64 ~ppc ~ppc64 ~sparc keywording 923968 0 CONFIRMED dev-python/pyarrow-15.0.0 fails to configure on sparc: CMake Error at cmake_modules/SetupCxxFlags.cmake:42 (message): Unknown system processor 921245 0 CONFIRMED media-video/rav1e-0.6.6 fails to compile on sparc: Assertion `DT.dominates(RHead, LHead) && "No dominance between recurrences used by one SCEV?"' failed. 920956 0 CONFIRMED dev-python/pygame-2.5.2: pygame.tests.font_test SIGBUS on sparc 920737 0 CONFIRMED sparc64-solaris Prefix no longer supported ### Issue details Furthermore we can look at single issues: $ gcli -a gentoo issues -i 920737 all comments NUMBER : 920737 TITLE : sparc64-solaris Prefix no longer supported CREATED : 2023-12-26T19:20:58Z PRODUCT : Gentoo Linux COMPONENT : Profiles AUTHOR : Tom Williams STATE : CONFIRMED LABELS : none ASSIGNEES : prefix ORIGINAL POST Resurrecting a Prefix install on Solaris 11.4 SPARC. It was working rather well for me; after a hiatus I had hoped to use it again but my first emerge --sync has removed the profile needed to merge any updates or new packages. I note commit 8e006b67e06a19fae10c6059c7fc5ede88834601 in May 2023 removed the profile and keywording for prefixed installs. There is no associated comment. There doesn't seem to be a bug report in regards to the change (I'm quite sure almost nobody uses it, so probably fair enough) Any easy way to restore the profile for now? Eventually Solaris/SPARC and thus Prefix will be gone anyway, but useful for now. Thanks for your continued efforts. AUTHOR : sam DATE : 2023-12-26T19:21:33Z I think at the very least, when removing Prefix support in future, a 'deprecated' file should be added to the relevant profiles asking if anyone is using it to step forward. AUTHOR : grobian DATE : 2023-12-26T22:58:12Z Solaris 11.4 itself is a problem. I doubt you ever had it "working". AUTHOR : grobian DATE : 2023-12-26T22:59:57Z Linux sparc team is not relevant here $ ## GitLab ### Configuring an account for use with a token First you need to generate a token: 1. Click on your avatar in the top left corner 1. Choose Preferences in the popup menu 1. Select `Access tokens` in the preference menu 1. Click the `Add new token` button 1. Choose some reasonable values - The token name can be your hostname e.g. `gcli $(hostname)` - Clear the expiration date. It will be defaulted to some high value by GitLab. - Select the `api` scope Now click `Create personal access token`. Save this token - **do not share it with anyone else**. You can now update your gcli config in `$HOME/.config/gcli/config`: ```conf defaults { gitlab-default-account=gitlab-com ... } gitlab-com { account= token= forge-type=gitlab } ``` After that you should be able to run the following command: $ gcli -t gitlab issues -o herrhotzenplotz -r gcli If this process errors out check the above steps. If you believe this is a bug, please report it at our issue tracker! ## Gitea The steps here are roughly the same as with GitLab. To generate a token: 1. Click your avatar in the top-right corner 1. Choose `Settings` in the popup menu 1. Select `Applications` in the menu on the left 1. Under `Generate new token` enter a reasonable token name 1. Click the `Generate token` button 1. Save the token - **do not share it with anyone else**. You can now update your gcli config file in `$HOME/.config/gcli/config`: ```conf defaults { gitea-default-account=codeberg-org ... } codeberg-org { account= token= forge-type=gitea api-base=https://codeberg.org/api/v1 } ``` The example here uses Codeberg. Update these fields as needed for your own use case. gcli-2.9.1/docs/website/tutorial/footer.html000066400000000000000000000000201507017207500210560ustar00rootroot00000000000000 gcli-2.9.1/docs/website/tutorial/gen.sh000077500000000000000000000045631507017207500200220ustar00rootroot00000000000000#!/bin/sh # Set the following options: # -e: Exit immediately if any command exits with a non-zero status (error). # -u: Treat unset variables as errors, causing the script to exit. set -eu # # Static Site generator for the tutorial. # # You will need cmark for this to work. # if ! command -v cmark >/dev/null 2>&1; then echo "cmark is required but it's not installed. Exiting." >&2 exit 1 fi header() { TITLE="${1}" PREVURL="${2-}" NEXTURL="${3-}" sed -e "s/{{TITLE_PLACEHOLDER}}/${TITLE}/g" \ -e "s/{{PREVURL}}/${PREVURL}/g" \ -e "s/{{NEXURL}}/${NEXTURL}/g" top.html } footer() { cat footer.html } pagination() { PREVDOC="$1" PREVTITLE="$2" NEXTDOC="$3" NEXTTITLE="$4" echo "" } genindex() { header "Index" cat <A GCLI Tutorial

This document is aimed at those who are new to gcli and want get started using it.

Table of contents

EOF echo "
    " awk -F\\t '{printf "
  1. %s
  2. \n", $1, $2}' < toc echo "
" footer } genpage() { PAGETITLE="$1" PAGEMDFILE="$2" PREVDOC="$3" PREVTITLE="$4" NEXTDOC="$5" NEXTTITLE="$6" header "${PAGETITLE}" "${PREVDOC}" "${NEXTDOC}" pagination "${PREVDOC}" "${PREVTITLE}" "${NEXTDOC}" "${NEXTTITLE}" echo "
" cmark -t html < "${PAGEMDFILE}" echo "
" echo "
" pagination "${PREVDOC}" "${PREVTITLE}" "${NEXTDOC}" "${NEXTTITLE}" footer } prevhtmldoc="" prevtitlename="" while IFS="$(printf '\t')" read -r htmldoc title; do mddoc="${htmldoc%.html}.md" read -r nexthtmldoc nexttitle < "${htmldoc}" # Update the previous document filename and title for the next iteration prevhtmldoc="$htmldoc" prevtitlename="$title" done < toc genindex > index.html gcli-2.9.1/docs/website/tutorial/old_index.md000066400000000000000000000004471507017207500211760ustar00rootroot00000000000000# GCLI Tutorial This document is aimed at those who are new to gcli and want get started using it. ## Table of contents 1. [Installing GCLI](./02-Installation.html) 1. [First steps](./03-First-Steps.html) #### Details about issues #### Further reading ### Creating issues ### First issue gcli-2.9.1/docs/website/tutorial/toc000066400000000000000000000005611507017207500174140ustar00rootroot0000000000000001-Installation.html Installation 02-First-Steps.html First Steps 03-Find-Documentation.html How to find documentation 04-Account-Setup.html Setting up an account 05-Creating-an-issue.html Creating an issue 06-Commenting.html Interacting and commenting 07-Creating-a-pull-request.html Creating a Pull Request 08-Other-forges.html Other forges (Gitea, Gitlab, Bugzilla) gcli-2.9.1/docs/website/tutorial/top.html000066400000000000000000000016611507017207500203760ustar00rootroot00000000000000 GCLI Tutorial | {{TITLE_PLACEHOLDER}} gcli-2.9.1/include/000077500000000000000000000000001507017207500140705ustar00rootroot00000000000000gcli-2.9.1/include/gcli/000077500000000000000000000000001507017207500150065ustar00rootroot00000000000000gcli-2.9.1/include/gcli/attachments.h000066400000000000000000000041511507017207500174730ustar00rootroot00000000000000/* * Copyright 2023 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_ATTACHMENTS_H #define GCLI_ATTACHMENTS_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include struct gcli_attachment { gcli_id id; bool is_obsolete; time_t created_at; char *author; char *file_name; char *summary; char *content_type; char *data_base64; }; struct gcli_attachment_list { struct gcli_attachment *attachments; size_t attachments_size; }; void gcli_attachments_free(struct gcli_attachment_list *list); void gcli_attachment_free(struct gcli_attachment *attachment); int gcli_attachment_get_content(struct gcli_ctx *const ctx, gcli_id const id, FILE *out); #endif /* GCLI_ATTACHMENTS_H */ gcli-2.9.1/include/gcli/base64.h000066400000000000000000000033041507017207500162430ustar00rootroot00000000000000/* * Copyright 2023 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_BASE64_H #define GCLI_BASE64_H #include #include int gcli_decode_base64(struct gcli_ctx *ctx, char const *input, char *buffer, size_t buffer_size); int gcli_base64_decode_print(struct gcli_ctx *ctx, FILE *out, char const *const input); #endif /* GCLI_BASE64_H */ gcli-2.9.1/include/gcli/bugzilla/000077500000000000000000000000001507017207500166175ustar00rootroot00000000000000gcli-2.9.1/include/gcli/bugzilla/api.h000066400000000000000000000031361507017207500175440ustar00rootroot00000000000000/* * Copyright 2023 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_BUGZILLA_API_H #define GCLI_BUGZILLA_API_H #ifdef HAVE_CONFIG_H #include #endif #include char const *bugzilla_api_error_string(struct gcli_ctx *ctx, struct gcli_fetch_buffer *it); #endif /* GCLI_BUGZILLA_API_H */ gcli-2.9.1/include/gcli/bugzilla/attachments.h000066400000000000000000000031651507017207500213100ustar00rootroot00000000000000/* * Copyright 2023 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_BUGZILLA_ATTACHMENTS_H #define GCLI_BUGZILLA_ATTACHMENTS_H #include int bugzilla_attachment_get_content(struct gcli_ctx *ctx, gcli_id attachment_id, FILE *output); #endif /* GCLI_BUGZILLA_ATTACHMENTS_H */ gcli-2.9.1/include/gcli/bugzilla/bugs-parser.h000066400000000000000000000063531507017207500212310ustar00rootroot00000000000000/* * Copyright 2023 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_BUGZILLA_BUGS_PARSER_H #define GCLI_BUGZILLA_BUGS_PARSER_H #include #include #include #include #include int parse_bugzilla_bug_comments_dictionary_skip_first(struct gcli_ctx *const ctx, struct json_stream *stream, struct gcli_comment_list *out); int parse_bugzilla_comments_array_skip_first(struct gcli_ctx *ctx, struct json_stream *stream, struct gcli_comment_list *out); int parse_bugzilla_bug_comments_dictionary_only_first(struct gcli_ctx *const ctx, struct json_stream *stream, char **out); int parse_bugzilla_comments_array_only_first(struct gcli_ctx *ctx, struct json_stream *stream, char **out); int parse_bugzilla_assignee(struct gcli_ctx *ctx, struct json_stream *stream, struct gcli_issue *out); int parse_bugzilla_bug_attachments_dict(struct gcli_ctx *ctx, struct json_stream *stream, struct gcli_attachment_list *out); int parse_bugzilla_attachment_content_only_first(struct gcli_ctx *ctx, struct json_stream *stream, struct gcli_attachment *out); int parse_bugzilla_single_comments_array_only_first(struct gcli_ctx *ctx, struct json_stream *stream, struct gcli_comment *out); #endif /* GCLI_BUGZILLA_BUGS_PARSER_H */ gcli-2.9.1/include/gcli/bugzilla/bugs.h000066400000000000000000000052341507017207500177340ustar00rootroot00000000000000/* * Copyright 2023 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_BUGZILLA_BUGS_H #define GCLI_BUGZILLA_BUGS_H #include #include #include #include #include int bugzilla_get_bugs(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_issue_fetch_details const *details, int const max, struct gcli_issue_list *out); int bugzilla_get_bug(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_issue *out); int bugzilla_bug_get_comments(struct gcli_ctx *const ctx, struct gcli_path const *bug_path, struct gcli_comment_list *out); int bugzilla_bug_get_comment(struct gcli_ctx *ctx, struct gcli_path const *target, enum comment_target_type target_type, gcli_id comment_id, struct gcli_comment *out); int bugzilla_bug_get_attachments(struct gcli_ctx *ctx, struct gcli_path const *bug_path, struct gcli_attachment_list *const out); int bugzilla_bug_submit(struct gcli_ctx *ctx, struct gcli_submit_issue_options *opts, struct gcli_issue *out); #endif /* GCLI_BUGZILLA_BUGS_H */ gcli-2.9.1/include/gcli/bugzilla/comment.h000066400000000000000000000031361507017207500204350ustar00rootroot00000000000000/* * Copyright 2025 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_BUGZILLA_COMMENT_H #define GCLI_BUGZILLA_COMMENT_H #include int bugzilla_submit_comment(struct gcli_ctx *ctx, struct gcli_submit_comment_opts const *opts); #endif /* GCLI_BUGZILLA_COMMENT_H */ gcli-2.9.1/include/gcli/bugzilla/config.h000066400000000000000000000030441507017207500202360ustar00rootroot00000000000000/* * Copyright 2023 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_BUGZILLA_CONFIG_H #define GCLI_BUGZILLA_CONFIG_H #include char *bugzilla_make_authheader(struct gcli_ctx *ctx, char const *token); #endif /* GCLI_BUGZILLA_CONFIG_H */ gcli-2.9.1/include/gcli/cmd/000077500000000000000000000000001507017207500155515ustar00rootroot00000000000000gcli-2.9.1/include/gcli/cmd/actions.h000066400000000000000000000060311507017207500173620ustar00rootroot00000000000000/* * Copyright 2025 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_ACTIONS_H #define GCLI_CMD_ACTIONS_H #include #include #include enum { GCLI_EX_OK = 0, GCLI_EX_USAGE = 1, GCLI_EX_DATAERR = 2, }; typedef int (*gcli_cmd_action_handler)( struct gcli_path const *path, /* path to the object */ void *item, /* path to the fetched item or NULL */ int *argc, char **argv[]); /* argument vector (for incremental getopt parsing) */ typedef int (*gcli_cmd_action_fetcher)( struct gcli_ctx *, struct gcli_path const *, void *item); typedef void (*gcli_cmd_action_freeer)(void *item); /* definition of an action */ struct gcli_cmd_action { char *name; /* name that this action is invoked as */ char *help; /* short description of this action */ bool use_pager; /* whether to pipe the output through a pager in an interactive context */ bool needs_item; /* whether we must pass the fetched item or can just pass a NULL */ gcli_cmd_action_handler handler; /* the action handler */ }; /* Maximum number of items */ #define GCLI_ACTION_LIST_MAX 32 struct gcli_cmd_actions { gcli_cmd_action_fetcher fetch_item; gcli_cmd_action_freeer free_item; size_t item_size; struct gcli_cmd_action defs[GCLI_ACTION_LIST_MAX]; }; int gcli_cmd_actions_handle(struct gcli_cmd_actions const *actions, struct gcli_path const *path, int *argc, char ***argv); int gcli_cmd_action_handle(struct gcli_cmd_actions const *actions, struct gcli_path const *path, char *cmd_input); #endif /* GCLI_CMD_ACTIONS_H */ gcli-2.9.1/include/gcli/cmd/attachments.h000066400000000000000000000027671507017207500202510ustar00rootroot00000000000000/* * Copyright 2023 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_ATTACHMENTS_H #define GCLI_CMD_ATTACHMENTS_H int subcommand_attachments(int argc, char *argv[]); #endif /* GCLI_CMD_ATTACHMENTS_H */ gcli-2.9.1/include/gcli/cmd/ci.h000066400000000000000000000034411507017207500163170ustar00rootroot00000000000000/* * Copyright 2023 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_CI_H #define GCLI_CMD_CI_H #ifdef HAVE_CONFIG_H #include #endif #include void github_print_checks(struct github_check_list const *checks); void github_print_checks(struct github_check_list const *const list); int github_checks(struct gcli_path const *const repo_path, char const *const ref, int const max); int subcommand_ci(int argc, char *argv[]); #endif /* GCLI_CMD_CI_H */ gcli-2.9.1/include/gcli/cmd/cmd.h000066400000000000000000000052551507017207500164740ustar00rootroot00000000000000/* * Copyright 2022-2025 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_CMD_H #define GCLI_CMD_CMD_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include extern struct gcli_ctx *g_clictx; static inline char * shift(int *argc, char ***argv) { if (*argc == 0) errx(1, "error: Not enough arguments"); (*argc)--; return *((*argv)++); } void version(void); void longversion(void); void copyright(void); void check_owner_and_repo(const char **owner, const char **repo); void check_path(struct gcli_path *path); void parse_labels_options( int *argc, char ***argv, const char ***_add_labels, size_t *_add_labels_size, const char ***_remove_labels, size_t *_remove_labels_size); void delete_repo(bool always_yes, struct gcli_path const *path); /* List of subcommand entry points */ int subcommand_api(int argc, char *argv[]); void gcli_pretty_print(char const *input, int indent, int maxlinelen, FILE *stream); void gcli_pretty_print_diff(char const *const input, int indent); char *gcli_cmd_realpath(char const *const restrict pathname); bool gcli_cmd_should_do_always_yes(void); void gcli_cmd_save_message(char const *); bool gcli_cmd_can_recall_message(void); char *gcli_cmd_recall_message(void); void gcli_cmd_recall_message_interactive(char **out); #endif /* GCLI_CMD_CMD_H */ gcli-2.9.1/include/gcli/cmd/cmdconfig.h000066400000000000000000000062441507017207500176610ustar00rootroot00000000000000/* * Copyright 2021-2025 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_CMDCONFIG_H #define GCLI_CMD_CMDCONFIG_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include struct gcli_config_entry { TAILQ_ENTRY(gcli_config_entry) next; gcli_sv key; gcli_sv value; }; TAILQ_HEAD(gcli_config_entries, gcli_config_entry); int gcli_config_parse_args(struct gcli_ctx *ctx, int *argc, char ***argv); int gcli_config_init_ctx(struct gcli_ctx *ctx); void gcli_config_get_upstream_parts(struct gcli_ctx *ctx, gcli_sv *owner, gcli_sv *repo); char *gcli_config_get_apibase(struct gcli_ctx *); gcli_sv gcli_config_find_by_key(struct gcli_ctx *ctx, char const *section_name, char const *key); char *gcli_config_get_editor(struct gcli_ctx *ctx); char *gcli_config_get_token(struct gcli_ctx *ctx); char *gcli_config_get_pager(struct gcli_ctx *ctx); char *gcli_config_get_url_open_program(struct gcli_ctx *ctx); char *gcli_config_get_account_name(struct gcli_ctx *ctx); gcli_sv gcli_config_get_upstream(struct gcli_ctx *ctx); gcli_sv gcli_config_get_base(struct gcli_ctx *ctx); gcli_forge_type gcli_config_get_forge_type(struct gcli_ctx *ctx); gcli_sv gcli_config_get_override_default_account(struct gcli_ctx *ctx); bool gcli_config_pr_inhibit_delete_source_branch(struct gcli_ctx *ctx); int gcli_config_get_repo(struct gcli_ctx *ctx, char const **, char const **); int gcli_config_get_remote(struct gcli_ctx *ctx, char const **remote); bool gcli_config_have_colours(struct gcli_ctx *ctx); bool gcli_config_display_progress_spinner(struct gcli_ctx *ctx); bool gcli_config_render_markdown(struct gcli_ctx *ctx); bool gcli_config_enable_experimental(struct gcli_ctx *ctx); struct gcli_config_entries const *gcli_config_get_section_entries( struct gcli_ctx *ctx, char const *section_name); #endif /* GCLI_CMD_CMDCONFIG_H */ gcli-2.9.1/include/gcli/cmd/colour.h000066400000000000000000000041041507017207500172240ustar00rootroot00000000000000/* * Copyright 2022-2025 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 COLOR_H #define COLOR_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include #define GCLI_256COLOR_DONE 0x3F0FAF00 #define GCLI_256COLOR_OPEN 0x04FF0100 enum { GCLI_COLOR_BLACK, GCLI_COLOR_RED, GCLI_COLOR_GREEN, GCLI_COLOR_YELLOW, GCLI_COLOR_BLUE, GCLI_COLOR_MAGENTA, GCLI_COLOR_CYAN, GCLI_COLOR_WHITE, GCLI_COLOR_DEFAULT, }; char const *gcli_setcolour256(uint32_t colourcode); char const *gcli_resetcolour(void); char const *gcli_setcolour(int colour); char const *gcli_state_colour_sv(gcli_sv const state); char const *gcli_state_colour_str(char const *it); char const *gcli_setbold(void); char const *gcli_resetbold(void); #endif /* COLOR_H */ gcli-2.9.1/include/gcli/cmd/comment.h000066400000000000000000000034011507017207500173620ustar00rootroot00000000000000/* * Copyright 2023 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_COMMENT_H #define GCLI_CMD_COMMENT_H #ifdef HAVE_CONFIG_H #include #endif #include #include int gcli_issue_comments(struct gcli_path const *path); int gcli_pull_comments(struct gcli_path const *pull_path); void gcli_print_comment_list(struct gcli_comment_list const *list); int subcommand_comment(int argc, char *argv[]); #endif /* GCLI_CMD_COMMENT_H */ gcli-2.9.1/include/gcli/cmd/config.h000066400000000000000000000032121507017207500171650ustar00rootroot00000000000000/* * Copyright 2023 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_CONFIG_H #define GCLI_CMD_CONFIG_H #ifdef HAVE_CONFIG_H #include #endif #include #include void gcli_sshkeys_print_keys(struct gcli_sshkey_list const *list); int subcommand_config(int argc, char *argv[]); #endif /* GCLI_CMD_CONFIG_H */ gcli-2.9.1/include/gcli/cmd/editor.h000066400000000000000000000033471507017207500172170ustar00rootroot00000000000000/* * Copyright 2021-2025 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_EDITOR_H #define GCLI_CMD_EDITOR_H #ifdef HAVE_CONFIG_H #include #endif #include #include char *gcli_editor_get_user_message( struct gcli_ctx *ctx, void (*initializer)(struct gcli_ctx *, FILE *, void *), void *user_data); int gcli_editor_open_file(struct gcli_ctx *ctx, char const *const path); #endif /* GCLI_CMD_EDITOR_H */ gcli-2.9.1/include/gcli/cmd/forks.h000066400000000000000000000032701507017207500170500ustar00rootroot00000000000000/* * Copyright 2023 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_FORKS_H #define GCLI_CMD_FORKS_H #ifdef HAVE_CONFIG_H #include #endif #include #include int subcommand_forks(int argc, char *argv[]); void gcli_print_forks(enum gcli_output_flags flags, struct gcli_fork_list const *list, int max); #endif /* GCLI_CMD_FORKS_H */ gcli-2.9.1/include/gcli/cmd/gists.h000066400000000000000000000032501507017207500170530ustar00rootroot00000000000000/* * Copyright 2023 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_GISTS_H #define GCLI_CMD_GISTS_H #ifdef HAVE_CONFIG_H #include #endif #include int subcommand_gists(int argc, char *argv[]); void gcli_print_gists(enum gcli_output_flags flags, struct gcli_gist_list const *list, int max); #endif /* GCLI_CMD_GISTS_H */ gcli-2.9.1/include/gcli/cmd/gitconfig.h000066400000000000000000000042151507017207500176750ustar00rootroot00000000000000/* * Copyright 2021-2025 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_GITCONFIG_H #define GCLI_CMD_GITCONFIG_H #ifdef HAVE_CONFIG_H #include #endif struct gcli_gitremote { char *name; char *owner; char *repo; char *url; gcli_forge_type forge_type; }; gcli_sv gcli_gitconfig_get_current_branch(void); 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(struct gcli_ctx *ctx, char const *const remote_name, char const **const owner, char const **const repo, int *const forge); int gcli_gitconfig_get_remote(struct gcli_ctx *ctx, gcli_forge_type type, char const **remote); #endif /* GCLI_CMD_GITCONFIG_H */ gcli-2.9.1/include/gcli/cmd/interactive.h000066400000000000000000000033121507017207500202360ustar00rootroot00000000000000/* * 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 #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); #endif /* GCLI_CMD_INTERACTIVE_H */ gcli-2.9.1/include/gcli/cmd/issues.h000066400000000000000000000036241507017207500172420ustar00rootroot00000000000000/* * Copyright 2023-2025 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_ISSUES_H #define GCLI_CMD_ISSUES_H #ifdef HAVE_CONFIG_H #include #endif #include #include extern struct gcli_cmd_actions gcli_issue_actions; void gcli_print_issues(enum gcli_output_flags const flags, struct gcli_issue_list const *const list, int const max); void gcli_issue_print_summary(struct gcli_issue const *const it); void gcli_issue_print_op(struct gcli_issue const *const it); int subcommand_issues(int argc, char *argv[]); #endif /* GCLI_CMD_ISSUES_H */ gcli-2.9.1/include/gcli/cmd/labels.h000066400000000000000000000031641507017207500171700ustar00rootroot00000000000000/* * Copyright 2023 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_LABELS_H #define GCLI_CMD_LABELS_H #ifdef HAVE_CONFIG_H #include #endif #include void gcli_labels_print(struct gcli_label_list const *list, int max); int subcommand_labels(int argc, char *argv[]); #endif /* GCLI_CMD_LABELS_H */ gcli-2.9.1/include/gcli/cmd/milestones.h000066400000000000000000000034221507017207500201050ustar00rootroot00000000000000/* * Copyright 2023 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_MILESTONES_H #define GCLI_CMD_MILESTONES_H #ifdef HAVE_CONFIG_H #include #endif #include void gcli_print_milestones(struct gcli_ctx *ctx, struct gcli_milestone_list const *it, int max); void gcli_print_milestone(struct gcli_ctx *ctx, struct gcli_milestone const *it); int subcommand_milestones(int argc, char *argv[]); #endif /* GCLI_CMD_MILESTONES_H */ gcli-2.9.1/include/gcli/cmd/open.h000066400000000000000000000027261507017207500166720ustar00rootroot00000000000000/* * Copyright 2025 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_OPEN_H #define GCLI_CMD_OPEN_H int gcli_cmd_open_url(char const *url); #endif /* GCLI_CMD_OPEN_H */ gcli-2.9.1/include/gcli/cmd/pipelines.h000066400000000000000000000036061507017207500177170ustar00rootroot00000000000000/* * Copyright 2023 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_PIPELINES_H #define GCLI_CMD_PIPELINES_H #include #include void gitlab_print_pipelines(struct gitlab_pipeline_list const *const list); int gitlab_mr_pipelines(struct gcli_path const *const path); int gitlab_pipeline_jobs(struct gcli_path const *const path, int count); void gitlab_print_jobs(struct gitlab_job_list const *const list); void gitlab_print_job_status(struct gitlab_job const *const job); int subcommand_pipelines(int argc, char *argv[]); #endif /* GCLI_CMD_PIPELINES_H */ gcli-2.9.1/include/gcli/cmd/pull_reviews.h000066400000000000000000000031011507017207500204350ustar00rootroot00000000000000/* * Copyright 2022 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_PULL_REVIEWS_H #define GCLI_CMD_PULL_REVIEWS_H #ifdef HAVE_CONFIG_H #include #endif /* HAVE_CONFIG_H */ void do_review_session(struct gcli_path const *path); #endif /* GCLI_CMD_PULL_REVIEWS_H */ gcli-2.9.1/include/gcli/cmd/pulls.h000066400000000000000000000041311507017207500170600ustar00rootroot00000000000000/* * Copyright 2023-2025 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_PULLS_H #define GCLI_CMD_PULLS_H #ifdef HAVE_CONFIG_H #include #endif #include #include extern struct gcli_cmd_actions gcli_pull_actions; void gcli_print_pulls(enum gcli_output_flags flags, struct gcli_pull_list const *list, int max); int gcli_print_pull_diff(FILE *stream, char const *owner, char const *reponame, int pr_number); void gcli_pull_print(struct gcli_pull const *pull); void gcli_pull_print_op(struct gcli_pull const *pull); int gcli_pull_checks(struct gcli_path const *path); void gcli_print_commits(struct gcli_commit_list const *const list); int subcommand_pulls(int argc, char *argv[]); #endif /* GCLI_CMD_PULLS_H */ gcli-2.9.1/include/gcli/cmd/releases.h000066400000000000000000000032711507017207500175300ustar00rootroot00000000000000/* * Copyright 2023 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_RELEASES_H #define GCLI_CMD_RELEASES_H #ifdef HAVE_CONFIG_H #include #endif #include void gcli_releases_print(enum gcli_output_flags flags, struct gcli_release_list const *list, int max); int subcommand_releases(int argc, char *argv[]); #endif /* GCLI_CMD_RELEASES_H */ gcli-2.9.1/include/gcli/cmd/repos.h000066400000000000000000000033541507017207500170570ustar00rootroot00000000000000/* * Copyright 2023 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_REPOS_H #define GCLI_CMD_REPOS_H #ifdef HAVE_CONFIG_H #include #endif #include #include void gcli_print_repos(enum gcli_output_flags flags, struct gcli_repo_list const *repos, int max); void gcli_repo_print(struct gcli_repo const *it); int subcommand_repos(int argc, char *argv[]); #endif /* GCLI_CMD_REPOS_H */ gcli-2.9.1/include/gcli/cmd/snippets.h000066400000000000000000000034121507017207500175670ustar00rootroot00000000000000/* * Copyright 2023 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_SNIPPETS_H #define GCLI_CMD_SNIPPETS_H #ifdef HAVE_CONFIG_H #include #endif #include #include void gcli_snippets_print(enum gcli_output_flags const flags, struct gcli_gitlab_snippet_list const *const list, int const max); int subcommand_snippets(int argc, char *argv[]); #endif /* GCLI_CMD_SNIPPETS_H */ gcli-2.9.1/include/gcli/cmd/status.h000066400000000000000000000032471507017207500172530ustar00rootroot00000000000000/* * Copyright 2023 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_STATUS_H #define GCLI_CMD_STATUS_H #ifdef HAVE_CONFIG_H #include #endif #include #include int gcli_status(int count); void gcli_print_notifications(struct gcli_notification_list const *); int subcommand_status(int argc, char *argv[]); #endif /* GCLI_CMD_STATUS_H */ gcli-2.9.1/include/gcli/cmd/status_interactive.h000066400000000000000000000030541507017207500216440ustar00rootroot00000000000000/* * 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_STATUS_INTERACTIVE_H #define GCLI_CMD_STATUS_INTERACTIVE_H #ifdef HAVE_CONFIG_H #include "config.h" #endif int gcli_status_interactive(void); #endif /* GCLI_CMD_STATUS_INTERACTIVE_H */ gcli-2.9.1/include/gcli/cmd/table.h000066400000000000000000000077661507017207500170310ustar00rootroot00000000000000/* * Copyright 2022-2025 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_TABLE_H #define GCLI_CMD_TABLE_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include typedef void *gcli_tbl; typedef void *gcli_dict; /** Flags for table column definitions */ enum gcli_tblcol_flags { /* column is as string and colour is derived from its contents. */ GCLI_TBLCOL_STATECOLOURED = 1, /* Right-justify the column */ GCLI_TBLCOL_JUSTIFYR = 2, /* Make it bold */ GCLI_TBLCOL_BOLD = 4, /* Explicit colour - provide the colour to gcli_tbl_add_row first * and second the content of the cell. */ GCLI_TBLCOL_COLOUREXPL = 8, /* 256 colour handling. Just like the above */ GCLI_TBLCOL_256COLOUR = 16, /* Have a column spacing to the right of one instead of two spaces */ GCLI_TBLCOL_TIGHT = 32, }; enum gcli_tblcoltype { GCLI_TBLCOLTYPE_INT, /* integer */ GCLI_TBLCOLTYPE_LONG, /* signed long int */ GCLI_TBLCOLTYPE_ID, /* some ID type (uint64_t) */ GCLI_TBLCOLTYPE_STRING, /* C string */ GCLI_TBLCOLTYPE_TIME_T, /* a time_t unix timestamp */ GCLI_TBLCOLTYPE_DOUBLE, /* double precision float */ GCLI_TBLCOLTYPE_BOOL, /* yes/no */ }; /** A single table column */ struct gcli_tblcoldef { char const *name; /* name of the column, also displayed in first row */ int type; /* type of values in this column */ int flags; /* flags about this column */ }; /* Init a table printer */ gcli_tbl gcli_tbl_begin(struct gcli_tblcoldef const *cols, size_t cols_size); /* Print the table contents and free all the resources allocated in * the table */ void gcli_tbl_end(gcli_tbl table); /* Add a single to an initialized table */ int gcli_tbl_add_row(gcli_tbl table, ...); gcli_dict gcli_dict_begin(void); int gcli_dict_add(gcli_dict list, char const *key, int flags, uint32_t colour_args, char const *fmt, ...) PRINTF_FORMAT(5, 6); int gcli_dict_add_string(gcli_dict list, char const *key, int flags, uint32_t colour_args, char const *str); int gcli_dict_add_timestamp(gcli_dict list, char const *key, int flags, uint32_t colour_args, time_t stamp); int gcli_dict_add_sv_list(gcli_dict dict, char const *key, gcli_sv const *list, size_t list_size); int gcli_dict_add_string_list(gcli_dict dict, char const *const key, char const *const *list, size_t const list_size); int gcli_dict_end(gcli_dict _list); #endif /* GCLI_CMD_TABLE_H */ gcli-2.9.1/include/gcli/comments.h000066400000000000000000000056141507017207500170120ustar00rootroot00000000000000/* * Copyright 2021-2025 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 COMMENTS_H #define COMMENTS_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include struct gcli_comment { char *author; /* Login name of the comment author */ time_t date; /* Creation date of the comment */ gcli_id id; /* id of the comment */ char *body; /* Raw text of the comment */ }; struct gcli_comment_list { struct gcli_comment *comments; /* List of comments */ size_t comments_size; /* Size of the list */ }; struct gcli_submit_comment_opts { enum comment_target_type { ISSUE_COMMENT, PR_COMMENT } target_type; struct gcli_path target; char const *message; }; void gcli_comments_free(struct gcli_comment_list *list); void gcli_comment_free(struct gcli_comment *const it); int gcli_get_comment(struct gcli_ctx *ctx, struct gcli_path const *target, enum comment_target_type target_type, gcli_id comment_id, struct gcli_comment *out); int gcli_get_issue_comments(struct gcli_ctx *ctx, struct gcli_path const *issue_path, struct gcli_comment_list *out); int gcli_get_pull_comments(struct gcli_ctx *ctx, struct gcli_path const *pull_path, struct gcli_comment_list *out); int gcli_comment_submit(struct gcli_ctx *ctx, struct gcli_submit_comment_opts const *opts); #endif /* COMMENTS_H */ gcli-2.9.1/include/gcli/ctx.h000066400000000000000000000042001507017207500157510ustar00rootroot00000000000000/* * Copyright 2023-2025 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_CTX_H #define GCLI_CTX_H #include #include /* Strictly internal structure containing the gcli library context * data */ struct gcli_ctx { CURL *curl; char *curl_useragent; void *usrdata; char *last_error; char *apibase; /* generated by a call to get_apibase */ char *(*get_token)(struct gcli_ctx *); gcli_forge_type (*get_forge_type)(struct gcli_ctx *ctx); char *(*get_apibase)(struct gcli_ctx *); void (*report_progress)(bool done); int verbosity; }; /* Error routine */ int gcli_error(struct gcli_ctx *ctx, char const *const fmt, ...); /* mostly concerning warn(x) */ char *gcli_get_apibase(struct gcli_ctx *ctx); char *gcli_get_authheader(struct gcli_ctx *ctx); char *gcli_get_token(struct gcli_ctx *ctx); #endif /* GCLI_CTX_H */ gcli-2.9.1/include/gcli/curl.h000066400000000000000000000065541507017207500161360ustar00rootroot00000000000000/* * Copyright 2021-2025 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 CURL_H #define CURL_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include typedef int (*parsefn)(struct gcli_ctx *, struct json_stream *stream, void *list, size_t *listsize); typedef void (*filterfn)(void *list, size_t *listsize, void const *userdata); struct gcli_fetch_buffer { char *data; size_t length; }; struct gcli_fetch_list_ctx { void *listp; /* pointer to pointer of start of list */ size_t *sizep; /* pointer to list size */ int max; parsefn parse; /* json parse routine */ filterfn filter; /* optional filter */ void const *userdata; }; void gcli_fetch_buffer_free(struct gcli_fetch_buffer *buffer); int gcli_fetch(struct gcli_ctx *ctx, char const *url, char **pagination_next, struct gcli_fetch_buffer *out); int gcli_curl(struct gcli_ctx *ctx, FILE *stream, char const *url, char const *content_type); int gcli_fetch_with_method(struct gcli_ctx *ctx, char const *method, char const *url, char const *data, char **pagination_next, struct gcli_fetch_buffer *out); int gcli_post_upload(struct gcli_ctx *ctx, char const *url, char const *content_type, void *buffer, size_t buffer_size, struct gcli_fetch_buffer *out); int gcli_curl_gitea_upload_attachment(struct gcli_ctx *ctx, char const *url, char const *filename, struct gcli_fetch_buffer *out); int gcli_curl_test_success(struct gcli_ctx *ctx, char const *url); char *gcli_urlencode(char const *); gcli_sv gcli_urlencode_sv(gcli_sv const); char *gcli_urldecode(struct gcli_ctx *ctx, char const *input); int gcli_fetch_list(struct gcli_ctx *ctx, char *url, struct gcli_fetch_list_ctx *fctx); #endif /* CURL_H */ gcli-2.9.1/include/gcli/date_time.h000066400000000000000000000036311507017207500171150ustar00rootroot00000000000000/* * Copyright 2023 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_DATE_TIME_H #define GCLI_DATE_TIME_H #ifdef HAVE_CONFIG_H #include #endif #include #include enum { DATEFMT_ISO8601, DATEFMT_GITLAB, }; int gcli_normalize_date(struct gcli_ctx *ctx, int fmt, char const *const input, char *output, size_t const output_size); int gcli_parse_iso8601_date_time(struct gcli_ctx *ctx, char const *input, time_t *out); int gcli_format_as_localtime(struct gcli_ctx *ctx, time_t timestamp, char **out); #endif /* GCLI_DATE_TIME_H */ gcli-2.9.1/include/gcli/diffutil.h000066400000000000000000000125171507017207500167730ustar00rootroot00000000000000/* * Copyright 2023 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_DIFFUTIL_H #define GCLI_DIFFUTIL_H #include #include #include #include struct gcli_patch; struct gcli_patch_series; struct gcli_diff_hunk { TAILQ_ENTRY(gcli_diff_hunk) next; int range_a_start, range_a_length, range_r_start, range_r_length; char *context_info; char *body; int diff_line_offset; /* line offset of this hunk inside the diff */ }; struct gcli_diff { TAILQ_ENTRY(gcli_diff) next; /* Tailq ntex pointer */ struct gcli_patch *patch; /* optional: the patch this diff belongs to */ char *file_a, *file_b; char *hash_a, *hash_b; char *file_mode; int new_file_mode; char *r_file, *a_file; /* file with removals and additions */ TAILQ_HEAD(gcli_diff_hunks, gcli_diff_hunk) hunks; }; struct gcli_patch { char *prelude; /* Text leading up to the first diff */ char *commit_hash; /* commit hash for this patch */ struct gcli_patch_series *patch_series; /* the patch series this patch is a part of */ TAILQ_ENTRY(gcli_patch) next; /* Next pointer in patch series */ // FIXME: is this declaration really needed? TAILQ_HEAD(gcli_diffs, gcli_diff) diffs; }; struct gcli_patch_series { TAILQ_HEAD(, gcli_patch) patches; char *prelude; }; struct gcli_diff_parser { char const *buf, *hd; size_t buf_size; char const *filename; int col, row; int diff_line_offset; /* The parser was initialised with gcli_diff_parser_from_file * which allocates on the heap. We need to free this when * asked to clean up the parser. */ bool buf_needs_free; }; /* A single comment referring to a chunk in a dif */ struct gcli_diff_comment { TAILQ_ENTRY(gcli_diff_comment) next; struct { char *filename; int start_row, end_row; } before, after; /* range info for before and after applying hunk */ bool start_is_in_new, end_is_in_new; int diff_line_offset; /* line offset inside the diff */ char *commit_hash; /* The commit this comment refers to */ char *comment; /* text of the comment */ char *diff_text; /* the diff text this comment refers to */ }; TAILQ_HEAD(gcli_diff_comments, gcli_diff_comment); int gcli_diff_parser_from_buffer(char const *buf, size_t buf_size, char const *filename, struct gcli_diff_parser *out); int gcli_diff_parser_from_file(FILE *f, char const *filename, struct gcli_diff_parser *out); int gcli_parse_diff(struct gcli_diff_parser *parser, struct gcli_diff *out); int gcli_parse_patch_series(struct gcli_diff_parser *parser, struct gcli_patch_series *out); int gcli_parse_patch(struct gcli_diff_parser *parser, struct gcli_patch *out); int gcli_patch_get_comments(struct gcli_patch const *patch, struct gcli_diff_comments *out); int gcli_patch_parse_prelude(struct gcli_diff_parser *parser, struct gcli_patch *out); int gcli_parse_patch(struct gcli_diff_parser *parser, struct gcli_patch *out); int gcli_parse_diff(struct gcli_diff_parser *parser, struct gcli_diff *out); int gcli_patch_parse_prelude(struct gcli_diff_parser *parser, struct gcli_patch *out); int gcli_patch_get_comments(struct gcli_patch const *patch, struct gcli_diff_comments *out); int gcli_patch_series_get_comments(struct gcli_patch_series const *series, struct gcli_diff_comments *out); void gcli_free_diff_parser(struct gcli_diff_parser *parser); void gcli_free_diff(struct gcli_diff *diff); void gcli_free_diff_hunk(struct gcli_diff_hunk *hunk); void gcli_free_patch(struct gcli_patch *patch); void gcli_free_patch_series(struct gcli_patch_series *series); void gcli_free_diff(struct gcli_diff *diff); int gcli_patch_series_get_comments(struct gcli_patch_series const *series, struct gcli_diff_comments *out); #endif /* GCLI_DIFFUTIL_H */ gcli-2.9.1/include/gcli/forges.h000066400000000000000000000353551507017207500164570ustar00rootroot00000000000000/* * Copyright 2021-2025 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 FORGES_H #define FORGES_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include /* Hopefully temporary hack */ typedef int (*gcli_get_pull_checks_cb)( struct gcli_ctx *, struct gcli_path const *, struct gcli_pull_checks_list *); /** * Struct of function pointers to perform actions in the given * forge. It is like a plugin system to dispatch. */ struct gcli_forge_descriptor { /** * Submit a comment to a pull/mr or issue */ int (*perform_submit_comment)( struct gcli_ctx *ctx, struct gcli_submit_comment_opts const *opts); /** * List comments on the given issue */ int (*get_issue_comments)( struct gcli_ctx *ctx, struct gcli_path const *issue_path, struct gcli_comment_list *out); /** * List comments on the given PR */ int (*get_pull_comments)( struct gcli_ctx *ctx, struct gcli_path const *pull_path, struct gcli_comment_list *out); /** * Get a specific comment on an issue or pull request with the given ID */ int (*get_comment)( struct gcli_ctx *ctx, struct gcli_path const *target, enum comment_target_type target_type, gcli_id comment_id, struct gcli_comment *out); /** * List forks of the given repo */ int (*get_forks)( struct gcli_ctx *ctx, struct gcli_path const *repo_path, int max, struct gcli_fork_list *out); /** * Fork the given repo into the owner _in */ int (*fork_create)( struct gcli_ctx *ctx, struct gcli_path const *repo_path, char const *_in); /** * Get a list of issues on the given repo */ int (*search_issues)( struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_issue_fetch_details const *details, int max, struct gcli_issue_list *out); /** * Get a summary of an issue */ int (*get_issue_summary)( struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_issue *out); /** * Close the given issue */ int (*issue_close)( struct gcli_ctx *ctx, struct gcli_path const *path); /** * Reopen the given issue */ int (*issue_reopen)( struct gcli_ctx *ctx, struct gcli_path const *path); /** * Assign an issue to a user */ int (*issue_assign)( struct gcli_ctx *ctx, struct gcli_path const *issue_path, char const *assignee); /** * Add labels to issues */ int (*issue_add_labels)( struct gcli_ctx *ctx, struct gcli_path const *issue_path, char const *const labels[], size_t labels_size); /** * Removes labels from issues */ int (*issue_remove_labels)( struct gcli_ctx *ctx, struct gcli_path const *issue_path, char const *const labels[], size_t labels_size); /** * Submit an issue */ int (*perform_submit_issue)( struct gcli_ctx *ctx, struct gcli_submit_issue_options *opts, struct gcli_issue *out); /** * Change the title of an issue */ int (*issue_set_title)( struct gcli_ctx *ctx, struct gcli_path const *issue_path, char const *new_title); /** * Get attachments of an issue */ int (*get_issue_attachments)( struct gcli_ctx *ctx, struct gcli_path const *issue_path, struct gcli_attachment_list *out); /** * Change the OP (original post) of the issue */ int (*issue_set_op)( struct gcli_ctx *ctx, struct gcli_path const *issue_path, char const *new_op); /** * Dump the contents of the attachment to the given file */ int (*attachment_get_content)( struct gcli_ctx *ctx, gcli_id id, FILE *out); /* Issue quirk bitmask */ enum { GCLI_ISSUE_QUIRKS_LOCKED = 0x1, GCLI_ISSUE_QUIRKS_COMMENTS = 0x2, GCLI_ISSUE_QUIRKS_PROD_COMP = 0x4, GCLI_ISSUE_QUIRKS_URL = 0x8, } const issue_quirks; /** * Bitmask of exceptions/fields that the forge doesn't support */ enum { GCLI_MILESTONE_QUIRKS_EXPIRED = 0x1, GCLI_MILESTONE_QUIRKS_DUEDATE = 0x2, GCLI_MILESTONE_QUIRKS_PULLS = 0x4, GCLI_MILESTONE_QUIRKS_NISSUES = 0x8, } const milestone_quirks; /** * Get list of milestones */ int (*get_milestones)( struct gcli_ctx *ctx, struct gcli_path const *path, int max, struct gcli_milestone_list *out); /** * Get a single milestone */ int (*get_milestone)( struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_milestone *out); /** * create a milestone */ int (*create_milestone)( struct gcli_ctx *ctx, struct gcli_path const *repo, struct gcli_milestone_create_args const *args); /** * delete a milestone */ int (*delete_milestone)( struct gcli_ctx *ctx, struct gcli_path const *path); /** * delete a milestone */ int (*milestone_set_duedate)( struct gcli_ctx *ctx, struct gcli_path const *path, char const *date); /** * Get list of issues attached to this milestone */ int (*get_milestone_issues)( struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue_list *out); /** Assign an issue to a milestone */ int (*issue_set_milestone)( struct gcli_ctx *ctx, struct gcli_path const *issue_path, gcli_id milestone); /** * Clear the milestones of an issue */ int (*issue_clear_milestone)( struct gcli_ctx *ctx, struct gcli_path const *issue_path); /** * Get a list of PRs/MRs on the given repo */ int (*search_pulls)( struct gcli_ctx *ctx, struct gcli_path const *repo_path, struct gcli_pull_fetch_details const *details, int max, struct gcli_pull_list *out); /** * Fetch the PR diff into the file */ int (*pull_get_diff)( struct gcli_ctx *ctx, FILE *stream, struct gcli_path const *path); /** * Fetch the PR patch series into the file */ int (*pull_get_patch)( struct gcli_ctx *ctx, FILE *stream, struct gcli_path const *const pull_path); /** * Fetch reviews on this pull request */ int (*pull_get_reviews)( struct gcli_ctx *ctx, struct gcli_path const *pull_path, struct gcli_pull_reviews *out); /** * get review threads */ int (*pull_get_review_threads)( struct gcli_ctx *ctx, struct gcli_path const *pull_path, struct gcli_pull_review_thread *out); /** * Return a list of checks associated with the given pull. * * The type of the returned list depends on the forge type. See * the definition of struct gcli_pull_checks_list. */ gcli_get_pull_checks_cb get_pull_checks; /** * Merge the given PR/MR */ int (*pull_merge)( struct gcli_ctx *ctx, struct gcli_path const *path, enum gcli_merge_flags flags); /** * Reopen the given PR/MR */ int (*pull_reopen)( struct gcli_ctx *ctx, struct gcli_path const *path); /** * Close the given PR/MR */ int (*pull_close)( struct gcli_ctx *ctx, struct gcli_path const *path); /** * Quirks when creating pull requests */ enum gcli_pull_quirks { GCLI_PULL_QUIRK_AUTOMERGE = 0x01, /* forge does not support automerge */ } pull_quirks; /** * Submit PR/MR */ int (*perform_submit_pull)( struct gcli_ctx *ctx, struct gcli_submit_pull_options *opts); /** * Get a list of commits in the given PR/MR */ int (*get_pull_commits)( struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_commit_list *out); /** Bitmask of unsupported fields in the pull summary for this * forge */ enum gcli_pull_summary_quirks { GCLI_PRS_QUIRK_ADDDEL = 0x01, GCLI_PRS_QUIRK_COMMITS = 0x02, GCLI_PRS_QUIRK_CHANGES = 0x04, GCLI_PRS_QUIRK_MERGED = 0x08, GCLI_PRS_QUIRK_DRAFT = 0x10, GCLI_PRS_QUIRK_COVERAGE = 0x20, GCLI_PRS_QUIRK_AUTOMERGE = 0x40, } pull_summary_quirks; /** * Get a summary of the given PR/MR */ int (*get_pull)( struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_pull *out); /** * Add labels to Pull Requests */ int (*pull_add_labels)( struct gcli_ctx *ctx, struct gcli_path const *pull_path, char const *const labels[], size_t labels_size); /** * Removes labels from Pull Requests */ int (*pull_remove_labels)( struct gcli_ctx *ctx, struct gcli_path const *pull_path, char const *const labels[], size_t labels_size); /** * Assign a PR to a milestone */ int (*pull_set_milestone)( struct gcli_ctx *ctx, struct gcli_path const *pull_path, gcli_id milestone_id); /** * Clear a milestone on a PR */ int (*pull_clear_milestone)( struct gcli_ctx *ctx, struct gcli_path const *issue_path); /** * Request review of a given pull request by a user */ int (*pull_add_reviewer)( struct gcli_ctx *ctx, struct gcli_path const *path, char const *username); /** * assign this pull request to the given user */ int (*pull_assign)( struct gcli_ctx *ctx, struct gcli_path const *path, char const *username); /** * Change the title of a pull request */ int (*pull_set_title)( struct gcli_ctx *ctx, struct gcli_path const *path, char const *new_title); /** * Create a review on a PR */ int (*pull_create_review)( struct gcli_ctx *ctx, struct gcli_pull_create_review_details const *details); /** Checkout this PR */ int (*pull_checkout)( struct gcli_ctx *ctx, char const *remote, struct gcli_path const *pull_path); /** * Get a list of releases in the given repo */ int (*get_releases)( struct gcli_ctx *ctx, struct gcli_path const *repo_path, int max, struct gcli_release_list *out); /** * Create a new release */ int (*create_release)( struct gcli_ctx *ctx, struct gcli_create_release_args const *release); /** * Delete the release */ int (*delete_release)( struct gcli_ctx *ctx, struct gcli_path const *repo_path, char const *id); /** * Get a list of labels that are valid in the given repository */ int (*get_labels)( struct gcli_ctx *ctx, struct gcli_path const *path, int max, struct gcli_label_list *out); /** * Get a single label */ int (*get_label)( struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_label *out); /** * Create the given label * * The ID will be filled in for you */ int (*create_label)( struct gcli_ctx *ctx, struct gcli_path const *repo_path, struct gcli_label *label); /** * Delete the given label */ int (*delete_label)( struct gcli_ctx *ctx, struct gcli_path const *path); /** * change the title of a label */ int (*label_set_title)( struct gcli_ctx *ctx, struct gcli_path const *path, char const *new_name); /** * change the description of a label */ int (*label_set_description)( struct gcli_ctx *ctx, struct gcli_path const *path, char const *new_description); /** * change the colour of a label */ int (*label_set_colour)( struct gcli_ctx *ctx, struct gcli_path const *path, uint32_t colour_rgb); /** * Get a list of repos of the given owner */ int (*get_repos)( struct gcli_ctx *ctx, char const *owner, int max, struct gcli_repo_list *out); /** * Create the given repo */ int (*repo_create)( struct gcli_ctx *ctx, struct gcli_repo_create_options const *options, struct gcli_repo *out); /** * Delete the given repo */ int (*repo_delete)( struct gcli_ctx *ctx, struct gcli_path const *path); /** * Change the visibility level of a repository */ int (*repo_set_visibility)( struct gcli_ctx *ctx, struct gcli_path const *path, gcli_repo_visibility vis); /** * Status summary for the account */ int (*get_notifications)( struct gcli_ctx *ctx, int max, struct gcli_notification_list *notifications); /** * Given a notification fetch the target's comments */ int (*notification_get_comments)( struct gcli_ctx *ctx, struct gcli_notification const *notification, struct gcli_comment_list *out); /** * Mark notification with the given id as read * * Returns 0 on success or negative code on failure. */ int (*notification_mark_as_read)( struct gcli_ctx *ctx, char const *id); /** * Get an the http authentication header for use by curl */ char *(*make_authheader)(struct gcli_ctx *ctx, char const *token); /** * Get list of SSH keys */ int (*get_sshkeys)(struct gcli_ctx *ctx, struct gcli_sshkey_list *); /** * Add an SSH public key */ int (*add_sshkey)( struct gcli_ctx *ctx, char const *title, char const *public_key_path, struct gcli_sshkey *out); /** * Delete an SSH public key by its ID */ int (*delete_sshkey)(struct gcli_ctx *ctx, gcli_id id); /** * Get the error string from the API */ char const *(*get_api_error_string)( struct gcli_ctx *ctx, struct gcli_fetch_buffer *); /** * A key in the user json object sent by the API that represents * the user name */ char const *user_object_key; }; struct gcli_forge_descriptor const *gcli_forge(struct gcli_ctx *ctx); /** A macro used for calling one of the dispatch points above. * * It check whether the given function pointer is null. If it is it will return * an error message otherwise the function is called with the specified * arguments. */ #define gcli_null_check_call(routine, ctx, ...) \ do { \ struct gcli_forge_descriptor const *const forge = gcli_forge(ctx); \ \ if (forge->routine) { \ return forge->routine(ctx, __VA_ARGS__); \ } else { \ return gcli_error(ctx, #routine " is not available on this forge"); \ } \ } while (0) #endif /* FORGES_H */ gcli-2.9.1/include/gcli/forks.h000066400000000000000000000041451507017207500163070ustar00rootroot00000000000000/* * Copyright 2021-2025 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 FORK_H #define FORK_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include struct gcli_fork { char *full_name; char *owner; time_t date; int forks; }; struct gcli_fork_list { struct gcli_fork *forks; size_t forks_size; }; int gcli_get_forks(struct gcli_ctx *ctx, struct gcli_path const *path, int max, struct gcli_fork_list *out); int gcli_fork_create(struct gcli_ctx *ctx, struct gcli_path const *repo_path, char const *in); void gcli_fork_delete(char const *owner, char const *repo); void gcli_forks_free(struct gcli_fork_list *list); void gcli_fork_free(struct gcli_fork *fork); #endif /* FORK_H */ gcli-2.9.1/include/gcli/gcli.h000066400000000000000000000055301507017207500161000ustar00rootroot00000000000000/* * Copyright 2021-2025 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_H #define GCLI_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include enum gcli_output_flags { OUTPUT_SORTED = (1 << 0), OUTPUT_LONG = (1 << 1), }; typedef enum gcli_forge_type { GCLI_FORGE_GITHUB, GCLI_FORGE_GITLAB, GCLI_FORGE_GITEA, GCLI_FORGE_BUGZILLA, } gcli_forge_type; typedef uint64_t gcli_id; #define PRIid PRIu64 #ifdef IN_LIBGCLI #include void gcli_clear_ptr(void *ptr); #endif /* IN_LIBGCLI */ struct gcli_ctx; char const *gcli_init(struct gcli_ctx **, gcli_forge_type (*get_forge_type)(struct gcli_ctx *), char *(*get_authheader)(struct gcli_ctx *), char *(*get_apibase)(struct gcli_ctx *)); enum { GCLI_VERBOSITY_NORMAL = 0, GCLI_VERBOSITY_QUIET = 1, GCLI_VERBOSITY_VERBOSE = 2, }; void gcli_setverbosity(struct gcli_ctx *, int); int gcli_getverbosity(struct gcli_ctx *); bool gcli_be_verbose(struct gcli_ctx *); bool gcli_be_quiet(struct gcli_ctx *); void gcli_warn(struct gcli_ctx *, char const *fmt, ...) PRINTF_FORMAT(2, 3); void gcli_warnx(struct gcli_ctx *, char const *fmt, ...) PRINTF_FORMAT(2, 3); void *gcli_get_userdata(struct gcli_ctx const *); void gcli_set_userdata(struct gcli_ctx *, void *usrdata); void gcli_set_progress_func(struct gcli_ctx *, void (*pfunc)(bool done)); void gcli_destroy(struct gcli_ctx **ctx); char const *gcli_get_error(struct gcli_ctx *ctx); #endif /* GCLI_H */ gcli-2.9.1/include/gcli/gitea/000077500000000000000000000000001507017207500160775ustar00rootroot00000000000000gcli-2.9.1/include/gcli/gitea/comments.h000066400000000000000000000037621507017207500201050ustar00rootroot00000000000000/* * Copyright 2022 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 GITEA_COMMENTS_H #define GITEA_COMMENTS_H #ifdef HAVE_CONFIG_H #include #endif #include #include int gitea_get_comment(struct gcli_ctx *ctx, struct gcli_path const *target, enum comment_target_type target_type, gcli_id comment_id, struct gcli_comment *out); int gitea_get_comments(struct gcli_ctx *ctx, struct gcli_path const *issue_path, struct gcli_comment_list *out); int gitea_perform_submit_comment(struct gcli_ctx *ctx, struct gcli_submit_comment_opts const *opts); #endif /* GITEA_COMMENTS_H */ gcli-2.9.1/include/gcli/gitea/config.h000066400000000000000000000030771507017207500175240ustar00rootroot00000000000000/* * Copyright 2022-2025 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 GITEA_CONFIG_H #define GITEA_CONFIG_H #ifdef HAVE_CONFIG_H #include #endif #include char *gitea_make_authheader(struct gcli_ctx *ctx, char const *token); #endif /* GITEA_CONFIG_H */ gcli-2.9.1/include/gcli/gitea/forks.h000066400000000000000000000034311507017207500173750ustar00rootroot00000000000000/* * Copyright 2022 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 GITEA_FORKS_H #define GITEA_FORKS_H #ifdef HAVE_CONFIG_H #include #endif #include #include int gitea_get_forks(struct gcli_ctx *ctx, struct gcli_path const *path, int max, struct gcli_fork_list *out); int gitea_fork_create(struct gcli_ctx *ctx, struct gcli_path const *repo_path, char const *_in); #endif /* GITEA_FORKS_H */ gcli-2.9.1/include/gcli/gitea/issues.h000066400000000000000000000065431507017207500175730ustar00rootroot00000000000000/* * Copyright 2022-2025 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 GITEA_ISSUES_H #define GITEA_ISSUES_H #ifdef HAVE_CONFIG_H #include #endif #include int gitea_issue_make_url(struct gcli_ctx *ctx, struct gcli_path const *path, char **url, char const *suffix_fmt, ...); int gitea_issues_search(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_issue_fetch_details const *details, int max, struct gcli_issue_list *out); int gitea_get_issue_summary(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_issue *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, struct gcli_path const *path); int gitea_issue_reopen(struct gcli_ctx *ctx, struct gcli_path const *path); int gitea_issue_assign(struct gcli_ctx *ctx, struct gcli_path const *path, char const *assignee); int gitea_issue_add_labels(struct gcli_ctx *ctx, struct gcli_path const *path, char const *const labels[], size_t labels_size); int gitea_issue_remove_labels(struct gcli_ctx *ctx, struct gcli_path const *path, char const *const labels[], size_t labels_size); int gitea_issue_set_milestone(struct gcli_ctx *ctx, struct gcli_path const *issue_path, gcli_id milestone); int gitea_issue_clear_milestone(struct gcli_ctx *ctx, struct gcli_path const *const issue_path); int gitea_issue_set_title(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, char const *const new_title); int gitea_issue_set_op(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const new_op); #endif /* GITEA_ISSUES_H */ gcli-2.9.1/include/gcli/gitea/labels.h000066400000000000000000000046251507017207500175210ustar00rootroot00000000000000/* * Copyright 2022-2025 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 GITEA_LABELS_H #define GITEA_LABELS_H #ifdef HAVE_CONFIG_H #include #endif #include #include int gitea_get_label(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_label *out); int gitea_get_labels(struct gcli_ctx *ctx, struct gcli_path const *path, int max, struct gcli_label_list *out); int gitea_create_label(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_label *label); int gitea_delete_label(struct gcli_ctx *ctx, struct gcli_path const *repo_path); int gitea_label_set_title(struct gcli_ctx *ctx, struct gcli_path const *path, char const *new_name); int gitea_label_set_description(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const description); int gitea_label_set_colour(struct gcli_ctx *ctx, struct gcli_path const *path, uint32_t colour); #endif /* GITEA_LABELS_H */ gcli-2.9.1/include/gcli/gitea/milestones.h000066400000000000000000000046251507017207500204410ustar00rootroot00000000000000/* * Copyright 2023-2025 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_GITEA_MILESTONES_H #define GCLI_GITEA_MILESTONES_H #include int gitea_get_milestones(struct gcli_ctx *ctx, struct gcli_path const *path, int max, struct gcli_milestone_list *out); int gitea_get_milestone(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_milestone *out); int gitea_create_milestone(struct gcli_ctx *ctx, struct gcli_path const *repo, struct gcli_milestone_create_args const *args); int gitea_delete_milestone(struct gcli_ctx *ctx, struct gcli_path const *const path); int gitea_milestone_set_duedate(struct gcli_ctx *ctx, struct gcli_path const *path, char const *date); int gitea_milestone_get_issues(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_issue_list *out); #endif /* GCLI_GITEA_MILESTONES_H */ gcli-2.9.1/include/gcli/gitea/pulls.h000066400000000000000000000071541507017207500174160ustar00rootroot00000000000000/* * Copyright 2022-2025 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 GITEA_PULLS_H #define GITEA_PULLS_H #ifdef HAVE_CONFIG_H #include #endif #include #include int gitea_search_pulls(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_pull_fetch_details const *details, int const max, struct gcli_pull_list *const out); int gitea_get_pull(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_pull *out); int gitea_get_pull_commits(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_commit_list *out); int gitea_pull_submit(struct gcli_ctx *ctx, struct gcli_submit_pull_options *opts); int gitea_pull_merge(struct gcli_ctx *ctx, struct gcli_path const *path, enum gcli_merge_flags flags); int gitea_pull_close(struct gcli_ctx *ctx, struct gcli_path const *path); int gitea_pull_reopen(struct gcli_ctx *ctx, struct gcli_path const *path); int gitea_pull_get_diff(struct gcli_ctx *ctx, FILE *stream, struct gcli_path const *const path); int gitea_pull_get_patch(struct gcli_ctx *ctx, FILE *stream, struct gcli_path const *pull_path); int gitea_pull_get_checks(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_pull_checks_list *out); int gitea_pull_set_milestone(struct gcli_ctx *ctx, struct gcli_path const *const pull_path, gcli_id milestone_id); int gitea_pull_clear_milestone(struct gcli_ctx *ctx, struct gcli_path const *const pull_path); int gitea_pull_add_reviewer(struct gcli_ctx *ctx, struct gcli_path const *path, char const *username); int gitea_pull_assign(struct gcli_ctx *ctx, struct gcli_path const *path, char const *username); int gitea_pull_set_title(struct gcli_ctx *ctx, struct gcli_path const *path, char const *const title); int gitea_pull_get_reviews(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_pull_reviews *out); #endif /* GITEA_PULLS_H */ gcli-2.9.1/include/gcli/gitea/releases.h000066400000000000000000000036641507017207500200640ustar00rootroot00000000000000/* * Copyright 2022-2025 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 GITEA_RELEASES_H #define GITEA_RELEASES_H #ifdef HAVE_CONFIG_H #include #endif #include #include int gitea_get_releases(struct gcli_ctx *ctx, struct gcli_path const *repo_path, int max, struct gcli_release_list *list); int gitea_create_release(struct gcli_ctx *ctx, struct gcli_create_release_args const *release); int gitea_delete_release(struct gcli_ctx *ctx, struct gcli_path const *path, char const *id); #endif /* GITEA_RELEASES_H */ gcli-2.9.1/include/gcli/gitea/repos.h000066400000000000000000000043741507017207500174100ustar00rootroot00000000000000/* * Copyright 2022 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 GITEA_REPOS_H #define GITEA_REPOS_H #ifdef HAVE_CONFIG_H #include #endif #include int gitea_get_repos(struct gcli_ctx *ctx, char const *owner, int max, struct gcli_repo_list *out); int gitea_get_own_repos(struct gcli_ctx *ctx, int max, struct gcli_repo_list *out); int gitea_repo_create(struct gcli_ctx *ctx, struct gcli_repo_create_options const *options, struct gcli_repo *out); int gitea_repo_delete(struct gcli_ctx *ctx, struct gcli_path const *path); int gitea_repo_set_visibility(struct gcli_ctx *ctx, struct gcli_path const *path, gcli_repo_visibility vis); int gitea_repo_make_url(struct gcli_ctx *ctx, struct gcli_path const *path, char **url, char const *suffix_fmt, ...); #endif /* GITEA_REPOS_H */ gcli-2.9.1/include/gcli/gitea/sshkeys.h000066400000000000000000000033521507017207500177440ustar00rootroot00000000000000/* * Copyright 2023 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_GITEA_SSHKEYS_H #define GCLI_GITEA_SSHKEYS_H #include int gitea_get_sshkeys(struct gcli_ctx *ctx, struct gcli_sshkey_list *out); int gitea_add_sshkey(struct gcli_ctx *ctx, char const *title, char const *public_key_data, struct gcli_sshkey *out); int gitea_delete_sshkey(struct gcli_ctx *ctx, gcli_id id); #endif /* GCLI_GITEA_SSHKEYS_H */ gcli-2.9.1/include/gcli/gitea/status.h000066400000000000000000000032151507017207500175740ustar00rootroot00000000000000/* * Copyright 2023 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 GITEA_STATUS_H #define GITEA_STATUS_H #include int gitea_get_notifications(struct gcli_ctx *ctx, int max, struct gcli_notification_list *out); int gitea_notification_mark_as_read(struct gcli_ctx *ctx, char const *id); #endif /* GITEA_STATUS_H */ gcli-2.9.1/include/gcli/github/000077500000000000000000000000001507017207500162705ustar00rootroot00000000000000gcli-2.9.1/include/gcli/github/api.h000066400000000000000000000031151507017207500172120ustar00rootroot00000000000000/* * Copyright 2021, 2022 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 GITHUB_API_H #define GITHUB_API_H #ifdef HAVE_CONFIG_H #include #endif #include char const *github_api_error_string(struct gcli_ctx *ctx, struct gcli_fetch_buffer *it); #endif /* GITHUB_API_H */ gcli-2.9.1/include/gcli/github/checkout.h000066400000000000000000000030641507017207500202510ustar00rootroot00000000000000/* * 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 GITHUB_CHECKOUT_H #define GITHUB_CHECKOUT_H int github_pull_checkout(struct gcli_ctx *ctx, char const *remote, struct gcli_path const *pull_path); #endif /* GITHUB_CHECKOUT_H */ gcli-2.9.1/include/gcli/github/checks.h000066400000000000000000000040651507017207500177060ustar00rootroot00000000000000/* * Copyright 2022-2025 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 GITHUB_CHECKS_H #define GITHUB_CHECKS_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include struct gcli_github_check { char *name; char *status; char *conclusion; char *started_at; char *completed_at; gcli_id id; }; struct github_check_list { struct gcli_github_check *checks; size_t checks_size; }; int github_get_checks(struct gcli_ctx *ctx, struct gcli_path const *repo_path, char const *ref, int max, struct github_check_list *checks); void github_free_checks(struct github_check_list *checks); void gcli_github_check_free(struct gcli_github_check *check); #endif /* GITHUB_CHECKS_H */ gcli-2.9.1/include/gcli/github/comments.h000066400000000000000000000042241507017207500202700ustar00rootroot00000000000000/* * Copyright 2021, 2022 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 GITHUB_COMMENTS_H #define GITHUB_COMMENTS_H #ifdef HAVE_CONFIG_H #include #endif #include #include int github_perform_submit_comment(struct gcli_ctx *ctx, struct gcli_submit_comment_opts const *opts); int github_get_comment(struct gcli_ctx *ctx, struct gcli_path const *path, enum comment_target_type target_type, gcli_id comment_id, struct gcli_comment *out); int github_get_comments(struct gcli_ctx *ctx, struct gcli_path const *issue_path, struct gcli_comment_list *out); int github_fetch_comments(struct gcli_ctx *ctx, char *url, struct gcli_comment_list *const out); #endif /* GITHUB_COMMENTS_H */ gcli-2.9.1/include/gcli/github/config.h000066400000000000000000000031031507017207500177030ustar00rootroot00000000000000/* * Copyright 2021-2025 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 GITHUB_CONFIG_H #define GITHUB_CONFIG_H #ifdef HAVE_CONFIG_H #include #endif #include char *github_make_authheader(struct gcli_ctx *ctx, char const *token); #endif /* GITHUB_CONFIG_H */ gcli-2.9.1/include/gcli/github/forks.h000066400000000000000000000034561507017207500175750ustar00rootroot00000000000000/* * Copyright 2021, 2022 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) p * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef GITHUB_FORKS_H #define GITHUB_FORKS_H #ifdef HAVE_CONFIG_H #include #endif #include #include int github_get_forks(struct gcli_ctx *ctx, struct gcli_path const *repo_path, int max, struct gcli_fork_list *out); int github_fork_create(struct gcli_ctx *ctx, struct gcli_path const *repo_path, char const *_in); #endif /* GITHUB_FORKS_H */ gcli-2.9.1/include/gcli/github/gists.h000066400000000000000000000055511507017207500176000ustar00rootroot00000000000000/* * Copyright 2021-2025 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_GITHUB_GISTS_H #define GCLI_GITHUB_GISTS_H #ifdef HAVE_CONFIG_H #include #endif #include #include struct gcli_gist_file { char *filename; char *language; char *url; char *type; size_t size; }; struct gcli_gist_list { struct gcli_gist *gists; size_t gists_size; }; struct gcli_gist { char *id; char *owner; char *url; char *date; char *git_pull_url; char *description; struct gcli_gist_file *files; size_t files_size; }; struct gcli_new_gist { FILE *file; char const *file_name; char const *gist_description; }; int gcli_get_gists(struct gcli_ctx *ctx, char const *user, int max, struct gcli_gist_list *list); int gcli_get_gist(struct gcli_ctx *ctx, char const *gist_id, struct gcli_gist *out); int gcli_create_gist(struct gcli_ctx *ctx, struct gcli_new_gist); int gcli_delete_gist(struct gcli_ctx *ctx, char const *gist_id); void gcli_gists_free(struct gcli_gist_list *list); void gcli_gist_free(struct gcli_gist *g); /** * NOTE(Nico): Because of idiots designing a web API, we get a list of * files in a gist NOT as an array but as an object whose keys are the * file names. The objects describing the files obviously contain the * file name again. Whatever...here's a hack. Blame GitHub. */ int parse_github_gist_files_idiot_hack(struct gcli_ctx *ctx, struct json_stream *stream, struct gcli_gist *gist); #endif /* GCLI_GITHUB_GISTS_H */ gcli-2.9.1/include/gcli/github/issues.h000066400000000000000000000072351507017207500177630ustar00rootroot00000000000000/* * Copyright 2021-2025 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_ISSUES_H #define GCLI_ISSUES_H #ifdef HAVE_CONFIG_H #include #endif #include #include int github_issue_make_url(struct gcli_ctx *ctx, struct gcli_path const *, char **url, char const *suffix_fmt, ...); int github_fetch_issues(struct gcli_ctx *ctx, char *url, int max, struct gcli_issue_list *out); int github_issues_search(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue_fetch_details const *details, int max, struct gcli_issue_list *out); int github_fetch_issue(struct gcli_ctx *ctx, char *url, struct gcli_issue *out); int github_get_issue_summary(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_issue *out); int github_issue_close(struct gcli_ctx *ctx, struct gcli_path const *path); int github_issue_reopen(struct gcli_ctx *ctx, struct gcli_path const *path); int github_perform_submit_issue(struct gcli_ctx *ctx, struct gcli_submit_issue_options *opts, struct gcli_issue *out); int github_issue_assign(struct gcli_ctx *ctx, struct gcli_path const *issue_path, char const *assignee); int github_issue_add_labels(struct gcli_ctx *ctx, struct gcli_path const *issue_path, char const *const labels[], size_t labels_size); int github_issue_remove_labels(struct gcli_ctx *ctx, struct gcli_path const *path, char const *const labels[], size_t labels_size); int github_issue_set_milestone(struct gcli_ctx *ctx, struct gcli_path const *issue_path, gcli_id milestone); int github_issue_clear_milestone(struct gcli_ctx *ctx, struct gcli_path const *issue_path); int github_issue_set_title(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, char const *const new_title); int github_issue_set_op(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const new_op); #endif /* GCLI_ISSUES_H */ gcli-2.9.1/include/gcli/github/labels.h000066400000000000000000000046641507017207500177150ustar00rootroot00000000000000/* * Copyright 2022-2025 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 GITHUB_LABELS_H #define GITHUB_LABELS_H #ifdef HAVE_CONFIG_H #include #endif #include #include int github_get_label(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_label *out); int github_get_labels(struct gcli_ctx *ctx, struct gcli_path const *path, int max, struct gcli_label_list *out); int github_create_label(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_label *label); int github_delete_label(struct gcli_ctx *ctx, struct gcli_path const *path); int github_label_set_title(struct gcli_ctx *ctx, struct gcli_path const *path, char const *new_name); int github_label_set_description(struct gcli_ctx *ctx, struct gcli_path const *path, char const *new_description); int github_label_set_colour(struct gcli_ctx *ctx, struct gcli_path const *path, uint32_t colour); #endif /* GITHUB_LABELS_H */ gcli-2.9.1/include/gcli/github/milestones.h000066400000000000000000000046161507017207500206320ustar00rootroot00000000000000/* * Copyright 2023-2025 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_GITHUB_MILESTONES_H #define GCLI_GITHUB_MILESTONES_H #include int github_get_milestones(struct gcli_ctx *ctx, struct gcli_path const *path, int max, struct gcli_milestone_list *out); int github_get_milestone(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_milestone *out); int github_create_milestone(struct gcli_ctx *ctx, struct gcli_path const *repo, struct gcli_milestone_create_args const *args); int github_delete_milestone(struct gcli_ctx *ctx, struct gcli_path const *path); int github_milestone_get_issues(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue_list *out); int github_milestone_set_duedate(struct gcli_ctx *ctx, struct gcli_path const *path, char const *date); #endif /* GCLI_GITHUB_MILESTONES_H */ gcli-2.9.1/include/gcli/github/path.h000066400000000000000000000031671507017207500174040ustar00rootroot00000000000000/* * Copyright 2025 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_GITHUB_PATH_H #define GCLI_GITHUB_PATH_H #include int github_path_normalise(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_path *const out_path); #endif // GCLI_GITHUB_PATH_H gcli-2.9.1/include/gcli/github/pulls.h000066400000000000000000000074551507017207500176130ustar00rootroot00000000000000/* * Copyright 2021-2025 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 GITHUB_PULLS_H #define GITHUB_PULLS_H #ifdef HAVE_CONFIG_H #include #endif #include #include int github_pull_make_url(struct gcli_ctx *ctx, struct gcli_path const *path, char **url, char const *fmt, ...); int github_search_pulls(struct gcli_ctx *ctx, struct gcli_path const *repo_path, 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, struct gcli_path const *path); int github_pull_get_patch(struct gcli_ctx *ctx, FILE *stream, struct gcli_path const *path); int github_print_pull_patch(struct gcli_ctx *ctx, FILE *stream, char const *owner, char const *reponame, gcli_id pr_number); int github_pull_get_checks(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_pull_checks_list *out); int github_pull_merge(struct gcli_ctx *ctx, struct gcli_path const *path, enum gcli_merge_flags flags); int github_pull_reopen(struct gcli_ctx *ctx, struct gcli_path const *path); int github_pull_close(struct gcli_ctx *ctx, struct gcli_path const *path); int github_perform_submit_pull(struct gcli_ctx *ctx, struct gcli_submit_pull_options *opts); int github_get_pull_commits(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_commit_list *out); int github_get_pull(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_pull *out); gcli_sv github_pull_try_derive_head(void); int github_pull_add_reviewer(struct gcli_ctx *ctx, struct gcli_path const *path, char const *username); int github_pull_set_title(struct gcli_ctx *ctx, struct gcli_path const *path, char const *new_title); int github_pull_create_review(struct gcli_ctx *ctx, struct gcli_pull_create_review_details const *details); int github_pull_get_reviews(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_pull_reviews *out); int github_pull_get_review_threads(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_pull_review_thread *out); #endif /* GITHUB_PULLS_H */ gcli-2.9.1/include/gcli/github/releases.h000066400000000000000000000037351507017207500202540ustar00rootroot00000000000000/* * Copyright 2021-2025 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 GITHUB_RELEASES_H #define GITHUB_RELEASES_H #ifdef HAVE_CONFIG_H #include #endif #include #include int github_get_releases(struct gcli_ctx *ctx, struct gcli_path const *repo_path, int max, struct gcli_release_list *list); int github_create_release(struct gcli_ctx *ctx, struct gcli_create_release_args const *release); int github_delete_release(struct gcli_ctx *ctx, struct gcli_path const *repo_path, char const *id); #endif /* GITHUB_RELEASES_H */ gcli-2.9.1/include/gcli/github/repos.h000066400000000000000000000045341507017207500175770ustar00rootroot00000000000000/* * Copyright 2021, 2022 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 GITHUB_REPOS_H #define GITHUB_REPOS_H #ifdef HAVE_CONFIG_H #include #endif #include int github_repo_make_url(struct gcli_ctx *ctx, struct gcli_path const *path, char **url, char const *suffix_fmt, ...); int github_user_is_org(struct gcli_ctx *ctx, char const *e_owner); int github_get_repos(struct gcli_ctx *ctx, char const *owner, int max, struct gcli_repo_list *out); int github_get_own_repos(struct gcli_ctx *ctx, int max, struct gcli_repo_list *out); int github_repo_delete(struct gcli_ctx *ctx, struct gcli_path const *const path); int github_repo_create(struct gcli_ctx *ctx, struct gcli_repo_create_options const *options, struct gcli_repo *out); int github_repo_set_visibility(struct gcli_ctx *ctx, struct gcli_path const *path, gcli_repo_visibility vis); #endif /* GITHUB_REPOS_H */ gcli-2.9.1/include/gcli/github/sshkeys.h000066400000000000000000000033641507017207500201400ustar00rootroot00000000000000/* * Copyright 2023 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_GITHUB_SSHKEYS_H #define GCLI_GITHUB_SSHKEYS_H #include int github_get_sshkeys(struct gcli_ctx *ctx, struct gcli_sshkey_list *out); int github_add_sshkey(struct gcli_ctx *ctx, char const *const title, char const *const pubkey, struct gcli_sshkey *out); int github_delete_sshkey(struct gcli_ctx *ctx, gcli_id id); #endif /* GCLI_GITHUB_SSHKEYS_H */ gcli-2.9.1/include/gcli/github/status.h000066400000000000000000000033101507017207500177610ustar00rootroot00000000000000/* * Copyright 2022-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 GITHUB_STATUS_H #define GITHUB_STATUS_H #ifdef HAVE_CONFIG_H #include #endif #include int github_get_notifications(struct gcli_ctx *ctx, int max, struct gcli_notification_list *out); int github_notification_mark_as_read(struct gcli_ctx *ctx, char const *id); #endif /* GITHUB_STATUS_H */ gcli-2.9.1/include/gcli/gitlab/000077500000000000000000000000001507017207500162505ustar00rootroot00000000000000gcli-2.9.1/include/gcli/gitlab/api.h000066400000000000000000000037501507017207500171770ustar00rootroot00000000000000/* * Copyright 2021, 2022 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 GITLAB_API_H #define GITLAB_API_H #ifdef HAVE_CONFIG_H #include #endif #include /* Gitlab returns error messages in many strange ways. Since the * keys are different we try to produce a good error message by looking * for all possible keys and then returning a sensible value. This struct * contains a collection of found messages. */ struct gitlab_error_data { char *message; char *error_description; char *error; }; char const *gitlab_api_error_string(struct gcli_ctx *ctx, struct gcli_fetch_buffer *buf); int gitlab_user_id(struct gcli_ctx *ctx, char const *user_name); #endif /* GITLAB_API_H */ gcli-2.9.1/include/gcli/gitlab/checkout.h000066400000000000000000000031031507017207500202230ustar00rootroot00000000000000/* * 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 GITLAB_CHECKOUT_H #define GITLAB_CHECKOUT_H #include int gitlab_mr_checkout(struct gcli_ctx *ctx, char const *remote, struct gcli_path const *path); #endif /* GITLAB_CHECKOUT_H */ gcli-2.9.1/include/gcli/gitlab/comments.h000066400000000000000000000045211507017207500202500ustar00rootroot00000000000000/* * Copyright 2021, 2022 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 GITLAB_COMMENTS_H #define GITLAB_COMMENTS_H #ifdef HAVE_CONFIG_H #include #endif #include #include int gitlab_perform_submit_comment(struct gcli_ctx *ctx, struct gcli_submit_comment_opts const *opts); int gitlab_get_comment(struct gcli_ctx *ctx, struct gcli_path const *target, enum comment_target_type target_type, gcli_id comment_id, struct gcli_comment *out); int gitlab_get_issue_comments(struct gcli_ctx *ctx, struct gcli_path const *issue_path, struct gcli_comment_list *out); int gitlab_get_mr_comments(struct gcli_ctx *ctx, struct gcli_path const *mr_path, struct gcli_comment_list *out); int gitlab_fetch_comments(struct gcli_ctx *ctx, char *url, struct gcli_comment_list *const out); #endif /* GITLAB_COMMENTS_H */ gcli-2.9.1/include/gcli/gitlab/config.h000066400000000000000000000031031507017207500176630ustar00rootroot00000000000000/* * Copyright 2021-2025 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 GITLAB_CONFIG_H #define GITLAB_CONFIG_H #ifdef HAVE_CONFIG_H #include #endif #include char *gitlab_make_authheader(struct gcli_ctx *ctx, char const *token); #endif /* GITLAB_CONFIG_H */ gcli-2.9.1/include/gcli/gitlab/forks.h000066400000000000000000000034171507017207500175520ustar00rootroot00000000000000/* * Copyright 2021, 2022 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 GITLAB_FORKS_H #define GITLAB_FORKS_H #ifdef HAVE_CONFIG_H #include #endif #include int gitlab_get_forks(struct gcli_ctx *ctx, struct gcli_path const *path, int max, struct gcli_fork_list *out); int gitlab_fork_create(struct gcli_ctx *ctx, struct gcli_path const *repo_path, char const *in); #endif /* GITLAB_FORKS_H */ gcli-2.9.1/include/gcli/gitlab/issues.h000066400000000000000000000072261507017207500177430ustar00rootroot00000000000000/* * Copyright 2021-2025 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 GITLAB_ISSUES_H #define GITLAB_ISSUES_H #ifdef HAVE_CONFIG_H #include #endif #include #include int gitlab_issue_make_url(struct gcli_ctx *ctx, struct gcli_path const *path, char **url, char const *suffix_fmt, ...); int gitlab_fetch_issues(struct gcli_ctx *ctx, char *url, int max, struct gcli_issue_list *out); int gitlab_fetch_issue(struct gcli_ctx *ctx, char *url, struct gcli_issue *out); int gitlab_issues_search(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_issue_fetch_details const *details, int max, struct gcli_issue_list *out); int gitlab_get_issue_summary(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_issue *out); int gitlab_issue_close(struct gcli_ctx *ctx, struct gcli_path const *const path); int gitlab_issue_reopen(struct gcli_ctx *ctx, struct gcli_path const *const path); int gitlab_issue_assign(struct gcli_ctx *ctx, struct gcli_path const *issue_path, char const *assignee); int gitlab_perform_submit_issue(struct gcli_ctx *ctx, struct gcli_submit_issue_options *opts, struct gcli_issue *out); int gitlab_issue_add_labels(struct gcli_ctx *ctx, struct gcli_path const *path, char const *const labels[], size_t labels_size); int gitlab_issue_remove_labels(struct gcli_ctx *ctx, struct gcli_path const *path, char const *const labels[], size_t labels_size); int gitlab_issue_set_milestone(struct gcli_ctx *ctx, struct gcli_path const *issue_path, gcli_id milestone); int gitlab_issue_clear_milestone(struct gcli_ctx *ctx, struct gcli_path const *issue_path); int gitlab_issue_set_title(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, char const *new_title); int gitlab_issue_set_op(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const new_op); #endif /* GITLAB_ISSUES_H */ gcli-2.9.1/include/gcli/gitlab/labels.h000066400000000000000000000046451507017207500176740ustar00rootroot00000000000000/* * Copyright 2022-2025 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 GITLAB_LABELS_H #define GITLAB_LABELS_H #ifdef HAVE_CONFIG_H #include #endif #include #include int gitlab_get_labels(struct gcli_ctx *ctx, struct gcli_path const *path, int max, struct gcli_label_list *out); int gitlab_get_label(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_label *const out); int gitlab_create_label(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_label *label); int gitlab_delete_label(struct gcli_ctx *ctx, struct gcli_path const *path); int gitlab_label_set_title(struct gcli_ctx *ctx, struct gcli_path const *path, char const *new_name); int gitlab_label_set_description(struct gcli_ctx *ctx, struct gcli_path const *path, char const *new_description); int gitlab_label_set_colour(struct gcli_ctx *ctx, struct gcli_path const *path, uint32_t colour); #endif /* GITLAB_LABELS_H */ gcli-2.9.1/include/gcli/gitlab/merge_requests.h000066400000000000000000000114161507017207500214560ustar00rootroot00000000000000/* * Copyright 2021-2025 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 GITLAB_MERGE_REQUESTS_H #define GITLAB_MERGE_REQUESTS_H #ifdef HAVE_CONFIG_H #include #endif #include struct gitlab_user_id_list { gcli_id *users; size_t users_size; }; /* Structs used for internal patch generator. Gitlab does not provide * an endpoint for doing this properly. */ struct gitlab_diff { char *diff; char *old_path; char *new_path; char *a_mode; char *b_mode; bool new_file; bool renamed_file; bool deleted_file; }; struct gitlab_diff_list { struct gitlab_diff *diffs; size_t diffs_size; }; struct gitlab_mr_version { gcli_id id; char *head_commit, *base_commit, *start_commit; }; struct gitlab_mr_version_list { struct gitlab_mr_version *versions; size_t versions_size; }; int gitlab_mr_make_url(struct gcli_ctx *ctx, struct gcli_path const *path, char **url, char const *const suffix_fmt, ...); int gitlab_fetch_mrs(struct gcli_ctx *ctx, char *url, int max, struct gcli_pull_list *list); int gitlab_get_mrs(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_pull_fetch_details const *details, int max, struct gcli_pull_list *out); int gitlab_mr_get_diff(struct gcli_ctx *ctx, FILE *stream, struct gcli_path const *path); int gitlab_mr_get_patch(struct gcli_ctx *ctx, FILE *stream, struct gcli_path const *path); int gitlab_mr_merge(struct gcli_ctx *ctx, struct gcli_path const *path, enum gcli_merge_flags flags); int gitlab_mr_close(struct gcli_ctx *ctx, struct gcli_path const *path); int gitlab_mr_reopen(struct gcli_ctx *ctx, struct gcli_path const *path); int gitlab_get_pull(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_pull *out); int gitlab_get_pull_commits(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_commit_list *out); int gitlab_perform_submit_mr(struct gcli_ctx *ctx, struct gcli_submit_pull_options *opts); int gitlab_mr_add_labels(struct gcli_ctx *ctx, struct gcli_path const *path, char const *const labels[], size_t labels_size); int gitlab_mr_remove_labels(struct gcli_ctx *ctx, struct gcli_path const *path, char const *const labels[], size_t labels_size); int gitlab_mr_set_milestone(struct gcli_ctx *ctx, struct gcli_path const *path, gcli_id milestone_id); int gitlab_mr_clear_milestone(struct gcli_ctx *ctx, struct gcli_path const *const mr_path); int gitlab_mr_add_reviewer(struct gcli_ctx *ctx, struct gcli_path const *path, char const *username); int gitlab_mr_add_assignee(struct gcli_ctx *ctx, struct gcli_path const *path, char const *username); int gitlab_mr_set_title(struct gcli_ctx *ctx, struct gcli_path const *path, char const *new_title); int gitlab_mr_create_review(struct gcli_ctx *ctx, struct gcli_pull_create_review_details const *details); int gitlab_mr_approve(struct gcli_ctx *ctx, struct gcli_path const *path); int gitlab_mr_unapprove(struct gcli_ctx *ctx, struct gcli_path const *path); void gitlab_mr_version_free(struct gitlab_mr_version *); void gitlab_mr_version_list_free(struct gitlab_mr_version_list *); #endif /* GITLAB_MERGE_REQUESTS_H */ gcli-2.9.1/include/gcli/gitlab/milestones.h000066400000000000000000000046431507017207500206120ustar00rootroot00000000000000/* * Copyright 2023-2025 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_GITLAB_MILESTONES_H #define GCLI_GITLAB_MILESTONES_H #ifdef HAVE_CONFIG_H #include #endif #include int gitlab_get_milestones(struct gcli_ctx *ctx, struct gcli_path const *path, int max, struct gcli_milestone_list *const out); int gitlab_create_milestone(struct gcli_ctx *ctx, struct gcli_path const *repo, struct gcli_milestone_create_args const *args); int gitlab_delete_milestone(struct gcli_ctx *ctx, struct gcli_path const *path); int gitlab_get_milestone(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_milestone *out); int gitlab_milestone_get_issues(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_issue_list *out); int gitlab_milestone_set_duedate(struct gcli_ctx *ctx, struct gcli_path const *path, char const *date); #endif /* GCLI_GITLAB_MILESTONES_H */ gcli-2.9.1/include/gcli/gitlab/pipelines.h000066400000000000000000000073531507017207500204210ustar00rootroot00000000000000/* * Copyright 2022-2025 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 GITLAB_PIPELINES_H #define GITLAB_PIPELINES_H #ifdef HAVE_CONFIG_H #include #endif #include struct gitlab_pipeline { gcli_id id; char *status; time_t created_at; time_t updated_at; char *ref; char *sha; char *source; char *name; char *web_url; }; struct gitlab_pipeline_list { struct gitlab_pipeline *pipelines; size_t pipelines_size; }; struct gitlab_job { gcli_id id; char *status; char *stage; char *name; char *ref; time_t created_at; time_t started_at; time_t finished_at; double duration; char *runner_name; char *runner_description; double coverage; char *web_url; }; struct gitlab_job_list { struct gitlab_job *jobs; size_t jobs_size; }; int gitlab_get_pipelines(struct gcli_ctx *ctx, struct gcli_path const *repo_path, int max, struct gitlab_pipeline_list *out); int gitlab_get_pipeline(struct gcli_ctx *ctx, struct gcli_path const *pipeline_path, struct gitlab_pipeline *out); void gitlab_pipeline_free(struct gitlab_pipeline *pipeline); void gitlab_pipelines_free(struct gitlab_pipeline_list *list); int gitlab_get_pipeline_jobs(struct gcli_ctx *ctx, struct gcli_path const *pipeline_path, int count, struct gitlab_job_list *out); int gitlab_get_pipeline_children(struct gcli_ctx *ctx, struct gcli_path const *pipeline_path, int count, struct gitlab_pipeline_list *out); void gitlab_free_jobs(struct gitlab_job_list *jobs); void gitlab_free_job(struct gitlab_job *job); int gitlab_job_get_log(struct gcli_ctx *ctx, struct gcli_path const *job_path, FILE *stream); int gitlab_job_cancel(struct gcli_ctx *ctx, struct gcli_path const *job_path); int gitlab_job_retry(struct gcli_ctx *ctx, struct gcli_path const *job_path); int gitlab_job_download_artifacts(struct gcli_ctx *ctx, struct gcli_path const *job_path, char const *outfile); int gitlab_get_mr_pipelines(struct gcli_ctx *ctx, struct gcli_path const *path, struct gitlab_pipeline_list *list); int gitlab_get_job(struct gcli_ctx *ctx, struct gcli_path const *job_path, struct gitlab_job *const out); #endif /* GITLAB_PIPELINES_H */ gcli-2.9.1/include/gcli/gitlab/releases.h000066400000000000000000000040441507017207500202260ustar00rootroot00000000000000/* * Copyright 2021-2025 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 GITLAB_RELEASES_H #define GITLAB_RELEASES_H #ifdef HAVE_CONFIG_H #include #endif #include #include int gitlab_get_releases(struct gcli_ctx *ctx, struct gcli_path const *repo_path, int max, struct gcli_release_list *list); int gitlab_create_release(struct gcli_ctx *ctx, struct gcli_create_release_args const *release); int gitlab_delete_release(struct gcli_ctx *ctx, struct gcli_path const *path, char const *id); void gitlab_fixup_release_assets(struct gcli_ctx *ctx, struct gcli_release *const release); #endif /* GITLAB_RELEASES_H */ gcli-2.9.1/include/gcli/gitlab/repos.h000066400000000000000000000046071507017207500175600ustar00rootroot00000000000000/* * Copyright 2021, 2022 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 GITLAB_REPOS_H #define GITLAB_REPOS_H #ifdef HAVE_CONFIG_H #include #endif #include int gitlab_repo_make_url(struct gcli_ctx *ctx, struct gcli_path const *path, char **url, char const *suffix_fmt, ...); int gitlab_get_repo(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_repo *out); int gitlab_get_repos(struct gcli_ctx *ctx, char const *owner, int max, struct gcli_repo_list *out); int gitlab_get_own_repos(struct gcli_ctx *ctx, int max, struct gcli_repo_list *out); int gitlab_repo_delete(struct gcli_ctx *ctx, struct gcli_path const *path); int gitlab_repo_create(struct gcli_ctx *ctx, struct gcli_repo_create_options const *options, struct gcli_repo *out); int gitlab_repo_set_visibility(struct gcli_ctx *ctx, struct gcli_path const *path, gcli_repo_visibility vis); #endif /* GITLAB_REPOS_H */ gcli-2.9.1/include/gcli/gitlab/snippets.h000066400000000000000000000042061507017207500202700ustar00rootroot00000000000000/* * Copyright 2021, 2022 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 GITLAB_SNIPPETS_H #define GITLAB_SNIPPETS_H #ifdef HAVE_CONFIG_H #include #endif #include struct gcli_gitlab_snippet { gcli_id id; char *title; char *filename; char *date; char *author; char *visibility; char *raw_url; }; struct gcli_gitlab_snippet_list { struct gcli_gitlab_snippet *snippets; size_t snippets_size; }; void gcli_snippets_free(struct gcli_gitlab_snippet_list *list); int gcli_snippets_get(struct gcli_ctx *ctx, int max, struct gcli_gitlab_snippet_list *out); int gcli_snippet_delete(struct gcli_ctx *ctx, char const *snippet_id); int gcli_snippet_get(struct gcli_ctx *ctx, char const *snippet_id, FILE *stream); void gcli_gitlab_snippet_free(struct gcli_gitlab_snippet *snippet); #endif /* GITLAB_SNIPPETS_H */ gcli-2.9.1/include/gcli/gitlab/sshkeys.h000066400000000000000000000034541507017207500201200ustar00rootroot00000000000000/* * Copyright 2023 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_GITLAB_SSHKEYS_H #define GCLI_GITLAB_SSHKEYS_H #ifdef HAVE_CONFIG_H #include #endif #include int gitlab_get_sshkeys(struct gcli_ctx *ctx, struct gcli_sshkey_list *list); int gitlab_add_sshkey(struct gcli_ctx *ctx, char const *const title, char const *const pubkey, struct gcli_sshkey *const out); int gitlab_delete_sshkey(struct gcli_ctx *ctx, gcli_id id); #endif /* GCLI_GITLAB_SSHKEYS_H */ gcli-2.9.1/include/gcli/gitlab/status.h000066400000000000000000000033101507017207500177410ustar00rootroot00000000000000/* * Copyright 2022-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 GITLAB_STATUS_H #define GITLAB_STATUS_H #ifdef HAVE_CONFIG_H #include #endif #include int gitlab_get_notifications(struct gcli_ctx *ctx, int max, struct gcli_notification_list *out); int gitlab_notification_mark_as_read(struct gcli_ctx *ctx, char const *id); #endif /* GITLAB_STATUS_H */ gcli-2.9.1/include/gcli/issues.h000066400000000000000000000110731507017207500164740ustar00rootroot00000000000000/* * Copyright 2021-2025 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 ISSUES_H #define ISSUES_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include struct gcli_issue { gcli_id number; char *title; char *product; /* only on Bugzilla */ char *component; /* only on Bugzilla */ char *url; /* only on Bugzilla */ time_t created_at; char *author; char *state; int comments; bool locked; char *body; char **labels; size_t labels_size; char **assignees; size_t assignees_size; /* workaround for GitHub where PRs are also issues */ int is_pr; char *milestone; char *web_url; }; struct gcli_submit_issue_options { char const *owner; char const *repo; char *title; char *body; struct gcli_nvlist extra; }; struct gcli_issue_list { struct gcli_issue *issues; size_t issues_size; }; struct gcli_issue_fetch_details { bool all; /* disregard the issue state */ char const *author; /* filter issues by this author*/ char const *label; /* filter by the given label */ char const *milestone; /* filter by the given milestone */ char const *assignee; /* filter by the given assignee */ char const *search_term; /* a search term or NULL if unspecified */ }; int gcli_issues_search(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_issue_fetch_details const *details, int max, struct gcli_issue_list *out); void gcli_issues_free(struct gcli_issue_list *); int gcli_get_issue(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_issue *out); void gcli_issue_free(struct gcli_issue *it); int gcli_issue_close(struct gcli_ctx *ctx, struct gcli_path const *path); int gcli_issue_reopen(struct gcli_ctx *ctx, struct gcli_path const *path); int gcli_issue_submit(struct gcli_ctx *ctx, struct gcli_submit_issue_options *); int gcli_issue_assign(struct gcli_ctx *ctx, struct gcli_path const *issue_path, char const *assignee); int gcli_issue_add_labels(struct gcli_ctx *ctx, struct gcli_path const *issue_path, char const *const labels[], size_t labels_size); int gcli_issue_remove_labels(struct gcli_ctx *ctx, struct gcli_path const *issue_path, char const *const labels[], size_t labels_size); int gcli_issue_set_milestone(struct gcli_ctx *ctx, struct gcli_path const *const path, int milestone); int gcli_issue_clear_milestone(struct gcli_ctx *cxt, struct gcli_path const *const issue_path); int gcli_issue_set_title(struct gcli_ctx *ctx, struct gcli_path const *issue_path, char const *new_title); int gcli_issue_get_attachments(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_attachment_list *attachments); int gcli_issue_set_op(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *new_op); #endif /* ISSUES_H */ gcli-2.9.1/include/gcli/json_gen.h000066400000000000000000000053271507017207500167700ustar00rootroot00000000000000/* * Copyright 2023 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_JSON_GEN_H #define GCLI_JSON_GEN_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include enum { GCLI_JSONGEN_ARRAY = 1, GCLI_JSONGEN_OBJECT = 2, }; struct gcli_jsongen { char *buffer; size_t buffer_size; size_t buffer_capacity; int scopes[32]; /* scope stack */ size_t scopes_size; /* scope stack pointer */ bool await_object_value; /* when in an object scope set to true if * we expect a value and not a key */ bool first_elem; /* first element in object/array */ }; int gcli_jsongen_init(struct gcli_jsongen *gen); void gcli_jsongen_free(struct gcli_jsongen *gen); char *gcli_jsongen_to_string(struct gcli_jsongen *gen); int gcli_jsongen_begin_object(struct gcli_jsongen *gen); int gcli_jsongen_end_object(struct gcli_jsongen *gen); int gcli_jsongen_begin_array(struct gcli_jsongen *gen); int gcli_jsongen_end_array(struct gcli_jsongen *gen); int gcli_jsongen_objmember(struct gcli_jsongen *gen, char const *key); int gcli_jsongen_number(struct gcli_jsongen *gen, long long num); int gcli_jsongen_id(struct gcli_jsongen *gen, gcli_id const id); int gcli_jsongen_string(struct gcli_jsongen *gen, char const *value); int gcli_jsongen_bool(struct gcli_jsongen *gen, bool value); int gcli_jsongen_null(struct gcli_jsongen *gen); #endif /* GCLI_JSON_GEN_H */ gcli-2.9.1/include/gcli/json_util.h000066400000000000000000000142721507017207500171730ustar00rootroot00000000000000/* * Copyright 2021-2025 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 JSON_UTIL_H #define JSON_UTIL_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #define get_int(ctx, input, out) get_int_(ctx, input, out, __func__) #define get_id(ctx, input, out) get_id_(ctx, input, out, __func__) #define get_long(ctx, input, out) get_long_(ctx, input, out, __func__) #define get_size_t(ctx, input, out) get_size_t_(ctx, input, out, __func__) #define get_double(ctx, input, out) get_double_(ctx, input, out, __func__) #define get_parse_int(ctx, input, out) get_parse_int_(ctx, input, out, __func__) #define get_bool(ctx, input, out) get_bool_(ctx, input, out, __func__) #define get_bool_relaxed(ctx, input, out) get_bool_relaxed_(ctx, input, out, __func__) #define get_string(ctx, input, out) get_string_(ctx, input, out, __func__) #define get_sv(ctx, input, out) get_sv_(ctx, input, out, __func__) #define get_user(ctx, input, out) get_user_(ctx, input, out, __func__) #define get_label(ctx, input, out) get_label_(ctx, input, out, __func__) #define get_is_string(ctx, input, out) ((void)ctx, (*out = json_next(input) == JSON_STRING), 1) #define get_int_to_string(ctx, input, out) get_int_to_string_(ctx, input, out, __func__) #define get_iso8601_time(ctx, input, out) get_iso8601_time_(ctx, input, out, __func__) #define get_url_path(ctx, input, out) get_url_path_(ctx, input, out, __func__) #define get_gitlab_notification_target(ctx, input, out) get_gitlab_notification_target_(ctx, input, out, __func__) int get_int_(struct gcli_ctx *ctx, json_stream *input, int *out, char const *function); int get_id_(struct gcli_ctx *ctx, json_stream *input, gcli_id *out, char const *function); int get_long_(struct gcli_ctx *ctx, json_stream *input, long *out, char const *function); int get_size_t_(struct gcli_ctx *ctx, json_stream *input, size_t *out, char const *function); int get_double_(struct gcli_ctx *ctx, json_stream *input, double *out, char const *function); int get_parse_int_(struct gcli_ctx *ctx, json_stream *input, long *out, char const *function); int get_bool_(struct gcli_ctx *ctx, json_stream *input, bool *out, char const *function); int get_bool_relaxed_(struct gcli_ctx *ctx, json_stream *input, bool *out, char const *function); int get_string_(struct gcli_ctx *ctx, json_stream *input, char **out, char const *function); int get_sv_(struct gcli_ctx *ctx, json_stream *input, gcli_sv *out, char const *function); int get_user_(struct gcli_ctx *ctx, json_stream *input, char **out, char const *function); int get_label_(struct gcli_ctx *ctx, json_stream *input, char const **out, char const *function); int get_iso8601_time_(struct gcli_ctx *ctx, json_stream *input, time_t *out, char const *function); int get_github_style_colour(struct gcli_ctx *ctx, json_stream *input, uint32_t *out); int get_gitlab_style_colour(struct gcli_ctx *ctx, json_stream *input, uint32_t *out); int get_github_is_pr(struct gcli_ctx *ctx, json_stream *input, int *out); int get_github_notification_target_type(struct gcli_ctx *ctx, json_stream *input, enum gcli_notification_target_type *out); int get_gitlab_notification_target_type(struct gcli_ctx *ctx, json_stream *input, enum gcli_notification_target_type *out); int get_gitea_notification_target_type(struct gcli_ctx *ctx, json_stream *input, enum gcli_notification_target_type *out); int get_gitlab_can_be_merged(struct gcli_ctx *ctx, json_stream *input, bool *out); int get_gitea_visibility(struct gcli_ctx *ctx, json_stream *input, char **out); gcli_sv gcli_json_escape(gcli_sv); #define gcli_json_escape_cstr(x) (gcli_json_escape(SV((char *)(x))).data) int gcli_json_advance(struct gcli_ctx *ctx, json_stream *input, char const *fmt, ...); int get_url_path_(struct gcli_ctx *ctx, json_stream *input, struct gcli_path *out, char const *function); int get_gitlab_notification_target_(struct gcli_ctx *ctx, json_stream *input, struct gcli_path *out, char const *function); static inline char const * gcli_json_bool(bool it) { return it ? "true" : "false"; } static inline int get_int_to_string_(struct gcli_ctx *ctx, json_stream *input, char **out, char const *const fn) { int rc; long val; rc = get_long_(ctx, input, &val, fn); if (rc < 0) return rc; *out = gcli_asprintf("%ld", val); return 0; } #define SKIP_OBJECT_VALUE(stream) \ do { \ enum json_type value_type = json_next(stream); \ \ switch (value_type) { \ case JSON_ARRAY: \ json_skip_until(stream, JSON_ARRAY_END); \ break; \ case JSON_OBJECT: \ json_skip_until(stream, JSON_OBJECT_END); \ break; \ default: \ break; \ } \ } while (0) #endif /* JSON_UTIL_H */ gcli-2.9.1/include/gcli/labels.h000066400000000000000000000052261507017207500164260ustar00rootroot00000000000000/* * Copyright 2022-2025 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 LABELS_H #define LABELS_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include struct gcli_label { gcli_id id; char *name; char *description; uint32_t colour; }; struct gcli_label_list { struct gcli_label *labels; size_t labels_size; }; int gcli_get_labels(struct gcli_ctx *ctx, struct gcli_path const *path, int max, struct gcli_label_list *out); int gcli_get_label(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_label *out); void gcli_free_label(struct gcli_label *label); void gcli_free_labels(struct gcli_label_list *labels); int gcli_create_label(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_label *label); int gcli_delete_label(struct gcli_ctx *ctx, struct gcli_path const *path); int gcli_label_set_title(struct gcli_ctx *ctx, struct gcli_path const *path, char const *new_name); int gcli_label_set_description(struct gcli_ctx *ctx, struct gcli_path const *path, char const *new_description); int gcli_label_set_colour(struct gcli_ctx *ctx, struct gcli_path const *path, uint32_t colour_rgb); #endif /* LABELS_H */ gcli-2.9.1/include/gcli/milestones.h000066400000000000000000000060201507017207500173370ustar00rootroot00000000000000/* * Copyright 2023-2025 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_MILESTONES_H #define GCLI_MILESTONES_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include struct gcli_milestone { gcli_id id; char *title; char *state; time_t created_at; /* Extended info */ char *description; time_t updated_at; time_t due_date; bool expired; /* Github and Gitea Specific */ int open_issues; int closed_issues; char *web_url; }; struct gcli_milestone_list { struct gcli_milestone *milestones; size_t milestones_size; }; struct gcli_milestone_create_args { char *title; char *description; }; int gcli_get_milestones(struct gcli_ctx *ctx, struct gcli_path const *path, int max, struct gcli_milestone_list *out); int gcli_get_milestone(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_milestone *out); int gcli_create_milestone(struct gcli_ctx *ctx, struct gcli_path const *repo_path, struct gcli_milestone_create_args const *args); int gcli_delete_milestone(struct gcli_ctx *ctx, struct gcli_path const *const path); void gcli_free_milestone(struct gcli_milestone *it); void gcli_free_milestones(struct gcli_milestone_list *it); int gcli_milestone_get_issues(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_issue_list *out); int gcli_milestone_set_duedate(struct gcli_ctx *ctx, struct gcli_path const *path, char const *date); #endif /* GCLI_MILESTONES_H */ gcli-2.9.1/include/gcli/nvlist.h000066400000000000000000000040041507017207500164740ustar00rootroot00000000000000/* * Copyright 2023 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_NVLIST_H #define GCLI_NVLIST_H #ifdef HAVE_CONFIG_H #include #endif /* HAVE_CONFIG_H */ #include struct gcli_nvpair { TAILQ_ENTRY(gcli_nvpair) next; char *key; char *value; }; TAILQ_HEAD(gcli_nvlist, gcli_nvpair); int gcli_nvlist_init(struct gcli_nvlist *list); int gcli_nvlist_free(struct gcli_nvlist *list); int gcli_nvlist_append(struct gcli_nvlist *list, char *key, char *value); char const *gcli_nvlist_find(struct gcli_nvlist const *list, char const *key); char const *gcli_nvlist_find_or(struct gcli_nvlist const *list, char const *key, char const *alternative); #endif /* GCLI_NVLIST_H */ gcli-2.9.1/include/gcli/path.h000066400000000000000000000040401507017207500161110ustar00rootroot00000000000000/* * Copyright 2024-2025 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_PATH_H #define GCLI_PATH_H #include struct gcli_path { gcli_forge_type forge_type; enum { GCLI_PATH_DEFAULT = 0, GCLI_PATH_URL, GCLI_PATH_BUGZILLA, GCLI_PATH_ID, GCLI_PATH_PID_ID, GCLI_PATH_NAMED, /* owner, repo, string-id, required for Github labels */ } kind; union { struct { char *owner; char *repo; gcli_id id; } as_default; struct { gcli_id project_id; gcli_id id; } as_pid_id; struct { char *product; char *component; } as_bugzilla; gcli_id as_id; char *as_url; struct { char *owner; char *repo; char *id; } as_named; }; }; void gcli_path_free(struct gcli_path *); #endif /* GCLI_PATH_H */ gcli-2.9.1/include/gcli/pgen.h000066400000000000000000000030601507017207500161070ustar00rootroot00000000000000#ifndef PGEN_H #define PGEN_H #ifdef HAVE_CONFIG_H #include #endif #include #include /* PGen command line options */ enum { DUMP_PLAIN = 0, DUMP_C = 1, DUMP_H = 2 }; extern int dumptype; extern FILE *outfile; extern char *outfilename; /* Types used in the parser to represent nodes in the AST */ struct strlit { char *text; }; struct ident { char *text; }; struct objentry { enum { OBJENTRY_SIMPLE, OBJENTRY_ARRAY, OBJENTRY_CONTINUATION } kind; /* either a simple field or an array */ char *jsonname; char *name; char *type; char *parser; struct objentry *next; /* linked list */ }; struct objparser { enum { OBJPARSER_ENTRIES, OBJPARSER_SELECT } kind; char *name; char *returntype; bool is_struct; struct objentry *entries; struct { char *fieldtype; char *fieldname; } select; }; struct arrayparser { char *name; bool is_struct; char *returntype; char *parser; }; void yyerror(char const *message); /* Functions to dump data before starting the actual parser */ void header_dump_c(void); void header_dump_h(void); /* Functions called while parsing */ void objparser_dump_c(struct objparser *); void objparser_dump_h(struct objparser *); void objparser_dump_plain(struct objparser *); void arrayparser_dump_c(struct arrayparser *); void arrayparser_dump_h(struct arrayparser *); void include_dump_c(char const *); void include_dump_h(char const *); /* Functions called after parsing */ void footer_dump_h(void); #endif /* PGEN_H */ gcli-2.9.1/include/gcli/port/000077500000000000000000000000001507017207500157725ustar00rootroot00000000000000gcli-2.9.1/include/gcli/port/err.h000066400000000000000000000033651507017207500167420ustar00rootroot00000000000000/* * Copyright 2021-2025 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_PORT_ERR_H #define GCLI_PORT_ERR_H #include /* error functions */ /* print a formatted error message and exit with code */ void errx(int code, const char *fmt, ...) PRINTF_FORMAT(2, 3); /* print a formatted error message, the error retrieved from errno and exit with code */ void err(int code, const char *fmt, ...) PRINTF_FORMAT(2, 3); #endif /* GCLI_PORT_ERR_H */ gcli-2.9.1/include/gcli/port/port.h000066400000000000000000000035071507017207500171340ustar00rootroot00000000000000/* * Copyright 2021-2025 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_PORT_PORT_H #define GCLI_PORT_PORT_H #if defined(__GNUC__) || defined(__clang__) /* https://gcc.gnu.org/onlinedocs/gcc-4.7.2/gcc/Function-Attributes.html */ #define PRINTF_FORMAT(STRING_INDEX, FIRST_TO_CHECK) __attribute__ ((format (printf, STRING_INDEX, FIRST_TO_CHECK))) #else #define PRINTF_FORMAT(STRING_INDEX, FIRST_TO_CHECK) #endif #ifndef ARRAY_SIZE # define ARRAY_SIZE(xs) (sizeof(xs) / sizeof(xs[0])) #endif /* ARRAY_SIZE */ #endif /* GCLI_PORT_PORT_H */ gcli-2.9.1/include/gcli/port/string.h000066400000000000000000000040041507017207500174470ustar00rootroot00000000000000/* * Copyright 2021-2025 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_PORT_STRING_H #define GCLI_PORT_STRING_H #include #include #include #include /* string functions */ char *gcli_strndup(const char *it, size_t len); char *gcli_asprintf(const char *fmt, ...) PRINTF_FORMAT(1, 2); char *gcli_vasprintf(char const *const fmt, va_list vp); /* modifies the underlying string */ char *gcli_strip_suffix(char *it, const char *suffix); static inline bool gcli_strempty(char const *const str) { if (str == NULL) return true; return *str == '\0'; } char *gcli_join_with(char const *const items[], size_t const items_size, char const *sep); #endif /* GCLI_PORT_STRING_H */ gcli-2.9.1/include/gcli/port/sv.h000066400000000000000000000046761507017207500166100ustar00rootroot00000000000000/* * Copyright 2021-2025 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_PORT_SV_H #define GCLI_PORT_SV_H #include #include #include /* stringview */ typedef struct sv gcli_sv; struct sv { char *data; size_t length; }; #define SV(x) (gcli_sv) { .data = x, .length = strlen(x) } #define SV_FMT "%.*s" #define SV_ARGS(x) (int)x.length, x.data #define SV_NULL (gcli_sv) {0} static inline gcli_sv gcli_sv_from_parts(char *buf, size_t len) { return (gcli_sv) { .data = buf, .length = len }; } gcli_sv gcli_sv_trim_front(gcli_sv); gcli_sv gcli_sv_trim(gcli_sv); gcli_sv gcli_sv_chop_until(gcli_sv *, char); gcli_sv gcli_sv_chop_to_last(gcli_sv *, char); bool gcli_sv_has_prefix(gcli_sv, const char *); bool gcli_sv_eq(const gcli_sv, const gcli_sv); bool gcli_sv_eq_to(const gcli_sv, const char *); gcli_sv gcli_sv_fmt(const char *fmt, ...) PRINTF_FORMAT(1, 2); char *gcli_sv_to_cstr(gcli_sv); gcli_sv gcli_sv_strip_suffix(gcli_sv, const char *suffix); gcli_sv gcli_sv_append(gcli_sv this, gcli_sv const that); static inline bool gcli_sv_null(gcli_sv it) { return it.data == NULL && it.length == 0; } #endif // GCLI_PORT_SV_H gcli-2.9.1/include/gcli/port/util.h000066400000000000000000000040271507017207500171230ustar00rootroot00000000000000/* * Copyright 2021-2025 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_PORT_UTIL_H #define GCLI_PORT_UTIL_H #include #include /* for convenience */ #define gcli_unimplemented errx(42, "%s: unimplemented", __func__) #define gcli_notreached errx(42, "%s: unreachable", __func__) /* Weird minimum function */ static inline int gcli_min(int x, int y) { if (x < 0) return y; else if (y < 0) return x; else if (x < y) return x; else return y; } int gcli_read_file(char const *path, char **buffer); /* interactive user functions */ bool gcli_yesno(const char *fmt, ...) PRINTF_FORMAT(1, 2); static inline const char * gcli_bool_yesno(bool x) { return x ? "yes" : "no"; } #endif /* GCLI_PORT_UTIL_H */ gcli-2.9.1/include/gcli/pulls.h000066400000000000000000000212631507017207500163220ustar00rootroot00000000000000/* * Copyright 2021-2025 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 PULLS_H #define PULLS_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include struct gcli_pull_list { struct gcli_pull *pulls; size_t pulls_size; }; struct gcli_pull { char *author; char *state; char *title; char *body; time_t created_at; char *commits_link; char *head_label; char *base_label; char *head_sha; char *base_sha; char *start_sha; char *milestone; gcli_id id; gcli_id number; char *node_id; /* Github: GraphQL compat */ int comments; int additions; int deletions; int commits; int changed_files; int head_pipeline_id; /* GitLab specific */ char *coverage; /* Gitlab Specific */ char *web_url; char **labels; size_t labels_size; char **reviewers; /**< User names */ size_t reviewers_size; /**< Number of elements in the reviewers list */ char **assignees; size_t assignees_size; bool merged; bool mergeable; bool draft; bool automerge; }; struct gcli_commit { char *sha, *long_sha, *message, *date, *author, *email; }; struct gcli_commit_list { struct gcli_commit *commits; size_t commits_size; }; /* Options to submit to the gh api for creating a PR */ struct gcli_submit_pull_options { struct gcli_path target_repo; char const *target_branch; char const *from; char const *title; 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 */ }; struct gcli_pull_fetch_details { 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 */ }; enum { GCLI_REVIEW_ACCEPT_CHANGES = 1, GCLI_REVIEW_REQUEST_CHANGES = 2, GCLI_REVIEW_COMMENT = 3, }; enum gcli_pull_approval_state { GCLI_PULL_APPROVED = GCLI_REVIEW_ACCEPT_CHANGES, GCLI_PULL_UNAPPROVED = GCLI_REVIEW_REQUEST_CHANGES, }; struct gcli_review_meta_line { TAILQ_ENTRY(gcli_review_meta_line) next; char *entry; }; struct gcli_pull_create_review_details { struct gcli_path path; struct gcli_diff_comments comments; char const *body; /* string containing the prelude message by the user */ TAILQ_HEAD(, gcli_review_meta_line) meta_lines; int review_state; }; /** Generic list of checks ran on a pull request * * NOTE: KEEP THIS ORDER! WE DEPEND ON THE ABI HERE. * * For github the type of checks is gitlab_check* * For gitlab the type of checks is struct gitlab_pipeline* * * You can cast this type to the list type of either one of them. */ struct gcli_pull_checks_list { void *checks; size_t checks_size; int forge_type; }; /* PR/MR reviews */ struct gcli_pull_review { gcli_id id; char *author; char *state; time_t submitted_at; char *body; }; struct gcli_pull_reviews { struct gcli_pull_review *reviews; size_t reviews_size; }; /* comments in a review */ struct gcli_pull_review_comment; TAILQ_HEAD(gcli_pull_review_thread, gcli_pull_review_comment); struct gcli_pull_review_comment { /* threaded comments */ TAILQ_ENTRY(gcli_pull_review_comment) next; struct gcli_pull_review_thread replies; /* actual fields */ gcli_id id; gcli_id in_reply_to; char *author; char *body; char *path; char *diff_hunk; time_t created_at; }; struct gcli_pull_review_comments { struct gcli_pull_review_comment *comments; size_t comments_size; }; int gcli_search_pulls(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_pull_fetch_details const *details, int max, struct gcli_pull_list *out); void gcli_pull_free(struct gcli_pull *it); void gcli_pulls_free(struct gcli_pull_list *list); int gcli_pull_get_diff(struct gcli_ctx *ctx, FILE *fout, struct gcli_path const *path); int gcli_pull_get_checks(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_pull_checks_list *out); void gcli_pull_checks_free(struct gcli_pull_checks_list *list); int gcli_pull_get_commits(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_commit_list *out); void gcli_commits_free(struct gcli_commit_list *list); int gcli_get_pull(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_pull *out); 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 */ GCLI_PULL_MERGE_DELETEHEAD = 0x2, /* delete the source branch after merging */ }; int gcli_pull_merge(struct gcli_ctx *ctx, struct gcli_path const *const path, enum gcli_merge_flags flags); int gcli_pull_close(struct gcli_ctx *ctx, struct gcli_path const *const path); int gcli_pull_reopen(struct gcli_ctx *ctx, struct gcli_path const *const path); int gcli_pull_add_labels(struct gcli_ctx *ctx, struct gcli_path const *path, char const *const labels[], size_t labels_size); int gcli_pull_remove_labels(struct gcli_ctx *ctx, struct gcli_path const *path, char const *const labels[], size_t labels_size); int gcli_pull_set_milestone(struct gcli_ctx *ctx, struct gcli_path const *pull_path, int milestone_id); int gcli_pull_clear_milestone(struct gcli_ctx *ctx, struct gcli_path const *pull_path); int gcli_pull_add_reviewer(struct gcli_ctx *ctx, struct gcli_path const *path, char const *username); int gcli_pull_assign(struct gcli_ctx *ctx, struct gcli_path const *path, char const *username); int gcli_pull_get_patch(struct gcli_ctx *ctx, FILE *out, struct gcli_path const *path); int gcli_pull_set_title(struct gcli_ctx *ctx, struct gcli_path const *path, char const *new_title); int gcli_pull_create_review(struct gcli_ctx *ctx, struct gcli_pull_create_review_details const *details); char const *gcli_pull_get_meta_by_key(struct gcli_pull_create_review_details const *, char const *key); int gcli_pull_checkout(struct gcli_ctx *ctx, char const *remote, struct gcli_path const *pull_path); int gcli_pull_get_reviews(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_pull_reviews *out); void gcli_pull_reviews_free(struct gcli_pull_reviews *it); void gcli_pull_review_comments_free(struct gcli_pull_review_comments *it); int gcli_pull_get_review_threads(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_pull_review_thread *out); void gcli_pull_review_thread_free(struct gcli_pull_review_thread *thd); int gcli_pull_approve(struct gcli_ctx *ctx, struct gcli_path const *path, char const *const message); int gcli_pull_unapprove(struct gcli_ctx *ctx, struct gcli_path const *path, char const *const message); #endif /* PULLS_H */ gcli-2.9.1/include/gcli/releases.h000066400000000000000000000055241507017207500167700ustar00rootroot00000000000000/* * Copyright 2021-2025 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 RELEASES_H #define RELEASES_H #ifdef HAVE_CONFIG_H #include #endif #include #include struct gcli_release_asset { char *name; char *url; }; struct gcli_release { char *id; /* Probably shouldn't be called id */ struct gcli_release_asset *assets; size_t assets_size; char *name; char *body; char *author; time_t date; char *upload_url; bool draft; bool prerelease; }; struct gcli_release_list { struct gcli_release *releases; size_t releases_size; }; struct gcli_release_asset_upload { char *label; char *name; char *path; }; #define GCLI_RELEASE_MAX_ASSETS 16 struct gcli_create_release_args { struct gcli_path repo_path; char const *tag; char const *name; char *body; char const *commitish; bool draft; bool prerelease; struct gcli_release_asset_upload assets[GCLI_RELEASE_MAX_ASSETS]; size_t assets_size; }; int gcli_get_releases(struct gcli_ctx *ctx, struct gcli_path const *repo_path, int max, struct gcli_release_list *list); void gcli_free_releases(struct gcli_release_list *); int gcli_create_release(struct gcli_ctx *ctx, struct gcli_create_release_args const *); int gcli_release_push_asset(struct gcli_ctx *, struct gcli_create_release_args *, struct gcli_release_asset_upload); int gcli_delete_release(struct gcli_ctx *ctx, struct gcli_path const *repo_path, char const *id); void gcli_release_free(struct gcli_release *release); #endif /* RELEASES_H */ gcli-2.9.1/include/gcli/repos.h000066400000000000000000000050031507017207500163050ustar00rootroot00000000000000/* * Copyright 2021-2025 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 REPOS_H #define REPOS_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include struct gcli_repo { gcli_id id; char *full_name; char *name; char *owner; time_t date; char *visibility; bool is_fork; }; struct gcli_repo_list { struct gcli_repo *repos; size_t repos_size; }; struct gcli_repo_create_options { char *name; char *description; bool private; }; typedef enum { GCLI_REPO_VISIBILITY_PRIVATE = 1, GCLI_REPO_VISIBILITY_PUBLIC, } gcli_repo_visibility; int gcli_get_repos(struct gcli_ctx *ctx, char const *owner, int max, struct gcli_repo_list *list); void gcli_repos_free(struct gcli_repo_list *list); void gcli_repo_free(struct gcli_repo *it); int gcli_repo_delete(struct gcli_ctx *ctx, struct gcli_path const *path); int gcli_repo_create(struct gcli_ctx *ctx, struct gcli_repo_create_options const *, struct gcli_repo *out); int gcli_repo_set_visibility(struct gcli_ctx *ctx, struct gcli_path const *const path, gcli_repo_visibility visibility); #endif /* REPOS_H */ gcli-2.9.1/include/gcli/sshkeys.h000066400000000000000000000040271507017207500166530ustar00rootroot00000000000000/* * Copyright 2023 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_SSHKEYS_H #define GCLI_SSHKEYS_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include struct gcli_sshkey { gcli_id id; char *title; char *key; time_t created_at; }; struct gcli_sshkey_list { struct gcli_sshkey *keys; size_t keys_size; }; int gcli_sshkeys_get_keys(struct gcli_ctx *ctx, struct gcli_sshkey_list *out); int gcli_sshkeys_add_key(struct gcli_ctx *ctx, char const *title, char const *public_key_path, struct gcli_sshkey *out); int gcli_sshkeys_delete_key(struct gcli_ctx *ctx, gcli_id id); void gcli_sshkeys_free_keys(struct gcli_sshkey_list *list); #endif /* GCLI_SSHKEYS_H */ gcli-2.9.1/include/gcli/status.h000066400000000000000000000050751507017207500165110ustar00rootroot00000000000000/* * Copyright 2022-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 STATUS_H #define STATUS_H #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include enum gcli_notification_target_type { GCLI_NOTIFICATION_TARGET_INVALID = 0, GCLI_NOTIFICATION_TARGET_ISSUE, GCLI_NOTIFICATION_TARGET_PULL_REQUEST, GCLI_NOTIFICATION_TARGET_COMMIT, GCLI_NOTIFICATION_TARGET_EPIC, GCLI_NOTIFICATION_TARGET_REPOSITORY, GCLI_NOTIFICATION_TARGET_RELEASE, MAX_GCLI_NOTIFICATION_TARGET, }; struct gcli_notification { char *id; char *title; char *reason; char *date; enum gcli_notification_target_type type; char *repository; /* target specific data */ struct gcli_path target; }; struct gcli_notification_list { struct gcli_notification *notifications; size_t notifications_size; }; int gcli_get_notifications(struct gcli_ctx *ctx, int count, struct gcli_notification_list *out); int gcli_notification_mark_as_read(struct gcli_ctx *ctx, char const *id); void gcli_free_notification(struct gcli_notification *); void gcli_free_notifications(struct gcli_notification_list *); char const *gcli_notification_target_type_str(enum gcli_notification_target_type type); #endif /* STATUS_H */ gcli-2.9.1/include/gcli/url.h000066400000000000000000000040661507017207500157670ustar00rootroot00000000000000/* * Copyright 2025 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_URL_H #define GCLI_CMD_URL_H /* This URL parser is in no way standards compliant, it has only * the things it needs to be sufficiently able to parse git remote * URLs. */ #include /* struct representing the URL parts */ struct gcli_url { char *scheme, *user, *host, *port, *path; }; int gcli_parse_url(char const *url, struct gcli_url *out); void gcli_url_free(struct gcli_url *); void gcli_url_options_append(char **result, char const *key, char const *value); void gcli_url_options_appendf(char **result, char const *key, char const *fmt, ...) PRINTF_FORMAT(3, 4); #endif /* GCLI_CMD_URL_H */ gcli-2.9.1/include/gcli/waitproc.h000066400000000000000000000030531507017207500170100ustar00rootroot00000000000000/* * Copyright 2024-2025 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_WAITPROC_H #define GCLI_WAITPROC_H #include #include /* for pid_t */ int gcli_wait_proc_ok(struct gcli_ctx *ctx, pid_t pid); #endif /* GCLI_WAITPROC_H */ gcli-2.9.1/libgcli.pc.in000066400000000000000000000004571507017207500150110ustar00rootroot00000000000000# libgcli package-config file # prefix=@prefix@ exec_prefix=${prefix} libdir=@libdir@ includedir=@includedir@ Name: gcli Description: Library to interact with various Git forges Version: 2.0 URL: https://herrhotzenplotz.de/gcli Requires: libcurl >= 8.0 Libs: -L${libdir} -lgcli Cflags: -I${includedir} gcli-2.9.1/src/000077500000000000000000000000001507017207500132345ustar00rootroot00000000000000gcli-2.9.1/src/attachments.c000066400000000000000000000040321507017207500157120ustar00rootroot00000000000000/* * Copyright 2023-2025 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 void gcli_attachments_free(struct gcli_attachment_list *list) { for (size_t i = 0; i < list->attachments_size; ++i) { gcli_attachment_free(&list->attachments[i]); } gcli_clear_ptr(&list->attachments); list->attachments_size = 0; } void gcli_attachment_free(struct gcli_attachment *it) { gcli_clear_ptr(&it->author); gcli_clear_ptr(&it->file_name); gcli_clear_ptr(&it->summary); gcli_clear_ptr(&it->content_type); gcli_clear_ptr(&it->data_base64); } int gcli_attachment_get_content(struct gcli_ctx *const ctx, gcli_id const id, FILE *out) { gcli_null_check_call(attachment_get_content, ctx, id, out); } gcli-2.9.1/src/base64.c000066400000000000000000000100021507017207500144550ustar00rootroot00000000000000/* * Copyright 2023-2025 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 /* The code below is taken from my IRC chat bot and was originally written by * raym aka. Aritra Sarkar in 2022. */ int gcli_decode_base64(struct gcli_ctx *ctx, char const *input, char *buffer, size_t buffer_size) { char const digits[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxy" "z0123456789+/"; char digit = 0; unsigned long octets = 0; size_t bits_rem = 0; size_t length = 0; memset(buffer, 0, buffer_size); while ((digit = *input++)) { if (digit == '=') { if (bits_rem % 8 == 0) /* Unnecessary padding char */ return gcli_error(ctx, "invalid base64 input"); do { if (digit != '=') return gcli_error(ctx, "invalid base64 input"); octets >>= 2; bits_rem -= 2; digit = *input++; } while (bits_rem % 8 != 0); if (digit) return gcli_error(ctx, "invalid base64 input"); size_t byte_count = 0; while (bits_rem > 0) { unsigned char octet = octets & 0xff; if (octet == '\0') return gcli_error(ctx, "null-character encountered during base64 decode"); buffer[length + (bits_rem / 8) - 1] = (char) octet; octets >>= 8; bits_rem -= 8; byte_count++; } length += byte_count; return 0; } size_t sextet = 0; /* Lookup index for a digit. We shall perform a linear search * for the index of the digit. Since there are only 64 digits, * this should be done in a jiffy. */ for ( ; sextet < 64; sextet++) if (digits[sextet] == digit) break; if (sextet == 64) /* Oops! We couldn't lookup the index of `digit` */ return gcli_error(ctx, "invalid base64 input"); octets = (octets << 6) | sextet; bits_rem += 6; /* 4 sextets (24 bits) of base64 input yields 3 bytes */ if (bits_rem == 24) { while (bits_rem > 0) { unsigned char octet = octets & 0xff; if (octet == '\0') return gcli_error(ctx, "null-character encountered during base64 decode"); buffer[length + (bits_rem / 8) - 1] = (char) octet; octets >>= 8; bits_rem -= 8; } length += 3; } } if (bits_rem > 0) return gcli_error(ctx, "invalid base64 input"); return 0; } int gcli_base64_decode_print(struct gcli_ctx *ctx, FILE *out, char const *const input) { int rc = 0; char *buffer = NULL; size_t buffer_size = 0, input_size = 0; input_size = strlen(input); /* account for BASE64 inflation */ buffer_size = (input_size / 4) * 3; buffer = calloc(1, buffer_size); rc = gcli_decode_base64(ctx, input, buffer, buffer_size); if (rc < 0) return rc; fwrite(buffer, buffer_size, 1, out); gcli_clear_ptr(&buffer); return 0; } gcli-2.9.1/src/bugzilla/000077500000000000000000000000001507017207500150455ustar00rootroot00000000000000gcli-2.9.1/src/bugzilla/api.c000066400000000000000000000035271507017207500157710ustar00rootroot00000000000000/* * Copyright 2023-2025 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 char const * bugzilla_api_error_string(struct gcli_ctx *ctx, struct gcli_fetch_buffer *const buf) { struct json_stream stream = {0}; int rc; char *msg; json_open_buffer(&stream, buf->data, buf->length); rc = parse_bugzilla_get_error(ctx, &stream, &msg); json_close(&stream); if (rc < 0) return strdup("no message: failed to parse error response"); else return msg; } gcli-2.9.1/src/bugzilla/attachments.c000066400000000000000000000045231507017207500175300ustar00rootroot00000000000000/* * Copyright 2023-2025 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 int bugzilla_attachment_get_content(struct gcli_ctx *ctx, gcli_id attachment_id, FILE *output) { int rc = 0; char *url; struct gcli_fetch_buffer buffer = {0}; struct json_stream stream = {0}; struct gcli_attachment attachment = {0}; url = gcli_asprintf("%s/rest/bug/attachment/%"PRIid, gcli_get_apibase(ctx), attachment_id); rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc < 0) goto error_fetch; json_open_buffer(&stream, buffer.data, buffer.length); rc = parse_bugzilla_attachment_content(ctx, &stream, &attachment); if (rc < 0) goto error_parse; rc = gcli_base64_decode_print(ctx, output, attachment.data_base64); gcli_attachment_free(&attachment); error_parse: json_close(&stream); gcli_fetch_buffer_free(&buffer); error_fetch: gcli_clear_ptr(&url); return rc; } gcli-2.9.1/src/bugzilla/bugs-parser.c000066400000000000000000000145671507017207500174600ustar00rootroot00000000000000/* * Copyright 2023-2025 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. */ /* Parser helpers for Bugzilla */ #include #include #include #include int parse_bugzilla_comments_array_skip_first(struct gcli_ctx *ctx, struct json_stream *stream, struct gcli_comment_list *out) { int rc = 0; if (json_next(stream) != JSON_ARRAY) return gcli_error(ctx, "expected array for comments array"); SKIP_OBJECT_VALUE(stream); while (json_peek(stream) != JSON_ARRAY_END) { out->comments = realloc(out->comments, sizeof(*out->comments) * (out->comments_size + 1)); memset(&out->comments[out->comments_size], 0, sizeof(out->comments[out->comments_size])); rc = parse_bugzilla_comment(ctx, stream, &out->comments[out->comments_size++]); if (rc < 0) return rc; } if (json_next(stream) != JSON_ARRAY_END) return gcli_error(ctx, "unexpected element in array while parsing"); return 0; } int parse_bugzilla_comments_array_only_first(struct gcli_ctx *ctx, struct json_stream *stream, char **out) { int rc = 0; if (json_next(stream) != JSON_ARRAY) return gcli_error(ctx, "expected array for comments array"); rc = parse_bugzilla_comment_text(ctx, stream, out); if (rc < 0) return rc; while (json_peek(stream) != JSON_ARRAY_END) { SKIP_OBJECT_VALUE(stream); } if (json_next(stream) != JSON_ARRAY_END) return gcli_error(ctx, "unexpected element in array while parsing"); return 0; } int parse_bugzilla_bug_comments_dictionary_skip_first(struct gcli_ctx *const ctx, struct json_stream *stream, struct gcli_comment_list *out) { enum json_type next = JSON_NULL; int rc = 0; if (json_next(stream) != JSON_OBJECT) return gcli_error(ctx, "expected bugzilla comments dictionary"); while ((next = json_next(stream)) == JSON_STRING) { rc = parse_bugzilla_comments_internal_skip_first(ctx, stream, out); if (rc < 0) return rc; } if (next != JSON_OBJECT_END) return gcli_error(ctx, "unclosed bugzilla comments dictionary"); return rc; } int parse_bugzilla_bug_comments_dictionary_only_first(struct gcli_ctx *const ctx, struct json_stream *stream, char **out) { enum json_type next = JSON_NULL; int rc = 0; if (json_next(stream) != JSON_OBJECT) return gcli_error(ctx, "expected bugzilla comments dictionary"); while ((next = json_next(stream)) == JSON_STRING) { rc = parse_bugzilla_comments_internal_only_first(ctx, stream, out); if (rc < 0) return rc; } if (next != JSON_OBJECT_END) return gcli_error(ctx, "unclosed bugzilla comments dictionary"); return rc; } int parse_bugzilla_assignee(struct gcli_ctx *ctx, struct json_stream *stream, struct gcli_issue *out) { out->assignees = calloc(1, sizeof (*out->assignees)); out->assignees_size = 1; return get_string(ctx, stream, out->assignees); } int parse_bugzilla_bug_attachments_dict(struct gcli_ctx *ctx, struct json_stream *stream, struct gcli_attachment_list *out) { enum json_type next = JSON_NULL; int rc = 0; if (json_next(stream) != JSON_OBJECT) return gcli_error(ctx, "expected bugzilla attachments dictionary"); while ((next = json_next(stream)) == JSON_STRING) { rc = parse_bugzilla_bug_attachments_internal(ctx, stream, &out->attachments, &out->attachments_size); if (rc < 0) return rc; } if (next != JSON_OBJECT_END) return gcli_error(ctx, "unclosed bugzilla attachments dictionary"); return rc; } int parse_bugzilla_attachment_content_only_first(struct gcli_ctx *ctx, struct json_stream *stream, struct gcli_attachment *out) { enum json_type next = JSON_NULL; int rc = 0; if (json_next(stream) != JSON_OBJECT) return gcli_error(ctx, "expected bugzilla attachments dictionary"); while ((next = json_next(stream)) == JSON_STRING) { rc = parse_bugzilla_bug_attachment(ctx, stream, out); if (rc < 0) return rc; } if (next != JSON_OBJECT_END) return gcli_error(ctx, "unclosed bugzilla attachments dictionary"); return rc; } int parse_bugzilla_single_comments_array_only_first(struct gcli_ctx *ctx, struct json_stream *stream, struct gcli_comment *out) { enum json_type next = JSON_NULL; int rc = 0; if (json_next(stream) != JSON_OBJECT) return gcli_error(ctx, "expected bugzilla comments dictionary"); while ((next = json_next(stream)) == JSON_STRING) { rc = parse_bugzilla_comment(ctx, stream, out); if (rc < 0) return rc; } if (next != JSON_OBJECT_END) return gcli_error(ctx, "unclosed bugzilla comments dictionary"); return rc; } gcli-2.9.1/src/bugzilla/bugs.c000066400000000000000000000260161507017207500161560ustar00rootroot00000000000000/* * Copyright 2023-2025 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 #include #include #include int bugzilla_get_bugs(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue_fetch_details const *details, int const max, struct gcli_issue_list *out) { char *url = NULL, *suffix = NULL; struct gcli_fetch_buffer buffer = {0}; int rc = 0; if (path->kind != GCLI_PATH_BUGZILLA) return gcli_error(ctx, "unsupported path kind for bugzilla"); /* Note(Nico): Most of the options here are not very well * documented. Specifically the order= parameter I have figured out by * reading the code and trying things until it worked. */ gcli_url_options_append(&suffix, "order", "bug_id DESC,"); /* TODO: handle the max = -1 case */ gcli_url_options_appendf(&suffix, "limit", "%d", max); if (details->all) { gcli_url_options_append(&suffix, "status", "All"); } else { gcli_url_options_append(&suffix, "status", "Open"); gcli_url_options_append(&suffix, "status", "New"); } gcli_url_options_append(&suffix, "product", path->as_bugzilla.product); gcli_url_options_append(&suffix, "component", path->as_bugzilla.component); gcli_url_options_append(&suffix, "creator", details->author); gcli_url_options_append(&suffix, "assigned_to", details->assignee); gcli_url_options_append(&suffix, "quicksearch", details->search_term); url = gcli_asprintf("%s/rest/bug%s", gcli_get_apibase(ctx), suffix); gcli_clear_ptr(&suffix); rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc == 0) { struct json_stream stream = {0}; json_open_buffer(&stream, buffer.data, buffer.length); rc = parse_bugzilla_bugs(ctx, &stream, out); json_close(&stream); } gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&url); return rc; } int bugzilla_bug_get_comments(struct gcli_ctx *const ctx, struct gcli_path const *const path, struct gcli_comment_list *out) { int rc = 0; struct gcli_fetch_buffer buffer = {0}; struct json_stream stream = {0}; char *url = NULL; if (path->kind != GCLI_PATH_ID) return gcli_error(ctx, "bad path kind for Bugzilla comments"); url = gcli_asprintf("%s/rest/bug/%"PRIid"/comment?include_fields=_all", gcli_get_apibase(ctx), path->as_id); rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc < 0) goto error_fetch; json_open_buffer(&stream, buffer.data, buffer.length); rc = parse_bugzilla_comments(ctx, &stream, out); json_close(&stream); gcli_fetch_buffer_free(&buffer); error_fetch: gcli_clear_ptr(&url); return rc; } int bugzilla_bug_get_comment(struct gcli_ctx *const ctx, struct gcli_path const *const target, enum comment_target_type const target_type, gcli_id const comment_id, struct gcli_comment *const out) { char *url = NULL; int rc = 0; struct gcli_fetch_buffer buffer = {0}; struct json_stream stream = {0}; (void) target; (void) target_type; url = gcli_asprintf("%s/rest/bug/comment/%"PRIid"?include_fields=_all", gcli_get_apibase(ctx), comment_id); rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc < 0) goto error_fetch; json_open_buffer(&stream, buffer.data, buffer.length); rc = parse_bugzilla_single_comment(ctx, &stream, out); json_close(&stream); gcli_fetch_buffer_free(&buffer); error_fetch: gcli_clear_ptr(&url); return rc; } static int bugzilla_bug_get_op(struct gcli_ctx *ctx, gcli_id const bug_id, char **out) { int rc = 0; struct gcli_fetch_buffer buffer = {0}; struct json_stream stream = {0}; char *url = NULL; url = gcli_asprintf("%s/rest/bug/%"PRIid"/comment?include_fields=_all", gcli_get_apibase(ctx), bug_id); rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc < 0) goto error_fetch; json_open_buffer(&stream, buffer.data, buffer.length); rc = parse_bugzilla_bug_op(ctx, &stream, out); json_close(&stream); gcli_fetch_buffer_free(&buffer); error_fetch: gcli_clear_ptr(&url); return rc; } int bugzilla_get_bug(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue *out) { int rc = 0; char *url; struct gcli_fetch_buffer buffer = {0}; struct gcli_issue_list list = {0}; struct json_stream stream = {0}; gcli_id bug_id; if (path->kind != GCLI_PATH_ID) return gcli_error(ctx, "Getting a single bug on Bugzilla requires an ID path"); bug_id = path->as_id; url = gcli_asprintf("%s/rest/bug?limit=1&id=%"PRIid, gcli_get_apibase(ctx), bug_id); rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc < 0) goto error_fetch; json_open_buffer(&stream, buffer.data, buffer.length); rc = parse_bugzilla_bugs(ctx, &stream, &list); if (rc < 0) goto error_parse; if (list.issues_size == 0) { rc = gcli_error(ctx, "no bug with id %"PRIid, bug_id); goto error_no_such_bug; } if (list.issues_size > 0) { assert(list.issues_size == 1); memcpy(out, &list.issues[0], sizeof(*out)); } /* don't use gcli_issues_free because it frees data behind pointers we * just copied */ gcli_clear_ptr(&list.issues); /* insert the web-url which is not provided by the API ... */ out->web_url = gcli_asprintf("%s/show_bug.cgi?id=%"PRIid, gcli_get_apibase(ctx), bug_id); /* The OP is in the comments. Fetch it separately. */ rc = bugzilla_bug_get_op(ctx, bug_id, &out->body); error_no_such_bug: error_parse: json_close(&stream); gcli_fetch_buffer_free(&buffer); error_fetch: gcli_clear_ptr(&url); return rc; } int bugzilla_bug_get_attachments(struct gcli_ctx *ctx, struct gcli_path const *const bug_path, struct gcli_attachment_list *const out) { int rc = 0; char *url = NULL; struct gcli_fetch_buffer buffer = {0}; struct json_stream stream = {0}; if (bug_path->kind != GCLI_PATH_ID) return gcli_error(ctx, "Getting bug attachments requires a ID path"); url = gcli_asprintf("%s/rest/bug/%"PRIid"/attachment", gcli_get_apibase(ctx), bug_path->as_id); rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc < 0) goto error_fetch; json_open_buffer(&stream, buffer.data, buffer.length); rc = parse_bugzilla_bug_attachments(ctx, &stream, out); json_close(&stream); gcli_fetch_buffer_free(&buffer); error_fetch: gcli_clear_ptr(&url); return rc; } static void add_extra_options(struct gcli_nvlist const *list, struct gcli_jsongen *gen) { static struct extra_opt { char const *json_name; char const *cli_name; char const *default_value; } extra_opts[] = { { .json_name = "op_sys", .cli_name = "os", .default_value = "All" }, { .json_name = "rep_platform", .cli_name = "hardware", .default_value = "All" }, { .json_name = "version", .cli_name = "version", .default_value = "unspecified" }, }; static size_t extra_opts_size = ARRAY_SIZE(extra_opts); for (size_t i = 0; i < extra_opts_size; ++i) { struct extra_opt const *o = &extra_opts[i]; char const *const val = gcli_nvlist_find_or( list, o->json_name, o->default_value); gcli_jsongen_objmember(gen, o->json_name); gcli_jsongen_string(gen, val); } } int 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; struct gcli_jsongen gen = {0}; struct gcli_fetch_buffer buffer = {0}, *_buffer = NULL; int rc = 0; /* prepare data for payload generation */ if (product == NULL) return gcli_error(ctx, "product must not be empty"); if (component == NULL) return gcli_error(ctx, "component must not be empty"); token = gcli_get_token(ctx); if (!token) return gcli_error(ctx, "creating bugs on bugzilla requires a token"); /* generate payload */ rc = gcli_jsongen_init(&gen); if (rc < 0) { gcli_error(ctx, "failed to init json generator"); goto err_jsongen_init; } /* * { * "product" : "TestProduct", * "component" : "TestComponent", * "summary" : "'This is a test bug - please disregard", * "description": ..., * } */ gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "product"); gcli_jsongen_string(&gen, product); gcli_jsongen_objmember(&gen, "component"); gcli_jsongen_string(&gen, component); gcli_jsongen_objmember(&gen, "summary"); gcli_jsongen_string(&gen, summary); if (description) { gcli_jsongen_objmember(&gen, "description"); gcli_jsongen_string(&gen, description); } gcli_jsongen_objmember(&gen, "api_key"); gcli_jsongen_string(&gen, token); add_extra_options(&opts->extra, &gen); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); /* generate url and perform request */ url = gcli_asprintf("%s/rest/bug", gcli_get_apibase(ctx)); if (out) _buffer = &buffer; rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, _buffer); if (out && rc == 0) { struct json_stream stream = {0}; struct gcli_path bug_id_path = {0}; json_open_buffer(&stream, buffer.data, buffer.length); rc = parse_bugzilla_bug_creation_result( ctx, &stream, &bug_id_path.as_id); json_close(&stream); bug_id_path.kind = GCLI_PATH_ID; if (rc == 0) rc = bugzilla_get_bug(ctx, &bug_id_path, out); } gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&url); gcli_clear_ptr(&payload); err_jsongen_init: gcli_clear_ptr(&token); return rc; } gcli-2.9.1/src/bugzilla/comment.c000066400000000000000000000053741507017207500166640ustar00rootroot00000000000000/* * Copyright 2025 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 int bugzilla_submit_comment(struct gcli_ctx *ctx, struct gcli_submit_comment_opts const *const opts) { char *url = NULL, *payload = NULL, *token = NULL; int rc = 0; struct gcli_jsongen gen = {0}; struct gcli_path const *const tgt = &opts->target; if (tgt->kind != GCLI_PATH_ID) { return gcli_error( ctx, "bad path kind for submitting Bugzilla comment" ); } /* grab a valid API token */ token = gcli_get_token(ctx); if (!token) return gcli_error(ctx, "creating comments on bugzilla requires a token"); /* construct URL */ url = gcli_asprintf("%s/rest/bug/%"PRIid"/comment", gcli_get_apibase(ctx), tgt->as_id); /* construct payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "comment"); gcli_jsongen_string(&gen, opts->message); gcli_jsongen_objmember(&gen, "is_private"); gcli_jsongen_bool(&gen, false); gcli_jsongen_objmember(&gen, "api_key"); gcli_jsongen_string(&gen, token); } 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 */ gcli_clear_ptr(&url); gcli_clear_ptr(&payload); gcli_clear_ptr(&token); return rc; } gcli-2.9.1/src/bugzilla/config.c000066400000000000000000000030041507017207500164530ustar00rootroot00000000000000/* * Copyright 2023-2025 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 char * bugzilla_make_authheader(struct gcli_ctx *ctx, char const *const token) { (void) ctx; (void) token; return NULL; } gcli-2.9.1/src/cmd/000077500000000000000000000000001507017207500137775ustar00rootroot00000000000000gcli-2.9.1/src/cmd/actions.c000066400000000000000000000120231507017207500156010ustar00rootroot00000000000000/* * Copyright 2025 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 static struct gcli_cmd_action const * find_action(struct gcli_cmd_actions const *const actions, char const *const action_name) { struct gcli_cmd_action const *a = NULL; for (a = actions->defs; a->name != NULL; a += 1) { if (strcmp(a->name, action_name) == 0) { return a; } } return NULL; } int gcli_cmd_actions_handle(struct gcli_cmd_actions const *const actions, struct gcli_path const *const path, int *argc, char ***argv) { void *item = NULL; int rc = 0; if (*argc < 1) { fprintf(stderr, "gcli: error: missing action\n"); return 1; } /* check until we don't have any more remaining arguments */ for (;;) { /* fetch the action name */ char *const action_name = (*argv)[0]; /* look for an action definition */ struct gcli_cmd_action const *const action = find_action( actions, action_name); if (action == NULL) { fprintf(stderr, "gcli: error: unknown action '%s'\n", action_name); rc = GCLI_EX_USAGE; break; } /* check whether we need to fetch the item */ if (action->needs_item && item == NULL) { item = calloc(1, actions->item_size); if (item == NULL) { err(1, "calloc failed"); } rc = actions->fetch_item(g_clictx, path, item); if (rc < 0) { fprintf(stderr, "gcli: error: failed to fetch: %s\n", gcli_get_error(g_clictx)); rc = GCLI_EX_DATAERR; break; } } /* handle the action */ rc = action->handler(path, item, argc, argv); if (rc < 0) { fprintf(stderr, "gcli: action %s failed\n", action_name); break; } shift(argc, argv); if (*argc == 0) break; fputc('\n', stdout); } if (item) { actions->free_item(item); free(item); item = NULL; } return rc; } struct into_pager_args { int argc; char **argv; struct gcli_path const *path; void *item; struct gcli_cmd_action const *action; }; static int into_pager_fn(void *data) { struct into_pager_args *args = data; return args->action->handler( args->path, args->item, &args->argc, &args->argv); } int gcli_cmd_action_handle(struct gcli_cmd_actions const *actions, struct gcli_path const *path, char *cmd_input) { enum { argv_size = 32 }; char *_argv[argv_size]; /* storage */ char *argfront = cmd_input; int argc = 0, rc = 0; struct gcli_cmd_action const *action = NULL; void *item = NULL; char **argv = &_argv[0]; /* pointer to storage but type is corrected */ /* Split arguments by spaces and collect into argc/argv */ for (;;) { char *argnext = strchr(argfront, ' '); if (argc == argv_size) err(1, "gcli: error: too many arguments"); argv[argc++] = argfront; if (!argnext) break; *argnext++ = '\0'; argfront = argnext; } action = find_action(actions, argv[0]); if (action == NULL) { fprintf(stderr, "gcli: error: no such action: %s\n", argv[0]); return GCLI_EX_USAGE; } if (action->needs_item) { item = calloc(1, actions->item_size); if (item == NULL) err(1, "calloc"); rc = actions->fetch_item(g_clictx, path, item); if (rc < 0) { fprintf(stderr, "gcli: error: failed to fetch item: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } } if (action->use_pager) { struct into_pager_args args = { .argc = argc, .argv = argv, .path = path, .item = item, .action = action, }; rc = gcli_cmd_into_pager(into_pager_fn, &args); } else { rc = action->handler(path, item, &argc, &argv); } if (item) { actions->free_item(item); free(item); item = NULL; } return rc; } gcli-2.9.1/src/cmd/api.c000066400000000000000000000064031507017207500147170ustar00rootroot00000000000000/* * Copyright 2022-2025 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. */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include static void usage(void) { fprintf(stderr, "usage: gcli api [-a] \n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -a Fetch all pages of data\n"); fprintf(stderr, " path Path to put after the API base URL\n"); fprintf(stderr, "\n"); version(); copyright(); } static void fetch_all(char *_url) { char *url = NULL, *next_url = NULL; url = _url; do { struct gcli_fetch_buffer buffer = {0}; if (gcli_fetch(g_clictx, url, &next_url, &buffer) < 0) errx(1, "gcli: error: failed to fetch data: %s", gcli_get_error(g_clictx)); fwrite(buffer.data, buffer.length, 1, stdout); gcli_fetch_buffer_free(&buffer); if (url != _url) free(url); } while ((url = next_url)); } int subcommand_api(int argc, char *argv[]) { char *url = NULL, *path = NULL; int ch, do_all = 0; struct option options[] = { { .name = "all", .has_arg = no_argument, .flag = NULL, .val = 'a' }, {0} }; while ((ch = getopt_long(argc, argv, "+a", options, NULL)) != -1) { switch (ch) { case 'a': do_all = 1; break; default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; if (argc == 1) { path = shift(&argc, &argv); } else { if (!argc) errx(1, "gcli: error: missing path"); else errx(1, "gcli: error: too many arguments"); } if (path[0] == '/') url = gcli_asprintf("%s%s", gcli_get_apibase(g_clictx), path); else url = gcli_asprintf("%s/%s", gcli_get_apibase(g_clictx), path); if (do_all) fetch_all(url); else if (gcli_curl(g_clictx, stdout, url, "application/json") < 0) errx(1, "gcli: error: failed to fetch data: %s", gcli_get_error(g_clictx)); free(url); return EXIT_SUCCESS; } gcli-2.9.1/src/cmd/attachments.c000066400000000000000000000111121507017207500164520ustar00rootroot00000000000000/* * Copyright 2023-2025 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 #include static void usage(void) { fprintf(stderr, "usage: gcli [options] attachments -i actions...\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -i id Execute the given actions for the specified attachment id.\n"); fprintf(stderr, "ACTIONS:\n"); fprintf(stderr, " get [-o path] Fetch and dump the contents of the " "attachments to the given path or stdout\n"); fprintf(stderr, "\n"); version(); } static int action_attachment_get(int *argc, char ***argv, gcli_id const id) { int ch, rc = 0; bool oflag_seen = false; FILE *outfile = NULL; struct option options[] = { { .name = "output", .has_arg = required_argument, .flag = NULL, .val = 'o' }, {0}, }; while ((ch = getopt_long(*argc, *argv, "+o:", options, NULL)) != -1) { switch (ch) { case 'o': { outfile = fopen(optarg, "w"); if (!outfile) { fprintf(stderr, "gcli: failed to open »%s«: %s\n", optarg, strerror(errno)); return EXIT_FAILURE; } oflag_seen = true; } break; default: { usage(); return EXIT_FAILURE; } break; } } *argc -= optind; *argv += optind; optind = 0; /* reset */ /* -o wasn't specified */ if (outfile == NULL) outfile = stdout; rc = gcli_attachment_get_content(g_clictx, id, outfile); if (rc < 0) { fprintf(stderr, "gcli: failed to get attachment: %s\n", gcli_get_error(g_clictx)); return EXIT_FAILURE; } if (oflag_seen) fclose(outfile); outfile = NULL; return EXIT_SUCCESS; } static struct action { char const *const name; int (*fn)(int *argc, char ***argv, gcli_id const id); } const actions[] = { { .name = "get", .fn = action_attachment_get }, }; static size_t const actions_size = ARRAY_SIZE(actions); static struct action const * find_action(char const *const name) { for (size_t i = 0; i < actions_size; ++i) { if (strcmp(name, actions[i].name) == 0) return &actions[i]; } return NULL; } int subcommand_attachments(int argc, char *argv[]) { int ch; gcli_id iflag; bool iflag_seen = false; struct option options[] = { { .name = "id", .has_arg = required_argument, .flag = NULL, .val = 'i' }, {0}, }; while ((ch = getopt_long(argc, argv, "+i:", options, NULL)) != -1) { switch (ch) { case 'i': { char *endptr; iflag_seen = true; iflag = strtoull(optarg, &endptr, 10); if (optarg + strlen(optarg) != endptr) { fprintf(stderr, "gcli: bad attachment id »%s«\n", optarg); return EXIT_FAILURE; } } break; default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; optind = 0; /* reset */ if (!iflag_seen) { fprintf(stderr, "gcli: missing -i flag\n"); usage(); return EXIT_FAILURE; } if (argc == 0) { fprintf(stderr, "gcli: missing actions\n"); usage(); return EXIT_FAILURE; } while (argc) { int rc; char const *const action_name = *argv; struct action const *const action = find_action(action_name); if (action == NULL) { fprintf(stderr, "gcli: %s: no such action\n", action_name); usage(); return EXIT_FAILURE; } rc = action->fn(&argc, &argv, iflag); if (rc) return rc; } return 0; } gcli-2.9.1/src/cmd/ci.c000066400000000000000000000121741507017207500145430ustar00rootroot00000000000000/* * Copyright 2022-2025 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. */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include static void usage(void) { fprintf(stderr, "usage: gcli ci [-o owner -r repo] [-n number] ref\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -o owner The repository owner\n"); fprintf(stderr, " -r repo The repository name\n"); fprintf(stderr, " -n number Number of check runs to fetch (-1 = everything)\n"); fprintf(stderr, "\n"); version(); copyright(); } void github_print_checks(struct github_check_list const *const list) { gcli_tbl table; struct gcli_tblcoldef cols[] = { { .name = "ID", .type = GCLI_TBLCOLTYPE_ID, .flags = GCLI_TBLCOL_JUSTIFYR }, { .name = "STATUS", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "CONCLUSION", .type = GCLI_TBLCOLTYPE_STRING, .flags = GCLI_TBLCOL_STATECOLOURED }, { .name = "STARTED", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "COMPLETED", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "NAME", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, }; if (!list->checks_size) { fprintf(stderr, "No checks\n"); return; } table = gcli_tbl_begin(cols, ARRAY_SIZE(cols)); if (!table) errx(1, "gcli: error: could not init table"); for (size_t i = 0; i < list->checks_size; ++i) { gcli_tbl_add_row(table, list->checks[i].id, list->checks[i].status, list->checks[i].conclusion, list->checks[i].started_at, list->checks[i].completed_at, list->checks[i].name); } gcli_tbl_end(table); } int github_checks(struct gcli_path const *const path, char const *const ref, int const max) { struct github_check_list list = {0}; int rc = 0; rc = github_get_checks(g_clictx, path, ref, max, &list); if (rc < 0) return rc; github_print_checks(&list); github_free_checks(&list); return rc; } int subcommand_ci(int argc, char *argv[]) { int ch = 0; struct gcli_path repo_path = {0}; char const *ref = NULL; int count = -1; /* fetch all checks by default */ /* Parse options */ struct option const options[] = { {.name = "repo", .has_arg = required_argument, .flag = NULL, .val = 'r'}, {.name = "owner", .has_arg = required_argument, .flag = NULL, .val = 'o'}, {.name = "count", .has_arg = required_argument, .flag = NULL, .val = 'c'}, {0} }; while ((ch = getopt_long(argc, argv, "n:o:r:", options, NULL)) != -1) { switch (ch) { case 'o': repo_path.as_default.owner = optarg; break; case 'r': repo_path.as_default.repo = optarg; break; case 'n': { char *endptr = NULL; count = strtol(optarg, &endptr, 10); if (endptr != (optarg + strlen(optarg))) err(1, "ci: cannot parse argument to -n"); } break; case '?': default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; /* Check that we have exactly one left argument and print proper * error messages */ if (argc < 1) { fprintf(stderr, "gcli: error: missing ref\n"); usage(); return EXIT_FAILURE; } if (argc > 1) { fprintf(stderr, "gcli: error: stray arguments\n"); usage(); return EXIT_FAILURE; } /* Save the ref */ ref = argv[0]; check_path(&repo_path); /* Make sure we are actually talking about a github remote because * we might be incorrectly inferring it */ if (gcli_config_get_forge_type(g_clictx) != GCLI_FORGE_GITHUB) errx(1, "gcli: error: The ci subcommand only works for GitHub. " "Use gcli -t github ... to force a GitHub remote."); if (github_checks(&repo_path, ref, count) < 0) errx(1, "gcli: error: failed to get github checks: %s", gcli_get_error(g_clictx)); return EXIT_SUCCESS; } gcli-2.9.1/src/cmd/cmd.c000066400000000000000000000257301507017207500147150ustar00rootroot00000000000000/* * Copyright 2022-2025 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. */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_LIBLOWDOWN #include #include #include #endif #if defined(HAVE_LIBREADLINE) && !defined(HAVE_LIBEDIT) #define USE_READLINE 1 #include #endif #if defined(HAVE_LIBEDIT) #define USE_LIBEDIT 1 #include #endif void copyright(void) { fprintf( stderr, "Copyright 2021-2025 Nico Sonack " " and contributors.\n"); } void version(void) { fprintf(stderr, PACKAGE_STRING" ("HOSTOS")\n"); } void longversion(void) { version(); fprintf(stderr, "Using %s\n", curl_version()); fprintf(stderr, "Using vendored pdjson library\n"); #ifdef USE_READLINE fprintf(stderr, "Using readline version %d.%d\n", RL_VERSION_MAJOR, RL_VERSION_MINOR); #endif /* USE_READLINE */ #ifdef USE_LIBEDIT fprintf(stderr, "Using libedit version %d.%d\n", LIBEDIT_MAJOR, LIBEDIT_MINOR); #endif /* USE_LIBEDIT */ #ifdef HAVE_LIBLOWDOWN fprintf(stderr, "Using liblowdown\n"); #endif /* HAVE_LIBLOWDOWN */ fprintf(stderr, "\n"); fprintf(stderr, "Project website: "PACKAGE_URL"\n"); fprintf(stderr, "Bug reports: "PACKAGE_BUGREPORT"\n"); } void check_owner_and_repo(const char **owner, const char **repo) { /* HACK */ if (gcli_config_get_forge_type(g_clictx) == GCLI_FORGE_BUGZILLA) return; /* If no remote was specified, try to autodetect */ if ((*owner == NULL) != (*repo == NULL)) errx(1, "gcli: error: missing either explicit owner or 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)); } } void check_path(struct gcli_path *path) { /* Two special cases for Bugzilla support: * * When no ID was specified with bugzilla we only have a combination of * product/component. in this case we force the path kind to BUGZILLA. * * The other case is a (possibly) missing product and component but an * ID was set. In this case we change the path kind to GCLI_PATH_ID. * We don't ignore product/component because that would be incorrect * and/or leak memory. * * For reasons of human error the juggling below is done such that if * someone by accident breaks the ABI of gcli_path this doesn't fall * apart. */ if (gcli_config_get_forge_type(g_clictx) == GCLI_FORGE_BUGZILLA && path->kind == GCLI_PATH_DEFAULT) { /* first case */ if (path->as_default.id == 0) { char *const product = path->as_default.owner; char *const component = path->as_default.repo; path->kind = GCLI_PATH_BUGZILLA; path->as_bugzilla.product = product; path->as_bugzilla.component = component; return; /* no more checking required */ } /* second case */ if (path->as_default.id != 0 && path->as_default.owner == NULL && path->as_default.repo == NULL) { gcli_id const id = path->as_default.id; path->kind = GCLI_PATH_ID; path->as_id = id; return; } } check_owner_and_repo( (char const **)&path->as_default.owner, (char const **)&path->as_default.repo); } /* Parses (and updates) the given argument list into two seperate lists: * * --add -> add_labels * --remove -> remove_labels */ void parse_labels_options(int *argc, char ***argv, const char ***_add_labels, size_t *_add_labels_size, const char ***_remove_labels, size_t *_remove_labels_size) { const char **add_labels = NULL, **remove_labels = NULL; size_t add_labels_size = 0, remove_labels_size = 0; /* Collect add/delete labels */ while (*argc >= 3) { char const *const action = (*argv)[1]; char const *const name = (*argv)[2]; if (strcmp(action, "add") == 0) { add_labels = realloc( add_labels, (add_labels_size + 1) * sizeof(*add_labels)); add_labels[add_labels_size++] = name; } else if (strcmp(action, "remove") == 0) { remove_labels = realloc( remove_labels, (remove_labels_size + 1) * sizeof(*remove_labels)); remove_labels[remove_labels_size++] = name; } else { break; } *argc -= 2; *argv += 2; } *_add_labels = add_labels; *_add_labels_size = add_labels_size; *_remove_labels = remove_labels; *_remove_labels_size = remove_labels_size; } /* delete the repo (and ask for confirmation) * * NOTE: this procedure is here because it is used by both the forks * and repo subcommand. Ideally it should be moved into the 'repos' * code but I don't wanna make it exported from there. */ void delete_repo(bool always_yes, struct gcli_path const *const path) { bool delete = false; if (!always_yes) { delete = gcli_yesno("Are you sure you want to delete the repo?"); } else { delete = true; } if (!delete) errx(1, "gcli: Operation aborted"); if (gcli_repo_delete(g_clictx, path) < 0) errx(1, "gcli: error: failed to delete repo"); } #ifdef HAVE_LIBLOWDOWN static void gcli_render_markdown(char const *input, int indent, int maxlinelen, FILE *stream) { size_t input_size; struct lowdown_buf *out; struct lowdown_doc *doc; struct lowdown_node *n; struct lowdown_opts opts = {0}; void *rndr; input_size = strlen(input); if (setlocale(LC_CTYPE, "en_US.UTF-8") == NULL) err(1, NULL); opts.feat |= LOWDOWN_FENCED|LOWDOWN_TASKLIST|LOWDOWN_TABLES; if (!gcli_config_have_colours(g_clictx)) opts.oflags |= (LOWDOWN_TERM_NOANSI|LOWDOWN_TERM_NOCOLOUR); /* Lowdown 1.4.0 broke the api in a minor version update. Work around * this by checking versions. * * See: https://github.com/kristapsdz/lowdown/issues/148 and * https://github.com/kristapsdz/lowdown/releases/tag/VERSION_1_4_0 */ #if (LIBLOWDOWN_MAJOR == 1 && LIBLOWDOWN_MINOR >= 4) || LIBLOWDOWN_MAJOR >= 2 opts.term.vmargin = 1; opts.term.hmargin = indent; /* Not only did the minor version break the API but also behaviour ... */ opts.term.cols = maxlinelen; #else opts.vmargin = 1; opts.hmargin = indent - 4; /* somehow there's always 4 spaces being emitted by lowdown */ opts.cols = maxlinelen; #endif if ((doc = lowdown_doc_new(&opts)) == NULL) err(1, NULL); if ((n = lowdown_doc_parse(doc, NULL, input, input_size, NULL)) == NULL) err(1, NULL); if ((out = lowdown_buf_new(256)) == NULL) err(1, NULL); if ((rndr = lowdown_term_new(&opts)) == NULL) err(1, NULL); if (!lowdown_term_rndr(out, rndr, n)) err(1, NULL); fwrite(out->data, 1, out->size, stream); lowdown_term_free(rndr); lowdown_buf_free(out); lowdown_node_free(n); lowdown_doc_free(doc); } #endif static int word_length(const char *x) { int l = 0; while (*x && !isspace(*x++)) l++; return l; } void gcli_pretty_print(const char *input, int indent, int maxlinelen, FILE *out) { const char *it = input; if (!it) return; #ifdef HAVE_LIBLOWDOWN if (gcli_config_render_markdown(g_clictx)) { gcli_render_markdown(input, indent, maxlinelen, out); return; } #endif while (*it) { int linelength = indent; fprintf(out, "%*.*s", indent, indent, ""); do { int w = word_length(it) + 1; if (it[w - 1] == '\n') { fprintf(out, "%.*s", w - 1, it); it += w; break; } else if (it[w - 1] == '\0') { w -= 1; } fprintf(out, "%.*s", w, it); it += w; linelength += w; } while (*it && (linelength < maxlinelen)); fputc('\n', out); } } void gcli_pretty_print_diff(char const *const input, int indent) { char const *hd = input; for (;;) { char const *eol; char const *start_colour, *end_colour; size_t linelen; if (hd == NULL || *hd == '\0') return; eol = strchr(hd, '\n'); if (eol == NULL) eol = hd + strlen(hd); linelen = eol - hd; end_colour = gcli_resetcolour(); if (*hd == '+') start_colour = gcli_setcolour(GCLI_COLOR_GREEN); else if (*hd == '-') start_colour = gcli_setcolour(GCLI_COLOR_RED); else start_colour = ""; printf("%*.*s%s%.*s%s\n", indent, indent, "", start_colour, (int)linelen, hd, end_colour); hd = eol + 1; } } /* portability kludge for Slowlaris which to this day doesn't support * resolved_path to be NULL. */ char * gcli_cmd_realpath(char const *const restrict pathname) { char *resolved_path = NULL; #if defined(_XOPEN_SOURCE) && _XOPEN_SOURCE < 700 /* This system is certainly very old! Assume that PATH_MAX is defined. * If not well there you go. Yes, this is flawed and ugly. */ resolved_path = calloc(PATH_MAX, 1); #endif return realpath(pathname, resolved_path); } bool gcli_cmd_should_do_always_yes(void) { return !isatty(STDIN_FILENO); } void gcli_cmd_save_message(char const *const message) { FILE *f = fopen("gcli_message", "w"); if (!f) { fprintf(stderr, "gcli: warning: failed to open 'gcli_message' " "for write, cannot save message\n"); return; } fputs(message, f); fclose(f); fprintf(stderr, "gcli: Message was saved in 'gcli_message'. " "Re-run the command to recall it.\n"); } bool gcli_cmd_can_recall_message(void) { return !access("gcli_message", R_OK); } char * gcli_cmd_recall_message(void) { char *result = NULL; int rc = 0; /* read */ rc = gcli_read_file("gcli_message", &result); if (rc < 0) return NULL; /* delete old message file */ rc = unlink("gcli_message"); if (rc < 0) { fprintf(stderr, "gcli: warning: cannot delete gcli_message: %s\n", strerror(errno)); } return result; } void gcli_cmd_recall_message_interactive(char **out) { /* recall an old message if needed, skip if there is a template */ if (*out == NULL && gcli_cmd_can_recall_message()) { if (gcli_yesno("Recall previously saved message?")) *out = gcli_cmd_recall_message(); } } gcli-2.9.1/src/cmd/cmdconfig.c000066400000000000000000000636671507017207500161160ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include #include #include #include #include #include #include #include #include #include #include struct gcli_config_section { TAILQ_ENTRY(gcli_config_section) next; struct gcli_config_entries entries; gcli_sv title; }; struct gcli_config { TAILQ_HEAD(gcli_config_sections, gcli_config_section) sections; char const *override_default_account; char const *override_remote; int override_forgetype; int colours_disabled; /* NO_COLOR set or output is not a TTY */ int force_colours; /* -c option was given */ int no_spinner; /* don't show a progress spinner */ int no_markdown; /* do not render markdown (when built with lowdown) */ int enable_experimental; /* enable experimental features */ gcli_sv buffer; char *file_content; bool inited; }; struct gcli_dotgcli { struct gcli_config_entries entries; gcli_sv buffer; char *file_content; bool has_been_searched_for; bool has_been_found; }; struct cmd_ctx { struct gcli_config config; struct gcli_dotgcli local_config; }; static inline struct gcli_dotgcli * ctx_dotgcli(struct gcli_ctx *ctx) { struct cmd_ctx *cctx = gcli_get_userdata(ctx); return &cctx->local_config; } static inline struct gcli_config * ctx_config(struct gcli_ctx *ctx) { struct cmd_ctx *cctx = gcli_get_userdata(ctx); return &cctx->config; } static bool should_init_dotgcli(struct gcli_ctx *ctx) { struct gcli_dotgcli *dgcli = ctx_dotgcli(ctx); return !dgcli->has_been_searched_for || (dgcli->has_been_searched_for && !dgcli->has_been_found); } static char const * find_dotgcli(void) { char *curr_dir_path = NULL; char *dotgcli = NULL; DIR *curr_dir = NULL; struct dirent *ent = NULL; curr_dir_path = getcwd(NULL, 128); if (!curr_dir_path) err(1, "gcli: getcwd"); /* Here we are trying to traverse upwards through the directory * tree, searching for a directory called .git. * Starting point is ".".*/ do { curr_dir = opendir(curr_dir_path); if (!curr_dir) err(1, "gcli: opendir"); while ((ent = readdir(curr_dir))) { if (strcmp(".", ent->d_name) == 0 || strcmp("..", ent->d_name) == 0) continue; if (strcmp(".gcli", ent->d_name) == 0) { size_t len = strlen(curr_dir_path); dotgcli = malloc(len + strlen(ent->d_name) + 2); memcpy(dotgcli, curr_dir_path, len); dotgcli[len] = '/'; memcpy(dotgcli + len + 1, ent->d_name, strlen(ent->d_name)); dotgcli[len + 1 + strlen(ent->d_name)] = 0; break; } } if (!dotgcli) { size_t len = strlen(curr_dir_path); char *tmp = malloc(len + sizeof("/..")); memcpy(tmp, curr_dir_path, len); memcpy(tmp + len, "/..", sizeof("/..")); free(curr_dir_path); curr_dir_path = gcli_cmd_realpath(tmp); if (!curr_dir_path) err(1, "gcli: realpath at %s", tmp); free(tmp); if (strcmp("/", curr_dir_path) == 0) { free(curr_dir_path); closedir(curr_dir); // At this point we know for sure that we cannot find // a .gcli and thus return a NULL pointer return NULL; } } closedir(curr_dir); } while (dotgcli == NULL); free(curr_dir_path); return dotgcli; } static void init_local_config(struct gcli_ctx *ctx) { if (!should_init_dotgcli(ctx)) { return; } char const *path = find_dotgcli(); struct gcli_dotgcli *dgcli = ctx_dotgcli(ctx); if (!path) { dgcli->has_been_searched_for = true; dgcli->has_been_found = false; return; } dgcli->has_been_searched_for = true; dgcli->has_been_found = true; int len = gcli_read_file(path, &dgcli->file_content); if (len < 0) err(1, "gcli: unable to open config file"); dgcli->buffer = gcli_sv_from_parts(dgcli->file_content, len); dgcli->buffer = gcli_sv_trim_front(dgcli->buffer); int curr_line = 1; while (dgcli->buffer.length > 0) { gcli_sv line = gcli_sv_chop_until(&dgcli->buffer, '\n'); line = gcli_sv_trim(line); if (line.length == 0) errx(1, "gcli: %s:%d: Unexpected end of line", path, curr_line); // Comments if (line.data[0] == '#') { dgcli->buffer = gcli_sv_trim_front(dgcli->buffer); curr_line++; continue; } gcli_sv key = gcli_sv_chop_until(&line, '='); key = gcli_sv_trim(key); if (key.length == 0) errx(1, "gcli: %s:%d: empty key", path, curr_line); line.data += 1; line.length -= 1; gcli_sv value = gcli_sv_trim(line); struct gcli_config_entry *entry = calloc(1, sizeof(*entry)); TAILQ_INSERT_TAIL(&dgcli->entries, entry, next); entry->key = key; entry->value = value; dgcli->buffer = gcli_sv_trim_front(dgcli->buffer); curr_line++; } free((void *)path); } struct config_parser { gcli_sv buffer; int line; char const *filename; }; static void skip_ws_and_comments(struct config_parser *input) { again: while (input->buffer.length > 0) { switch (input->buffer.data[0]) { case '\n': input->line++; /* fallthrough */ case ' ': case '\t': case '\r': input->buffer.data += 1; input->buffer.length -= 1; break; default: goto not_whitespace; } } return; not_whitespace: if (input->buffer.data[0] == '#') { /* This is a comment */ gcli_sv_chop_until(&input->buffer, '\n'); goto again; } } static void parse_section_entry(struct config_parser *input, struct gcli_config_section *section) { struct gcli_config_entry *entry = calloc(1, sizeof(*entry)); TAILQ_INSERT_TAIL(§ion->entries, entry, next); gcli_sv key = gcli_sv_chop_until(&input->buffer, '='); if (key.length == 0) errx(1, "gcli: %s:%d: empty key", input->filename, input->line); input->buffer.data += 1; input->buffer.length -= 1; gcli_sv value = gcli_sv_chop_until(&input->buffer, '\n'); entry->key = gcli_sv_trim(key); entry->value = gcli_sv_trim(value); } static gcli_sv parse_section_title(struct config_parser *input) { size_t len = 0; if (input->buffer.length == 0) errx(1, "gcli: %s:%d: unexpected end of input in section title", input->filename, input->line); while (!isspace(input->buffer.data[len]) && input->buffer.data[len] != '{') len++; gcli_sv title = gcli_sv_from_parts(input->buffer.data, len); input->buffer.data += len; input->buffer.length -= len; skip_ws_and_comments(input); if (input->buffer.length == 0) errx(1, "gcli: %s:%d: unexpected end of input", input->filename, input->line); if (input->buffer.data[0] != '{') errx(1, "gcli: %s:%d: expected '{'", input->filename, input->line); input->buffer.length -= 1; input->buffer.data += 1; skip_ws_and_comments(input); return title; } static void parse_config_section(struct gcli_config *cfg, struct config_parser *input) { struct gcli_config_section *section = NULL; section = calloc(1, sizeof(*section)); TAILQ_INSERT_TAIL(&cfg->sections, section, next); section->title = parse_section_title(input); section->entries = (struct gcli_config_entries) TAILQ_HEAD_INITIALIZER(section->entries); while (input->buffer.length > 0 && input->buffer.data[0] != '}') { skip_ws_and_comments(input); parse_section_entry(input, section); skip_ws_and_comments(input); } if (input->buffer.length == 0) errx(1, "gcli: %s:%d: missing '}' before end of file", input->filename, input->line); input->buffer.length -= 1; input->buffer.data += 1; } static void parse_config_file(struct gcli_config *cfg, struct config_parser *input) { skip_ws_and_comments(input); while (input->buffer.length > 0) { parse_config_section(cfg, input); skip_ws_and_comments(input); } } /** * Try to load up the local config file if it exists. If we succeed, * return 0. Otherwise return -1. */ static struct gcli_config * ensure_config(struct gcli_ctx *ctx) { struct gcli_config *cfg = ctx_config(ctx); char *file_path = NULL; struct config_parser parser = {0}; if (cfg->inited) return cfg; cfg->inited = true; file_path = getenv("XDG_CONFIG_HOME"); if (!file_path) { file_path = getenv("HOME"); if (!file_path) { gcli_warnx(ctx, "Neither XDG_CONFIG_HOME nor HOME set in env"); return cfg; } /* * Code duplication to avoid leaking pointers */ file_path = gcli_asprintf("%s/.config/gcli/config", file_path); } else { file_path = gcli_asprintf("%s/gcli/config", file_path); } if (access(file_path, R_OK) < 0) { gcli_warn(ctx, "gcli: cannot access config file at %s", file_path); return cfg; } int len = gcli_read_file(file_path, &cfg->file_content); if (len < 0) err(1, "gcli: unable to open config file"); cfg->buffer = gcli_sv_from_parts(cfg->file_content, len); cfg->buffer = gcli_sv_trim_front(cfg->buffer); parser.buffer = cfg->buffer; parser.line = 1; parser.filename = file_path; parse_config_file(cfg, &parser); free((void *)file_path); return cfg; } /** Check input for a value that indicates yes/true */ static int string_means_true(char const *const tmp) { size_t tmplen = strlen(tmp) + 1; char *tmp_lower = malloc(tmplen); strncpy(tmp_lower, tmp, tmplen); for (size_t i = 0; i < tmplen - 1; ++i) { tmp_lower[i] = tolower(tmp_lower[i]); } int is_yes = strcmp(tmp_lower, "1") == 0 || strcmp(tmp_lower, "yes") == 0 || strcmp(tmp_lower, "true") == 0; free(tmp_lower); return is_yes; } static bool string_means_false(char const *const tmp) { return !string_means_true(tmp); } /* readenv: Read values of environment variables and pre-populate the * config structure. */ static void readenv(struct gcli_config *cfg) { char *tmp; /* A default override account. Can be overridden again by * specifying -a */ if ((tmp = getenv("GCLI_ACCOUNT"))) cfg->override_default_account = tmp; /* NO_COLOR: https://no-color.org/ * * Note: the example implementation code on the website is * semantically buggy as it just checks for the variable being set * to ANYTHING. If you set it to 0 to indicate that you want * colours it will still disable colour output. This explicitly * checks the value of the variable if it is set. I purposefully * violate the definition to get expected and sane behaviour. */ tmp = getenv("NO_COLOR"); if (tmp && tmp[0] != '\0') cfg->colours_disabled = string_means_true(tmp); if ((tmp = getenv("GCLI_NOSPINNER"))) cfg->no_spinner = string_means_true(tmp); if ((tmp = getenv("GCLI_RENDER_MARKDOWN"))) cfg->no_markdown = string_means_false(tmp); if ((tmp = getenv("GCLI_ENABLE_EXPERIMENTAL"))) cfg->enable_experimental = string_means_true(tmp); } int gcli_config_init_ctx(struct gcli_ctx *ctx) { struct cmd_ctx *cctx = calloc(1, sizeof(*cctx)); gcli_set_userdata(ctx, cctx); cctx->config.sections = (struct gcli_config_sections) TAILQ_HEAD_INITIALIZER(cctx->config.sections); cctx->local_config.entries = (struct gcli_config_entries) TAILQ_HEAD_INITIALIZER(cctx->local_config.entries); return 0; } int gcli_config_parse_args(struct gcli_ctx *ctx, int *argc, char ***argv) { /* These are the very first options passed to the gcli command * itself. It is the first ever getopt call we do to parse any * arguments. Only global options that do not alter subcommand * specific behaviour should be accepted here. */ struct gcli_config *cfg = ctx_config(ctx); int ch; const struct option options[] = { { .name = "account", .has_arg = required_argument, .flag = NULL, .val = 'a' }, { .name = "remote", .has_arg = required_argument, .flag = NULL, .val = 'r' }, { .name = "colours", .has_arg = no_argument, .flag = &cfg->colours_disabled, .val = 0 }, { .name = "no-spinner", .has_arg = no_argument, .flag = &cfg->no_spinner, .val = 1 }, { .name = "no-markdown", .has_arg = no_argument, .flag = &cfg->no_markdown, .val = 1 }, { .name = "type", .has_arg = required_argument, .flag = NULL, .val = 't' }, { .name = "quiet", .has_arg = no_argument, .flag = NULL, .val = 'q' }, { .name = "verbose", .has_arg = no_argument, .flag = NULL, .val = 'v' }, { .name = "version", .has_arg = no_argument, .flag = NULL, .val = 'V' }, {0}, }; /* by default we are not verbose */ gcli_setverbosity(g_clictx, GCLI_VERBOSITY_NORMAL); /* Before we parse options, invalidate the override type so it * doesn't get confused later */ cfg->override_forgetype = -1; /* Start off by pre-populating the config structure */ readenv(cfg); while ((ch = getopt_long(*argc, *argv, "+a:r:cqvt:V", options, NULL)) != -1) { switch (ch) { case 'a': { cfg->override_default_account = optarg; } break; case 'r': { cfg->override_remote = optarg; } break; case 'c': { cfg->force_colours = 1; } break; case 'q': { gcli_setverbosity(ctx, GCLI_VERBOSITY_QUIET); } break; case 'v': { gcli_setverbosity(ctx, GCLI_VERBOSITY_VERBOSE); } break; case 't': { if (strcmp(optarg, "github") == 0) { cfg->override_forgetype = GCLI_FORGE_GITHUB; } else if (strcmp(optarg, "gitlab") == 0) { cfg->override_forgetype = GCLI_FORGE_GITLAB; } else if (strcmp(optarg, "gitea") == 0) { cfg->override_forgetype = GCLI_FORGE_GITEA; } else if (strcmp(optarg, "bugzilla") == 0) { cfg->override_forgetype = GCLI_FORGE_BUGZILLA; } else { fprintf(stderr, "gcli: error: unknown forge type '%s'. " "Have either github, gitlab or gitea.\n", optarg); return EXIT_FAILURE; } } break; case 'V': { longversion(); /* call exit here because if we return an OK we would continue * running the gcli command. we do not want this as this flag * only ever prints the version and exits. */ exit(EXIT_SUCCESS); } break; case 0: break; case '?': default: return EXIT_FAILURE; } } *argc -= optind; *argv += optind; /* This one is a little odd: We are going to call getopt_long * again. Eventually. But since this is a global variable and the * getopt parser is reusing it, we need to reset it to zero. On * BSDs there is also the optreset variable, but it doesn't exist * on Solaris. I will thus not depend on it as it seems to be * working without it. */ optind = 0; cfg->inited = false; return EXIT_SUCCESS; } static struct gcli_config_section const * find_section(struct gcli_config *cfg, char const *name) { struct gcli_config_section *section; TAILQ_FOREACH(section, &cfg->sections, next) { if (gcli_sv_eq_to(section->title, name)) return section; } return NULL; } struct gcli_config_entries const * gcli_config_get_section_entries(struct gcli_ctx *ctx, char const *section_name) { struct gcli_config_section const *s; struct gcli_config *cfg; cfg = ensure_config(ctx); s = find_section(cfg, section_name); if (s == NULL) return NULL; else return &s->entries; } gcli_sv gcli_config_find_by_key(struct gcli_ctx *ctx, char const *section_name, char const *key) { struct gcli_config_entry *entry; struct gcli_config *cfg = ensure_config(ctx); struct gcli_config_section const *const section = find_section(cfg, section_name); if (!section) { gcli_warnx(ctx, "gcli: no config section with name '%s'", section_name); return SV_NULL; } TAILQ_FOREACH(entry, §ion->entries, next) { if (gcli_sv_eq_to(entry->key, key)) return entry->value; } return SV_NULL; } static gcli_sv gcli_local_config_find_by_key(struct gcli_ctx *ctx, char const *const key) { struct gcli_dotgcli *lcfg = ctx_dotgcli(ctx); struct gcli_config_entry *entry; TAILQ_FOREACH(entry, &lcfg->entries, next) { if (gcli_sv_eq_to(entry->key, key)) return entry->value; } return SV_NULL; } char * gcli_config_get_editor(struct gcli_ctx *ctx) { ensure_config(ctx); return gcli_sv_to_cstr(gcli_config_find_by_key(ctx, "defaults", "editor")); } char * gcli_config_get_pager(struct gcli_ctx *ctx) { ensure_config(ctx); return gcli_sv_to_cstr(gcli_config_find_by_key(ctx, "defaults", "pager")); } char * gcli_config_get_url_open_program(struct gcli_ctx *ctx) { ensure_config(ctx); return gcli_sv_to_cstr( gcli_config_find_by_key(ctx, "defaults", "url-open-program") ); } static char const *const default_account_entry_names[] = { [GCLI_FORGE_GITHUB] = "github-default-account", [GCLI_FORGE_GITLAB] = "gitlab-default-account", [GCLI_FORGE_GITEA] = "gitea-default-account", [GCLI_FORGE_BUGZILLA] = "bugzilla-default-account",}; static char * get_default_account(struct gcli_ctx *ctx, gcli_forge_type ftype) { char const *const defaultname = default_account_entry_names[ftype]; gcli_sv act = gcli_config_find_by_key(ctx, "defaults", defaultname); if (!act.length) return NULL; return gcli_sv_to_cstr(act); } static char * gcli_config_get_account(struct gcli_ctx *ctx) { struct gcli_config *cfg = ctx_config(ctx); gcli_forge_type ftype = gcli_config_get_forge_type(ctx); char *account; if (cfg->override_default_account) { account = strdup(cfg->override_default_account); } else { account = get_default_account(ctx, ftype); } return account; } static char const *const default_urls[] = { [GCLI_FORGE_GITHUB] = "https://api.github.com", [GCLI_FORGE_GITLAB] = "https://gitlab.com/api/v4", [GCLI_FORGE_GITEA] = "https://codeberg.org/api/v1", [GCLI_FORGE_BUGZILLA] = "https://bugs.freebsd.org/bugzilla", }; char * gcli_config_get_apibase(struct gcli_ctx *ctx) { char *acct = gcli_config_get_account(ctx); char *url = NULL; if (acct) { gcli_sv url_sv = {0}; url_sv = gcli_config_find_by_key(ctx, acct, "api-base"); /* https://github.com/herrhotzenplotz/gcli/issues/138 * * Above is correct behaviour. Below is bad/buggy behaviour. * Check for the second (buggy) option apibase and * use it if needed. */ if (gcli_sv_null(url_sv)) url_sv = gcli_config_find_by_key(ctx, acct, "apibase"); if (!gcli_sv_null(url_sv)) url = gcli_sv_to_cstr(url_sv); } if (!url) url = strdup(default_urls[gcli_config_get_forge_type(ctx)]); free(acct); return url; } char * gcli_config_get_account_name(struct gcli_ctx *ctx) { char *account = gcli_config_get_account(ctx); gcli_sv actname; if (!account) return NULL; actname = gcli_config_find_by_key(ctx, account, "account"); free(account); return gcli_sv_to_cstr(actname); } static char * get_account_token(struct gcli_ctx *ctx) { char *account; gcli_sv token; account = gcli_config_get_account(ctx); if (!account) return NULL; token = gcli_config_find_by_key(ctx, account, "token"); free(account); return gcli_sv_to_cstr(token); } char * gcli_config_get_token(struct gcli_ctx *ctx) { ensure_config(ctx); return get_account_token(ctx); } gcli_sv gcli_config_get_upstream(struct gcli_ctx *ctx) { init_local_config(ctx); return gcli_local_config_find_by_key(ctx, "pr.upstream"); } bool gcli_config_pr_inhibit_delete_source_branch(struct gcli_ctx *ctx) { gcli_sv val; init_local_config(ctx); val = gcli_local_config_find_by_key(ctx, "pr.inhibit-delete-source-branch"); return gcli_sv_eq_to(val, "yes"); } void gcli_config_get_upstream_parts(struct gcli_ctx *ctx, gcli_sv *const owner, gcli_sv *const repo) { ensure_config(ctx); gcli_sv upstream = gcli_config_get_upstream(ctx); *owner = gcli_sv_chop_until(&upstream, '/'); /* Sanity check: did we actually reach the '/'? */ if (*upstream.data != '/') errx(1, "gcli: .gcli has invalid upstream format. expected owner/repo"); upstream.data += 1; upstream.length -= 1; *repo = upstream; } gcli_sv gcli_config_get_base(struct gcli_ctx *ctx) { init_local_config(ctx); return gcli_local_config_find_by_key(ctx, "pr.base"); } gcli_sv gcli_config_get_override_default_account(struct gcli_ctx *ctx) { struct gcli_config *cfg; init_local_config(ctx); cfg = ctx_config(ctx); if (cfg->override_default_account) return SV((char *)cfg->override_default_account); else return SV_NULL; } static gcli_forge_type gcli_config_get_forge_type_internal(struct gcli_ctx *ctx) { struct gcli_config *cfg = ctx_config(ctx); /* Hard override */ if (cfg->override_forgetype >= 0) return cfg->override_forgetype; ensure_config(ctx); init_local_config(ctx); gcli_sv entry = {0}; if (cfg->override_default_account) { char const *section = cfg->override_default_account; entry = gcli_config_find_by_key(ctx, section, "forge-type"); if (gcli_sv_null(entry)) errx(1, "gcli: error: given default override account not found or " "missing forge-type"); } else { entry = gcli_local_config_find_by_key(ctx, "forge-type"); } if (!gcli_sv_null(entry)) { if (gcli_sv_eq_to(entry, "github")) return GCLI_FORGE_GITHUB; else if (gcli_sv_eq_to(entry, "gitlab")) return GCLI_FORGE_GITLAB; else if (gcli_sv_eq_to(entry, "gitea")) return GCLI_FORGE_GITEA; else if (gcli_sv_eq_to(entry, "bugzilla")) return GCLI_FORGE_BUGZILLA; else errx(1, "gcli: unknown forge type "SV_FMT, SV_ARGS(entry)); } /* As a last resort, try to infer from the git remote */ int const type = gcli_gitconfig_get_forgetype(ctx, cfg->override_remote); if (type < 0) errx(1, "gcli: error: cannot infer forge type. " "use -t to overrride manually."); return type; } gcli_forge_type gcli_config_get_forge_type(struct gcli_ctx *ctx) { gcli_forge_type const result = gcli_config_get_forge_type_internal(ctx); /* print the type if verbose */ if (gcli_be_verbose(ctx)) { static int have_printed_forge_type = 0; static char const *const ftype_name[] = { [GCLI_FORGE_GITHUB] = "GitHub", [GCLI_FORGE_GITLAB] = "GitLab", [GCLI_FORGE_GITEA] = "Gitea", [GCLI_FORGE_BUGZILLA] = "Bugzilla", }; if (!have_printed_forge_type) { have_printed_forge_type = 1; fprintf(stderr, "gcli: info: forge type is %s\n", ftype_name[result]); } } return result; } int gcli_config_get_remote(struct gcli_ctx *ctx, char const **remote) { struct gcli_config *cfg; gcli_forge_type type; int rc; cfg = ensure_config(ctx); if (cfg->override_remote) { *remote = strdup(cfg->override_remote); return 0; } type = gcli_config_get_forge_type(ctx); rc = gcli_gitconfig_get_remote(ctx, type, remote); return rc; } int gcli_config_get_repo(struct gcli_ctx *ctx, char const **const owner, char const **const repo) { gcli_sv upstream = {0}; struct gcli_config *cfg; cfg = ensure_config(ctx); if (cfg->override_remote) { 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) return gcli_error(ctx, "forge types are inconsistent"); } return 0; } if ((upstream = gcli_config_get_upstream(ctx)).length != 0) { gcli_sv const owner_sv = gcli_sv_chop_until(&upstream, '/'); gcli_sv const repo_sv = gcli_sv_from_parts(upstream.data + 1, upstream.length - 1); *owner = gcli_sv_to_cstr(owner_sv); *repo = gcli_sv_to_cstr(repo_sv); return 0; } return gcli_gitconfig_repo_by_remote(ctx, NULL, owner, repo, NULL); } bool gcli_config_have_colours(struct gcli_ctx *ctx) { static bool tested_tty = 0; struct gcli_config *cfg; cfg = ctx_config(ctx); if (cfg->force_colours) return true; if (cfg->colours_disabled) return false; if (tested_tty) return !cfg->colours_disabled; if (isatty(STDOUT_FILENO)) cfg->colours_disabled = false; else cfg->colours_disabled = true; tested_tty = true; return !cfg->colours_disabled; } bool gcli_config_display_progress_spinner(struct gcli_ctx *ctx) { ensure_config(ctx); struct gcli_config *cfg; cfg = ctx_config(ctx); if (cfg->no_spinner) return false; gcli_sv cfg_entry = gcli_config_find_by_key(ctx, "defaults", "disable-spinner"); if (gcli_sv_null(cfg_entry)) return true; if (string_means_true(gcli_sv_to_cstr(cfg_entry))) return false; return true; } bool gcli_config_render_markdown(struct gcli_ctx *ctx) { ensure_config(ctx); struct gcli_config *cfg; cfg = ctx_config(ctx); if (cfg->no_markdown) return false; gcli_sv cfg_entry = gcli_config_find_by_key(ctx, "defaults", "render-markdown"); if (gcli_sv_null(cfg_entry)) return true; if (string_means_false(gcli_sv_to_cstr(cfg_entry))) { cfg->no_markdown = true; return false; } return true; } bool gcli_config_enable_experimental(struct gcli_ctx *ctx) { ensure_config(ctx); struct gcli_config *cfg; cfg = ctx_config(ctx); if (cfg->enable_experimental) return true; gcli_sv cfg_entry = gcli_config_find_by_key(ctx, "defaults", "enable-experimental"); if (gcli_sv_null(cfg_entry)) return false; return string_means_true(gcli_sv_to_cstr(cfg_entry)); } gcli-2.9.1/src/cmd/colour.c000066400000000000000000000123671507017207500154570ustar00rootroot00000000000000/* * Copyright 2022-2025 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 static struct { uint32_t code; char *sequence; } colour_table[1024]; static size_t colour_table_size; static void clean_colour_table(void) { for (size_t i = 0; i < colour_table_size; ++i) free(colour_table[i].sequence); } static char const * colour_cache_lookup(uint32_t const code) { for (size_t i = 0; i < colour_table_size; ++i) { if (colour_table[i].code == code) return colour_table[i].sequence; } return NULL; } static void colour_cache_insert(uint32_t const code, char *sequence) { colour_table[colour_table_size].code = code; colour_table[colour_table_size].sequence = sequence; colour_table_size++; } char const * gcli_setcolour256(uint32_t const code) { char *result = NULL; char const *oldresult = NULL; if (!gcli_config_have_colours(g_clictx)) return ""; if (colour_table_size == 0) atexit(clean_colour_table); oldresult = colour_cache_lookup(code); if (oldresult) return oldresult; result = gcli_asprintf("\033[48;2;%02d;%02d;%02dm", (code & 0xFF000000) >> 24, (code & 0x00FF0000) >> 16, (code & 0x0000FF00) >> 8); colour_cache_insert(code, result); return result; } const char * gcli_resetcolour(void) { if (!gcli_config_have_colours(g_clictx)) return ""; return "\033[m"; } const char * gcli_setcolour(int code) { if (!gcli_config_have_colours(g_clictx)) return ""; switch (code) { case GCLI_COLOR_BLACK: return "\033[30m"; case GCLI_COLOR_RED: return "\033[31m"; case GCLI_COLOR_GREEN: return "\033[32m"; case GCLI_COLOR_YELLOW: return "\033[33m"; case GCLI_COLOR_BLUE: return "\033[34m"; case GCLI_COLOR_MAGENTA: return "\033[35m"; case GCLI_COLOR_CYAN: return "\033[36m"; case GCLI_COLOR_WHITE: return "\033[37m"; case GCLI_COLOR_DEFAULT: return "\033[39m"; default: gcli_notreached; } return NULL; } char const * gcli_setbold(void) { if (!gcli_config_have_colours(g_clictx)) return ""; else return "\033[1m"; } char const * gcli_resetbold(void) { if (!gcli_config_have_colours(g_clictx)) return ""; else return "\033[22m"; } char const * gcli_state_colour_str(char const *it) { if (it) return gcli_state_colour_sv(SV((char *)it)); else return ""; } static const struct { char const *name; int code; } state_colour_table[] = { { .name = "open", .code = GCLI_COLOR_GREEN }, { .name = "Open", .code = GCLI_COLOR_GREEN }, { .name = "active", .code = GCLI_COLOR_GREEN }, { .name = "success", .code = GCLI_COLOR_GREEN }, { .name = "APPROVED", .code = GCLI_COLOR_GREEN }, { .name = "merged", .code = GCLI_COLOR_MAGENTA }, { .name = "closed", .code = GCLI_COLOR_RED }, { .name = "Closed", .code = GCLI_COLOR_RED }, { .name = "failed", .code = GCLI_COLOR_RED }, { .name = "canceled", .code = GCLI_COLOR_RED }, /* orthography has left the channel */ { .name = "failure", .code = GCLI_COLOR_RED }, { .name = "running", .code = GCLI_COLOR_BLUE }, { .name = "created", .code = GCLI_COLOR_BLUE }, { .name = "New", .code = GCLI_COLOR_BLUE }, { .name = "COMMENTED", .code = GCLI_COLOR_BLUE }, { .name = "pending", .code = GCLI_COLOR_CYAN }, { .name = "In Progress", .code = GCLI_COLOR_CYAN }, { .name = "CHANGES_REQUESTED", .code = GCLI_COLOR_RED }, }; char const * gcli_state_colour_sv(gcli_sv const state) { if (!gcli_sv_null(state)) { for (size_t i = 0; i < ARRAY_SIZE(state_colour_table); ++i) { if (gcli_sv_has_prefix(state, state_colour_table[i].name)) return gcli_setcolour(state_colour_table[i].code); } } return gcli_setcolour(GCLI_COLOR_DEFAULT); } gcli-2.9.1/src/cmd/comment.c000066400000000000000000000201171507017207500156060ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include #include #include #include #include #include static void usage(void) { fprintf(stderr, "usage: gcli comment [-o owner -r repo] [-p pr | -i issue] [-y]\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -o owner The repository owner\n"); fprintf(stderr, " -r repo The repository name\n"); fprintf(stderr, " -p pr PR id to comment under\n"); fprintf(stderr, " -i issue issue id to comment under\n"); fprintf(stderr, " -R comment-id Reply to the comment with the given ID\n"); fprintf(stderr, " -y Do not ask for confirmation\n"); version(); copyright(); } struct submit_ctx { struct gcli_submit_comment_opts opts; struct gcli_comment reply_comment; }; void gcli_print_prefixed(FILE *f, char const *const text, char const *const prefix) { char const *bol = text; while (bol) { char const *const eol = strchr(bol, '\n'); size_t const len = eol ? (size_t)(eol - bol) : strlen(bol); fprintf(f, "%s%.*s\n", prefix, (int)len, bol); if (!eol) break; bol = eol + 1; } } static void comment_init(struct gcli_ctx *ctx, FILE *f, void *_data) { struct submit_ctx *sctx = _data; const char *target_type = NULL; switch (sctx->opts.target_type) { case ISSUE_COMMENT: target_type = "issue"; break; case PR_COMMENT: { switch (gcli_config_get_forge_type(ctx)) { case GCLI_FORGE_GITEA: case GCLI_FORGE_GITHUB: target_type = "Pull Request"; break; case GCLI_FORGE_GITLAB: target_type = "Merge Request"; break; case GCLI_FORGE_BUGZILLA: /* FIXME think about this one */ assert(0 && "unreachable"); break; } } break; } /* In case we reply to a comment, put the comment prefixed with * '> ' into the file first. */ if (sctx->reply_comment.body) gcli_print_prefixed(f, sctx->reply_comment.body, "> "); fprintf( f, "! Enter your comment above, save and exit.\n" "! All lines with a leading '!' are discarded and will not\n" "! appear in your comment.\n" "!\n" "! vim: ft=markdown\n"); /* XXX */ if (sctx->opts.target.kind == GCLI_PATH_DEFAULT) { fprintf(f, "! COMMENT IN : %s/%s %s #%"PRIid"\n", sctx->opts.target.as_default.owner, sctx->opts.target.as_default.repo, target_type, sctx->opts.target.as_default.id); } } static char * gcli_comment_get_message(struct submit_ctx *info) { return gcli_editor_get_user_message(g_clictx, comment_init, info); } static int comment_submit(struct submit_ctx *sctx, int always_yes) { int rc = 0; char *message; message = gcli_comment_get_message(sctx); sctx->opts.message = message; if (message == NULL) errx(1, "gcli: empty message. aborting."); fprintf(stdout, "You will be commenting the following:\n"); gcli_pretty_print(sctx->opts.message, 4, 80, stdout); if (!always_yes) { if (!gcli_yesno("Is this okay?")) errx(1, "Aborted by user"); } rc = gcli_comment_submit(g_clictx, &sctx->opts); free(message); sctx->opts.message = NULL; return rc; } int gcli_issue_comments(struct gcli_path const *const path) { struct gcli_comment_list list = {0}; int rc = 0; rc = gcli_get_issue_comments(g_clictx, path, &list); if (rc < 0) return rc; gcli_print_comment_list(&list); gcli_comments_free(&list); return rc; } int gcli_pull_comments(struct gcli_path const *const pull_path) { struct gcli_comment_list list = {0}; int rc = 0; rc = gcli_get_pull_comments(g_clictx, pull_path, &list); if (rc < 0) return rc; gcli_print_comment_list(&list); gcli_comments_free(&list); return rc; } void gcli_print_comment_list(struct gcli_comment_list const *const list) { for (size_t i = 0; i < list->comments_size; ++i) { int rc = 0; char *date = NULL; rc = gcli_format_as_localtime(g_clictx, list->comments[i].date, &date); if (rc < 0) err(1, "gcli: error: couldn't format timestamp"); printf("AUTHOR : %s%s%s\n" "DATE : %s\n" "ID : %"PRIid"\n", gcli_setbold(), list->comments[i].author, gcli_resetbold(), date, list->comments[i].id); gcli_pretty_print(list->comments[i].body, 9, 80, stdout); putchar('\n'); free(date); } } int subcommand_comment(int argc, char *argv[]) { int ch, rc = 0; struct submit_ctx sctx = {0}; bool always_yes = false; gcli_id reply_to_id = 0; struct option const options[] = { { .name = "yes", .has_arg = no_argument, .flag = NULL, .val = 'y' }, { .name = "repo", .has_arg = required_argument, .flag = NULL, .val = 'r' }, { .name = "owner", .has_arg = required_argument, .flag = NULL, .val = 'o' }, { .name = "issue", .has_arg = required_argument, .flag = NULL, .val = 'i' }, { .name = "pull", .has_arg = required_argument, .flag = NULL, .val = 'p' }, { .name = "in-reply-to", .has_arg = required_argument, .flag = NULL, .val = 'R' }, {0}, }; always_yes = gcli_cmd_should_do_always_yes(); while ((ch = getopt_long(argc, argv, "yr:o:i:p:R:", options, NULL)) != -1) { switch (ch) { case 'r': sctx.opts.target.as_default.repo = optarg; break; case 'o': sctx.opts.target.as_default.owner = optarg; break; case 'p': sctx.opts.target_type = PR_COMMENT; goto parse_target_id; case 'i': sctx.opts.target_type = ISSUE_COMMENT; parse_target_id: { char *endptr; sctx.opts.target.as_default.id = strtoul(optarg, &endptr, 10); if (endptr != optarg + strlen(optarg)) err(1, "gcli: error: Cannot parse issue/PR number"); } break; case 'y': always_yes = true; break; case 'R': { char *endptr = NULL; reply_to_id = strtoul(optarg, &endptr, 10); if (endptr != optarg + strlen(optarg)) err(1, "gcli: error: cannot parse comment id"); } break; default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; check_path(&sctx.opts.target); if (!sctx.opts.target.as_default.id) { fprintf(stderr, "gcli: error: missing issue/PR number (use -i/-p)\n"); usage(); return EXIT_FAILURE; } if (reply_to_id) { rc = gcli_get_comment(g_clictx, &sctx.opts.target, sctx.opts.target_type, reply_to_id, &sctx.reply_comment); if (rc < 0) { errx(1, "gcli: error: failed to fetch comment for reply: %s", gcli_get_error(g_clictx)); } } rc = comment_submit(&sctx, always_yes); gcli_comment_free(&sctx.reply_comment); if (rc < 0) errx(1, "gcli: error: failed to submit comment: %s", gcli_get_error(g_clictx)); return EXIT_SUCCESS; } gcli-2.9.1/src/cmd/config.c000066400000000000000000000131761507017207500154200ustar00rootroot00000000000000/* * Copyright 2023 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. */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include #include static void usage(void) { fprintf(stderr, "usage: gcli config ssh\n"); fprintf(stderr, " gcli config ssh add --title some-title --key path/to/key.pub\n"); fprintf(stderr, " gcli config ssh delete id\n"); fprintf(stderr, "\n"); version(); copyright(); } void gcli_sshkeys_print_keys(struct gcli_sshkey_list const *list) { gcli_tbl *tbl; struct gcli_tblcoldef cols[] = { { .name = "ID", .type = GCLI_TBLCOLTYPE_ID, .flags = 0 }, { .name = "CREATED", .type = GCLI_TBLCOLTYPE_TIME_T, .flags = 0 }, { .name = "TITLE", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, }; if (list->keys_size == 0) { printf("No SSH keys\n"); return; } tbl = gcli_tbl_begin(cols, ARRAY_SIZE(cols)); for (size_t i = 0; i < list->keys_size; ++i) { gcli_tbl_add_row(tbl, list->keys[i].id, list->keys[i].created_at, list->keys[i].title); } gcli_tbl_end(tbl); } static int list_sshkeys(void) { struct gcli_sshkey_list list = {0}; if (gcli_sshkeys_get_keys(g_clictx, &list) < 0) { fprintf(stderr, "gcli: error: could not get list of SSH keys\n"); return EXIT_FAILURE; } gcli_sshkeys_print_keys(&list); gcli_sshkeys_free_keys(&list); return 0; } static int add_sshkey(int argc, char *argv[]) { char *title = NULL, *keypath = NULL; int ch; struct option options[] = { { .name = "title", .has_arg = required_argument, .flag = NULL, .val = 't' }, { .name = "key", .has_arg = required_argument, .flag = NULL, .val = 'k' }, { 0 }, }; while ((ch = getopt_long(argc, argv, "+t:k:", options, NULL)) != -1) { switch (ch) { case 't': { title = optarg; } break; case 'k': { keypath = optarg; if (access(keypath, R_OK) < 0) { fprintf(stderr, "gcli: error: cannot access %s: %s\n", keypath, strerror(errno)); return EXIT_FAILURE; } } break; default: { usage(); return EXIT_FAILURE; } break; } } if (title == NULL) { fprintf(stderr, "gcli: error: missing title\n"); usage(); return EXIT_FAILURE; } if (keypath == NULL) { fprintf(stderr, "gcli: error: missing public key path\n"); usage(); return EXIT_FAILURE; } if (gcli_sshkeys_add_key(g_clictx, title, keypath, NULL) < 0) return EXIT_FAILURE; return EXIT_SUCCESS; } static int delete_sshkey(int argc, char *argv[]) { int id; char *endptr; /* skip 'delete' keyword */ --argc; ++argv; if (argc != 1) { fprintf(stderr, "gcli: error: incorrect number of arguments\n"); usage(); return EXIT_FAILURE; } /* parse the id */ id = strtol(argv[0], &endptr, 10); if (endptr != argv[0] + strlen(argv[0])) { fprintf(stderr, "gcli: error: could not parse ID of SSH key to delete\n"); return EXIT_FAILURE; } if (gcli_sshkeys_delete_key(g_clictx, id) < 0) return EXIT_FAILURE; return EXIT_SUCCESS; } static int subcommand_ssh(int argc, char *argv[]) { char *cmdname; if (--argc == 0) return list_sshkeys(); cmdname = *(++argv); if (strcmp(cmdname, "add") == 0) return add_sshkey(argc, argv); else if (strcmp(cmdname, "delete") == 0) return delete_sshkey(argc, argv); fprintf(stderr, "gcli: error: unrecognised subcommand »%s«.\n", cmdname); usage(); return EXIT_FAILURE; } struct subcommand { char const *const name; int (*fn)(int argc, char *argv[]); } subcommands[] = { { .name = "ssh", .fn = subcommand_ssh }, }; int subcommand_config(int argc, char *argv[]) { int ch; struct option options[] = { {0} }; while ((ch = getopt_long(argc, argv, "+", options, NULL)) != -1) { switch (ch) { default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; /* Check if the user gave us at least one option for this * subcommand */ if (argc == 0) { fprintf(stderr, "gcli: error: missing subcommand for config\n"); usage(); return EXIT_FAILURE; } for (size_t i = 0; i < ARRAY_SIZE(subcommands); ++i) { if (strcmp(argv[0], subcommands[i].name) == 0) return subcommands[i].fn(argc, argv); } fprintf(stderr, "gcli: error: unrecognised config subcommand »%s«\n", argv[0]); usage(); return EXIT_FAILURE; } gcli-2.9.1/src/cmd/editor.c000066400000000000000000000073541507017207500154420ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include #include static char * get_env_editor(void) { static char const *const env_vars[] = { "GIT_EDITOR", "VISUAL", "EDITOR", NULL }; size_t i = 0; char *result = NULL; do result = getenv(env_vars[i]); while (!result && env_vars[++i]); return result; } static void edit(struct gcli_ctx *ctx, char const *filename) { char *editor = get_env_editor(); char *env_editor = editor; if (!editor) { editor = gcli_config_get_editor(ctx); if (!editor) errx(1, "I have no editor. Either set editor=... in your config " "file or set the EDITOR environment variable."); } pid_t pid = fork(); if (pid == 0) { if (execlp(editor, editor, filename, NULL) < 0) err(1, "execlp"); } else { int status; if (waitpid(pid, &status, 0) < 0) err(1, "waitpid"); if (!(WIFEXITED(status))) errx(1, "Editor child exited abnormally"); if (WEXITSTATUS(status) != 0) errx(1, "Aborting PR. Editor command exited with code %d", WEXITSTATUS(status)); } if (!env_editor) free(editor); } char * gcli_editor_get_user_message( struct gcli_ctx *ctx, void (*file_initializer)(struct gcli_ctx *, FILE *, void *), void *user_data) { char filename[31] = "/tmp/gcli_message.XXXXXXX\0"; int fd = mkstemp(filename); FILE *file = fdopen(fd, "w"); file_initializer(ctx, file, user_data); fclose(file); edit(ctx, filename); char *file_content = NULL; int len = gcli_read_file(filename, &file_content); if (len < 0) err(1, "read_file"); gcli_sv result = {0}; gcli_sv buffer = gcli_sv_from_parts(file_content, (size_t)len); buffer = gcli_sv_trim_front(buffer); while (buffer.length > 0) { gcli_sv line = gcli_sv_chop_until(&buffer, '\n'); if (buffer.length > 0) { buffer.length -= 1; buffer.data += 1; line.length += 1; } if (line.length > 0 && line.data[0] == '!') continue; result = gcli_sv_append(result, line); } free(file_content); unlink(filename); /* When the input is empty, the data pointer is going to be NULL. * Do not access it in this case. */ if (result.length) result.data[result.length] = '\0'; return result.data; } int gcli_editor_open_file(struct gcli_ctx *ctx, char const *const path) { edit(ctx, path); return 0; } gcli-2.9.1/src/cmd/forks.c000066400000000000000000000166101507017207500152730ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include #include #include #include #include static void usage(void) { fprintf(stderr, "usage: gcli forks create [-o owner -r repo] [-i target] [-y]\n"); fprintf(stderr, " gcli forks [-o owner -r repo] [-n number] [-s] [-y] [delete]\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -o owner The repository owner\n"); fprintf(stderr, " -r repo The repository name\n"); fprintf(stderr, " -i target Name of org or user to create the fork in\n"); fprintf(stderr, " -n number Number of forks to fetch (-1 = everything)\n"); fprintf(stderr, " -s Print (sort) in reverse order\n"); fprintf(stderr, " -y Do not ask for confirmation\n"); fprintf(stderr, "\n"); version(); copyright(); } void gcli_print_forks(enum gcli_output_flags const flags, struct gcli_fork_list const *const list, int const max) { size_t n; gcli_tbl table; struct gcli_tblcoldef cols[] = { { .name = "OWNER", .type = GCLI_TBLCOLTYPE_STRING, .flags = GCLI_TBLCOL_BOLD }, { .name = "DATE", .type = GCLI_TBLCOLTYPE_TIME_T, .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) { puts("No forks"); return; } /* Determine number of items to print */ if (max < 0 || (size_t)(max) > list->forks_size) n = list->forks_size; else n = max; table = gcli_tbl_begin(cols, ARRAY_SIZE(cols)); if (!table) errx(1, "gcli: error: could not initialize table"); if (flags & OUTPUT_SORTED) { for (size_t i = 0; i < n; ++i) { gcli_tbl_add_row(table, list->forks[n-i-1].owner, list->forks[n-i-1].date, list->forks[n-i-1].forks, list->forks[n-i-1].full_name); } } else { for (size_t i = 0; i < n; ++i) { gcli_tbl_add_row(table, list->forks[i].owner, list->forks[i].date, list->forks[i].forks, list->forks[i].full_name); } } gcli_tbl_end(table); } static int subcommand_forks_create(int argc, char *argv[]) { bool always_yes = false; char *in = NULL; int ch; struct gcli_path repo_path = {0}; struct option const options[] = { { .name = "repo", .has_arg = required_argument, .flag = NULL, .val = 'r' }, { .name = "owner", .has_arg = required_argument, .flag = NULL, .val = 'o' }, { .name = "into", .has_arg = required_argument, .flag = NULL, .val = 'i' }, { .name = "yes", .has_arg = no_argument, .flag = NULL, .val = 'y' }, {0}, }; always_yes = gcli_cmd_should_do_always_yes(); while ((ch = getopt_long(argc, argv, "yo:r:i:", options, NULL)) != -1) { switch (ch) { case 'o': repo_path.as_default.owner = optarg; break; case 'r': repo_path.as_default.repo = optarg; break; case 'i': in = optarg; break; case 'y': always_yes = true; break; case '?': default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; check_path(&repo_path); if (gcli_fork_create(g_clictx, &repo_path, in) < 0) errx(1, "gcli: error: failed to fork repository: %s", gcli_get_error(g_clictx)); if (!always_yes) { if (!gcli_yesno("Do you want to add a remote for the fork?")) return EXIT_SUCCESS; } if (!in) { if ((in = gcli_config_get_account_name(g_clictx)) == NULL) { errx(1, "gcli: error: could not fetch account: %s", gcli_get_error(g_clictx)); } } gcli_gitconfig_add_fork_remote(in, repo_path.as_default.repo); return EXIT_SUCCESS; } int subcommand_forks(int argc, char *argv[]) { struct gcli_fork_list forks = {0}; struct gcli_path repo_path = {0}; int ch = 0; int count = 30; bool always_yes = false; enum gcli_output_flags flags = 0; /* detect whether we wanna create a fork */ if (argc > 1 && (strcmp(argv[1], "create") == 0)) { shift(&argc, &argv); return subcommand_forks_create(argc, argv); } struct option const options[] = { { .name = "repo", .has_arg = required_argument, .flag = NULL, .val = 'r' }, { .name = "owner", .has_arg = required_argument, .flag = NULL, .val = 'o' }, { .name = "count", .has_arg = required_argument, .flag = NULL, .val = 'n' }, { .name = "yes", .has_arg = no_argument, .flag = NULL, .val = 'y' }, { .name = "sorted", .has_arg = no_argument, .flag = NULL, .val = 's' }, {0}, }; always_yes = gcli_cmd_should_do_always_yes(); while ((ch = getopt_long(argc, argv, "n:o:r:ys", options, NULL)) != -1) { switch (ch) { case 'o': repo_path.as_default.owner = optarg; break; case 'r': repo_path.as_default.repo = optarg; break; case 'y': always_yes = true; break; case 'n': { char *endptr = NULL; count = strtol(optarg, &endptr, 10); if (endptr != (optarg + strlen(optarg))) err(1, "gcli: error: unable to parse forks count argument"); if (count == 0) errx(1, "gcli: error: forks count must not be zero"); } break; case 's': flags |= OUTPUT_SORTED; break; case '?': default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; check_path(&repo_path); if (argc == 0) { if (gcli_get_forks(g_clictx, &repo_path, count, &forks) < 0) errx(1, "gcli: error: could not get forks: %s", gcli_get_error(g_clictx)); gcli_print_forks(flags, &forks, count); gcli_forks_free(&forks); return EXIT_SUCCESS; } for (size_t i = 0; i < (size_t)argc; ++i) { char const *action = argv[i]; if (strcmp(action, "delete") == 0) { delete_repo(always_yes, &repo_path); } else { fprintf(stderr, "gcli: error: forks: unknown action '%s'\n", action); } } return EXIT_SUCCESS; } gcli-2.9.1/src/cmd/gcli.c000066400000000000000000000252131507017207500150640ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #ifdef HAVE_GETOPT_h #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static void usage(void); static int subcommand_version(int argc, char *argv[]) { (void) argc; (void) argv; longversion(); copyright(); return EXIT_SUCCESS; } static struct subcommand { char const *cmd_name; char const *docstring; int (*fn)(int, char **); } default_subcommands[] = { { .cmd_name = "ci", .fn = subcommand_ci, .docstring = "GitHub CI status info" }, { .cmd_name = "comment", .fn = subcommand_comment, .docstring = "Comment under issues and PRs" }, { .cmd_name = "config", .fn = subcommand_config, .docstring = "Configure forges" }, { .cmd_name = "forks", .fn = subcommand_forks, .docstring = "Create, delete and list repository forks" }, { .cmd_name = "gists", .fn = subcommand_gists, .docstring = "Create, fetch and list GitHub Gists" }, { .cmd_name = "issues", .fn = subcommand_issues, .docstring = "Manage issues" }, { .cmd_name = "labels", .fn = subcommand_labels, .docstring = "Manage issue and PR labels" }, { .cmd_name = "milestones", .fn = subcommand_milestones, .docstring = "Milestone handling" }, { .cmd_name = "pipelines", .fn = subcommand_pipelines, .docstring = "GitLab CI management" }, { .cmd_name = "pulls", .fn = subcommand_pulls, .docstring = "Create, view and manage PRs" }, { .cmd_name = "releases", .fn = subcommand_releases, .docstring = "Manage releases of repositories" }, { .cmd_name = "repos", .fn = subcommand_repos, .docstring = "Remote Repository management" }, { .cmd_name = "snippets", .fn = subcommand_snippets, .docstring = "Fetch and list GitLab snippets" }, { .cmd_name = "status", .fn = subcommand_status, .docstring = "General user status and notifications" }, { .cmd_name = "attachments", .fn = subcommand_attachments, .docstring = "Bugzilla Attachments management" }, { .cmd_name = "api", .fn = subcommand_api, .docstring = "Fetch plain JSON info from an API (for debugging purposes)" }, { .cmd_name = "version", .fn = subcommand_version, .docstring = "Print version" }, }; static struct subcommand *subcommands = NULL; static size_t subcommands_size = 0; static void usage(void) { fprintf(stderr, "usage: gcli [options] subcommand\n\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -a account Use the configured account instead of inferring it\n"); fprintf(stderr, " -r remote Infer account from the given git remote\n"); fprintf(stderr, " -t type Force the account type:\n"); fprintf(stderr, " - github (default: github.com)\n"); fprintf(stderr, " - gitlab (default: gitlab.com)\n"); fprintf(stderr, " - gitea (default: codeberg.org)\n"); fprintf(stderr, " - bugzilla (default: bugs.freebsd.org)\n"); fprintf(stderr, " -c Force colour and text formatting\n"); fprintf(stderr, " --no-spinner Disable the animated spinner\n"); fprintf(stderr, " --no-markdown Disable markdown rendering\n"); fprintf(stderr, " -q Be quiet. (Not implemented yet)\n"); fprintf(stderr, " -v Be verbose\n"); fprintf(stderr, " -V | --version Print version and exit\n\n"); fprintf(stderr, "SUBCOMMANDS:\n"); for (size_t i = 0; i < subcommands_size; ++i) { fprintf(stderr, " %-15.15s %s\n", subcommands[i].cmd_name, subcommands[i].docstring); } fprintf(stderr, "\n"); version(); copyright(); } /** The CMD global gcli context */ struct gcli_ctx *g_clictx = NULL; static void gcli_progress_func(bool const done) { char spinner[] = "|/-\\"; static size_t const spinner_elems = sizeof(spinner) / sizeof(*spinner); static int spinner_idx = 0; static int have_checked_stderr = 0, stderr_is_tty = 1; /* Check if stderr is a tty */ if (!have_checked_stderr) { stderr_is_tty = isatty(STDERR_FILENO); have_checked_stderr = 1; } if (!stderr_is_tty) return; /* Clear out the line when done */ if (done) { fprintf(stderr, " \r"); } else { fprintf(stderr, "Wait... %c\r", spinner[spinner_idx]); spinner_idx = (spinner_idx + 1) % (spinner_elems - 1); } } /* Abbreviated form matching: * * - we presort the subcommands array alphabetised * - then we can simply match by prefix */ static int subcommand_compare(void const *s1, void const *s2) { struct subcommand const *sc1 = s1; struct subcommand const *sc2 = s2; return strcmp(sc1->cmd_name, sc2->cmd_name); } static void presort_subcommands(void) { qsort(subcommands, subcommands_size, sizeof(*subcommands), subcommand_compare); } static bool is_unique_match(size_t const idx, char const *const name, size_t const name_len) { /* Last match is always unique */ if (idx + 1 == subcommands_size) return true; for (size_t i = idx + 1; i < subcommands_size; ++i) { if (strncmp(name, subcommands[i].cmd_name, name_len)) return true; /* doesn't match. meaning this one is unique. */ else break; /* we found a duplicate prefix. */ } fprintf(stderr, "gcli: error: %s: subcommand is ambiguous. could be one of:\n", name); /* List until either the end or until we don't match any more prefixes */ for (size_t i = idx; i < subcommands_size; ++i) { if (strncmp(name, subcommands[i].cmd_name, name_len)) break; fprintf(stderr, " - %-13.13s %s\n", subcommands[i].cmd_name, subcommands[i].docstring); } fprintf(stderr, "\n"); return false; } enum { LOOKUP_NOSUCHCMD = 1, LOOKUP_AMBIGUOUS, }; static struct subcommand const * find_subcommand(char const *const name, int *error) { size_t const name_len = strlen(name); for (size_t i = 0; i < subcommands_size; ++i) { if (strncmp(subcommands[i].cmd_name, name, name_len) == 0) { /* At least the prefix matches. Check that it is a unique match. */ if (!is_unique_match(i, name, name_len)) { if (error) *error = LOOKUP_AMBIGUOUS; return NULL; } return subcommands + i; } } /* no match */ fprintf(stderr, "gcli: error: %s: no such subcommand\n", name); if (error) *error = LOOKUP_NOSUCHCMD; return NULL; } static void add_subcommand_alias(char const *alias_name, char const *alias_for) { char *docstring; struct subcommand const *old_sc; struct subcommand *new_sc; int (*old_fn)(int, char **); old_sc = find_subcommand(alias_for, NULL); if (old_sc == NULL) { fprintf(stderr, "gcli: note: this error occured while defining the alias »%s«\n", alias_name); exit(EXIT_FAILURE); } old_fn = old_sc->fn; docstring = gcli_asprintf("Alias for %s", alias_for); subcommands = realloc(subcommands, (subcommands_size + 1) * sizeof(*subcommands)); /* Copy in data */ new_sc = &subcommands[subcommands_size++]; new_sc->cmd_name = alias_name; new_sc->fn = old_fn; new_sc->docstring = docstring; } static void install_aliases(void) { struct gcli_config_entries const *entries; struct gcli_config_entry const *entry; add_subcommand_alias("pr", "pulls"); /* Search for aliases in the user config file */ entries = gcli_config_get_section_entries(g_clictx, "aliases"); if (!entries) return; TAILQ_FOREACH(entry, entries, next) { char *alias_name, *alias_for; alias_name = gcli_sv_to_cstr(entry->key); alias_for = gcli_sv_to_cstr(entry->value); add_subcommand_alias(alias_name, alias_for); free(alias_for); } } static void setup_subcommand_table(void) { subcommands = calloc(ARRAY_SIZE(default_subcommands), sizeof(*subcommands)); memcpy(subcommands, default_subcommands, sizeof(default_subcommands)); subcommands_size = ARRAY_SIZE(default_subcommands); } int main(int argc, char *argv[]) { char const *errmsg; struct subcommand const *sc; int error_reason; errmsg = gcli_init(&g_clictx, gcli_config_get_forge_type, gcli_config_get_token, gcli_config_get_apibase); if (errmsg) errx(1, "gcli: error: %s", errmsg); if (gcli_config_init_ctx(g_clictx) < 0) errx(1, "gcli: error: failed to init context: %s", gcli_get_error(g_clictx)); /* Initial setup */ setup_subcommand_table(); /* Install subcommand aliases into subcommand table */ install_aliases(); /* Sorts the subcommands array alphabatically */ presort_subcommands(); /* Parse first arguments */ if (gcli_config_parse_args(g_clictx, &argc, &argv)) { usage(); return EXIT_FAILURE; } /* Make sure we have a subcommand */ if (argc == 0) { fprintf(stderr, "gcli: error: missing subcommand\n"); usage(); return EXIT_FAILURE; } /* Search for the subcommand */ sc = find_subcommand(argv[0], &error_reason); if (sc == NULL) { if (error_reason == LOOKUP_AMBIGUOUS) version(); else usage(); return EXIT_FAILURE; } if(gcli_config_display_progress_spinner(g_clictx)) gcli_set_progress_func(g_clictx, gcli_progress_func); return sc->fn(argc, argv); } gcli-2.9.1/src/cmd/gists.c000066400000000000000000000261601507017207500153010ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include #include #include #include #include #include #include static void usage(void) { fprintf(stderr, "usage: gcli gists [-u user] [-n number] [-s]\n"); fprintf(stderr, " gcli gists create [-d description] [-f file] name\n"); fprintf(stderr, " gcli gists delete [-y] id\n"); fprintf(stderr, " gcli gists get id name\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -d description Description of the gist\n"); fprintf(stderr, " -f file Path to file to upload (otherwise stdin)\n"); fprintf(stderr, " -l Print a long list instead of a short table\n"); fprintf(stderr, " -n number Number of gists to fetch\n"); fprintf(stderr, " -s Print (sort) in reverse order\n"); fprintf(stderr, " -u user User for whom to list gists\n"); fprintf(stderr, " -y Do not ask for confirmation\n"); fprintf(stderr, "\n"); version(); copyright(); } static char const * human_readable_size(size_t const s) { if (s < 1024) return gcli_asprintf("%zu B", s); if (s < 1024 * 1024) return gcli_asprintf("%zu KiB", s / 1024); if (s < 1024 * 1024 * 1024) return gcli_asprintf("%zu MiB", s / (1024 * 1024)); return gcli_asprintf("%zu GiB", s / (1024 * 1024 * 1024)); } static inline char const * language_fmt(char const *it) { if (it) return it; else return "Unknown"; } static void print_gist_file(struct gcli_gist_file const *const file) { printf(" • %-15.15s %-8.8s %-s\n", language_fmt(file->language), human_readable_size(file->size), file->filename); } static void print_gist(enum gcli_output_flags const flags, struct gcli_gist const *const gist) { (void) flags; printf(" ID : %s%s%s\n" "OWNER : %s%s%s\n" "DESCR : %s\n" " DATE : %s\n" " URL : %s\n" " PULL : %s\n", gcli_setcolour(GCLI_COLOR_YELLOW), gist->id, gcli_resetcolour(), gcli_setbold(), gist->owner, gcli_resetbold(), gist->description, gist->date, gist->url, gist->git_pull_url); printf("FILES : %-15.15s %-8.8s %-s\n", "LANGUAGE", "SIZE", "FILENAME"); for (size_t i = 0; i < gist->files_size; ++i) print_gist_file(&gist->files[i]); printf("\n"); } static void gcli_print_gists_long(enum gcli_output_flags const flags, struct gcli_gist_list const *const list, int const max) { size_t n; if (max < 0 || (size_t)(max) > list->gists_size) n = list->gists_size; else n = max; if (flags & OUTPUT_SORTED) { for (size_t i = 0; i < n; ++i) print_gist(flags, &list->gists[n-i-1]); } else { for (size_t i = 0; i < n; ++i) print_gist(flags, &list->gists[i]); } } static void gcli_print_gists_short(enum gcli_output_flags const flags, struct gcli_gist_list const *const list, int const max) { size_t n; gcli_tbl table; struct gcli_tblcoldef cols[] = { { .name = "ID", .type = GCLI_TBLCOLTYPE_STRING, .flags = GCLI_TBLCOL_COLOUREXPL }, { .name = "OWNER", .type = GCLI_TBLCOLTYPE_STRING, .flags = GCLI_TBLCOL_BOLD }, { .name = "DATE", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "FILES", .type = GCLI_TBLCOLTYPE_INT, .flags = GCLI_TBLCOL_JUSTIFYR }, { .name = "DESCRIPTION", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, }; if (max < 0 || (size_t)(max) > list->gists_size) n = list->gists_size; else n = max; table = gcli_tbl_begin(cols, ARRAY_SIZE(cols)); if (!table) errx(1, "gcli: error: could not init table"); if (flags & OUTPUT_SORTED) { for (size_t i = 0; i < n; ++i) { gcli_tbl_add_row(table, GCLI_COLOR_YELLOW, list->gists[n-i-1].id, list->gists[n-i-1].owner, list->gists[n-i-1].date, (int)list->gists[n-i-1].files_size, /* For safety pass it as int */ list->gists[n-i-1].description); } } else { for (size_t i = 0; i < n; ++i) { gcli_tbl_add_row(table, GCLI_COLOR_YELLOW, list->gists[i].id, list->gists[i].owner, list->gists[i].date, (int)list->gists[i].files_size, list->gists[i].description); } } gcli_tbl_end(table); } void gcli_print_gists(enum gcli_output_flags const flags, struct gcli_gist_list const *const list, int const max) { if (list->gists_size == 0) { puts("No Gists"); return; } if (flags & OUTPUT_LONG) /* if we are in long mode (no pun intended) */ gcli_print_gists_long(flags, list, max); else /* real mode (bad joke, I know) */ gcli_print_gists_short(flags, list, max); } static int subcommand_gist_get(int argc, char *argv[]) { shift(&argc, &argv); /* Discard the *get* */ char const *gist_id = shift(&argc, &argv); char const *file_name = shift(&argc, &argv); struct gcli_gist gist = {0}; struct gcli_gist_file *file = NULL; if (gcli_get_gist(g_clictx, gist_id, &gist) < 0) errx(1, "gcli: error: failed to get gist: %s", gcli_get_error(g_clictx)); for (size_t f = 0; f < gist.files_size; ++f) { if (strcmp(gist.files[f].filename, file_name) == 0) { file = &gist.files[f]; break; } } if (!file) errx(1, "gcli: error: %s: no such file in gist with id %s", file_name, gist_id); if (isatty(STDOUT_FILENO) && (file->size >= 4 * 1024 * 1024)) errx(1, "gcli: error: File is bigger than 4 MiB, refusing to print to stdout."); if (gcli_curl(g_clictx, stdout, file->url, file->type) < 0) errx(1, "gcli: error: failed to fetch gist: %s", gcli_get_error(g_clictx)); gcli_gist_free(&gist); return EXIT_SUCCESS; } static int subcommand_gist_create(int argc, char *argv[]) { char const *file = NULL; int ch; struct gcli_new_gist opts = {0}; struct option const options[] = { { .name = "file", .has_arg = required_argument, .flag = NULL, .val = 'r' }, { .name = "description", .has_arg = required_argument, .flag = NULL, .val = 'd' }, {0}, }; while ((ch = getopt_long(argc, argv, "f:d:", options, NULL)) != -1) { switch (ch) { case 'f': file = optarg; break; case 'd': opts.gist_description = optarg; break; case '?': default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; if (argc != 1) { fprintf(stderr, "gcli: error: missing file name for gist\n"); usage(); return EXIT_FAILURE; } opts.file_name = shift(&argc, &argv); if (file) { if ((opts.file = fopen(file, "r")) == NULL) err(1, "gcli: error: cannot open file"); } else { opts.file = stdin; } if (!opts.gist_description) opts.gist_description = "gcli paste"; if (gcli_create_gist(g_clictx, opts) < 0) errx(1, "gcli: error: failed to create gist: %s", gcli_get_error(g_clictx)); return EXIT_SUCCESS; } static int subcommand_gist_delete(int argc, char *argv[]) { bool always_yes = false; char const *gist_id = NULL; int ch; struct option const options[] = { { .name = "yes", .has_arg = no_argument, .flag = NULL, .val = 'y' }, {0}, }; always_yes = gcli_cmd_should_do_always_yes(); while ((ch = getopt_long(argc, argv, "y", options, NULL)) != -1) { switch (ch) { case 'y': always_yes = true; break; case '?': default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; gist_id = shift(&argc, &argv); if (!always_yes && !gcli_yesno("Are you sure you want to delete this gist?")) errx(1, "gcli: Aborted by user"); gcli_delete_gist(g_clictx, gist_id); return EXIT_SUCCESS; } static struct { char const *name; int (*fn)(int, char **); } gist_subcommands[] = { { .name = "get", .fn = subcommand_gist_get }, { .name = "create", .fn = subcommand_gist_create }, { .name = "delete", .fn = subcommand_gist_delete }, }; int subcommand_gists(int argc, char *argv[]) { char const *user = NULL; enum gcli_output_flags flags = 0; int ch; int count = 30; struct gcli_gist_list gists = {0}; /* Make sure we are looking at a GitHub forge */ if (gcli_config_get_forge_type(g_clictx) != GCLI_FORGE_GITHUB) { errx(1, "gcli: error: The gists subcommand only works for GitHub " "forges. Please use either -a or -t to force using a " "GitHub account."); } for (size_t i = 0; i < ARRAY_SIZE(gist_subcommands); ++i) { if (argc > 1 && strcmp(argv[1], gist_subcommands[i].name) == 0) { argc -= 1; argv += 1; return gist_subcommands[i].fn(argc, argv); } } struct option const options[] = { { .name = "user", .has_arg = required_argument, .flag = NULL, .val = 'u' }, { .name = "count", .has_arg = required_argument, .flag = NULL, .val = 'n' }, { .name = "sorted", .has_arg = no_argument, .flag = NULL, .val = 's' }, { .name = "long", .has_arg = no_argument, .flag = NULL, .val = 'l' }, {0}, }; while ((ch = getopt_long(argc, argv, "lsn:u:", options, NULL)) != -1) { switch (ch) { case 'u': user = optarg; break; case 'n': { char *endptr = NULL; count = strtol(optarg, &endptr, 10); if (endptr != (optarg + strlen(optarg))) err(1, "gists: cannot parse gists count"); } break; case 's': flags |= OUTPUT_SORTED; break; case 'l': flags |= OUTPUT_LONG; break; case '?': default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; if (gcli_get_gists(g_clictx, user, count, &gists) < 0) errx(1, "gcli: error: failed to get gists: %s", gcli_get_error(g_clictx)); gcli_print_gists(flags, &gists, count); gcli_gists_free(&gists); return EXIT_SUCCESS; } gcli-2.9.1/src/cmd/gitconfig.c000066400000000000000000000317301507017207500161200ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include #include #include #include #include #include #include #include #include #include #include #define MAX_REMOTES 64 static struct gcli_gitremote remotes[MAX_REMOTES]; static size_t remotes_size; /* Resolve a worktree .git if needed */ static char * resolve_worktree_gitdir_if_needed(char *dotgit) { struct stat sb = {0}; FILE *f; char *newdir = NULL; if (stat(dotgit, &sb) < 0) err(1, "gcli: stat"); /* Real .git directory */ if (S_ISDIR(sb.st_mode)) return dotgit; f = fopen(dotgit, "r"); if (!f) err(1, "gcli: fopen"); while (!ferror(f) && !feof(f)) { char *key, *value; char buf[256] = {0}; fgets(buf, sizeof buf, f); key = buf; value = strchr(buf, ':'); if (!value) continue; *value++ = '\0'; while (*value == ' ') ++value; if (strcmp(key, "gitdir") == 0) { size_t const len = strlen(value); newdir = strdup(value); /* trim off any potential newline character */ if (newdir[len - 1] == '\n') newdir[len - 1] = '\0'; break; } } if (newdir == NULL) errx(1, "gcli: error: .git is a file but does not contain a gitdir pointer"); fclose(f); /* In newer versions of git there is a file called "commondir" * in .git in a worktree. It contains the path to the real * .git directory */ { char *other_path = gcli_asprintf("%s/%s", newdir, "commondir"); if ((f = fopen(other_path, "r"))) { char buf[256] = {0}; char *tmp = newdir; fgets(buf, sizeof buf, f); size_t const len = strlen(buf); if (buf[len-1] == '\n') buf[len-1] = '\0'; newdir = gcli_asprintf("%s/%s", tmp, buf); free(tmp); fclose(f); } free(other_path); } free(dotgit); return newdir; } /* Search for a file named fname in the .git directory. * * This is ugly code. However, I don't see an easier way to do * this. */ static char * find_file_in_dotgit(char const *fname) { DIR *curr_dir = NULL; struct dirent *ent; char *curr_dir_path; char *dotgit = NULL; char *config_path = NULL; curr_dir_path = getcwd(NULL, 128); if (!curr_dir_path) err(1, "gcli: getcwd"); /* Here we are trying to traverse upwards through the directory * tree, searching for a directory called .git. * Starting point is ".".*/ do { curr_dir = opendir(curr_dir_path); if (!curr_dir) err(1, "gcli: opendir"); /* Read entries of the directory */ while ((ent = readdir(curr_dir))) { if (strcmp(".", ent->d_name) == 0 || strcmp("..", ent->d_name) == 0) continue; /* Is this the .git directory? If so, allocate some memory * to store the path into dotgit and append '/\0' */ if (strcmp(".git", ent->d_name) == 0) { size_t len = strlen(curr_dir_path); dotgit = malloc(len + strlen(ent->d_name) + 2); memcpy(dotgit, curr_dir_path, len); dotgit[len] = '/'; memcpy(dotgit + len + 1, ent->d_name, strlen(ent->d_name)); dotgit[len + 1 + strlen(ent->d_name)] = 0; break; } } /* If we reach this point and dotgit is NULL we couldn't find * the .git directory in the current directory. In this case * we append '..' to the path and resolve it. */ if (!dotgit) { size_t len = strlen(curr_dir_path); char *tmp = malloc(len + sizeof("/..")); memcpy(tmp, curr_dir_path, len); memcpy(tmp + len, "/..", sizeof("/..")); free(curr_dir_path); curr_dir_path = gcli_cmd_realpath(tmp); if (!curr_dir_path) err(1, "gcli: error: realpath at %s", tmp); free(tmp); /* Check if we reached the filesystem root */ if (strcmp("/", curr_dir_path) == 0) { free(curr_dir_path); closedir(curr_dir); gcli_warnx(g_clictx, "not a git repository"); return NULL; } } closedir(curr_dir); } while (dotgit == NULL); free(curr_dir_path); /* In case we are working with git worktrees, the .git might be a * file that contains a pointer to the actual .git directory. Here * we call into a function that resolves this link if needed. */ dotgit = resolve_worktree_gitdir_if_needed(dotgit); /* Now search for the file in the found .git directory */ curr_dir = opendir(dotgit); if (!curr_dir) err(1, "gcli: opendir"); while ((ent = readdir(curr_dir))) { /* skip over . and .. directory entries */ if (strcmp(".", ent->d_name) == 0 || strcmp("..", ent->d_name) == 0) continue; /* We found the config file, put together it's path and return * that */ if (strcmp(fname, ent->d_name) == 0) { int len = strlen(dotgit); config_path = malloc(len + 1 + sizeof(fname)); memcpy(config_path, dotgit, len); config_path[len] = '/'; memcpy(config_path + len + 1, fname, strlen(fname) + 1); closedir(curr_dir); free(dotgit); return config_path; } } errx(1, "gcli: error: .git without a config file"); return NULL; } char * gcli_find_gitconfig(void) { return find_file_in_dotgit("config"); } gcli_sv gcli_gitconfig_get_current_branch(void) { char const *HEAD; char *file_text; gcli_sv buffer; char prefix[] = "ref: refs/heads/"; HEAD = find_file_in_dotgit("HEAD"); if (!HEAD) return SV_NULL; int len = gcli_read_file(HEAD, &file_text); if (len < 0) err(1, "gcli: mmap"); buffer = gcli_sv_from_parts(file_text, len); if (gcli_sv_has_prefix(buffer, prefix)) { buffer.data += sizeof(prefix) - 1; buffer.length -= sizeof(prefix) - 1; return gcli_sv_trim(buffer); } else { free(file_text); return SV_NULL; } } static void parse_remote_url(struct gcli_gitremote *const remote) { char *tmp; int rc = 0; size_t n = 0; struct gcli_url url = {0}; rc = gcli_parse_url(remote->url, &url); if (rc < 0) { fprintf(stderr, "gcli: failed to parse remote url: %s. " "This is probably a bug.\n", remote->url); goto bail; } /* automagic forge type */ if (strcmp(url.host, "github.com") == 0) remote->forge_type = GCLI_FORGE_GITHUB; else if (strcmp(url.host, "gitlab.com") == 0) remote->forge_type = GCLI_FORGE_GITLAB; else if (strcmp(url.host, "codeberg.org") == 0) remote->forge_type = GCLI_FORGE_GITEA; tmp = strrchr(url.path, '/'); if (tmp == NULL) goto bail; remote->owner = gcli_strndup(url.path, tmp - url.path); /* skip over '/' */ tmp += 1; n = strlen(tmp); if (n > 4 && strcmp(tmp + (n - 4), ".git") == 0) n -= 4; remote->repo = gcli_strndup(tmp, n); bail: gcli_url_free(&url); } static void gitconfig_parse_remote(gcli_sv section_title, gcli_sv entry) { gcli_sv remote_name = SV_NULL; /* If there is no remote name, just return and continue with the * next section. I don't exactly know why there even are such * sections and what they are useful for, but ok. */ if (gcli_sv_eq_to(gcli_sv_trim(section_title), "remote")) return; /* the remote name is wrapped in double quotes */ gcli_sv_chop_until(§ion_title, '"'); /* skip the first quote */ section_title.data += 1; section_title.length -= 1; remote_name = gcli_sv_chop_until(§ion_title, '"'); while ((entry = gcli_sv_trim_front(entry)).length > 0) { if (gcli_sv_has_prefix(entry, "url")) { if (remotes_size == MAX_REMOTES) errx(1, "gcli: error: too many remotes"); struct gcli_gitremote *const remote = &remotes[remotes_size++]; remote->name = gcli_sv_to_cstr(remote_name); gcli_sv_chop_until(&entry, '='); entry.data += 1; entry.length -= 1; gcli_sv url = gcli_sv_trim(gcli_sv_chop_until(&entry, '\n')); remote->url = gcli_sv_to_cstr(url); remote->forge_type = -1; parse_remote_url(remote); } else { gcli_sv_chop_until(&entry, '\n'); } } } static void gcli_gitconfig_read_gitconfig(void) { char *path = NULL; gcli_sv buffer = {0}, filebuf = {0}; static int has_read_gitconfig = 0; if (has_read_gitconfig) return; has_read_gitconfig = 1; path = gcli_find_gitconfig(); if (!path) return; filebuf.length = gcli_read_file(path, &filebuf.data); buffer = filebuf; free(path); path = NULL; while (buffer.length > 0) { buffer = gcli_sv_trim_front(buffer); if (buffer.length == 0) break; /* TODO: Git Config files support comments */ if (*buffer.data != '[') errx(1, "gcli: error: invalid git config"); gcli_sv section_title = gcli_sv_chop_until(&buffer, ']'); section_title.length -= 1; section_title.data += 1; buffer.length -= 2; buffer.data += 2; gcli_sv entry = gcli_sv_chop_until(&buffer, '['); if (gcli_sv_has_prefix(section_title, "remote")) { gitconfig_parse_remote(section_title, entry); } else { // @@@: skip section } } free(filebuf.data); filebuf.length = 0; filebuf.data = NULL; } void gcli_gitconfig_add_fork_remote(char const *org, char const *repo) { char remote[64] = {0}; FILE *remote_list = popen("git remote", "r"); if (!remote_list) err(1, "gcli: popen"); /* TODO: Output informational messages */ /* Rename possibly existing origin remote to point at the * upstream */ while (fgets(remote, sizeof(remote), remote_list)) { if (strcmp(remote, "origin\n") == 0) { pid_t pid = 0; if ((pid = fork()) == 0) { printf("[INFO] git remote rename origin upstream\n"); execlp("git", "git", "remote", "rename", "origin", "upstream", NULL); } else if (pid > 0) { int status = 0; waitpid(pid, &status, 0); if (!(WIFEXITED(status) && (WEXITSTATUS(status) == 0))) errx(1, "gcli: git child process failed"); } else { err(1, "gcli: fork"); } break; } } pclose(remote_list); /* Add new remote */ { pid_t pid = 0; if ((pid = fork()) == 0) { char const *remote_url = gcli_asprintf( "git@github.com:%s/%s", org, repo); printf("[INFO] git remote add origin %s\n", remote_url); execlp("git", "git", "remote", "add", "origin", remote_url, NULL); } else if (pid > 0) { int status = 0; waitpid(pid, &status, 0); if (!(WIFEXITED(status) && (WEXITSTATUS(status) == 0))) errx(1, "gcli: git child process failed"); } else { err(1, "gcli: fork"); } } } /** * Return the gcli_forge_type for the given remote or -1 if * unknown */ int gcli_gitconfig_get_forgetype(struct gcli_ctx *ctx, char const *const remote_name) { (void) ctx; gcli_gitconfig_read_gitconfig(); if (remote_name) { for (size_t i = 0; i < remotes_size; ++i) { if (strcmp(remotes[i].name, remote_name) == 0) return remotes[i].forge_type; } } if (!remotes_size) { gcli_warn(ctx, "no remotes to auto-detect forge"); return -1; } return remotes[0].forge_type; } int 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) { for (size_t i = 0; i < remotes_size; ++i) { if (strcmp(remotes[i].name, remote) == 0) { *owner = remotes[i].owner; *repo = remotes[i].repo; if (forge) *forge = remotes[i].forge_type; return 0; } } return gcli_error(ctx, "no such remote: %s", remote); } if (!remotes_size) return gcli_error(ctx, "no remotes to auto-detect forge"); *owner = remotes[0].owner; *repo = remotes[0].repo; if (forge) *forge = remotes[0].forge_type; return 0; } int gcli_gitconfig_get_remote(struct gcli_ctx *ctx, gcli_forge_type const type, char const **remote) { gcli_gitconfig_read_gitconfig(); for (size_t i = 0; i < remotes_size; ++i) { if (remotes[i].forge_type == type) { *remote = remotes[i].url; return 0; } } return gcli_error(ctx, "no suitable remote for forge type"); } gcli-2.9.1/src/cmd/interactive.c000066400000000000000000000141161507017207500164630ustar00rootroot00000000000000/* * 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 #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. 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, ...) { 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 && *deflt) { snprintf(prompt + prompt_len, sizeof(prompt) - prompt_len, " [%s]: ", deflt); } else { strncat(prompt, ": ", sizeof(prompt) - prompt_len - 1); } for (;;) { result = get_input_line(prompt); 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; } static char const * find_pager_program(void) { char const *pager = NULL; pager = getenv("PAGER"); if (pager) return pager; pager = gcli_config_get_pager(g_clictx); if (pager) return pager; /* Use less as a more or less sane default. */ return "less"; } /* Run fn and pipe its output into a suitable pager */ int gcli_cmd_into_pager(int (*fn)(void *), void *data) { pid_t child; child = fork(); if (child < 0) err(1, "gcli: cannot fork"); /* Child process = fork again and run pager */ if (child == 0) { int fds[2] = {0, 0}; if (pipe(fds) < 0) err(1, "gcli: cannot pipe"); pid_t subchild = fork(); if (subchild < 0) err(1, "gcli: cannot fork"); /* In this child we run the function, the parent runs the pager */ if (subchild == 0) { if (dup2(fds[0], STDIN_FILENO) < 0) err(1, "gcli: dup2"); close(fds[1]); char const *pager = find_pager_program(); if (execlp(pager, pager, NULL) < 0) err(1, "gcli: cannot run pager"); } else { int status; close(fds[0]); if (dup2(fds[1], STDOUT_FILENO) < 0) err(1, "gcli: dup2"); fn(data); /* Unref both FDs such that the pipe gets closed. This * indicates to the pager that it has reached EOF */ close(STDOUT_FILENO); close(fds[1]); if (waitpid(subchild, &status, 0) < 0) err(1, "gcli: cannot wait for pager to exit"); exit(0); } } else { /* wait for printer to exit */ int status; if (waitpid(child, &status, 0) < 0) err(1, "gcli: cannot wait for child process"); } return 0; } gcli-2.9.1/src/cmd/issues.c000066400000000000000000000657151507017207500154740ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include #include #include #include #include #include #include #include #include #include #include static void usage(void) { 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"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -o owner The repository owner\n"); fprintf(stderr, " -r repo The repository name\n"); fprintf(stderr, " -y Do not ask for confirmation.\n"); fprintf(stderr, " -A author Only print issues by the given author\n"); fprintf(stderr, " -L label Filter issues by the given label\n"); fprintf(stderr, " -M milestone Filter issues by the given milestone\n"); fprintf(stderr, " -S assignee Filter issues by the given assignee\n"); fprintf(stderr, " -a Fetch everything including closed issues \n"); 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, " -T template Use the given file as a template for the issue\n"); fprintf(stderr, "ACTIONS:\n"); fprintf(stderr, " all Display both status and and op\n"); fprintf(stderr, " status Display status information\n"); fprintf(stderr, " op Display original post\n"); fprintf(stderr, " comments Display comments\n"); fprintf(stderr, " close Close the issue\n"); fprintf(stderr, " reopen Reopen a closed issue\n"); fprintf(stderr, " assign Assign the issue to the given user\n"); fprintf(stderr, " labels ... Add or remove labels:\n"); fprintf(stderr, " add \n"); fprintf(stderr, " remove \n"); fprintf(stderr, " milestone Assign this issue to the given milestone\n"); fprintf(stderr, " milestone -d Clear the assigned milestone of the given issue\n"); fprintf(stderr, " notes Alias for comments\n"); fprintf(stderr, " title Change the title of the issue\n"); fprintf(stderr, " open Open the issue in a web browser\n"); fprintf(stderr, " edit Edit the OP\n"); fprintf(stderr, "\n"); version(); copyright(); } void gcli_print_issues(enum gcli_output_flags const flags, struct gcli_issue_list const *const list, int const max) { int n, pruned = 0; gcli_tbl table; struct gcli_tblcoldef cols[] = { { .name = "NUMBER", .type = GCLI_TBLCOLTYPE_ID, .flags = GCLI_TBLCOL_JUSTIFYR }, { .name = "NOTES", .type = GCLI_TBLCOLTYPE_INT, .flags = GCLI_TBLCOL_JUSTIFYR }, { .name = "STATE", .type = GCLI_TBLCOLTYPE_STRING, .flags = GCLI_TBLCOL_STATECOLOURED }, { .name = "TITLE", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, }; if (list->issues_size == 0) { puts("No issues"); return; } table = gcli_tbl_begin(cols, ARRAY_SIZE(cols)); if (!table) errx(1, "gcli: could not init table printer"); /* Determine the correct number of items to print */ if (max < 0 || (size_t)(max) > list->issues_size) n = list->issues_size; else n = max; /* Iterate depending on the output order */ if (flags & OUTPUT_SORTED) { for (int i = 0; i < n; ++i) { if (!list->issues[n - 1 - 1].is_pr) { gcli_tbl_add_row(table, list->issues[n - i - 1].number, list->issues[n - i - 1].comments, list->issues[n - i - 1].state, list->issues[n - i - 1].title); } else { pruned++; } } } else { for (int i = 0; i < n; ++i) { if (!list->issues[i].is_pr) { gcli_tbl_add_row(table, list->issues[i].number, list->issues[i].comments, list->issues[i].state, list->issues[i].title); } else { pruned++; } } } /* Dump the table */ gcli_tbl_end(table); /* Inform the user that we pruned pull requests from the output */ if (pruned && gcli_getverbosity(g_clictx) != GCLI_VERBOSITY_QUIET) fprintf(stderr, "info: %d pull requests pruned\n", pruned); } void gcli_issue_print_summary(struct gcli_issue const *const it) { gcli_dict dict; uint32_t const quirks = gcli_forge(g_clictx)->issue_quirks; dict = gcli_dict_begin(); gcli_dict_add(dict, "NUMBER", 0, 0, "%"PRIid, it->number); gcli_dict_add(dict, "TITLE", 0, 0, "%s", it->title); gcli_dict_add_timestamp(dict, "CREATED", 0, 0, it->created_at); if ((quirks & GCLI_ISSUE_QUIRKS_PROD_COMP) == 0) { gcli_dict_add(dict, "PRODUCT", 0, 0, "%s", it->product); gcli_dict_add(dict, "COMPONENT", 0, 0, "%s", it->component); } gcli_dict_add(dict, "AUTHOR", GCLI_TBLCOL_BOLD, 0, "%s", it->author); gcli_dict_add(dict, "STATE", GCLI_TBLCOL_STATECOLOURED, 0, "%s", it->state); if ((quirks & GCLI_ISSUE_QUIRKS_URL) == 0 && !gcli_strempty(it->url)) gcli_dict_add(dict, "URL", 0, 0, "%s", it->url); if ((quirks & GCLI_ISSUE_QUIRKS_COMMENTS) == 0) gcli_dict_add(dict, "COMMENTS", 0, 0, "%d", it->comments); if ((quirks & GCLI_ISSUE_QUIRKS_LOCKED) == 0) gcli_dict_add(dict, "LOCKED", 0, 0, "%s", gcli_bool_yesno(it->locked)); if (!gcli_strempty(it->milestone)) gcli_dict_add(dict, "MILESTONE", 0, 0, "%s", it->milestone); if (it->labels_size) { gcli_dict_add_string_list(dict, "LABELS", (char const *const *)it->labels, it->labels_size); } else { gcli_dict_add(dict, "LABELS", 0, 0, "none"); } if (it->assignees_size) { gcli_dict_add_string_list(dict, "ASSIGNEES", (char const *const *)it->assignees, it->assignees_size); } else { gcli_dict_add(dict, "ASSIGNEES", 0, 0, "none"); } /* Dump the dictionary */ gcli_dict_end(dict); } void gcli_issue_print_op(struct gcli_issue const *const it) { if (it->body) gcli_pretty_print(it->body, 4, 80, stdout); } static void issue_init_user_file(struct gcli_ctx *ctx, FILE *stream, void *_opts) { (void) ctx; struct gcli_submit_issue_options *opts = _opts; /* recalled message or template */ if (opts->body) { fputs(opts->body, stream); free(opts->body); opts->body = NULL; } fprintf( stream, "! ISSUE TITLE : %s\n" "! Enter issue description above.\n" "! All lines starting with '!' will be discarded.\n" "!\n" "! vim: ft=markdown\n", opts->title); } static char * gcli_issue_get_user_message(struct gcli_submit_issue_options *opts) { return gcli_editor_get_user_message(g_clictx, issue_init_user_file, opts); } static int create_issue(struct gcli_submit_issue_options *opts, bool always_yes) { int rc; gcli_cmd_recall_message_interactive(&opts->body); opts->body = gcli_issue_get_user_message(opts); printf("The following issue will be created:\n" "\n" "TITLE : %s\n" "OWNER : %s\n" "REPO : %s\n" "MESSAGE :\n", opts->title, opts->owner, opts->repo); if (opts->body) gcli_pretty_print(opts->body, 4, 80, stdout); else puts("No message"); if (!always_yes) { if (!gcli_yesno("Do you want to continue?")) errx(1, "gcli: Submission aborted."); } rc = gcli_issue_submit(g_clictx, opts); 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", GCLI_PROMPT_RESULT_MANDATORY); 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) { char *hd = strdup(optarg); char *key = hd; char *value = NULL; hd = strchr(hd, '='); if (hd == NULL || *hd != '=') { fprintf(stderr, "gcli: -O expects a key-value-pair as key=value\n"); return -1; } *hd++ = '\0'; value = strdup(hd); /* make key and value separate allocations */ gcli_nvlist_append(&opts->extra, key, value); return 0; } static int subcommand_issue_create(int argc, char *argv[]) { int ch; struct gcli_submit_issue_options opts = {0}; bool always_yes = false; if (gcli_nvlist_init(&opts.extra) < 0) { fprintf(stderr, "gcli: failed to init nvlist: %s\n", strerror(errno)); return EXIT_FAILURE; } const struct option options[] = { { .name = "owner", .has_arg = required_argument, .flag = NULL, .val = 'o' }, { .name = "repo", .has_arg = required_argument, .flag = NULL, .val = 'r' }, { .name = "yes", .has_arg = no_argument, .flag = NULL, .val = 'y' }, { .name = "template", .has_arg = no_argument, .flag = NULL, .val = 'T' }, {0}, }; always_yes = gcli_cmd_should_do_always_yes(); while ((ch = getopt_long(argc, argv, "o:r:O:T:", options, NULL)) != -1) { switch (ch) { case 'o': opts.owner = optarg; break; case 'r': opts.repo = optarg; break; case 'y': always_yes = true; break; case 'O': { int rc = parse_submit_issue_option(&opts); if (rc < 0) return EXIT_FAILURE; } break; case 'T': /* template file */ if (gcli_read_file(optarg, &opts.body) < 0) { err(1, "gcli: error: failed to read file '%s'", optarg); } break; default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; if (argc == 0) return subcommand_issue_create_interactive(&opts); check_owner_and_repo(&opts.owner, &opts.repo); if (argc != 1) { fprintf(stderr, "gcli: error: Expected one argument for issue title\n"); usage(); return EXIT_FAILURE; } opts.title = argv[0]; if (create_issue(&opts, always_yes) < 0) errx(1, "gcli: error: failed to submit issue: %s", gcli_get_error(g_clictx)); gcli_nvlist_free(&opts.extra); return EXIT_SUCCESS; } static inline int handle_issues_actions(int argc, char *argv[], struct gcli_path const *path); int subcommand_issues(int argc, char *argv[]) { struct gcli_issue_list list = {0}; struct gcli_path path = {0}; char *endptr = NULL; int ch = 0, n = 30; struct gcli_issue_fetch_details details = {0}; enum gcli_output_flags flags = 0; /* detect whether we wanna create an issue */ if (argc > 1 && (strcmp(argv[1], "create") == 0)) { shift(&argc, &argv); return subcommand_issue_create(argc, argv); } const struct option options[] = { { .name = "all", .has_arg = no_argument, .flag = NULL, .val = 'a' }, { .name = "sorted", .has_arg = no_argument, .flag = NULL, .val = 's' }, { .name = "repo", .has_arg = required_argument, .flag = NULL, .val = 'r' }, { .name = "owner", .has_arg = required_argument, .flag = NULL, .val = 'o' }, { .name = "id", .has_arg = required_argument, .flag = NULL, .val = 'i' }, { .name = "count", .has_arg = required_argument, .flag = NULL, .val = 'n' }, { .name = "author", .has_arg = required_argument, .flag = NULL, .val = 'A', }, { .name = "label", .has_arg = required_argument, .flag = NULL, .val = 'L', }, { .name = "milestone", .has_arg = required_argument, .flag = NULL, .val = 'M', }, { .name = "assignee", .has_arg = required_argument, .flag = NULL, .val = 'S', }, {0}, }; /* parse options */ while ((ch = getopt_long(argc, argv, "+sn:o:r:i:aA:L:M:S:", options, NULL)) != -1) { switch (ch) { case 'o': path.as_default.owner = optarg; break; case 'r': path.as_default.repo = optarg; break; case 'i': { path.as_default.id = strtol(optarg, &endptr, 10); if (endptr != (optarg + strlen(optarg))) err(1, "gcli: error: cannot parse issue number"); if (path.as_default.id == 0) errx(1, "gcli: error: issue number is out of range"); } break; case 'n': { n = strtol(optarg, &endptr, 10); if (endptr != (optarg + strlen(optarg))) err(1, "gcli: error: cannot parse issue count"); if (n < -1) errx(1, "gcli: error: issue count is out of range"); if (n == 0) errx(1, "gcli: error: issue count must not be zero"); } break; case 'a': details.all = true; break; case 's': flags |= OUTPUT_SORTED; break; case 'A': { details.author = optarg; } break; case 'L': { details.label = optarg; } break; case 'M': { details.milestone = optarg; } break; case 'S': { details.assignee = optarg; } break; case '?': default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; check_path(&path); /* No issue number was given, so list all open issues */ if (path.as_default.id == 0) { /* Prepare search term if specified */ if (argc) details.search_term = gcli_join_with((char const *const *)argv, argc, " "); if (gcli_issues_search(g_clictx, &path, &details, n, &list) < 0) errx(1, "gcli: error: could not get issues: %s", gcli_get_error(g_clictx)); gcli_print_issues(flags, &list, n); gcli_issues_free(&list); return EXIT_SUCCESS; } /* require -a to not be set */ if (details.all) { fprintf(stderr, "gcli: error: -a cannot be combined with operations on " "an issue\n"); usage(); return EXIT_FAILURE; } /* Handle all the actions */ return handle_issues_actions(argc, argv, &path); } static int action_labels(struct gcli_path const *const issue_path, struct gcli_issue const *const issue, int *argc, char **argv[]) { char const **add_labels = NULL; size_t add_labels_size = 0; char const **remove_labels = NULL; size_t remove_labels_size = 0; int rc = 0; (void) issue; if (*argc < 2) { fprintf(stderr, "gcli: error: expected label operations\n"); return GCLI_EX_USAGE; } parse_labels_options(argc, argv, &add_labels, &add_labels_size, &remove_labels, &remove_labels_size); /* actually go about deleting and adding the labels */ if (add_labels_size) { rc = gcli_issue_add_labels(g_clictx, issue_path, add_labels, add_labels_size); if (rc < 0) { fprintf(stderr, "gcli: error: failed to add labels: %s\n", gcli_get_error(g_clictx)); rc = GCLI_EX_DATAERR; goto bail; } } if (remove_labels_size) { rc = gcli_issue_remove_labels(g_clictx, issue_path, remove_labels, remove_labels_size); if (rc < 0) { fprintf(stderr, "gcli: error: failed to remove labels: %s\n", gcli_get_error(g_clictx)); rc = GCLI_EX_DATAERR; goto bail; } } bail: free(add_labels); free(remove_labels); return rc; } static int action_all(struct gcli_path const *const path, struct gcli_issue const *issue, int *argc, char **argv[]) { (void) path; (void) argc; (void) argv; gcli_issue_print_summary(issue); puts("\nORIGINAL POST\n"); gcli_issue_print_op(issue); return GCLI_EX_OK; } static int action_comments(struct gcli_path const *const path, struct gcli_issue const *issue, int *argc, char **argv[]) { (void) issue; (void) argc; (void) argv; if (gcli_issue_comments(path) < 0) { fprintf(stderr, "gcli: error: failed to fetch issue comments: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_op(struct gcli_path const *const path, struct gcli_issue const *issue, int *argc, char **argv[]) { (void) path; (void) argc; (void) argv; gcli_issue_print_op(issue); return GCLI_EX_OK; } static int action_status(struct gcli_path const *const path, struct gcli_issue const *issue, int *argc, char **argv[]) { (void) path; (void) argc; (void) argv; gcli_issue_print_summary(issue); return GCLI_EX_OK; } static int action_close(struct gcli_path const *const path, struct gcli_issue const *issue, int *argc, char **argv[]) { (void) issue; (void) argc; (void) argv; if (gcli_issue_close(g_clictx, path) < 0) { fprintf(stderr, "gcli: error: failed to close issue: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_reopen(struct gcli_path const *const path, struct gcli_issue const *issue, int *argc, char **argv[]) { (void) issue; (void) argc; (void) argv; if (gcli_issue_reopen(g_clictx, path) < 0) { fprintf(stderr, "gcli: error: failed to reopen issue: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_assign(struct gcli_path const *const path, struct gcli_issue const *issue, int *argc, char **argv[]) { char const *assignee; (void) issue; if (*argc < 2) { fprintf(stderr, "gcli: error: missing assignee\n"); return GCLI_EX_USAGE; } *argc -= 1; *argv += 1; assignee = (*argv)[0]; if (gcli_issue_assign(g_clictx, path, assignee) < 0) { fprintf(stderr, "gcli: error: failed to assign issue: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_milestone(struct gcli_path const *const path, struct gcli_issue const *const issue, int *argc, char ***argv) { char const *milestone_str; char *endptr; int milestone, rc; (void) issue; /* Set the milestone for the issue * * Check that the user provided a milestone id */ if (*argc < 2) { fprintf(stderr, "gcli: error: missing milestone id\n"); return GCLI_EX_USAGE; } /* Fetch the milestone from the argument vector */ *argc -= 1; *argv += 1; milestone_str = (*argv)[0]; /* Check if the milestone_str is -d indicating that we should * clear the milestone */ if (strcmp(milestone_str, "-d") == 0) { rc = gcli_issue_clear_milestone(g_clictx, path); if (rc < 0) { fprintf(stderr, "gcli: error: could not clear milestone of issue: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } /* It is a milestone ID. Parse it. */ milestone = strtoul(milestone_str, &endptr, 10); /* Check successful for parse */ if (endptr != milestone_str + strlen(milestone_str)) { fprintf(stderr, "gcli: error: could not parse milestone id\n"); return GCLI_EX_USAGE; } /* Pass it to the dispatch */ if (gcli_issue_set_milestone(g_clictx, path, milestone) < 0) { fprintf(stderr, "gcli: error: could not assign milestone: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_title(struct gcli_path const *const path, struct gcli_issue const *const issue, int *argc, char **argv[]) { char const *new_title = NULL; int rc; (void) issue; if (*argc < 2) { fprintf(stderr, "gcli: error: missing new title\n"); return GCLI_EX_USAGE; } *argc -= 1; *argv += 1; new_title = (*argv)[0]; rc = gcli_issue_set_title(g_clictx, path, new_title); if (rc < 0) { fprintf(stderr, "gcli: error: failed to set new issue title: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static void gcli_print_attachments(struct gcli_attachment_list const *const list) { gcli_tbl tbl; struct gcli_tblcoldef columns[] = { { .name = "ID", .type = GCLI_TBLCOLTYPE_ID, .flags = GCLI_TBLCOL_JUSTIFYR }, { .name = "AUTHOR", .type = GCLI_TBLCOLTYPE_STRING, .flags = GCLI_TBLCOL_BOLD }, { .name = "CREATED", .type = GCLI_TBLCOLTYPE_TIME_T, .flags = 0 }, { .name = "CONTENT", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "OBSOLETE", .type = GCLI_TBLCOLTYPE_BOOL, .flags = 0 }, { .name = "FILENAME", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, }; tbl = gcli_tbl_begin(columns, ARRAY_SIZE(columns)); for (size_t i = 0; i < list->attachments_size; ++i) { struct gcli_attachment const *const it = &list->attachments[i]; gcli_tbl_add_row(tbl, it->id, it->author, it->created_at, it->content_type, it->is_obsolete, it->file_name); } gcli_tbl_end(tbl); } static int action_attachments(struct gcli_path const *const path, struct gcli_issue const *const issue, int *argc, char **argv[]) { struct gcli_attachment_list list = {0}; (void) issue; (void) argc; (void) argv; int rc = gcli_issue_get_attachments(g_clictx, path, &list); if (rc < 0) { fprintf(stderr, "gcli: error: failed to fetch attachments: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } gcli_print_attachments(&list); gcli_attachments_free(&list); return GCLI_EX_OK; } static int action_open(struct gcli_path const *const path, struct gcli_issue const *const issue, int *argc, char **argv[]) { int rc; (void) path; (void) argc; (void) argv; rc = gcli_cmd_open_url(issue->web_url); if (rc < 0) { fprintf(stderr, "gcli: error: failed to open url\n"); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } /* wrapper for const-correctness */ struct edit_issue_opts { struct gcli_issue const *issues; }; static void init_op_edit_file(struct gcli_ctx *ctx, FILE *stream, void *_opts) { (void) ctx; struct edit_issue_opts const *opts = _opts; fprintf( stream, "%s\n" "! Edit the original post as needed, save and exit.\n" "! All lines starting with '!' will be discarded.\n" "!\n" "! vim: ft=markdown\n", opts->issues->body); } static char * edit_op_message(struct gcli_issue const *issue) { struct edit_issue_opts opts = { issue }; return gcli_editor_get_user_message(g_clictx, init_op_edit_file, &opts); } static int action_edit(struct gcli_path const *const path, struct gcli_issue const *const issue, int *argc, char **argv[]) { char *new_message; int rc = GCLI_EX_OK; (void) path; (void) argc; (void) argv; /* let the user edit the message */ new_message = edit_op_message(issue); if (new_message == NULL) { fprintf(stderr, "gcli: error: failed to edit message file\n"); return GCLI_EX_DATAERR; } /* print it for double checking */ fprintf(stdout, "This is the final message:\n"); gcli_pretty_print(new_message, 4, 80, stdout); /* final confirmation */ if (!gcli_yesno("Do you want to continue?")) { fprintf(stderr, "gcli: Submission aborted.\n"); rc = GCLI_EX_DATAERR; goto done; } rc = gcli_issue_set_op(g_clictx, path, new_message); if (rc < 0) { fprintf(stderr, "gcli: error: failed to update original post: %s\n", gcli_get_error(g_clictx)); rc = GCLI_EX_DATAERR; goto done; } done: free(new_message); return rc; } struct gcli_cmd_actions gcli_issue_actions = { .fetch_item = (gcli_cmd_action_fetcher)gcli_get_issue, .free_item = (gcli_cmd_action_freeer)gcli_issue_free, .item_size = sizeof(struct gcli_issue), .defs = { { .name = "all", .needs_item = true, .handler = (gcli_cmd_action_handler)action_all, }, { .name = "comments", .needs_item = false, .handler = (gcli_cmd_action_handler)action_comments, .use_pager = true, }, { .name = "notes", .needs_item = false, .handler = (gcli_cmd_action_handler)action_comments, }, { .name = "op", .needs_item = true, .handler = (gcli_cmd_action_handler)action_op, }, { .name = "status", .needs_item = true, .handler = (gcli_cmd_action_handler)action_status, }, { .name = "close", .needs_item = false, .handler = (gcli_cmd_action_handler)action_close, }, { .name = "reopen", .needs_item = false, .handler = (gcli_cmd_action_handler)action_reopen, }, { .name = "assign", .needs_item = false, .handler = (gcli_cmd_action_handler)action_assign, }, { .name = "labels", .needs_item = false, .handler = (gcli_cmd_action_handler)action_labels, }, { .name = "milestone", .needs_item = false, .handler = (gcli_cmd_action_handler)action_milestone, }, { .name = "title", .needs_item = false, .handler = (gcli_cmd_action_handler)action_title, }, { .name = "attachments", .needs_item = false, .handler = (gcli_cmd_action_handler)action_attachments, }, { .name = "open", .needs_item = true, .handler = (gcli_cmd_action_handler)action_open, }, { .name = "edit", .needs_item = true, .handler = (gcli_cmd_action_handler)action_edit, }, }, }; static int handle_issues_actions(int argc, char *argv[], struct gcli_path const *const path) { int rc = 0; /* Check if the user missed out on supplying actions */ if (argc == 0) { fprintf(stderr, "gcli: error: no actions supplied\n"); usage(); return EXIT_FAILURE; } rc = gcli_cmd_actions_handle(&gcli_issue_actions, path, &argc, &argv); if (rc == GCLI_EX_USAGE) { usage(); } return !!rc; } gcli-2.9.1/src/cmd/labels.c000066400000000000000000000262201507017207500154070ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include #include #include #include #include static void usage(void) { fprintf(stderr, "usage: gcli labels create [-o owner -r repo] -n name -c colour -d description\n"); fprintf(stderr, " gcli labels [-o owner -r repo] -i name actions...\n"); fprintf(stderr, " gcli labels [-o owner -r repo] [-n number]\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -o owner The repository owner\n"); fprintf(stderr, " -r repo The repository name\n"); fprintf(stderr, " -i name Name of the lable to perform actions on\n"); fprintf(stderr, " -n number Number of labels to fetch (-1 = everything)\n"); fprintf(stderr, " -l name Name of the new label\n"); fprintf(stderr, " -c colour Six digit hex code of the label's colour\n"); fprintf(stderr, " -d description A short description of the label\n"); fprintf(stderr, "ACTIONS:\n"); fprintf(stderr, " status Show status information about the label\n"); fprintf(stderr, " name Change the name of the label\n"); fprintf(stderr, " description Change the description of the label\n"); fprintf(stderr, " colour Change the colour of the label\n"); fprintf(stderr, " delete Delete the label\n"); fprintf(stderr, "\n"); version(); copyright(); } void gcli_labels_print(struct gcli_label_list const *const list, int const max) { size_t n; gcli_tbl table; struct gcli_tblcoldef cols[] = { { .name = "ID", .type = GCLI_TBLCOLTYPE_ID, .flags = GCLI_TBLCOL_JUSTIFYR }, { .name = "", .type = GCLI_TBLCOLTYPE_STRING, .flags = GCLI_TBLCOL_256COLOUR|GCLI_TBLCOL_TIGHT }, { .name = "NAME", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "DESCRIPTION", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, }; /* Determine number of items to print */ if (max < 0 || (size_t)(max) > list->labels_size) n = list->labels_size; else n = max; /* Fill table */ table = gcli_tbl_begin(cols, ARRAY_SIZE(cols)); if (!table) errx(1, "gcli: error: could not init table"); for (size_t i = 0; i < n; ++i) { gcli_tbl_add_row(table, (long)list->labels[i].id, /* Cast is important here (#165) */ list->labels[i].colour, gcli_config_have_colours(g_clictx) ? " " : "", list->labels[i].name, list->labels[i].description); } gcli_tbl_end(table); } static void gcli_label_print(struct gcli_label *const label) { struct gcli_label_list labels = { .labels = label, .labels_size = 1 }; gcli_labels_print(&labels, 1); } static int parse_colour(char const *str, uint32_t *const out) { char *endptr = NULL; if (strlen(str) != 6) { fprintf(stderr, "gcli: error: colour must be a six-digit hexadecimal colour code\n"); return -1; } *out = strtol(str, &endptr, 16); if (endptr != (str + strlen(str))) { fprintf(stderr, "gcli: error: cannot parse colour\n"); return -1; } return 0; } static int subcommand_labels_create(int argc, char *argv[]) { int ch; struct gcli_label label = {0}; struct gcli_path repo_path = {0}; const struct option options[] = { {.name = "repo", .has_arg = required_argument, .val = 'r'}, {.name = "owner", .has_arg = required_argument, .val = 'o'}, {.name = "name", .has_arg = required_argument, .val = 'n'}, {.name = "colour", .has_arg = required_argument, .val = 'c'}, {.name = "description", .has_arg = required_argument, .val = 'd'}, {0} }; while ((ch = getopt_long(argc, argv, "n:o:r:d:c:", options, NULL)) != -1) { switch (ch) { case 'o': repo_path.as_default.owner = optarg; break; case 'r': repo_path.as_default.repo = optarg; break; case 'c': { if (parse_colour(optarg, &label.colour) < 0) return EXIT_FAILURE; } break; case 'd': { label.description = optarg; } break; case 'n': { label.name = optarg; } break; case '?': default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; check_path(&repo_path); if (!label.name) { fprintf(stderr, "gcli: error: missing name for label\n"); usage(); return EXIT_FAILURE; } if (!label.description) { fprintf(stderr, "gcli: error: missing description for label\n"); usage(); return EXIT_FAILURE; } if (gcli_create_label(g_clictx, &repo_path, &label) < 0) { errx(1, "gcli: error: failed to create label: %s", gcli_get_error(g_clictx)); } /* only if we are not quieted */ if (!gcli_be_quiet(g_clictx)) gcli_label_print(&label); return EXIT_SUCCESS; } static int action_delete(struct gcli_path const *const path, void *item, int *argc, char **argv[]) { int rc = 0; (void) item; /* unused */ (void) argc; (void) argv; rc = gcli_delete_label(g_clictx, path); if (rc < 0) { fprintf(stderr, "gcli: error: couldn't delete label: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_status(struct gcli_path const *const path, void *item, int *argc, char **argv[]) { (void) path; (void) argc; (void) argv; gcli_label_print(item); return GCLI_EX_OK; } static int action_name(struct gcli_path const *const path, void *item, int *argc, char **argv[]) { char const *new_name = NULL; int rc = 0; (void) item; /* unused */ /* check that we have enough arguments */ if (*argc < 2) { fprintf(stderr, "gcli: error: missing new name\n"); return GCLI_EX_USAGE; } /* pop off new name from argv */ new_name = (*argv)[1]; *argv += 1; *argc -= 1; rc = gcli_label_set_title(g_clictx, path, new_name); if (rc < 0) { fprintf(stderr, "gcli: error: failed to set title: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_description(struct gcli_path const *const path, void *item, int *argc, char **argv[]) { char const *new_description = NULL; int rc = 0; (void) item; /* unused */ /* check that we have enough arguments */ if (*argc < 2) { fprintf(stderr, "gcli: error: missing new description\n"); return GCLI_EX_USAGE; } /* pop off new description from argv */ new_description = (*argv)[1]; *argv += 1; *argc -= 1; rc = gcli_label_set_description(g_clictx, path, new_description); if (rc < 0) { fprintf(stderr, "gcli: error: failed to set description: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_colour(struct gcli_path const *const path, void *item, int *argc, char **argv[]) { char const *colour_str = NULL; uint32_t colour; int rc = 0; (void) item; /* unused */ /* check that we have enough arguments */ if (*argc < 2) { fprintf(stderr, "gcli: error: missing new colour\n"); return GCLI_EX_USAGE; } /* pop off new colour from argv */ colour_str = (*argv)[1]; *argv += 1; *argc -= 1; /* parse the colour into a uint32_t */ rc = parse_colour(colour_str, &colour); if (rc < 0) return GCLI_EX_USAGE; rc = gcli_label_set_colour(g_clictx, path, colour); if (rc < 0) { fprintf(stderr, "gcli: error: failed to set colour: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } struct gcli_cmd_actions label_actions = { .fetch_item = (gcli_cmd_action_fetcher)gcli_get_label, .free_item = (gcli_cmd_action_freeer)gcli_free_label, .item_size = sizeof(struct gcli_label), .defs = { { .name = "delete", .needs_item = false, .handler = action_delete, }, { .name = "status", .needs_item = true, .handler = action_status, }, { .name = "name", .needs_item = false, .handler = action_name, }, { .name = "colour", .needs_item = false, .handler = action_colour, }, { .name = "description", .needs_item = false, .handler = action_description, }, {0}, }, }; static int list_issues(struct gcli_path const *const path, int count) { struct gcli_label_list labels = {0}; if (gcli_get_labels(g_clictx, path, count, &labels) < 0) { fprintf(stderr, "gcli: error: could not fetch list of labels: %s\n", gcli_get_error(g_clictx)); return EXIT_FAILURE; } gcli_labels_print(&labels, count); gcli_free_labels(&labels); return EXIT_SUCCESS; } int subcommand_labels(int argc, char *argv[]) { int count = 30, ch, rc; struct gcli_path path = {0}; const struct option options[] = { {.name = "repo", .has_arg = required_argument, .flag = NULL, .val = 'r'}, {.name = "owner", .has_arg = required_argument, .flag = NULL, .val = 'o'}, {.name = "name", .has_arg = required_argument, .flag = NULL, .val = 'i'}, {.name = "count", .has_arg = required_argument, .flag = NULL, .val = 'n'}, {0} }; if (argc > 1 && strcmp("create", argv[1]) == 0) { return subcommand_labels_create(argc - 1, argv + 1); } path.kind = GCLI_PATH_NAMED; while ((ch = getopt_long(argc, argv, "n:o:r:i:", options, NULL)) != -1) { switch (ch) { case 'o': path.as_named.owner = optarg; break; case 'r': path.as_named.repo = optarg; break; case 'i': path.as_named.id = optarg; break; case 'n': { char *endptr = NULL; count = strtol(optarg, &endptr, 10); if (endptr != (optarg + strlen(optarg))) errx(1, "gcli: error: cannot parse label count"); if (count == 0) errx(1, "gcli: error: number of labels must not be zero"); } break; case '?': default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; check_path(&path); /* List labels if no id was given */ if (path.as_named.id == NULL) return list_issues(&path, count); /* otherwise handle actions */ rc = gcli_cmd_actions_handle(&label_actions, &path, &argc, &argv); if (rc == GCLI_EX_USAGE) usage(); return !!rc; } gcli-2.9.1/src/cmd/milestones.c000066400000000000000000000310201507017207500163210ustar00rootroot00000000000000/* * Copyright 2023-2025 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 #include #include #include #include static void usage(void) { fprintf(stderr, "usage: gcli milestones [-o owner -r repo]\n"); fprintf(stderr, " gcli milestones create [-o owner -r repo] -t title [-d description]\n"); fprintf(stderr, " gcli milestones [-o owner -r repo] -i milestone actions...\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -o owner The repository owner\n"); fprintf(stderr, " -r repo The repository name\n"); fprintf(stderr, " -i milestone Run actions for the given milestone id\n"); fprintf(stderr, " -t title Title of the milestone to create\n"); fprintf(stderr, " -d description Description the milestone to create\n"); fprintf(stderr, "ACTIONS:\n"); fprintf(stderr, " all Display both status information and issues for the milestone\n"); fprintf(stderr, " status Display general status information about the milestone\n"); fprintf(stderr, " issues List issues associated with the milestone\n"); fprintf(stderr, " set-duedate Set due date \n"); fprintf(stderr, " delete Delete this milestone\n"); fprintf(stderr, " open Open this milestone in a web browser\n"); fprintf(stderr, "\n"); version(); copyright(); } void gcli_print_milestones(struct gcli_milestone_list const *const list, int max) { size_t n; gcli_tbl tbl; struct gcli_tblcoldef cols[] = { { .name = "ID", .type = GCLI_TBLCOLTYPE_ID, .flags = GCLI_TBLCOL_JUSTIFYR }, { .name = "STATE", .type = GCLI_TBLCOLTYPE_STRING, .flags = GCLI_TBLCOL_STATECOLOURED }, { .name = "CREATED", .type = GCLI_TBLCOLTYPE_TIME_T, .flags = 0 }, { .name = "TITLE", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, }; if (!list->milestones_size) { puts("No milestones"); return; } tbl = gcli_tbl_begin(cols, ARRAY_SIZE(cols)); if (!tbl) errx(1, "gcli: error: could not init table printer"); if (max < 0 || (size_t)(max) > list->milestones_size) n = list->milestones_size; else n = max; for (size_t i = 0; i < n; ++i) { gcli_tbl_add_row(tbl, list->milestones[i].id, list->milestones[i].state, list->milestones[i].created_at, list->milestones[i].title); } gcli_tbl_end(tbl); } void gcli_print_milestone(struct gcli_milestone const *const milestone) { gcli_dict dict; uint32_t const quirks = gcli_forge(g_clictx)->milestone_quirks; dict = gcli_dict_begin(); gcli_dict_add(dict, "ID", 0, 0, "%"PRIid, milestone->id); gcli_dict_add_string(dict, "TITLE", 0, 0, milestone->title); gcli_dict_add_string(dict, "STATE", GCLI_TBLCOL_STATECOLOURED, 0, milestone->state); gcli_dict_add_timestamp(dict, "CREATED", 0, 0, milestone->created_at); gcli_dict_add_timestamp(dict, "UPDATED", 0, 0, milestone->created_at); if ((quirks & GCLI_MILESTONE_QUIRKS_DUEDATE) == 0) gcli_dict_add_timestamp(dict, "DUE", 0, 0, milestone->due_date); if ((quirks & GCLI_MILESTONE_QUIRKS_EXPIRED) == 0) gcli_dict_add_string(dict, "EXPIRED", 0, 0, gcli_bool_yesno(milestone->expired)); if ((quirks & GCLI_MILESTONE_QUIRKS_NISSUES) == 0) { gcli_dict_add(dict, "OPEN ISSUES", 0, 0, "%d", milestone->open_issues); gcli_dict_add(dict, "CLOSED ISSUES", 0, 0, "%d", milestone->closed_issues); } gcli_dict_end(dict); if (milestone->description && strlen(milestone->description)) { printf("\nDESCRIPTION:\n"); gcli_pretty_print(milestone->description, 4, 80, stdout); } } static int action_milestone_all(struct gcli_path const *const path, struct gcli_milestone const *item, int *argc, char **argv[]) { struct gcli_issue_list issues = {0}; int rc = 0; (void) argc; (void) argv; gcli_print_milestone(item); rc = gcli_milestone_get_issues(g_clictx, path, &issues); if (rc < 0) { fprintf(stderr, "gcli: error: failed to fetch issues: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } printf("\nISSUES:\n"); gcli_print_issues(0, &issues, -1); gcli_issues_free(&issues); return GCLI_EX_OK; } static int action_milestone_issues(struct gcli_path const *const path, struct gcli_milestone const *item, int *argc, char **argv[]) { int rc = 0; struct gcli_issue_list issues = {0}; (void) item; (void) argc; (void) argv; /* Fetch list of issues associated with milestone */ rc = gcli_milestone_get_issues(g_clictx, path, &issues); if (rc < 0) { fprintf(stderr, "gcli: error: failed to get issues: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } /* Print them as a table */ gcli_print_issues(0, &issues, -1); /* Cleanup */ gcli_issues_free(&issues); return GCLI_EX_OK; } static int action_milestone_status(struct gcli_path const *const path, struct gcli_milestone const *const milestone, int *argc, char **argv[]) { (void) path; (void) argc; (void) argv; gcli_print_milestone(milestone); return GCLI_EX_OK; } static int action_milestone_delete(struct gcli_path const *const path, struct gcli_milestone const *const milestone, int *argc, char **argv[]) { int rc; (void) milestone; (void) argc; (void) argv; /* Delete the milestone */ rc = gcli_delete_milestone(g_clictx, path); if (rc < 0) { fprintf(stderr, "gcli: error: could not delete milestone: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_milestone_set_duedate(struct gcli_path const *const path, struct gcli_milestone const *const milestone, int *argc, char **argv[]) { char *new_date = NULL; int rc = 0; (void) milestone; /* grab the the date that the user provided */ if (*argc < 2) { fprintf(stderr, "gcli: error: missing date for set-duedate\n"); return GCLI_EX_USAGE; } shift(argc, argv); new_date = (*argv)[0]; /* Do it! */ rc = gcli_milestone_set_duedate(g_clictx, path, new_date); if (rc < 0) { fprintf(stderr, "gcli: error: could not update milestone due date: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_milestone_open(struct gcli_path const *const path, struct gcli_milestone const *const milestone, int *argc, char **argv[]) { int rc; (void) path; (void) argc; (void) argv; rc = gcli_cmd_open_url(milestone->web_url); if (rc < 0) { fprintf(stderr, "gcli: error: failed to open url\n"); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } struct gcli_cmd_actions milestone_actions = { .fetch_item = (gcli_cmd_action_fetcher)gcli_get_milestone, .free_item = (gcli_cmd_action_freeer)gcli_free_milestone, .item_size = sizeof(struct gcli_milestone), .defs = { { .name = "all", .needs_item = true, .handler = (gcli_cmd_action_handler)action_milestone_all, }, { .name = "issues", .needs_item = false, .handler = (gcli_cmd_action_handler)action_milestone_issues, }, { .name = "status", .needs_item = true, .handler = (gcli_cmd_action_handler)action_milestone_status, }, { .name = "delete", .needs_item = false, .handler = (gcli_cmd_action_handler)action_milestone_delete, }, { .name = "set-duedate", .needs_item = false, .handler = (gcli_cmd_action_handler)action_milestone_set_duedate, }, { .name = "open", .needs_item = true, .handler = (gcli_cmd_action_handler)action_milestone_open, }, {0}, }, }; static int handle_milestone_actions(int argc, char *argv[], struct gcli_path const *const path) { int rc = 0; rc = gcli_cmd_actions_handle(&milestone_actions, path, &argc, &argv); if (rc == GCLI_EX_USAGE) { usage(); } return rc ? EXIT_FAILURE : EXIT_SUCCESS; } static int subcommand_milestone_create(int argc, char *argv[]) { int ch; struct gcli_milestone_create_args args = {0}; struct gcli_path repo = {0}; struct option const options[] = { { .name = "owner", .has_arg = required_argument, .flag = NULL, .val = 'o' }, { .name = "repo", .has_arg = required_argument, .flag = NULL, .val = 'r' }, { .name = "title", .has_arg = required_argument, .flag = NULL, .val = 't' }, { .name = "description", .has_arg = required_argument, .flag = NULL, .val = 'd' }, }; /* Read in options */ while ((ch = getopt_long(argc, argv, "+o:r:t:d:", options, NULL)) != -1) { switch (ch) { case 'o': repo.as_default.owner = optarg; break; case 'r': repo.as_default.repo = optarg; break; case 't': args.title = optarg; break; case 'd': args.description = optarg; break; default: usage(); return 1; } } argc -= optind; argv += optind; /* sanity check argumets */ if (argc) errx(1, "gcli: error: stray arguments"); /* make sure path is valid */ check_path(&repo); /* enforce the user to at least provide a title */ if (!args.title) errx(1, "gcli: error: missing milestone title"); /* actually create the milestone */ if (gcli_create_milestone(g_clictx, &repo, &args) < 0) errx(1, "gcli: error: could not create milestone: %s", gcli_get_error(g_clictx)); return 0; } int subcommand_milestones(int argc, char *argv[]) { int ch, rc, max = 30; struct gcli_path path = {0}; struct option const options[] = { { .name = "owner", .has_arg = required_argument, .flag = NULL, .val = 'o' }, { .name = "repo", .has_arg = required_argument, .flag = NULL, .val = 'r' }, { .name = "count", .has_arg = required_argument, .flag = NULL, .val = 'n' }, { .name = "id", .has_arg = required_argument, .flag = NULL, .val = 'i' }, }; /* detect whether we wanna create a milestone */ if (argc > 1 && (strcmp(argv[1], "create") == 0)) { shift(&argc, &argv); return subcommand_milestone_create(argc, argv); } /* Proceed with fetching information */ while ((ch = getopt_long(argc, argv, "+o:r:n:i:", options, NULL)) != -1) { switch (ch) { case 'o': { path.as_default.owner = optarg; } break; case 'r': { path.as_default.repo = optarg; } break; case 'n': { char *endptr; max = strtol(optarg, &endptr, 10); if (endptr != optarg + strlen(optarg)) errx(1, "gcli: error: cannot parse milestone count"); } break; case 'i': { char *endptr; path.as_default.id = strtoul(optarg, &endptr, 10); if (endptr != optarg + strlen(optarg)) errx(1, "gcli: error: cannot parse milestone id"); } break; default: { usage(); return 1; } } } argc -= optind; argv += optind; check_path(&path); if (path.as_default.id == 0) { struct gcli_milestone_list list = {0}; rc = gcli_get_milestones(g_clictx, &path, max, &list); if (rc < 0) { errx(1, "gcli: error: cannot get list of milestones: %s", gcli_get_error(g_clictx)); } gcli_print_milestones(&list, max); gcli_free_milestones(&list); return 0; } return handle_milestone_actions(argc, argv, &path); } gcli-2.9.1/src/cmd/open.c000066400000000000000000000041101507017207500151000ustar00rootroot00000000000000/* * Copyright 2025 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 #include int gcli_cmd_open_url(char const *const url) { pid_t p; int rc = 0; char *open_program = NULL; if (!url) { fprintf(stderr, "gcli: error: got no url from forge\n"); return -1; } p = fork(); if (p < 0) err(1, "fork"); /* wait for the child */ if (p != 0) return gcli_wait_proc_ok(g_clictx, p); /* look for an open-program */ open_program = gcli_config_get_url_open_program(g_clictx); if (open_program == NULL) open_program = "xdg-open"; rc = execlp(open_program, open_program, url, NULL); if (rc < 0) exit(EXIT_FAILURE); abort(); } gcli-2.9.1/src/cmd/pipelines.c000066400000000000000000000440001507017207500161310ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include #include #include #include #include #include static void usage(void) { fprintf(stderr, "usage: gcli pipelines [-o owner -r repo] [-n number]\n"); fprintf(stderr, " gcli pipelines [-o owner -r repo] -p pipeline pipeline-actions...\n"); fprintf(stderr, " gcli pipelines [-o owner -r repo] -j job [-n number] job-actions...\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -o owner The repository owner\n"); fprintf(stderr, " -r repo The repository name\n"); fprintf(stderr, " -p pipeline Run actions for the given pipeline\n"); fprintf(stderr, " -j job Run actions for the given job\n"); fprintf(stderr, " -n number Number of pipelines to fetch (-1 = everything)\n"); fprintf(stderr, "\n"); fprintf(stderr, "PIPELINE ACTIONS:\n"); fprintf(stderr, " all Show status of this pipeline (including jobs and children)\n"); fprintf(stderr, " children Print the list of child pipelines\n"); fprintf(stderr, " jobs Print the list of jobs of this pipeline\n"); fprintf(stderr, " open Open the pipeline in a web browser\n"); fprintf(stderr, "\n"); fprintf(stderr, "JOB ACTIONS:\n"); fprintf(stderr, " status Display status information\n"); fprintf(stderr, " artifacts [-o filename] Download a zip archive of the artifacts of the given job\n"); fprintf(stderr, " (default output filename: artifacts.zip)\n"); fprintf(stderr, " log Display job log\n"); fprintf(stderr, " cancel Cancel the job\n"); fprintf(stderr, " retry Retry the given job\n"); fprintf(stderr, " open Open the job in a web browser\n"); fprintf(stderr, "\n"); version(); copyright(); } void gitlab_print_pipelines(struct gitlab_pipeline_list const *const list) { gcli_tbl table; struct gcli_tblcoldef cols[] = { { .name = "ID", .type = GCLI_TBLCOLTYPE_ID, .flags = GCLI_TBLCOL_JUSTIFYR }, { .name = "STATUS", .type = GCLI_TBLCOLTYPE_STRING, .flags = GCLI_TBLCOL_STATECOLOURED }, { .name = "CREATED", .type = GCLI_TBLCOLTYPE_TIME_T, .flags = 0 }, { .name = "UPDATED", .type = GCLI_TBLCOLTYPE_TIME_T, .flags = 0 }, { .name = "NAME", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "REF", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, }; if (!list->pipelines_size) { printf("No pipelines\n"); return; } table = gcli_tbl_begin(cols, ARRAY_SIZE(cols)); if (!table) errx(1, "gcli: error: could not init table"); for (size_t i = 0; i < list->pipelines_size; ++i) { gcli_tbl_add_row(table, list->pipelines[i].id, list->pipelines[i].status, list->pipelines[i].created_at, list->pipelines[i].updated_at, list->pipelines[i].name, list->pipelines[i].ref); } gcli_tbl_end(table); } void gitlab_print_jobs(struct gitlab_job_list const *const list) { gcli_tbl table; struct gcli_tblcoldef cols[] = { { .name = "ID", .type = GCLI_TBLCOLTYPE_ID, .flags = GCLI_TBLCOL_JUSTIFYR }, { .name = "NAME", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "STATUS", .type = GCLI_TBLCOLTYPE_STRING, .flags = GCLI_TBLCOL_STATECOLOURED }, { .name = "STARTED", .type = GCLI_TBLCOLTYPE_TIME_T, .flags = 0 }, { .name = "FINISHED", .type = GCLI_TBLCOLTYPE_TIME_T, .flags = 0 }, { .name = "RUNNERDESC", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "REF", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, }; if (!list->jobs_size) { printf("No jobs\n"); return; } table = gcli_tbl_begin(cols, ARRAY_SIZE(cols)); if (!table) errx(1, "gcli: error: could not initialize table"); for (size_t i = 0; i < list->jobs_size; ++i) { gcli_tbl_add_row(table, list->jobs[i].id, list->jobs[i].name, list->jobs[i].status, list->jobs[i].started_at, list->jobs[i].finished_at, list->jobs[i].runner_description, list->jobs[i].ref); } gcli_tbl_end(table); } void gitlab_print_job_status(struct gitlab_job const *const job) { gcli_dict printer; printer = gcli_dict_begin(); gcli_dict_add(printer, "ID", 0, 0, "%"PRIid, job->id); gcli_dict_add_string(printer, "STATUS", GCLI_TBLCOL_STATECOLOURED, 0, job->status); gcli_dict_add_string(printer, "STAGE", 0, 0, job->stage); gcli_dict_add_string(printer, "NAME", GCLI_TBLCOL_BOLD, 0, job->name); gcli_dict_add_string(printer, "REF", GCLI_TBLCOL_COLOUREXPL, GCLI_COLOR_YELLOW, job->ref); gcli_dict_add_timestamp(printer, "CREATED", 0, 0, job->created_at); gcli_dict_add_timestamp(printer, "STARTED", 0, 0, job->started_at); gcli_dict_add_timestamp(printer, "FINISHED", 0, 0, job->finished_at); gcli_dict_add(printer, "DURATION", 0, 0, "%-.2lfs", job->duration); gcli_dict_add(printer, "COVERAGE", 0, 0, "%.1lf%%", job->coverage); gcli_dict_add_string(printer, "RUNNER NAME", 0, 0, job->runner_name); gcli_dict_add_string(printer, "RUNNER DESCR", 0, 0, job->runner_description); gcli_dict_end(printer); } void gitlab_print_pipeline(struct gitlab_pipeline const *const pipeline) { gcli_dict printer; printer = gcli_dict_begin(); gcli_dict_add(printer, "ID", 0, 0, "%"PRIid, pipeline->id); gcli_dict_add_string(printer, "NAME", 0, 0, pipeline->name ? pipeline->name : "N/A"); gcli_dict_add_string(printer, "STATUS", GCLI_TBLCOL_STATECOLOURED, 0, pipeline->status); gcli_dict_add_timestamp(printer, "CREATED", 0, 0, pipeline->created_at); gcli_dict_add_timestamp(printer, "UPDATED", 0, 0, pipeline->updated_at); gcli_dict_add_string(printer, "REF", GCLI_TBLCOL_COLOUREXPL, GCLI_COLOR_YELLOW, pipeline->ref); gcli_dict_add_string(printer, "SHA", GCLI_TBLCOL_COLOUREXPL, GCLI_COLOR_YELLOW, pipeline->sha); gcli_dict_add_string(printer, "SOURCE", 0, 0, pipeline->source); gcli_dict_end(printer); } static int action_pipeline_status(struct gcli_path const *path, struct gitlab_pipeline *pipeline, int *argc, char **argv[]) { (void) path; (void) argc; (void) argv; gitlab_print_pipeline(pipeline); return GCLI_EX_OK; } static int action_pipeline_jobs(struct gcli_path const *path, struct gitlab_pipeline *pipeline, int *argc, char **argv[]) { int rc = 0; struct gitlab_job_list jobs = {0}; (void) pipeline; (void) argc; (void) argv; rc = gitlab_get_pipeline_jobs(g_clictx, path, -1, &jobs); if (rc < 0) { fprintf(stderr, "gcli: error: failed to get pipeline jobs: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } gitlab_print_jobs(&jobs); gitlab_free_jobs(&jobs); return GCLI_EX_OK; } static int action_pipeline_children(struct gcli_path const *path, struct gitlab_pipeline *pipeline, int *argc, char **argv[]) { int rc = 0; struct gitlab_pipeline_list children = {0}; (void) pipeline; (void) argc; (void) argv; rc = gitlab_get_pipeline_children(g_clictx, path, -1, &children); if (rc < 0) { fprintf(stderr, "gcli: error: failed to get pipeline children: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } gitlab_print_pipelines(&children); gitlab_pipelines_free(&children); return GCLI_EX_OK; } static int action_pipeline_all(struct gcli_path const *path, struct gitlab_pipeline *pipeline, int *argc, char **argv[]) { int rc = 0; rc = action_pipeline_status(path, pipeline, argc, argv); if (rc) return rc; fprintf(stdout, "\n"); fprintf(stdout, "JOBS\n"); rc = action_pipeline_jobs(path, pipeline, argc, argv); if (rc) return rc; fprintf(stdout, "\n"); fprintf(stdout, "CHILDREN\n"); rc = action_pipeline_children(path, pipeline, argc, argv); return rc; } static int action_pipeline_open(struct gcli_path const *path, struct gitlab_pipeline *pipeline, int *argc, char **argv[]) { int rc; (void) path; (void) argc; (void) argv; rc = gcli_cmd_open_url(pipeline->web_url); if (rc < 0) { fprintf(stderr, "gcli: error: failed to open url\n"); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static struct gcli_cmd_actions const pipeline_actions = { .fetch_item = (gcli_cmd_action_fetcher)gitlab_get_pipeline, .free_item = (gcli_cmd_action_freeer)gitlab_pipeline_free, .item_size = sizeof(struct gitlab_pipeline), .defs = { { .name = "all", .needs_item = true, .handler = (gcli_cmd_action_handler)action_pipeline_all }, { .name = "status", .needs_item = true, .handler = (gcli_cmd_action_handler)action_pipeline_status }, { .name = "jobs", .needs_item = false, .handler = (gcli_cmd_action_handler)action_pipeline_jobs }, { .name = "children", .needs_item = false, .handler = (gcli_cmd_action_handler)action_pipeline_children }, { .name = "open", .needs_item = true, .handler = (gcli_cmd_action_handler)action_pipeline_open }, {0}, }, }; static int handle_pipeline_actions(struct gcli_path const *const pipeline_path, int argc, char *argv[]) { int rc = 0; if (argc == 0) { fprintf(stderr, "gcli: error: missing pipeline actions\n"); usage(); return EXIT_FAILURE; } rc = gcli_cmd_actions_handle(&pipeline_actions, pipeline_path, &argc, &argv); if (rc == GCLI_EX_USAGE) usage(); return !!rc; } /* ***************************** * Gitlab Jobs * *****************************/ static int action_job_status(struct gcli_path const *const path, struct gitlab_job const *const job, int *argc, char **argv[]) { (void) path; (void) argc; (void) argv; gitlab_print_job_status(job); return GCLI_EX_OK; } static int action_job_log(struct gcli_path const *const path, struct gitlab_job const *const job, int *argc, char **argv[]) { int rc = 0; (void) job; (void) argc; (void) argv; rc = gitlab_job_get_log(g_clictx, path, stdout); if (rc < 0) { fprintf(stderr, "gcli: error: failed to get job log: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_job_cancel(struct gcli_path const *const path, struct gitlab_job const *const job, int *argc, char **argv[]) { int rc = 0; (void) job; (void) argc; (void) argv; rc = gitlab_job_cancel(g_clictx, path); if (rc < 0) { fprintf(stderr, "gcli: error: failed to cancel the job: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_job_retry(struct gcli_path const *const path, struct gitlab_job const *const job, int *argc, char **argv[]) { int rc = 0; (void) job; (void) argc; (void) argv; rc = gitlab_job_retry(g_clictx, path); if (rc < 0) { fprintf(stderr, "gcli: error: failed to retry the job: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_job_artifacts(struct gcli_path const *const path, struct gitlab_job const *const job, int *argc, char **argv[]) { int rc = 0; char const *outfile = "artifacts.zip"; (void) job; if (*argc > 2 && strcmp((*argv)[1], "-o") == 0) { if (*argc < 3) { fprintf(stderr, "gcli: error: -o is missing the output filename\n"); usage(); return EXIT_FAILURE; } outfile = (*argv)[2]; *argc -= 2; *argv += 2; } rc = gitlab_job_download_artifacts(g_clictx, path, outfile); if (rc < 0) { fprintf(stderr, "gcli: error: failed to download file: %s\n", gcli_get_error(g_clictx)); return EXIT_FAILURE; } return EXIT_SUCCESS; } static int action_job_open(struct gcli_path const *path, struct gitlab_job *job, int *argc, char **argv[]) { int rc; (void) path; (void) argc; (void) argv; rc = gcli_cmd_open_url(job->web_url); if (rc < 0) { fprintf(stderr, "gcli: error: failed to open url\n"); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static struct gcli_cmd_actions job_actions = { .fetch_item = (gcli_cmd_action_fetcher)gitlab_get_job, .free_item = (gcli_cmd_action_freeer)gitlab_free_job, .item_size = sizeof(struct gitlab_job), .defs = { { .name = "log", .needs_item = false, .handler = (gcli_cmd_action_handler)action_job_log, }, { .name = "status", .needs_item = true, .handler = (gcli_cmd_action_handler)action_job_status, }, { .name = "cancel", .needs_item = false, .handler = (gcli_cmd_action_handler)action_job_cancel, }, { .name = "retry", .needs_item = false, .handler = (gcli_cmd_action_handler)action_job_retry, }, { .name = "artifacts", .needs_item = false, .handler = (gcli_cmd_action_handler)action_job_artifacts, }, { .name = "open", .needs_item = true, .handler = (gcli_cmd_action_handler)action_job_open, }, {0}, }, }; static int handle_job_actions(struct gcli_path const *const job_path, int argc, char *argv[]) { int rc = 0; /* Check if the user missed out on supplying actions */ if (argc == 0) { fprintf(stderr, "gcli: error: no actions supplied\n"); usage(); return GCLI_EX_USAGE; } rc = gcli_cmd_actions_handle(&job_actions, job_path, &argc, &argv); if (rc == GCLI_EX_USAGE) usage(); return !!rc; } static int list_pipelines(struct gcli_path const *const path, int max) { struct gitlab_pipeline_list list = {0}; int rc = 0; rc = gitlab_get_pipelines(g_clictx, path, max, &list); if (rc < 0) { fprintf(stderr, "gcli: failed to get pipelines: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } gitlab_print_pipelines(&list); gitlab_pipelines_free(&list); return GCLI_EX_OK; } int subcommand_pipelines(int argc, char *argv[]) { int ch = 0, count = 30, pflag = 0, jflag = 0; struct gcli_path path = {0}; /* Parse options */ const struct option options[] = { {.name = "repo", .has_arg = required_argument, .flag = NULL, .val = 'r'}, {.name = "owner", .has_arg = required_argument, .flag = NULL, .val = 'o'}, {.name = "count", .has_arg = required_argument, .flag = NULL, .val = 'c'}, {.name = "pipeline", .has_arg = required_argument, .flag = NULL, .val = 'p'}, {.name = "job", .has_arg = required_argument, .flag = NULL, .val = 'j'}, {0} }; while ((ch = getopt_long(argc, argv, "+n:o:r:p:j:", options, NULL)) != -1) { switch (ch) { case 'o': path.as_default.owner = optarg; break; case 'r': path.as_default.repo = optarg; break; case 'n': { char *endptr = NULL; count = strtol(optarg, &endptr, 10); if (endptr != (optarg + strlen(optarg))) { fprintf(stderr, "gcli: error: cannot parse argument to -n\n"); return EXIT_FAILURE; } } break; case 'p': { char *endptr = NULL; path.as_default.id = strtoul(optarg, &endptr, 10); if (endptr != (optarg + strlen(optarg))) { fprintf(stderr, "gcli: error: cannot parse argument to -p\n"); return EXIT_FAILURE; } pflag = 1; } break; case 'j': { char *endptr = NULL; path.as_default.id = strtoul(optarg, &endptr, 10); if (endptr != (optarg + strlen(optarg))) { fprintf(stderr, "gcli: error: cannot parse argument to -j\n"); return EXIT_FAILURE; } jflag = 1; } break; case '?': default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; if (pflag && jflag) { fprintf(stderr, "gcli: error: -p and -j are mutually exclusive\n"); usage(); return EXIT_FAILURE; } check_path(&path); /* Make sure we are actually talking about a gitlab remote because * we might be incorrectly inferring it */ if (gcli_config_get_forge_type(g_clictx) != GCLI_FORGE_GITLAB) { fprintf(stderr, "gcli: error: The pipelines subcommand only works for GitLab. " "Use gcli -t gitlab ... to force a GitLab remote.\n"); return EXIT_FAILURE; } /* In case a Pipeline ID was specified handle its actions */ if (pflag) return handle_pipeline_actions(&path, argc, argv); /* A Job ID was specified */ if (jflag) return handle_job_actions(&path, argc, argv); /* Neither a Job id nor a pipeline ID was specified - list all * pipelines instead */ if (argc != 0) { fprintf(stderr, "gcli: error: stray arguments\n"); usage(); return EXIT_FAILURE; } return list_pipelines(&path, count); } gcli-2.9.1/src/cmd/pull_reviews.c000066400000000000000000000176761507017207500167040ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include #include #include #include #include #include #include #include #include static char * get_review_file_cache_dir(void) { /* FIXME */ return gcli_asprintf("%s/.cache/gcli/reviews", getenv("HOME")); } unsigned long djb2(unsigned char const *str) { unsigned long hash = 5381; int c; while ((c = *str++)) hash = ((hash << 5) + hash) + c; return hash; } static int do_mkdir(char const *const path) { struct stat sb = {0}; if (stat(path, &sb) < 0) return mkdir(path, 0750); if (S_ISDIR(sb.st_mode)) return 0; errno = ENOTDIR; return -1; } static int mkdir_p(char const *const dirname) { char *d = NULL, *hd = NULL; size_t l; l = strlen(dirname); d = hd = strdup(dirname); if (*d == '/') d++; while (*d) { char *p = strchr(d, '/'); if (!p || !(*p)) break; *p = '\0'; if (do_mkdir(hd) < 0) return -1; *p = '/'; d = p+1; } if (dirname[l-1] != '/') { if (do_mkdir(hd) < 0) return -1; } free(hd); return 0; } static void ensure_cache_dir_exists(void) { char *dir = get_review_file_cache_dir(); if (mkdir_p(dir) < 0) err(1, "gcli: error: could not create cache directory"); free(dir); } static char * make_review_diff_file_name(struct gcli_path const *const path) { unsigned long hash = 0; assert(path->kind == GCLI_PATH_DEFAULT); hash ^= djb2((unsigned char const *)path->as_default.owner); hash ^= djb2((unsigned char const *)path->as_default.repo); return gcli_asprintf("%lx_%"PRIid".diff", hash, path->as_default.id); } static char * get_review_diff_file_name(struct gcli_path const *const path) { char *base = get_review_file_cache_dir(); char *file = make_review_diff_file_name(path); char *file_path = gcli_asprintf("%s/%s", base, file); free(base); free(file); return file_path; } struct review_ctx { char *diff_path; struct gcli_pull_create_review_details details; }; static void fetch_patch(struct review_ctx *ctx) { /* diff does not exist, fetch it! */ FILE *f = fopen(ctx->diff_path, "w"); if (f == NULL) err(1, "gcli: error: cannot open %s", ctx->diff_path); if (gcli_pull_get_diff(g_clictx, f, &ctx->details.path) < 0) { errx(1, "gcli: error: failed to get patch: %s", gcli_get_error(g_clictx)); } fclose(f); f = NULL; } /* Splits the prelude of a patch series into two parts: * * Lines starting with »GCLI: « * Lines not starting with this prefix. * * Lines starting with this prefix will be stored in a meta */ static void process_series_prelude(char *prelude, struct gcli_pull_create_review_details *details) { char *comment, *bol; size_t const p_len = strlen(prelude); bol = prelude; comment = calloc(p_len + 1, 1); TAILQ_INIT(&details->meta_lines); /* Loop through input line-by-line */ for (;;) { char *eol = strchr(bol, '\n'); size_t line_len; static char const gcli_pref[] = "GCLI: "; static size_t const gcli_pref_len = sizeof(gcli_pref) - 1; if (eol == NULL) eol = bol + strlen(bol); line_len = eol - bol; /* This line matches the prefix. Copy into meta */ if (line_len >= gcli_pref_len && strncmp(bol, gcli_pref, gcli_pref_len) == 0) { struct gcli_review_meta_line *ml; char *meta = calloc(line_len - gcli_pref_len + 1, 1); memcpy(meta, bol + gcli_pref_len, line_len - gcli_pref_len); ml = calloc(1, sizeof(*ml)); ml->entry = meta; TAILQ_INSERT_TAIL(&details->meta_lines, ml, next); } else { strncat(comment, bol, line_len + 1); } bol = eol + 1; if (*bol == '\0') break; } details->body = comment; } static void extract_patch_comments(struct review_ctx *ctx, struct gcli_diff_comments *out) { FILE *f = fopen(ctx->diff_path, "r"); struct gcli_diff_parser p = {0}; struct gcli_patch patch = {0}; if (gcli_diff_parser_from_file(f, ctx->diff_path, &p) < 0) err(1, "gcli: error: failed to open diff"); if (gcli_parse_patch(&p, &patch) < 0) errx(1, "gcli: error: failed to parse patch"); if (gcli_patch_get_comments(&patch, out) < 0) errx(1, "gcli: error: failed to get comments"); process_series_prelude(patch.prelude, &ctx->details); gcli_free_patch(&patch); gcli_free_diff_parser(&p); fclose(f); } static void edit_diff(struct review_ctx *ctx) { if (access(ctx->diff_path, F_OK) < 0) { fetch_patch(ctx); } else { /* The file exists, ask whether to open again or to delete and start over. */ if (gcli_yesno("There seems to already be a review in progress. Start over?")) fetch_patch(ctx); } gcli_editor_open_file(g_clictx, ctx->diff_path); extract_patch_comments(ctx, &ctx->details.comments); free(ctx->diff_path); } static void print_comment_list(struct gcli_diff_comments const *comments) { struct gcli_diff_comment const *comment; TAILQ_FOREACH(comment, comments, next) { printf("=====================================\n"); printf("%s:%d:\n", comment->after.filename, comment->after.start_row); gcli_pretty_print(comment->comment, 6, 80, stdout); printf("The diff is:\n\n"); gcli_pretty_print_diff(comment->diff_text, 0); } printf("=====================================\n"); } static int ask_for_review_state(void) { int state = 0; do { int c; printf("What do you want to do with the review? [Leave a (C)omment, (R)equest changes, (A)ccept, (P)ostpone] "); fflush(stdout); c = getchar(); switch (c) { case EOF: fprintf(stderr, "\nAborted\n"); exit(1); case 'a': case 'A': state = GCLI_REVIEW_ACCEPT_CHANGES; break; case 'r': case 'R': state = GCLI_REVIEW_REQUEST_CHANGES; break; case 'c': case 'C': state = GCLI_REVIEW_COMMENT; break; case 'p': case 'P': state = -1; break; default: fprintf(stderr, "gcli: error: unrecognised answer\n"); break; } } while (!state); return state; } void do_review_session(struct gcli_path const *const path) { struct review_ctx ctx = { .details = { .path = *path, }, .diff_path = get_review_diff_file_name(path), }; ensure_cache_dir_exists(); TAILQ_INIT(&ctx.details.comments); edit_diff(&ctx); printf("\nThese are your comments:\n"); print_comment_list(&ctx.details.comments); if (ctx.details.review_state == 0) ctx.details.review_state = ask_for_review_state(); if (ctx.details.review_state < 0) { printf("Review has been postponed. You can pick up again by rerunning the review subcommand.\n"); return; } if (gcli_pull_create_review(g_clictx, &ctx.details) < 0) errx(1, "gcli: error: failed to create review: %s", gcli_get_error(g_clictx)); } gcli-2.9.1/src/cmd/pulls.c000066400000000000000000001246551507017207500153170ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static void usage(void) { fprintf(stderr, "usage: gcli pulls create [-o owner -r repo] [-f from]\n"); fprintf(stderr, " [-t to] [-T template] [-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] [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"); fprintf(stderr, " -r repo The repository name\n"); fprintf(stderr, " -a When listing PRs, show everything including closed and merged PRs.\n"); fprintf(stderr, " When creating a PR enable automerge.\n"); fprintf(stderr, " -A author Filter pull requests by the given author\n"); fprintf(stderr, " -L label Filter pull requests by the given label\n"); fprintf(stderr, " -M milestone Filter pull requests by the given milestone\n"); fprintf(stderr, " -d Mark newly created PR as a draft\n"); fprintf(stderr, " -f owner:branch Specify the owner and branch of the fork that is the head of a PR.\n"); fprintf(stderr, " -l label Add the given label when creating the PR\n"); fprintf(stderr, " -n number Number of PRs to fetch (-1 = everything)\n"); fprintf(stderr, " -i id ID of PR to perform actions on\n"); fprintf(stderr, " -s Print (sort) in reverse order\n"); fprintf(stderr, " -t branch Specify target branch of the PR\n"); fprintf(stderr, " -T template Use the given file as a template for the pull message\n"); fprintf(stderr, " -y Do not ask for confirmation.\n"); fprintf(stderr, "ACTIONS:\n"); fprintf(stderr, " all Display status, commits, op and checks of the PR\n"); fprintf(stderr, " op Display original post\n"); fprintf(stderr, " status Display PR metadata\n"); fprintf(stderr, " comments Display comments\n"); fprintf(stderr, " notes Alias for notes\n"); fprintf(stderr, " commits Display commits of the PR\n"); fprintf(stderr, " ci Display CI/Pipeline status information about the PR\n"); fprintf(stderr, " merge [-s] [-D] Merge the PR (-s = squash commits, -D = inhibit deleting source branch)\n"); fprintf(stderr, " milestone Assign this PR to a milestone\n"); fprintf(stderr, " milestone -d Clear associated milestones from the PR\n"); fprintf(stderr, " close Close the PR\n"); fprintf(stderr, " reopen Reopen a closed PR\n"); fprintf(stderr, " labels ... Add or remove labels:\n"); fprintf(stderr, " add \n"); fprintf(stderr, " remove \n"); fprintf(stderr, " diff Display changes as diff\n"); fprintf(stderr, " patch Display changes as patch series\n"); fprintf(stderr, " title Change the title of the pull request\n"); fprintf(stderr, " request-review Add as a reviewer of the PR\n"); fprintf(stderr, " assign Assign the PR to \n"); fprintf(stderr, " checkout Do a git-checkout of this PR (GitHub- and GitLab only)\n"); fprintf(stderr, " open Open the PR in a web browser\n"); if (gcli_config_enable_experimental(g_clictx)) fprintf(stderr, " review Start a review of this PR\n"); fprintf(stderr, " reviews List reviews of this PR\n"); if (gcli_config_enable_experimental(g_clictx)) fprintf(stderr, " discussions Show a threaded view of review discussions\n"); fprintf(stderr, " approve Approve this PR\n"); fprintf(stderr, " unapprove Revoke approval on this PR\n"); fprintf(stderr, "\n"); version(); copyright(); } void gcli_print_pulls(enum gcli_output_flags const flags, struct gcli_pull_list const *const list, int const max) { int n; gcli_tbl table; struct gcli_tblcoldef cols[] = { { .name = "NUMBER", .type = GCLI_TBLCOLTYPE_ID, .flags = GCLI_TBLCOL_JUSTIFYR }, { .name = "STATE", .type = GCLI_TBLCOLTYPE_STRING, .flags = GCLI_TBLCOL_STATECOLOURED }, { .name = "MERGED", .type = GCLI_TBLCOLTYPE_BOOL, .flags = 0 }, { .name = "CREATOR", .type = GCLI_TBLCOLTYPE_STRING, .flags = GCLI_TBLCOL_BOLD }, { .name = "NOTES", .type = GCLI_TBLCOLTYPE_INT, .flags = GCLI_TBLCOL_JUSTIFYR }, { .name = "TITLE", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, }; if (list->pulls_size == 0) { puts("No Pull Requests"); return; } /* Determine number of items to print */ if (max < 0 || (size_t)(max) > list->pulls_size) n = list->pulls_size; else n = max; /* Fill the table */ table = gcli_tbl_begin(cols, ARRAY_SIZE(cols)); if (!table) errx(1, "gcli: error: cannot init table"); if (flags & OUTPUT_SORTED) { for (int i = 0; i < n; ++i) { gcli_tbl_add_row(table, list->pulls[n-i-1].number, list->pulls[n-i-1].state, list->pulls[n-i-1].merged, list->pulls[n-i-1].author, list->pulls[n-i-1].comments, list->pulls[n-i-1].title); } } else { for (int i = 0; i < n; ++i) { gcli_tbl_add_row(table, list->pulls[i].number, list->pulls[i].state, list->pulls[i].merged, list->pulls[i].author, list->pulls[i].comments, list->pulls[i].title); } } gcli_tbl_end(table); } int gcli_pull_print_diff(FILE *stream, struct gcli_path const *const path) { return gcli_pull_get_diff(g_clictx, stream, path); } int gcli_pull_print_patch(FILE *stream, struct gcli_path const *const path) { return gcli_pull_get_patch(g_clictx, stream, path); } void gcli_pull_print(struct gcli_pull const *const it) { gcli_dict dict; struct gcli_forge_descriptor const *const forge = gcli_forge(g_clictx); int const quirks = forge->pull_summary_quirks; dict = gcli_dict_begin(); gcli_dict_add(dict, "NUMBER", 0, 0, "%"PRIid, it->number); gcli_dict_add_string(dict, "TITLE", 0, 0, it->title); gcli_dict_add_string(dict, "HEAD", 0, 0, it->head_label); gcli_dict_add_string(dict, "BASE", 0, 0, it->base_label); gcli_dict_add_timestamp(dict, "CREATED", 0, 0, it->created_at); gcli_dict_add_string(dict, "AUTHOR", GCLI_TBLCOL_BOLD, 0, it->author); gcli_dict_add_string(dict, "STATE", GCLI_TBLCOL_STATECOLOURED, 0, it->state); gcli_dict_add(dict, "COMMENTS", 0, 0, "%d", it->comments); if (it->milestone) gcli_dict_add_string(dict, "MILESTONE", 0, 0, it->milestone); if ((quirks & GCLI_PRS_QUIRK_ADDDEL) == 0) /* FIXME: move printing colours into the dictionary printer? */ gcli_dict_add(dict, "ADD:DEL", 0, 0, "%s%d%s:%s%d%s", gcli_setcolour(GCLI_COLOR_GREEN), it->additions, gcli_resetcolour(), gcli_setcolour(GCLI_COLOR_RED), it->deletions, gcli_resetcolour()); if ((quirks & GCLI_PRS_QUIRK_COMMITS) == 0) gcli_dict_add(dict, "COMMITS", 0, 0, "%d", it->commits); if ((quirks & GCLI_PRS_QUIRK_CHANGES) == 0) gcli_dict_add(dict, "CHANGED", 0, 0, "%d", it->changed_files); if ((quirks & GCLI_PRS_QUIRK_AUTOMERGE) == 0) gcli_dict_add_string(dict, "AUTOMERGE", 0, 0, gcli_bool_yesno(it->automerge)); if ((quirks & GCLI_PRS_QUIRK_MERGED) == 0) gcli_dict_add_string(dict, "MERGED", 0, 0, gcli_bool_yesno(it->merged)); gcli_dict_add_string(dict, "MERGEABLE", 0, 0, gcli_bool_yesno(it->mergeable)); if ((quirks & GCLI_PRS_QUIRK_DRAFT) == 0) gcli_dict_add_string(dict, "DRAFT", 0, 0, gcli_bool_yesno(it->draft)); if ((quirks & GCLI_PRS_QUIRK_COVERAGE) == 0 && it->coverage) gcli_dict_add_string(dict, "COVERAGE", 0, 0, it->coverage); if (it->labels_size) { gcli_dict_add_string_list(dict, "LABELS", (char const *const *)it->labels, it->labels_size); } else { gcli_dict_add_string(dict, "LABELS", 0, 0, "none"); } if (it->assignees_size) { gcli_dict_add_string_list(dict, "ASSIGNEES", (char const *const *)it->assignees, it->assignees_size); } if (it->reviewers_size) { gcli_dict_add_string_list(dict, "REVIEWERS", /* cast needed because of nested const */ (char const *const *)it->reviewers, it->reviewers_size); } else { gcli_dict_add_string(dict, "REVIEWERS", 0, 0, "none"); } gcli_dict_end(dict); } void gcli_pull_print_op(struct gcli_pull const *const pull) { if (pull->body) gcli_pretty_print(pull->body, 4, 80, stdout); } static void gcli_print_checks_list(struct gcli_pull_checks_list const *const list) { switch (list->forge_type) { case GCLI_FORGE_GITHUB: github_print_checks((struct github_check_list const *)(list)); break; case GCLI_FORGE_GITLAB: gitlab_print_pipelines((struct gitlab_pipeline_list const*)(list)); break; default: assert(0 && "unreachable"); } } int gcli_pull_checks(struct gcli_path const *const path) { struct gcli_pull_checks_list list = {0}; gcli_forge_type t = gcli_config_get_forge_type(g_clictx); list.forge_type = t; switch (t) { case GCLI_FORGE_GITHUB: case GCLI_FORGE_GITLAB: { int rc = gcli_pull_get_checks(g_clictx, path, &list); if (rc < 0) return rc; gcli_print_checks_list(&list); gcli_pull_checks_free(&list); return 0; } break; default: puts("No checks."); return 0; /* no CI support / not implemented */ } } /** * Get a copy of the first line of the passed string. */ static char * cut_newline(char const *const _it) { char *it = strdup(_it); char *foo = it; while (*foo) { if (*foo == '\n') { *foo = 0; break; } foo += 1; } return it; } void gcli_print_commits(struct gcli_commit_list const *const list) { gcli_tbl table; struct gcli_tblcoldef cols[] = { { .name = "SHA", .type = GCLI_TBLCOLTYPE_STRING, .flags = GCLI_TBLCOL_COLOUREXPL }, { .name = "AUTHOR", .type = GCLI_TBLCOLTYPE_STRING, .flags = GCLI_TBLCOL_BOLD }, { .name = "EMAIL", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "DATE", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "MESSAGE", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, }; if (list->commits_size == 0) { puts("No commits"); return; } table = gcli_tbl_begin(cols, ARRAY_SIZE(cols)); if (!table) errx(1, "gcli: error: could not initialize table"); for (size_t i = 0; i < list->commits_size; ++i) { char *message = cut_newline(list->commits[i].message); gcli_tbl_add_row(table, GCLI_COLOR_YELLOW, list->commits[i].sha, list->commits[i].author, list->commits[i].email, list->commits[i].date, message); free(message); /* message is copied by the function above */ } gcli_tbl_end(table); } int gcli_pull_commits(struct gcli_path const *const path) { struct gcli_commit_list commits = {0}; int rc = 0; rc = gcli_pull_get_commits(g_clictx, path, &commits); if (rc < 0) return rc; gcli_print_commits(&commits); gcli_commits_free(&commits); return rc; } static void pull_init_user_file(struct gcli_ctx *ctx, FILE *stream, void *_opts) { struct gcli_submit_pull_options *opts = _opts; (void) ctx; /* recalled message or template */ if (opts->body) { fputs(opts->body, stream); free(opts->body); opts->body = NULL; } fprintf( stream, "! PR TITLE : %s\n" "! Enter PR comments above.\n" "! All lines starting with '!' will be discarded.\n" "!\n" "! vim: ft=markdown\n", opts->title); } static char * gcli_pull_get_user_message(struct gcli_submit_pull_options *opts) { return gcli_editor_get_user_message(g_clictx, pull_init_user_file, opts); } /* Hack to retrieve the owner / name of the target repository. * We may have to change this thing in the future as it is kinda silly. */ static char const * pull_request_target_owner(struct gcli_path const *const repo_path) { assert(repo_path->kind == GCLI_PATH_DEFAULT); return repo_path->as_default.owner; } static char const * pull_request_target_repo(struct gcli_path const *const repo_path) { assert(repo_path->kind == GCLI_PATH_DEFAULT); return repo_path->as_default.repo; } static int create_pull(struct gcli_submit_pull_options *const opts, bool always_yes) { int rc = 0; gcli_cmd_recall_message_interactive(&opts->body); opts->body = gcli_pull_get_user_message(opts); printf("The following PR will be created:\n" "\n" "TITLE : %s\n" "BASE : %s\n" "HEAD : %s\n" "IN : %s/%s\n" "MESSAGE :\n", opts->title, opts->target_branch, opts->from, pull_request_target_owner(&opts->target_repo), pull_request_target_repo(&opts->target_repo)); if (opts->body) gcli_pretty_print(opts->body, 4, 80, stdout); else puts("No message."); if (!always_yes) { if (!gcli_yesno("Do you want to continue?")) errx(1, "gcli: PR aborted."); } rc = gcli_pull_submit(g_clictx, opts); if (rc < 0) { fprintf(stderr, "gcli: error: failed to create pull: %s\n", gcli_get_error(g_clictx)); /* store message away in `gcli_message` */ if (opts->body) gcli_cmd_save_message(opts->body); } return rc; } static char const * pr_try_derive_head(void) { char const *account; gcli_sv branch = {0}; if ((account = gcli_config_get_account_name(g_clictx)) == NULL) { errx(1, "gcli: error: Cannot derive PR head. Please specify --from or set the" " account in the users gcli config file.\n" "gcli: note: %s", gcli_get_error(g_clictx)); } if (!(branch = gcli_gitconfig_get_current_branch()).length) { errx(1, "gcli: error: Cannot derive PR head. Please specify --from or, if you" " are in »detached HEAD« state, checkout the branch you" " want to pull request."); } return gcli_asprintf("%s:"SV_FMT, account, SV_ARGS(branch)); } static char * derive_head(void) { char const *account; gcli_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 gcli_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->target_repo.as_default.owner) opts->target_repo.as_default.owner = gcli_cmd_prompt("Owner", deflt_owner); if (!opts->target_repo.as_default.repo) opts->target_repo.as_default.repo = gcli_cmd_prompt("Repository", deflt_repo); if (!opts->target_branch) { char *tmp = NULL; gcli_sv base; base = gcli_config_get_base(g_clictx); if (base.length != 0) tmp = gcli_sv_to_cstr(base); opts->target_branch = gcli_cmd_prompt("To Branch", tmp); free(tmp); tmp = NULL; } /* Meta */ opts->title = gcli_cmd_prompt("Title", GCLI_PROMPT_RESULT_MANDATORY); opts->automerge = gcli_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) return EXIT_FAILURE; return EXIT_SUCCESS; } static int subcommand_pull_create(int argc, char *argv[]) { int ch; struct gcli_submit_pull_options opts = {0}; bool always_yes = 0; const struct option options[] = { { .name = "from", .has_arg = required_argument, .flag = NULL, .val = 'f' }, { .name = "to", .has_arg = required_argument, .flag = NULL, .val = 't' }, { .name = "owner", .has_arg = required_argument, .flag = NULL, .val = 'o' }, { .name = "repo", .has_arg = required_argument, .flag = NULL, .val = 'r' }, { .name = "draft", .has_arg = no_argument, .flag = &opts.draft, .val = 1 }, { .name = "label", .has_arg = required_argument, .flag = NULL, .val = 'l' }, { .name = "automerge", .has_arg = required_argument, .flag = NULL, .val = 'a' }, { .name = "reviewer", .has_arg = required_argument, .flag = NULL, .val = 'R' }, { .name = "template", .has_arg = required_argument, .flag = NULL, .val = 'T' }, {0}, }; always_yes = gcli_cmd_should_do_always_yes(); while ((ch = getopt_long(argc, argv, "ayf:t:do:r:l:R:T:", options, NULL)) != -1) { switch (ch) { case 'f': opts.from = optarg; break; case 't': opts.target_branch = optarg; break; case 'd': opts.draft = 1; break; case 'o': opts.target_repo.as_default.owner = optarg; break; case 'r': opts.target_repo.as_default.repo = optarg; break; case 'l': /* add a label */ opts.labels = realloc( 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; case 'a': opts.automerge = true; break; case 'T': /* template file */ if (gcli_read_file(optarg, &opts.body) < 0) { err(1, "gcli: error: failed to read file '%s'", optarg); } break; default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; if (argc == 0) return subcommand_pull_create_interactive(&opts); if (!opts.from) opts.from = pr_try_derive_head(); if (!opts.target_branch) { gcli_sv base = gcli_config_get_base(g_clictx); if (base.length == 0) errx(1, "gcli: error: PR base is missing. Please either specify " "--to branch-name or set pr.base in .gcli."); opts.target_branch = gcli_sv_to_cstr(base); } check_path(&opts.target_repo); if (argc != 1) { fprintf(stderr, "gcli: error: Missing title to PR\n"); usage(); return EXIT_FAILURE; } opts.title = argv[0]; if (create_pull(&opts, always_yes) < 0) return EXIT_FAILURE; free(opts.labels); return EXIT_SUCCESS; } /* Forward declaration */ static int handle_pull_actions(int argc, char *argv[], struct gcli_path const *const path); int subcommand_pulls(int argc, char *argv[]) { char *endptr = NULL; enum gcli_output_flags flags = 0; int ch = 0; int n = 30; /* how many prs to fetch at least */ struct gcli_path pull = {0}; struct gcli_pull_fetch_details details = {0}; struct gcli_pull_list pulls = {0}; /* detect whether we wanna create a PR */ if (argc > 1 && (strcmp(argv[1], "create") == 0)) { shift(&argc, &argv); return subcommand_pull_create(argc, argv); } struct option const options[] = { { .name = "all", .has_arg = no_argument, .flag = NULL, .val = 'a' }, { .name = "author", .has_arg = no_argument, .flag = NULL, .val = 'A' }, { .name = "label", .has_arg = no_argument, .flag = NULL, .val = 'L' }, { .name = "milestone", .has_arg = no_argument, .flag = NULL, .val = 'M' }, { .name = "sorted", .has_arg = no_argument, .flag = NULL, .val = 's' }, { .name = "count", .has_arg = required_argument, .flag = NULL, .val = 'n' }, { .name = "repo", .has_arg = required_argument, .flag = NULL, .val = 'r' }, { .name = "owner", .has_arg = required_argument, .flag = NULL, .val = 'o' }, { .name = "id", .has_arg = required_argument, .flag = NULL, .val = 'i' }, {0}, }; /* Parse commandline options */ while ((ch = getopt_long(argc, argv, "+n:o:r:i:asA:L:M:", options, NULL)) != -1) { switch (ch) { case 'o': pull.as_default.owner = optarg; break; case 'r': pull.as_default.repo = optarg; break; case 'i': { pull.as_default.id = strtoul(optarg, &endptr, 10); if (endptr != (optarg + strlen(optarg))) err(1, "gcli: error: cannot parse pr number »%s«", optarg); if (pull.as_default.id == 0) errx(1, "gcli: error: pr number is out of range"); } break; case 'n': { n = strtoul(optarg, &endptr, 10); if (endptr != (optarg + strlen(optarg))) err(1, "gcli: error: cannot parse pr count »%s«", optarg); if (n < -1) errx(1, "gcli: error: pr count is out of range"); if (n == 0) errx(1, "gcli: error: pr count must not be zero"); } break; case 'a': { details.all = true; } break; case 'A': { details.author = optarg; } break; case 'L': { details.label = optarg; } break; case 'M': { details.milestone = optarg; } break; case 's': { flags |= OUTPUT_SORTED; } break; case '?': default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; check_path(&pull); /* In case no explicit PR number was specified, list all * open PRs and exit */ if (pull.as_default.id == 0) { char *search_term = NULL; /* Trailing arguments indicate a search term */ if (argc) search_term = gcli_join_with((char const *const *)argv, argc, " "); details.search_term = search_term; if (gcli_search_pulls(g_clictx, &pull, &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; } /* If a PR number was given, require -a to be unset */ if (details.all || details.author) { fprintf(stderr, "gcli: error: -a and -A cannot be combined with operations on a PR\n"); usage(); return EXIT_FAILURE; } /* Hand off to actions handling */ return handle_pull_actions(argc, argv, &pull); } static int action_all(struct gcli_path const *path, struct gcli_pull *pull, int *argc, char **argv[]) { (void) argc; (void) argv; /* Print meta */ gcli_pull_print(pull); /* OP */ puts("\nORIGINAL POST"); gcli_pull_print_op(pull); /* Commits */ puts("\nCOMMITS"); if (gcli_pull_commits(path)) { fprintf(stderr, "gcli: error: failed to fetch pull request checks: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } /* Checks */ puts("\nCHECKS"); if (gcli_pull_checks(path)) { fprintf(stderr, "gcli: error: failed to fetch pull request checks: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_op(struct gcli_path const *const path, struct gcli_pull *pull, int *argc, char **argv[]) { (void) path; (void) argc; (void) argv; gcli_pull_print_op(pull); return GCLI_EX_OK; } static int action_status(struct gcli_path const *const path, struct gcli_pull *pull, int *argc, char **argv[]) { (void) path; (void) argc; (void) argv; gcli_pull_print(pull); return GCLI_EX_OK; } static int action_commits(struct gcli_path const *const path, struct gcli_pull *pull, int *argc, char **argv[]) { (void) pull; (void) argc; (void) argv; int const rc = gcli_pull_commits(path); if (rc < 0) { fprintf(stderr, "gcli: error: failed to fetch commits: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_diff(struct gcli_path const *const path, struct gcli_pull *pull, int *argc, char **argv[]) { (void) pull; (void) argc; (void) argv; if (gcli_pull_print_diff(stdout, path) < 0) { fprintf(stderr, "gcli: error: failed to fetch diff: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_patch(struct gcli_path const *const path, struct gcli_pull *pull, int *argc, char **argv[]) { (void) pull; (void) argc; (void) argv; if (gcli_pull_print_patch(stdout, path) < 0) { fprintf(stderr, "gcli: error: failed to fetch patch: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } /* aliased to notes */ static int action_comments(struct gcli_path const *const path, struct gcli_pull *pull, int *argc, char **argv[]) { (void) pull; (void) argc; (void) argv; if (gcli_pull_comments(path) < 0) { fprintf(stderr, "gcli: error: failed to fetch pull comments: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_ci(struct gcli_path const *const path, struct gcli_pull *pull, int *argc, char **argv[]) { (void) pull; (void) argc; (void) argv; if (gcli_pull_checks(path) < 0) { fprintf(stderr, "gcli: error: failed to fetch pull request checks: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_merge(struct gcli_path const *const path, struct gcli_pull *pull, int *argc, char **argv[]) { enum gcli_merge_flags flags = GCLI_PULL_MERGE_DELETEHEAD; (void) pull; /* Default behaviour */ if (gcli_config_pr_inhibit_delete_source_branch(g_clictx)) flags = 0; if (*argc > 1) { /* Check whether the user intends a squash-merge * and/or wants to delete the source branch of the * PR */ char const *word = (*argv)[1]; if (strcmp(word, "-s") == 0 || strcmp(word, "--squash") == 0) { *argc -= 1; *argv += 1; flags |= GCLI_PULL_MERGE_SQUASH; } else if (strcmp(word, "-D") == 0 || strcmp(word, "--inhibit-delete") == 0) { *argc -= 1; *argv += 1; flags &= ~GCLI_PULL_MERGE_DELETEHEAD; } } if (gcli_pull_merge(g_clictx, path, flags) < 0) { fprintf(stderr, "gcli: error: failed to merge pull request: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_close(struct gcli_path const *const path, struct gcli_pull *pull, int *argc, char **argv[]) { (void) pull; (void) argc; (void) argv; if (gcli_pull_close(g_clictx, path) < 0) { fprintf(stderr, "gcli: error: failed to close pull request: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_reopen(struct gcli_path const *const path, struct gcli_pull *pull, int *argc, char **argv[]) { (void) pull; (void) argc; (void) argv; if (gcli_pull_reopen(g_clictx, path) < 0) { fprintf(stderr, "gcli: error: failed to reopen pull request: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_labels(struct gcli_path const *const path, struct gcli_pull *pull, int *argc, char **argv[]) { const char **add_labels = NULL; size_t add_labels_size = 0; const char **remove_labels = NULL; size_t remove_labels_size = 0; int rc = 0; (void) pull; if (*argc == 0) { fprintf(stderr, "gcli: error: expected label action\n"); return GCLI_EX_USAGE; } parse_labels_options(argc, argv, &add_labels, &add_labels_size, &remove_labels, &remove_labels_size); /* actually go about deleting and adding the labels */ if (add_labels_size) { rc = gcli_pull_add_labels(g_clictx, path, add_labels, add_labels_size); if (rc < 0) { fprintf(stderr, "gcli: error: failed to add labels: %s\n", gcli_get_error(g_clictx)); rc = GCLI_EX_DATAERR; goto bail; } } if (remove_labels_size) { rc = gcli_pull_remove_labels(g_clictx, path, remove_labels, remove_labels_size); if (rc < 0) { fprintf(stderr, "gcli: error: failed to remove labels: %s\n", gcli_get_error(g_clictx)); rc = GCLI_EX_DATAERR; goto bail; } } bail: free(add_labels); free(remove_labels); return rc; } static int action_milestone(struct gcli_path const *const path, struct gcli_pull *pull, int *argc, char **argv[]) { char const *arg; (void) pull; if (*argc < 2) { fprintf(stderr, "gcli: error: missing arguments to milestone action\n"); return GCLI_EX_USAGE; } *argc -= 1; *argv += 1; arg = **argv; if (strcmp(arg, "-d") == 0) { if (gcli_pull_clear_milestone(g_clictx, path) < 0) { fprintf(stderr, "gcli: error: failed to clear milestone: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } } else { int milestone_id = 0; char *endptr; int rc = 0; milestone_id = strtoul(arg, &endptr, 10); if (endptr != arg + strlen(arg)) { fprintf(stderr, "gcli: error: cannot parse milestone id »%s«\n", arg); return GCLI_EX_DATAERR; } rc = gcli_pull_set_milestone(g_clictx, path, milestone_id); if (rc < 0) { fprintf(stderr, "gcli: error: failed to set milestone: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } } return GCLI_EX_OK; } static int action_request_review(struct gcli_path const *const path, struct gcli_pull *pull, int *argc, char **argv[]) { char const *reviewer; int rc; (void) pull; if (*argc < 2) { fprintf(stderr, "gcli: error: missing user name for reviewer\n"); return GCLI_EX_USAGE; } *argc -= 1; *argv += 1; reviewer = **argv; rc = gcli_pull_add_reviewer(g_clictx, path, reviewer); if (rc < 0) { fprintf(stderr, "gcli: error: failed to request review: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_assign(struct gcli_path const *const path, struct gcli_pull const *pull, int *argc, char **argv[]) { char const *assignee; int rc; (void) pull; if (*argc < 2) { fprintf(stderr, "gcli: error: missing assignee\n"); return GCLI_EX_USAGE; } *argc -= 1; *argv += 1; assignee = **argv; rc = gcli_pull_assign(g_clictx, path, assignee); if (rc < 0) { fprintf(stderr, "gcli: error: failed to assign: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_title(struct gcli_path const *const path, struct gcli_pull *pull, int *argc, char **argv[]) { int rc = 0; (void) pull; if (*argc < 2) { fprintf(stderr, "gcli: error: missing title\n"); return GCLI_EX_USAGE; } rc = gcli_pull_set_title(g_clictx, path, (*argv)[1]); if (rc < 0) { errx(1, "gcli: error: failed to update review title: %s", gcli_get_error(g_clictx)); } *argc -= 1; *argv += 1; return GCLI_EX_OK; } static int action_review(struct gcli_path const *const path, struct gcli_pull *pull, int *argc, char **argv[]) { (void) pull; (void) argc; (void) argv; if (gcli_config_enable_experimental(g_clictx) == false) { fprintf( stderr, "gcli: error: review is not available because it is " "considered experimental. To enable this feature set " "enable-experimental in your gcli config file or " "set GCLI_ENABLE_EXPERIMENTAL in your environment.\n" ); return GCLI_EX_DATAERR; } do_review_session(path); return GCLI_EX_OK; } static int action_checkout(struct gcli_path const *const path, struct gcli_pull *pull, int *argc, char **argv[]) { char const *remote; int rc = 0; (void) pull; (void) argc; (void) argv; rc = gcli_config_get_remote(g_clictx, &remote); if (rc < 0) { fprintf(stderr, "gcli: error: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } if (gcli_pull_checkout(g_clictx, remote, path) < 0) { fprintf( stderr, "gcli: error: failed to checkout pull: %s\n", gcli_get_error(g_clictx) ); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } static int action_open(struct gcli_path const *const path, struct gcli_pull const *const pull, int *argc, char **argv[]) { int rc; (void) path; (void) argc; (void) argv; rc = gcli_cmd_open_url(pull->web_url); if (rc < 0) { fprintf(stderr, "gcli: error: failed to open url\n"); return GCLI_EX_DATAERR; } return GCLI_EX_OK; } void gcli_pull_reviews_print(struct gcli_pull_reviews *list) { gcli_tbl table; struct gcli_tblcoldef columns[] = { { .name = "ID", .type = GCLI_TBLCOLTYPE_ID, .flags = 0 }, { .name = "STATE", .type = GCLI_TBLCOLTYPE_STRING, .flags = GCLI_TBLCOL_STATECOLOURED }, { .name = "DATE", .type = GCLI_TBLCOLTYPE_TIME_T, .flags = 0 }, { .name = "AUTHOR", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, }; if (list->reviews_size == 0) { printf("No reviews.\n"); return; } table = gcli_tbl_begin(columns, ARRAY_SIZE(columns)); for (size_t i = 0; i < list->reviews_size; ++i) { gcli_tbl_add_row( table, list->reviews[i].id, list->reviews[i].state, list->reviews[i].submitted_at, list->reviews[i].author); } gcli_tbl_end(table); } static void print_comment(struct gcli_pull_review_comment const *c, int const indent) { char *timebuf = NULL; int const shift_width = 4; int const shift = shift_width * indent; int rc = 0; rc = gcli_format_as_localtime(g_clictx, c->created_at, &timebuf); if (rc < 0) { fprintf(stderr, "gcli: error: failed to format timestamp: %s\n", gcli_get_error(g_clictx)); return; } printf("%*.*sAUTHOR : %s%s%s\n" "%*.*s DATE : %s\n" "%*.*s FILE : %s\n", shift, shift, "", gcli_setbold(), c->author, gcli_resetbold(), shift, shift, "", timebuf, shift, shift, "", c->path); /* print diff if one is attached and we are on the root comment */ if (c->diff_hunk && indent == 0) { printf("\n"); gcli_pretty_print_diff(c->diff_hunk, shift + 9); } gcli_pretty_print(c->body, shift + shift_width, 80, stdout); free(timebuf); timebuf = NULL; } static void print_thread(struct gcli_pull_review_thread const *thd, int indent) { struct gcli_pull_review_comment *comment = NULL; TAILQ_FOREACH(comment, thd, next) { print_comment(comment, indent); print_thread(&comment->replies, indent + 1); } } static void gcli_pull_review_threads_print(struct gcli_pull_review_thread const *const thd) { if (TAILQ_EMPTY(thd)) { printf("No comments\n"); return; } print_thread(thd, 0); } static int action_reviews(struct gcli_path const *const path, struct gcli_pull const *const pull, int *argc, char **argv[]) { int rc = 0; struct gcli_pull_reviews reviews = {0}; (void) pull; (void) argc; (void) argv; rc = gcli_pull_get_reviews(g_clictx, path, &reviews); if (rc < 0) { fprintf(stderr, "gcli: error: failed to fetch reviews: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } gcli_pull_reviews_print(&reviews); gcli_pull_reviews_free(&reviews); return GCLI_EX_OK; } static int action_discussion(struct gcli_path const *const path, struct gcli_pull const *const pull, int *argc, char **argv[]) { int rc = 0; struct gcli_pull_review_thread root = {0}; (void) pull; (void) argc; (void) argv; if (gcli_config_enable_experimental(g_clictx) == false) { fprintf( stderr, "gcli: error: discussion is not available because it is " "considered experimental. To enable this feature set " "enable-experimental in your gcli config file or " "set GCLI_ENABLE_EXPERIMENTAL in your environment.\n" ); return GCLI_EX_DATAERR; } rc = gcli_pull_get_review_threads(g_clictx, path, &root); if (rc < 0) { fprintf(stderr, "gcli: error: failed to fetch reviews: %s\n", gcli_get_error(g_clictx)); return GCLI_EX_DATAERR; } gcli_pull_review_threads_print(&root); gcli_pull_review_thread_free(&root); return GCLI_EX_OK; } static void approval_init(struct gcli_ctx *ctx, FILE *f, void *data) { (void) ctx; (void) data; fprintf(f, "\n"); fprintf(f, "! Enter your message above, save and exit.\n"); fprintf(f, "! All lines starting with '!' will be discarded.\n"); } static int approval_action(struct gcli_path const *const path, int (*fn)(struct gcli_ctx *ctx, struct gcli_path const *, char const *)) { int rc; char *message = NULL; if (gcli_yesno("Enter a message?")) { message = gcli_editor_get_user_message(g_clictx, approval_init, NULL); if (message == NULL) { fprintf(stderr, "gcli: message empty, aborting.\n"); return GCLI_EX_DATAERR; } } rc = fn(g_clictx, path, message); if (rc < 0) { fprintf(stderr, "gcli: error: failed to update pull: %s\n", gcli_get_error(g_clictx)); rc = GCLI_EX_DATAERR; } else { rc = GCLI_EX_OK; } free(message); return rc; } static int action_approve(struct gcli_path const *const path, struct gcli_pull const *const pull, int *argc, char **argv[]) { (void) pull; (void) argc; (void) argv; return approval_action(path, gcli_pull_approve); } static int action_unapprove(struct gcli_path const *const path, struct gcli_pull const *const pull, int *argc, char **argv[]) { (void) pull; (void) argc; (void) argv; return approval_action(path, gcli_pull_unapprove); } struct gcli_cmd_actions gcli_pull_actions = { .fetch_item = (gcli_cmd_action_fetcher)gcli_get_pull, .free_item = (gcli_cmd_action_freeer)gcli_pull_free, .item_size = sizeof(struct gcli_pull), .defs = { { .name = "all", .needs_item = true, .handler = (gcli_cmd_action_handler) action_all, }, { .name = "op", .needs_item = true, .handler = (gcli_cmd_action_handler) action_op, }, { .name = "status", .needs_item = true, .handler = (gcli_cmd_action_handler) action_status, }, { .name = "commits", .needs_item = false, .handler = (gcli_cmd_action_handler) action_commits, }, { .name = "diff", .needs_item = false, .handler = (gcli_cmd_action_handler) action_diff, }, { .name = "patch", .needs_item = false, .handler = (gcli_cmd_action_handler) action_patch, }, { .name = "notes", .needs_item = false, .handler = (gcli_cmd_action_handler) action_comments, .use_pager = true, }, { .name = "comments", .needs_item = false, .handler = (gcli_cmd_action_handler) action_comments, .use_pager = true, }, { .name = "ci", .needs_item = false, .handler = (gcli_cmd_action_handler) action_ci, }, { .name = "merge", .needs_item = false, .handler = (gcli_cmd_action_handler) action_merge, }, { .name = "close", .needs_item = false, .handler = (gcli_cmd_action_handler) action_close, }, { .name = "reopen", .needs_item = false, .handler = (gcli_cmd_action_handler) action_reopen, }, { .name = "labels", .needs_item = false, .handler = (gcli_cmd_action_handler) action_labels, }, { .name = "milestone", .needs_item = false, .handler = (gcli_cmd_action_handler) action_milestone, }, { .name = "request-review", .needs_item = false, .handler = (gcli_cmd_action_handler) action_request_review, }, { .name = "assign", .needs_item = false, .handler = (gcli_cmd_action_handler) action_assign, }, { .name = "title", .needs_item = false, .handler = (gcli_cmd_action_handler) action_title, }, { .name = "review", .needs_item = false, .handler = (gcli_cmd_action_handler) action_review, }, { .name = "checkout", .needs_item = false, .handler = (gcli_cmd_action_handler) action_checkout, }, { .name = "open", .needs_item = true, .handler = (gcli_cmd_action_handler) action_open, }, { .name = "reviews", .needs_item = false, .handler = (gcli_cmd_action_handler) action_reviews, }, { .name = "discussions", .needs_item = false, .handler = (gcli_cmd_action_handler) action_discussion, }, { .name = "approve", .needs_item = false, .handler = (gcli_cmd_action_handler) action_approve, }, { .name = "unapprove", .needs_item = false, .handler = (gcli_cmd_action_handler) action_unapprove, }, }, }; /** Handling routine for Pull Request related actions specified on the * command line. Make sure that the usage at the top is consistent * with the actions implemented here. */ static int handle_pull_actions(int argc, char *argv[], struct gcli_path const *const path) { int const rc = gcli_cmd_actions_handle(&gcli_pull_actions, path, &argc, &argv); if (rc == GCLI_EX_USAGE) usage(); if (rc) return 1; return 0; } gcli-2.9.1/src/cmd/releases.c000066400000000000000000000344721507017207500157600ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include #include #include static void usage(void) { fprintf(stderr, "usage: gcli releases create [-o owner -r repo] [-n name] " "[-y] [-d] [-p] [-a asset]\n"); fprintf(stderr, " [-c commitish] [-t tag]\n"); fprintf(stderr, " [-T message-template.md]\n"); fprintf(stderr, " gcli releases delete [-o owner -r repo] [-y] id\n"); fprintf(stderr, " gcli releases [-o owner -r repo] [-n number] [-s] [-l]\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -o owner The repository owner\n"); fprintf(stderr, " -r repo The repository name\n"); fprintf(stderr, " -a asset Path to file to upload as release asset\n"); fprintf(stderr, " -c committish A ref/commit/branch that the release is created from\n"); fprintf(stderr, " -d Mark as a release draft\n"); fprintf(stderr, " -l Print a long list instead of a short table\n"); fprintf(stderr, " -n name Name of the created release\n"); fprintf(stderr, " -n number Number of releases to fetch (-1 = everything)\n"); fprintf(stderr, " -p Mark as a prerelease\n"); fprintf(stderr, " -t tag Name for new tag\n"); fprintf(stderr, " -y Do not ask for confirmation\n"); fprintf(stderr, " -T path Use the given file as a template file for the release message\n"); fprintf(stderr, "\n"); version(); copyright(); } static void gcli_print_release(enum gcli_output_flags const flags, struct gcli_release const *const it) { gcli_dict dict; (void) flags; dict = gcli_dict_begin(); gcli_dict_add(dict, "ID", 0, 0, "%s", it->id); gcli_dict_add(dict, "NAME", 0, 0, "%s", it->name); gcli_dict_add(dict, "AUTHOR", 0, 0, "%s", it->author); gcli_dict_add_timestamp(dict, "DATE", 0, 0, it->date); gcli_dict_add_string(dict, "DRAFT", 0, 0, gcli_bool_yesno(it->draft)); gcli_dict_add_string(dict, "PRERELEASE", 0, 0, gcli_bool_yesno(it->prerelease)); gcli_dict_add_string(dict, "ASSETS", 0, 0, ""); /* asset urls */ for (size_t i = 0; i < it->assets_size; ++i) { gcli_dict_add(dict, "", 0, 0, "• %s", it->assets[i].name); gcli_dict_add(dict, "", 0, 0, " %s", it->assets[i].url); } gcli_dict_end(dict); /* body */ if (it->body) { putchar('\n'); gcli_pretty_print(it->body, 13, 80, stdout); } putchar('\n'); } static void gcli_releases_print_long(enum gcli_output_flags const flags, struct gcli_release_list const *const list, int const max) { int n; /* Determine how many items to print */ if (max < 0 || (size_t)(max) > list->releases_size) n = list->releases_size; else n = max; if (flags & OUTPUT_SORTED) { for (int i = 0; i < n; ++i) gcli_print_release(flags, &list->releases[n-i-1]); } else { for (int i = 0; i < n; ++i) gcli_print_release(flags, &list->releases[i]); } } static void gcli_releases_print_short(enum gcli_output_flags const flags, struct gcli_release_list const *const list, int const max) { size_t n; gcli_tbl table; struct gcli_tblcoldef cols[] = { { .name = "ID", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "DATE", .type = GCLI_TBLCOLTYPE_TIME_T, .flags = 0 }, { .name = "DRAFT", .type = GCLI_TBLCOLTYPE_BOOL, .flags = 0 }, { .name = "PRERELEASE", .type = GCLI_TBLCOLTYPE_BOOL, .flags = 0 }, { .name = "NAME", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, }; if (max < 0 || (size_t)(max) > list->releases_size) n = list->releases_size; else n = max; table = gcli_tbl_begin(cols, ARRAY_SIZE(cols)); if (!table) errx(1, "gcli: error: could not init table"); if (flags & OUTPUT_SORTED) { for (size_t i = 0; i < n; ++i) { gcli_tbl_add_row(table, list->releases[n-i-1].id, list->releases[n-i-1].date, list->releases[n-i-1].draft, list->releases[n-i-1].prerelease, list->releases[n-i-1].name); } } else { for (size_t i = 0; i < n; ++i) { gcli_tbl_add_row(table, list->releases[i].id, list->releases[i].date, list->releases[i].draft, list->releases[i].prerelease, list->releases[i].name); } } gcli_tbl_end(table); } void gcli_releases_print(enum gcli_output_flags const flags, struct gcli_release_list const *const list, int const max) { if (list->releases_size == 0) { puts("No releases"); return; } if (flags & OUTPUT_LONG) gcli_releases_print_long(flags, list, max); else gcli_releases_print_short(flags, list, max); } static void releasemsg_init(struct gcli_ctx *ctx, FILE *f, void *_data) { struct gcli_create_release_args *const info = _data; (void) ctx; /* paste template if one has been specified */ if (info->body) { fputs(info->body, f); /* clear out the template message, it's going to be * filled in by the caller */ free(info->body); info->body = NULL; } fprintf(f, "! Enter your release notes above, save and exit.\n" "! All lines with a leading '!' are discarded and will not\n" "! appear in the final release note.\n!\n"); if (info->repo_path.kind == GCLI_PATH_DEFAULT) { fprintf(f, "! IN : %s/%s\n", info->repo_path.as_default.owner, info->repo_path.as_default.repo); } fprintf(f, "! TAG NAME : %s\n", info->tag); if (info->name) { fprintf(f, "! NAME : %s\n", info->name); } fprintf(f, "!\n" "! vim: ft=markdown\n"); } static char * get_release_message(struct gcli_create_release_args const *info) { return gcli_editor_get_user_message(g_clictx, releasemsg_init, (void *)info); } static int subcommand_releases_create(int argc, char *argv[]) { struct gcli_create_release_args release = {0}; int ch, rc; bool always_yes = false; struct option const options[] = { { .name = "yes", .has_arg = no_argument, .flag = NULL, .val = 'y' }, { .name = "draft", .has_arg = no_argument, .flag = NULL, .val = 'd' }, { .name = "prerelease", .has_arg = no_argument, .flag = NULL, .val = 'p' }, { .name = "name", .has_arg = required_argument, .flag = NULL, .val = 'n' }, { .name = "tag", .has_arg = required_argument, .flag = NULL, .val = 't' }, { .name = "commitish", .has_arg = required_argument, .flag = NULL, .val = 'c' }, { .name = "repo", .has_arg = required_argument, .flag = NULL, .val = 'r' }, { .name = "owner", .has_arg = required_argument, .flag = NULL, .val = 'o' }, { .name = "asset", .has_arg = required_argument, .flag = NULL, .val = 'a' }, { .name = "template", .has_arg = required_argument, .flag = NULL, .val = 'T' }, {0}, }; always_yes = gcli_cmd_should_do_always_yes(); while ((ch = getopt_long(argc, argv, "ydpn:t:c:r:o:a:T:", options, NULL)) != -1) { switch (ch) { case 'd': release.draft = true; break; case 'p': release.prerelease = true; break; case 'n': release.name = optarg; break; case 't': release.tag = optarg; break; case 'c': release.commitish = optarg; break; case 'r': release.repo_path.as_default.repo = optarg; break; case 'o': release.repo_path.as_default.owner = optarg; break; case 'a': { struct gcli_release_asset_upload asset = { .path = optarg, .name = optarg, .label = "unused", }; if (gcli_release_push_asset(g_clictx, &release, asset) < 0) { errx(1, "gcli: error: failed to add asset: %s", gcli_get_error(g_clictx)); } } break; case 'y': { always_yes = true; } break; case 'T': { if (release.body) errx(1, "gcli: error: cannot specify -T twice"); rc = gcli_read_file(optarg, &release.body); if (rc < 0) errx(1, "gcli: cannot open file '%s'", optarg); } break; default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; check_path(&release.repo_path); /* make sure we have a tag for the release */ if (!release.tag) { fprintf(stderr, "gcli: error: releases create: missing tag name\n"); usage(); return EXIT_FAILURE; } /* -y without -T doesn't make sense */ if (always_yes && release.body == NULL) { fprintf(stderr, "gcli: error: cannot have empty release message " "together with --yes\n"); usage(); return EXIT_FAILURE; } /* interactive part, skip if non-interactive */ if (!always_yes) { release.body = get_release_message(&release); if (release.body == NULL) errx(1, "gcli: empty message. aborting."); if (!gcli_yesno("Do you want to create this release?")) errx(1, "gcli: Aborted by user"); } if (gcli_create_release(g_clictx, &release) < 0) { errx(1, "gcli: error: failed to create release: %s", gcli_get_error(g_clictx)); } free(release.body); release.body = NULL; return EXIT_SUCCESS; } static int subcommand_releases_delete(int argc, char *argv[]) { int ch = 0; bool always_yes = false; struct gcli_path repo_path = {0}; struct option const options[] = { { .name = "repo", .has_arg = required_argument, .flag = NULL, .val = 'r' }, { .name = "owner", .has_arg = required_argument, .flag = NULL, .val = 'o' }, { .name = "yes", .has_arg = no_argument, .flag = NULL, .val = 'y' }, {0} }; always_yes = gcli_cmd_should_do_always_yes(); while ((ch = getopt_long(argc, argv, "yo:r:", options, NULL)) != -1) { switch (ch) { case 'o': repo_path.as_default.owner = optarg; break; case 'r': repo_path.as_default.repo = optarg; break; case 'y': always_yes = true; break; case '?': default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; check_path(&repo_path); /* make sure the user supplied the release id */ if (argc != 1) { fprintf(stderr, "gcli: error: releases delete: missing release id\n"); usage(); return EXIT_FAILURE; } if (!always_yes) if (!gcli_yesno("Are you sure you want to delete this release?")) errx(1, "gcli: Aborted by user"); if (gcli_delete_release(g_clictx, &repo_path, argv[0]) < 0) { errx(1, "gcli: error: failed to delete the release: %s", gcli_get_error(g_clictx)); } return EXIT_SUCCESS; } static struct { char const *name; int (*fn)(int, char **); } releases_subcommands[] = { { .name = "delete", .fn = subcommand_releases_delete }, { .name = "create", .fn = subcommand_releases_create }, }; int subcommand_releases(int argc, char *argv[]) { enum gcli_output_flags flags = 0; int ch, count = 30; struct gcli_path repo_path = {0}; struct gcli_release_list releases = {0}; if (argc > 1) { for (size_t i = 0; i < ARRAY_SIZE(releases_subcommands); ++i) { if (strcmp(releases_subcommands[i].name, argv[1]) == 0) return releases_subcommands[i].fn(argc - 1, argv + 1); } } /* List releases if none of the subcommands matched */ struct option const options[] = { { .name = "repo", .has_arg = required_argument, .flag = NULL, .val = 'r' }, { .name = "owner", .has_arg = required_argument, .flag = NULL, .val = 'o' }, { .name = "count", .has_arg = required_argument, .flag = NULL, .val = 'n' }, { .name = "long", .has_arg = no_argument, .flag = NULL, .val = 'l' }, { .name = "sorted", .has_arg = no_argument, .flag = NULL, .val = 's' }, {0} }; while ((ch = getopt_long(argc, argv, "sn:o:r:l", options, NULL)) != -1) { switch (ch) { case 'o': repo_path.as_default.owner = optarg; break; case 'r': repo_path.as_default.repo = optarg; break; case 'n': { char *endptr = NULL; count = strtol(optarg, &endptr, 10); if (endptr != (optarg + strlen(optarg))) err(1, "gcli: error: cannot parse release count"); if (count == 0) errx(1, "gcli: error: number of releases must not be zero"); } break; case 's': flags |= OUTPUT_SORTED; break; case 'l': flags |= OUTPUT_LONG; break; case '?': default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; /* sanity check */ if (argc > 0) { fprintf(stderr, "gcli: error: stray arguments\n"); usage(); return EXIT_FAILURE; } check_path(&repo_path); if (gcli_get_releases(g_clictx, &repo_path, count, &releases) < 0) { errx(1, "gcli: error: could not get releases: %s", gcli_get_error(g_clictx)); } gcli_releases_print(flags, &releases, count); gcli_free_releases(&releases); return EXIT_SUCCESS; } gcli-2.9.1/src/cmd/repos.c000066400000000000000000000251571507017207500153050ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include #include #include static void usage(void) { fprintf(stderr, "usage: gcli repos create -r repo [-d description] [-p]\n"); fprintf(stderr, " gcli repos [-o owner -r repo] [-n number] [-s]\n"); fprintf(stderr, " gcli repos [-o owner -r repo] actions...\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -o owner The repository owner\n"); fprintf(stderr, " -r repo The repository name\n"); fprintf(stderr, " -n number Number of repos to fetch (-1 = everything)\n"); fprintf(stderr, " -p Make the repo private\n"); fprintf(stderr, " -s Print (sort) in reverse order\n"); fprintf(stderr, "ACTIONS:\n"); fprintf(stderr, " delete [-y] Delete this repository:\n"); fprintf(stderr, " -y Do not ask for confirmation\n"); fprintf(stderr, " set-visibility Mark the reposity as public or private. Level may be one of:\n"); fprintf(stderr, " - public\n"); fprintf(stderr, " - private\n"); fprintf(stderr, "\n"); version(); copyright(); } void gcli_print_repos(enum gcli_output_flags const flags, struct gcli_repo_list const *const list, int const max) { size_t n; gcli_tbl table; struct gcli_tblcoldef cols[] = { { .name = "FORK", .type = GCLI_TBLCOLTYPE_BOOL, .flags = 0 }, { .name = "VISBLTY", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "DATE", .type = GCLI_TBLCOLTYPE_TIME_T, .flags = 0 }, { .name = "FULLNAME", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, }; if (list->repos_size == 0) { puts("No repos"); return; } /* Determine number of repos to print */ if (max < 0 || (size_t)(max) > list->repos_size) n = list->repos_size; else n = max; /* init table */ table = gcli_tbl_begin(cols, ARRAY_SIZE(cols)); if (!table) errx(1, "gcli: error: could not init table"); /* put data into table */ if (flags & OUTPUT_SORTED) { for (size_t i = 0; i < n; ++i) gcli_tbl_add_row(table, list->repos[n-i-1].is_fork, list->repos[n-i-1].visibility, list->repos[n-i-1].date, list->repos[n-i-1].full_name); } else { for (size_t i = 0; i < n; ++i) gcli_tbl_add_row(table, list->repos[i].is_fork, list->repos[i].visibility, list->repos[i].date, list->repos[i].full_name); } /* print it */ gcli_tbl_end(table); } void gcli_repo_print(struct gcli_repo const *it) { gcli_dict dict; dict = gcli_dict_begin(); gcli_dict_add(dict, "ID", 0, 0, "%"PRIid, it->id); gcli_dict_add(dict, "FULL NAME", 0, 0, "%s", it->full_name); gcli_dict_add(dict, "NAME", 0, 0, "%s", it->name); gcli_dict_add(dict, "OWNER", 0, 0, "%s", it->owner); gcli_dict_add_timestamp(dict, "DATE", 0, 0, it->date); gcli_dict_add(dict, "VISIBILITY", 0, 0, "%s", it->visibility); gcli_dict_add(dict, "IS FORK", 0, 0, "%s", gcli_bool_yesno(it->is_fork)); gcli_dict_end(dict); } static int subcommand_repos_create(int argc, char *argv[]) { int ch; struct gcli_repo_create_options create_options = {0}; struct gcli_repo repo = {0}; const struct option options[] = { { .name = "repo", .has_arg = required_argument, .flag = NULL, .val = 'r' }, { .name = "private", .has_arg = no_argument, .flag = NULL, .val = 'p' }, { .name = "description", .has_arg = required_argument, .flag = NULL, .val = 'd' }, {0}, }; while ((ch = getopt_long(argc, argv, "r:d:p", options, NULL)) != -1) { switch (ch) { case 'r': create_options.name = optarg; break; case 'd': create_options.description = optarg; break; case 'p': create_options.private = true; break; case '?': default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; if (!create_options.name) { fprintf(stderr, "gcli: name cannot be empty. please set a repository " "name with -r/--name\n"); usage(); return EXIT_FAILURE; } if (gcli_repo_create(g_clictx, &create_options, &repo) < 0) { errx(1, "gcli: error: failed to create repository: %s", gcli_get_error(g_clictx)); } gcli_repo_print(&repo); gcli_repo_free(&repo); return EXIT_SUCCESS; } static int action_delete(struct gcli_path const *const path, int *argc, char ***argv) { int ch; bool always_yes = false; struct option const options[] = { { .name = "yes", .has_arg = no_argument, .flag = NULL, .val = 'y' }, {0}, }; always_yes = gcli_cmd_should_do_always_yes(); while ((ch = getopt_long(*argc, *argv, "+y", options, NULL)) != -1) { switch (ch) { case 'y': always_yes = true; break; default: usage(); return EXIT_FAILURE; } } *argc -= optind; *argv += optind; delete_repo(always_yes, path); return 0; } static gcli_repo_visibility parse_visibility(char const *str) { if (strcmp(str, "public") == 0) { return GCLI_REPO_VISIBILITY_PUBLIC; } else if (strcmp(str, "private") == 0) { return GCLI_REPO_VISIBILITY_PRIVATE; } else { fprintf(stderr, "gcli: error: bad visibility level »%s«\n", str); return 1; } } /* Change the visibility level of a repository (e.g. public, private * etc) */ static int action_set_visibility(struct gcli_path const *const path, int *argc, char ***argv) { char const *visblty_str; gcli_repo_visibility visblty; int rc; if (*argc < 2) { fprintf(stderr, "gcli: error: missing visibility level\n"); return 1; } visblty_str = (*argv)[1]; *argv += 2; *argc -= 2; visblty = parse_visibility(visblty_str); if ((rc = gcli_repo_set_visibility(g_clictx, path, visblty)) < 0) { fprintf(stderr, "gcli: error: failed to set visibility: %s\n", gcli_get_error(g_clictx)); return 1; } return 0; } static struct action { char const *const name; int (*fn)(struct gcli_path const *const path, int *argc, char ***argv); } const actions[] = { { .name = "delete", .fn = action_delete }, { .name = "set-visibility", .fn = action_set_visibility }, }; static size_t const actions_size = ARRAY_SIZE(actions); static struct action const * find_action(char const *const name) { for (size_t i = 0; i < actions_size; ++i) { if (strcmp(name, actions[i].name) == 0) return &actions[i]; } return NULL; } int subcommand_repos(int argc, char *argv[]) { int ch, n = 30; char *owner = NULL; char *repo = NULL; struct gcli_repo_list repos = {0}; enum gcli_output_flags flags = 0; /* detect whether we wanna create a repo */ if (argc > 1 && (strcmp(argv[1], "create") == 0)) { shift(&argc, &argv); return subcommand_repos_create(argc, argv); } struct option const options[] = { { .name = "count", .has_arg = required_argument, .flag = NULL, .val = 'n' }, { .name = "repo", .has_arg = required_argument, .flag = NULL, .val = 'r' }, { .name = "owner", .has_arg = required_argument, .flag = NULL, .val = 'o' }, { .name = "sorted", .has_arg = no_argument, .flag = NULL, .val = 's' }, {0}, }; while ((ch = getopt_long(argc, argv, "+n:o:r:s", options, NULL)) != -1) { switch (ch) { case 'o': owner = optarg; break; case 'r': repo = optarg; break; case 's': flags |= OUTPUT_SORTED; break; case 'n': { char *endptr = NULL; n = strtol(optarg, &endptr, 10); if (endptr != (optarg + strlen(optarg))) err(1, "gcli: error: cannot parse repo count"); if (n == 0) errx(1, "gcli: error: number of repos must not be zero"); } break; case '?': default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; optind = 0; /* List repos of the owner */ if (argc == 0) { int rc = 0; if (repo) { fprintf(stderr, "gcli: error: no actions specified\n"); usage(); return EXIT_FAILURE; } if (!owner) owner = gcli_config_get_account_name(g_clictx); /* whenever there is no default account we would be passing NULL to * gcli_get_repos. This is bad since that causes segfaults down the * line. (https://github.com/herrhotzenplotz/gcli/issues/118) */ if (!owner) { fprintf(stderr, "gcli: error: no account specified or no default" " account configured. use -o to provide an explicit" " account name.\n"); return EXIT_FAILURE; } rc = gcli_get_repos(g_clictx, owner, n, &repos); if (rc < 0) { errx(1, "gcli: error: failed to fetch repos: %s", gcli_get_error(g_clictx)); } gcli_print_repos(flags, &repos, n); gcli_repos_free(&repos); } else { struct gcli_path path = { .as_default = { .owner = owner, .repo = repo, }, }; check_path(&path); while (argc) { struct action const *action = find_action(argv[0]); int rc = 0; if (!action) { fprintf(stderr, "gcli: error: unrecognised action »%s«\n", argv[0]); return EXIT_FAILURE; } rc = action->fn(&path, &argc, &argv); if (rc) return rc; } } return EXIT_SUCCESS; } gcli-2.9.1/src/cmd/snippets.c000066400000000000000000000174361507017207500160230ustar00rootroot00000000000000/* * Copyright 2022 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 static void usage(void) { fprintf(stderr, "usage: gcli snippets [-n number] [-sl]\n"); fprintf(stderr, " gcli snippets delete id\n"); fprintf(stderr, " gcli snippets get id\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -l Print a long list instead of a short table\n"); fprintf(stderr, " -n number Number of snippets to fetch\n"); fprintf(stderr, " -s Sort the output in reverse order\n"); fprintf(stderr, " -u user User for whom to list gists\n"); fprintf(stderr, "\n"); version(); copyright(); } static void gcli_print_snippet(enum gcli_output_flags const flags, struct gcli_gitlab_snippet const *const it) { gcli_dict dict; (void) flags; dict = gcli_dict_begin(); gcli_dict_add(dict, "ID", 0, 0, "%"PRIid, it->id); gcli_dict_add_string(dict, "TITLE", 0, 0, it->title); gcli_dict_add_string(dict, "AUTHOR", 0, 0, it->author); gcli_dict_add_string(dict, "FILE", 0, 0, it->filename); gcli_dict_add_string(dict, "DATE", 0, 0, it->date); gcli_dict_add_string(dict, "VSBLTY", 0, 0, it->visibility); gcli_dict_add_string(dict, "URL", 0, 0, it->raw_url); gcli_dict_end(dict); } static void gcli_print_snippets_long(enum gcli_output_flags const flags, struct gcli_gitlab_snippet_list const *const list, int const max) { int n; /* Determine number of items to print */ if (max < 0 || (size_t)(max) > list->snippets_size) n = list->snippets_size; else n = max; if (flags & OUTPUT_SORTED) { for (int i = 0; i < n; ++i) gcli_print_snippet(flags, &list->snippets[n-i-1]); } else { for (int i = 0; i < n; ++i) gcli_print_snippet(flags, &list->snippets[i]); } } static void gcli_print_snippets_short(enum gcli_output_flags const flags, struct gcli_gitlab_snippet_list const *const list, int const max) { int n; gcli_tbl table; struct gcli_tblcoldef cols[] = { { .name = "ID", .type = GCLI_TBLCOLTYPE_ID, .flags = GCLI_TBLCOL_JUSTIFYR }, { .name = "DATE", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "VISIBILITY", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "AUTHOR", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "TITLE", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, }; /* Determine number of items to print */ if (max < 0 || (size_t)(max) > list->snippets_size) n = list->snippets_size; else n = max; /* Fill table */ table = gcli_tbl_begin(cols, ARRAY_SIZE(cols)); if (!table) errx(1, "gcli: error: could not init table"); if (flags & OUTPUT_SORTED) { for (int i = 0; i < n; ++i) gcli_tbl_add_row(table, list->snippets[n-i-1].id, list->snippets[n-i-1].date, list->snippets[n-i-1].visibility, list->snippets[n-i-1].author, list->snippets[n-i-1].title); } else { for (int i = 0; i < n; ++i) gcli_tbl_add_row(table, list->snippets[i].id, list->snippets[i].date, list->snippets[i].visibility, list->snippets[i].author, list->snippets[i].title); } gcli_tbl_end(table); } void gcli_snippets_print(enum gcli_output_flags const flags, struct gcli_gitlab_snippet_list const *const list, int const max) { if (list->snippets_size == 0) { puts("No Snippets"); return; } if (flags & OUTPUT_LONG) gcli_print_snippets_long(flags, list, max); else gcli_print_snippets_short(flags, list, max); } static int subcommand_snippet_get(int argc, char *argv[]) { argc -= 1; argv += 1; if (!argc) { fprintf(stderr, "gcli: error: expected ID of snippet to fetch\n"); usage(); return EXIT_FAILURE; } char *snippet_id = shift(&argc, &argv); if (argc) { fprintf(stderr, "gcli: error: stray arguments\n"); usage(); return EXIT_FAILURE; } if (gcli_snippet_get(g_clictx, snippet_id, stdout) < 0) errx(1, "gcli: error: failed to fetch snippet contents: %s", gcli_get_error(g_clictx)); return EXIT_SUCCESS; } static int subcommand_snippet_delete(int argc, char *argv[]) { argc -= 1; argv += 1; if (!argc) { fprintf(stderr, "gcli: error: expected ID of snippet to delete\n"); usage(); return EXIT_FAILURE; } char *snippet_id = shift(&argc, &argv); if (argc) { fprintf(stderr, "gcli: error: trailing options\n"); usage(); return EXIT_FAILURE; } if (gcli_snippet_delete(g_clictx, snippet_id) < 0) errx(1, "gcli: error: failed to delete snippet: %s", gcli_get_error(g_clictx)); return EXIT_SUCCESS; } static struct snippet_subcommand { const char *name; int (*fn)(int argc, char *argv[]); } snippet_subcommands[] = { { .name = "get", .fn = subcommand_snippet_get }, { .name = "delete", .fn = subcommand_snippet_delete }, }; int subcommand_snippets(int argc, char *argv[]) { int ch; struct gcli_gitlab_snippet_list list = {0}; int count = 30; enum gcli_output_flags flags = 0; for (size_t i = 0; i < ARRAY_SIZE(snippet_subcommands); ++i) { if (argc > 1 && strcmp(argv[1], snippet_subcommands[i].name) == 0) { argc -= 1; argv += 1; return snippet_subcommands[i].fn(argc, argv); } } const struct option options[] = { { .name = "count", .has_arg = required_argument, .flag = NULL, .val = 'n' }, { .name = "sorted", .has_arg = no_argument, .flag = NULL, .val = 's' }, { .name = "long", .has_arg = no_argument, .flag = NULL, .val = 'l' }, {0}, }; while ((ch = getopt_long(argc, argv, "sn:l", options, NULL)) != -1) { switch (ch) { case 'n': { char *endptr = NULL; count = strtol(optarg, &endptr, 10); if (endptr != (optarg + strlen(optarg))) err(1, "gcli: error: cannot parse snippets count"); if (count == 0) errx(1, "gcli: error: snippets count must not be zero"); } break; case 's': flags |= OUTPUT_SORTED; break; case 'l': flags |= OUTPUT_LONG; break; case '?': default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; if (gcli_snippets_get(g_clictx, count, &list) < 0) errx(1, "gcli: error: failed to fetch snippets: %s", gcli_get_error(g_clictx)); gcli_snippets_print(flags, &list, count); gcli_snippets_free(&list); return EXIT_SUCCESS; } gcli-2.9.1/src/cmd/status.c000066400000000000000000000106201507017207500154650ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include static void usage(void) { fprintf(stderr, "usage: gcli status -m id\n"); fprintf(stderr, " gcli status -l [-n number]\n"); fprintf(stderr, " gcli status\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -l Print list of todo items and exit\n"); fprintf(stderr, " -n number Number of messages to fetch\n"); fprintf(stderr, " -m id Mark the given message as read\n"); fprintf(stderr, "\n"); version(); copyright(); } int gcli_status_list(int const count) { struct gcli_notification_list list = {0}; int rc = 0; rc = gcli_get_notifications(g_clictx, count, &list); if (rc < 0) return rc; gcli_print_notifications(&list); gcli_free_notifications(&list); return rc; } void gcli_print_notifications(struct gcli_notification_list const *const list) { for (size_t i = 0; i < list->notifications_size; ++i) { printf("%s - %s - %s", list->notifications[i].id, list->notifications[i].repository, gcli_notification_target_type_str(list->notifications[i].type)); printf(" - %s", list->notifications[i].date); if (list->notifications[i].reason) printf(" - %s", list->notifications[i].reason); printf("\n"); gcli_pretty_print(list->notifications[i].title, 4, 80, stdout); putchar('\n'); } } int subcommand_status(int argc, char *argv[]) { int count = 30, ch = 0, mark = 0, list = 0; char *endptr = NULL; const struct option options[] = { { .name = "count", .has_arg = required_argument, .flag = NULL, .val = 'n' }, { .name = "mark", .has_arg = no_argument, .flag = &mark, .val = 1 }, { .name = "list", .has_arg = no_argument, .flag = &list, .val = 1 }, {0} }; while ((ch = getopt_long(argc, argv, "n:ml", options, NULL)) != -1) { switch (ch) { case 'n': { count = strtol(optarg, &endptr, 10); if (endptr != optarg + strlen(optarg)) err(1, "gcli: error: cannot parse parameter to -n"); } break; case 'm': { mark = 1; } break; case 'l': { list = 1; } break; default: usage(); return EXIT_FAILURE; } } argc -= optind; argv += optind; if (list) { if (mark) { fprintf(stderr, "gcli: cannot use -m and -l together\n"); usage(); return EXIT_FAILURE; } gcli_status_list(count); } else if (mark) { if (count != 30) gcli_warnx(g_clictx, "gcli: ignoring -n/--count argument"); if (argc > 1) { fprintf(stderr, "gcli: error: too many arguments for marking notifications\n"); usage(); return EXIT_FAILURE; } if (argc < 1) { fprintf(stderr, "gcli: error: missing notification id to mark as read\n"); usage(); return EXIT_FAILURE; } if (gcli_notification_mark_as_read(g_clictx, argv[0]) < 0) errx(1, "gcli: error: failed to mark the notification as read: %s", gcli_get_error(g_clictx)); } else { return gcli_status_interactive(); } return EXIT_SUCCESS; } gcli-2.9.1/src/cmd/status_interactive.c000066400000000000000000000123331507017207500200650ustar00rootroot00000000000000/* * Copyright 2024-2025 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 #include #include static void print_notification_table(struct gcli_notification_list const *list) { gcli_tbl *table; struct gcli_tblcoldef const columns[] = { { .name = "NUMBER", .type = GCLI_TBLCOLTYPE_LONG, .flags = 0 }, { .name = "REPO", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "TYPE", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, { .name = "REASON", .type = GCLI_TBLCOLTYPE_STRING, .flags = 0 }, }; table = gcli_tbl_begin(columns, ARRAY_SIZE(columns)); for (size_t i = 0; i < list->notifications_size; ++i) { struct gcli_notification const *n = &list->notifications[i]; gcli_tbl_add_row(table, (long)i + 1, n->repository, gcli_notification_target_type_str(n->type), n->reason); } gcli_tbl_end(table); } static void refresh_notifications(struct gcli_notification_list *list) { int rc = 0; gcli_free_notifications(list); rc = gcli_get_notifications(g_clictx, -1, list); if (rc < 0) errx(1, "gcli: failed to fetch notifications: %s", gcli_get_error(g_clictx)); } static struct gcli_cmd_actions * notification_handlers[MAX_GCLI_NOTIFICATION_TARGET] = { [GCLI_NOTIFICATION_TARGET_ISSUE] = &gcli_issue_actions, [GCLI_NOTIFICATION_TARGET_PULL_REQUEST] = &gcli_pull_actions, }; static void status_interactive_notification(struct gcli_notification const *const notif) { char *user_input = NULL; if (notif->type >= MAX_GCLI_NOTIFICATION_TARGET) { fprintf(stderr, "gcli: error: bad notification type\n"); return; } if (notification_handlers[notif->type] == NULL) { fprintf(stderr, "gcli: error: notification type '%s' not supported\n", gcli_notification_target_type_str(notif->type)); return; } struct gcli_cmd_actions const *const actions = notification_handlers[notif->type]; for (;;) { int rc = 0; user_input = gcli_cmd_prompt("Enter action, done or quit", GCLI_PROMPT_RESULT_MANDATORY); /* plain quit action */ if (strcmp(user_input, "q") == 0 || strcmp(user_input, "quit") == 0) { free(user_input); return; } /* mark as done and quit */ if (strcmp(user_input, "d") == 0 || strcmp(user_input, "done") == 0) { rc = gcli_notification_mark_as_read(g_clictx, notif->id); if (rc < 0) fprintf(stderr, "gcli: error: %s\n", gcli_get_error(g_clictx)); free(user_input); return; } /* search and call action handler or error out */ rc = gcli_cmd_action_handle(actions, ¬if->target, user_input); if (rc < 0) { fprintf(stderr, "gcli: error: %s\n", gcli_get_error(g_clictx)); } free(user_input); } } int gcli_status_interactive(void) { struct gcli_notification_list list = {0}; char *user_input = NULL; refresh_notifications(&list); print_notification_table(&list); for (;;) { 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) { goto out; } else if (strcmp(user_input, "l") == 0 || strcmp(user_input, "list") == 0) { refresh_notifications(&list); print_notification_table(&list); } else { size_t number = 0; char *endptr = NULL; number = strtoul(user_input, &endptr, 10); if (endptr != user_input + strlen(user_input)) { fprintf(stderr, "gcli: bad notification number: %s\n", user_input); goto next; } if (number == 0 || number > list.notifications_size) { fprintf(stderr, "gcli: unknown notification number\n"); goto next; } status_interactive_notification(&list.notifications[number-1]); } next: free(user_input); user_input = NULL; } out: free(user_input); user_input = NULL; gcli_free_notifications(&list); return 0; } gcli-2.9.1/src/cmd/table.c000066400000000000000000000327021507017207500152360ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include #include #include #include /* A row */ struct gcli_tblrow; /* Internal state of a table printer. We return a handle to it in * gcli_table_init. */ struct gcli_tbl { struct gcli_tblcoldef const *cols; /* user provided column definitons */ int *col_widths; /* minimum width of the columns */ size_t cols_size; /* size of above arrays */ struct gcli_tblrow *rows; /* list of rows */ size_t rows_size; /* number of rows */ }; struct gcli_tblrow { struct { char *text; /* the text in the cell */ char const *colour; /* colour (ansi escape sequence) if * explicit fixed colour was given */ } *cells; }; /* Push a row into the table state */ static int table_pushrow(struct gcli_tbl *const table, struct gcli_tblrow row) { table->rows = realloc(table->rows, sizeof(*table->rows) * (table->rows_size + 1)); if (!table->rows) return -1; table->rows[table->rows_size++] = row; return 0; } /** Initialize the internal state structure of the table printer. */ gcli_tbl gcli_tbl_begin(struct gcli_tblcoldef const *const cols, size_t const cols_size) { struct gcli_tbl *tbl; /* Allocate the structure and fill in the handle */ tbl = calloc(1, sizeof(*tbl)); if (!tbl) return NULL; /* Reserve memory for the column sizes */ tbl->col_widths = calloc(cols_size, sizeof(*tbl->col_widths)); if (!tbl->col_widths) { free(tbl); return NULL; } /* Store the list of columns */ tbl->cols = cols; tbl->cols_size = cols_size; /* Check the headers */ for (size_t i = 0; i < cols_size; ++i) { /* Compute the header's length and use these as initial * values */ tbl->col_widths[i] = strlen(cols[i].name); } return tbl; } static void table_freerow(struct gcli_tblrow *row, size_t const cols) { for (size_t i = 0; i < cols; ++i) free(row->cells[i].text); free(row->cells); row->cells = NULL; } static int tablerow_add_cell(struct gcli_tbl *const table, struct gcli_tblrow *const row, size_t const col, va_list *vp) { int cell_size = 0; /* Extract the explicit colour code */ if (table->cols[col].flags & GCLI_TBLCOL_COLOUREXPL) { int code = va_arg(*vp, int); /* don't free that! it's allocated and free'ed inside colour.c */ row->cells[col].colour = gcli_setcolour(code); } else if (table->cols[col].flags & GCLI_TBLCOL_256COLOUR) { uint64_t hexcode = va_arg(*vp, uint64_t); /* see comment above */ row->cells[col].colour = gcli_setcolour256(hexcode); } /* Process the content */ switch (table->cols[col].type) { case GCLI_TBLCOLTYPE_INT: { row->cells[col].text = gcli_asprintf("%d", va_arg(*vp, int)); cell_size = strlen(row->cells[col].text); } break; case GCLI_TBLCOLTYPE_ID: { row->cells[col].text = gcli_asprintf("%"PRIid, va_arg(*vp, uint64_t)); cell_size = strlen(row->cells[col].text); } break; case GCLI_TBLCOLTYPE_LONG: { row->cells[col].text = gcli_asprintf("%ld", va_arg(*vp, long)); cell_size = strlen(row->cells[col].text); } break; case GCLI_TBLCOLTYPE_STRING: { char *it = va_arg(*vp, char *); if (!it) it = "N/A"; /* hack */ row->cells[col].text = strdup(it); cell_size = strlen(it); } break; case GCLI_TBLCOLTYPE_DOUBLE: { row->cells[col].text = gcli_asprintf("%lf", va_arg(*vp, double)); cell_size = strlen(row->cells[col].text); } break; case GCLI_TBLCOLTYPE_BOOL: { /* Do not use real _Bool type as it triggers a compiler bug in * LLVM clang 13 */ int val = va_arg(*vp, int); if (val) { row->cells[col].text = strdup("yes"); cell_size = 3; } else { row->cells[col].text = strdup("no"); cell_size = 2; } } break; case GCLI_TBLCOLTYPE_TIME_T: { int rc = 0; time_t val = va_arg(*vp, time_t); rc = gcli_format_as_localtime(g_clictx, val, &row->cells[col].text); if (rc < 0) return rc; cell_size = strlen(row->cells[col].text); } break; default: return -1; } /* Update the column width if needed */ if (table->col_widths[col] < cell_size) table->col_widths[col] = cell_size; return 0; } int gcli_tbl_add_row(gcli_tbl _table, ...) { va_list vp; struct gcli_tblrow row = {0}; struct gcli_tbl *table = (struct gcli_tbl *)(_table); /* reserve array of cells */ row.cells = calloc(table->cols_size, sizeof(*row.cells)); if (!row.cells) return -1; va_start(vp, _table); /* Step through all the columns and print the cells */ for (size_t i = 0; i < table->cols_size; ++i) { if (tablerow_add_cell(table, &row, i, &vp) < 0) { table_freerow(&row, table->cols_size); va_end(vp); return -1; } } va_end(vp); /* Push the row into the table */ if (table_pushrow(table, row) < 0) { table_freerow(&row, table->cols_size); return -1; } return 0; } static void pad(size_t const n) { for (size_t p = 0; p < n; ++p) putchar(' '); } static void dump_row(struct gcli_tbl const *const table, size_t const i) { struct gcli_tblrow const *const row = &table->rows[i]; for (size_t col = 0; col < table->cols_size; ++col) { /* Skip empty columns (as with colour indicators in no-colour * mode) */ if (table->col_widths[col] == 0) continue; /* If right justified and not last column, print padding */ if ((table->cols[col].flags & GCLI_TBLCOL_JUSTIFYR) && (col + 1) < table->cols_size) pad(table->col_widths[col] - strlen(row->cells[col].text)); /* State colour */ if (table->cols[col].flags & GCLI_TBLCOL_STATECOLOURED) printf("%s", gcli_state_colour_str(row->cells[col].text)); else if (table->cols[col].flags & (GCLI_TBLCOL_COLOUREXPL|GCLI_TBLCOL_256COLOUR)) printf("%s", row->cells[col].colour); /* Bold */ if (table->cols[col].flags & GCLI_TBLCOL_BOLD) printf("%s", gcli_setbold()); /* Print cell if it is not NULL, otherwise indicate it by * printing N/A */ printf("%s", row->cells[col].text ? row->cells[col].text : "N/A"); /* End colour */ if (table->cols[col].flags & (GCLI_TBLCOL_STATECOLOURED |GCLI_TBLCOL_COLOUREXPL |GCLI_TBLCOL_256COLOUR)) printf("%s", gcli_resetcolour()); /* Stop printing in bold */ if (table->cols[col].flags & GCLI_TBLCOL_BOLD) printf("%s", gcli_resetbold()); /* If not last column, print padding of 2 spaces */ if ((col + 1) < table->cols_size) { size_t padding = (table->cols[col].flags & GCLI_TBLCOL_TIGHT) ? 1 : 2; /* If left-justified, print justify-padding */ if (!(table->cols[col].flags & GCLI_TBLCOL_JUSTIFYR) && (col + 1) < table->cols_size) padding += table->col_widths[col] - strlen(row->cells[col].text); pad(padding); } } putchar('\n'); } static int gcli_tbl_dump(gcli_tbl const _table) { struct gcli_tbl const *const table = (struct gcli_tbl const *const)_table; for (size_t i = 0; i < table->cols_size; ++i) { size_t padding = 0; /* Skip empty columns e.g. in no-colour mode */ if (table->col_widths[i] == 0) continue; /* Check if we have tight column spacing */ if (table->cols[i].flags & GCLI_TBLCOL_TIGHT) padding = 1; else padding = 2; printf("%s", table->cols[i].name); if ((i + 1) < table->cols_size) pad(padding + table->col_widths[i] - strlen(table->cols[i].name)); } printf("\n"); for (size_t i = 0; i < table->rows_size; ++i) { dump_row(table, i); } return 0; } static void gcli_tbl_free(gcli_tbl _table) { struct gcli_tbl *tbl = (struct gcli_tbl *)_table; for (size_t row = 0; row < tbl->rows_size; ++row) table_freerow(&tbl->rows[row], tbl->cols_size); free(tbl->rows); free(tbl->col_widths); free(tbl); } void gcli_tbl_end(gcli_tbl tbl) { gcli_tbl_dump(tbl); gcli_tbl_free(tbl); } /* DICTIONARY *********************************************************/ struct gcli_dict { struct gcli_dict_entry { char *key; char *value; int flags; uint32_t colour_args; } *entries; size_t entries_size; size_t max_key_len; struct gcli_ctx *ctx; }; /* Create a new long list printer and return a handle to it */ gcli_dict gcli_dict_begin(void) { return calloc(1, sizeof(struct gcli_dict)); } static int gcli_dict_add_row(struct gcli_dict *list, char const *const key, int flags, int colour_args, char *value) { struct gcli_dict_entry *entry; size_t keylen; list->entries = realloc(list->entries, sizeof(*list->entries) * (list->entries_size + 1)); if (!list->entries) return -1; entry = &list->entries[list->entries_size++]; entry->key = strdup(key); entry->value = value; entry->flags = flags; entry->colour_args = colour_args; if ((keylen = strlen(key)) > list->max_key_len) list->max_key_len = keylen; return 0; } int gcli_dict_add(gcli_dict list, char const *const key, int flags, uint32_t colour_args, char const *const fmt, ...) { char tmp = 0, *result = NULL; size_t actual = 0; va_list vp; va_start(vp, fmt); actual = vsnprintf(&tmp, 1, fmt, vp); va_end(vp); result = calloc(1, actual + 1); if (!result) err(1, "calloc"); va_start(vp, fmt); vsnprintf(result, actual + 1, fmt, vp); va_end(vp); return gcli_dict_add_row(list, key, flags, colour_args, result); } int gcli_dict_add_string(gcli_dict list, char const *const key, int flags, uint32_t colour_args, char const *const str) { return gcli_dict_add_row(list, key, flags, colour_args, strdup(str ? str : "N/A")); } int gcli_dict_add_timestamp(gcli_dict list, char const *key, int flags, uint32_t colour_args, time_t stamp) { char *tmp = NULL; int rc = 0; rc = gcli_format_as_localtime(g_clictx, stamp, &tmp); if (rc < 0) return rc; return gcli_dict_add_row(list, key, flags, colour_args, tmp); } int gcli_dict_add_sv_list(gcli_dict dict, char const *const key, gcli_sv const *const list, size_t const list_size) { size_t totalsize = 0; char *catted, *hd; /* Sum of string lengths */ for (size_t i = 0; i < list_size; ++i) totalsize += list[i].length; /* Account for comma and space between each */ totalsize += (list_size - 1) * 2; /* concatenate the strings */ hd = catted = calloc(totalsize + 1, 1); for (size_t i = 0; i < list_size; ++i) { memcpy(hd, list[i].data, list[i].length); hd += list[i].length; if (i + 1 < list_size) { strcat(catted, ", "); hd += 2; } } /* Push the row into the state */ return gcli_dict_add_row(dict, key, 0, 0, catted); } int gcli_dict_add_string_list(gcli_dict dict, char const *const key, char const *const *list, size_t const list_size) { char *catted = gcli_join_with( (char const *const *)list, list_size, ", "); /* yolo */ /* Push the row into the state */ return gcli_dict_add_row(dict, key, 0, 0, catted); } static void gcli_dict_free(struct gcli_dict *list) { for (size_t i = 0; i < list->entries_size; ++i) { free(list->entries[i].key); free(list->entries[i].value); } free(list->entries); free(list); } int gcli_dict_end(gcli_dict _list) { struct gcli_dict *list = _list; for (size_t i = 0; i < list->entries_size; ++i) { int flags = list->entries[i].flags; pad(list->max_key_len - strlen(list->entries[i].key)); printf("%s : ", list->entries[i].key); if (flags & GCLI_TBLCOL_BOLD) printf("%s", gcli_setbold()); if (flags & GCLI_TBLCOL_COLOUREXPL) printf("%s", gcli_setcolour(list->entries[i].colour_args)); if (flags & GCLI_TBLCOL_STATECOLOURED) printf("%s", gcli_state_colour_str(list->entries[i].value)); if (flags & GCLI_TBLCOL_256COLOUR) printf("%s", gcli_setcolour256(list->entries[i].colour_args)); puts(list->entries[i].value); if (flags & (GCLI_TBLCOL_COLOUREXPL |GCLI_TBLCOL_STATECOLOURED |GCLI_TBLCOL_256COLOUR)) printf("%s", gcli_resetcolour()); if (flags & GCLI_TBLCOL_BOLD) printf("%s", gcli_resetbold()); } gcli_dict_free(list); return 0; } gcli-2.9.1/src/comments.c000066400000000000000000000054501507017207500152310ustar00rootroot00000000000000/* * Copyright 2021-2025 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 void gcli_comment_free(struct gcli_comment *const it) { gcli_clear_ptr(&it->author); gcli_clear_ptr(&it->body); } void gcli_comments_free(struct gcli_comment_list *const list) { for (size_t i = 0; i < list->comments_size; ++i) gcli_comment_free(&list->comments[i]); gcli_clear_ptr(&list->comments); list->comments_size = 0; } int gcli_get_comment(struct gcli_ctx *ctx, struct gcli_path const *const target, enum comment_target_type target_type, gcli_id const comment_id, struct gcli_comment *out) { gcli_null_check_call(get_comment, ctx, target, target_type, comment_id, out); } int gcli_get_issue_comments(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, struct gcli_comment_list *out) { gcli_null_check_call(get_issue_comments, ctx, issue_path, out); } int gcli_get_pull_comments(struct gcli_ctx *ctx, struct gcli_path const *const pull_path, struct gcli_comment_list *out) { gcli_null_check_call(get_pull_comments, ctx, pull_path, out); } int gcli_comment_submit(struct gcli_ctx *ctx, struct gcli_submit_comment_opts const *const opts) { gcli_null_check_call(perform_submit_comment, ctx, opts); } gcli-2.9.1/src/ctx.c000066400000000000000000000066511507017207500142060ustar00rootroot00000000000000/* * Copyright 2023-2025 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 #include int gcli_error(struct gcli_ctx *ctx, char const *const fmt, ...) { va_list vp; char *buf; size_t len; va_start(vp, fmt); len = vsnprintf(NULL, 0, fmt, vp); va_end(vp); buf = malloc(len + 1); va_start(vp, fmt); vsnprintf(buf, len + 1, fmt, vp); va_end(vp); if (ctx->last_error) gcli_clear_ptr(&ctx->last_error); ctx->last_error = buf; return -1; } void * gcli_get_userdata(struct gcli_ctx const *ctx) { return ctx->usrdata; } void gcli_set_userdata(struct gcli_ctx *ctx, void *usrdata) { ctx->usrdata = usrdata; } void gcli_set_progress_func(struct gcli_ctx *ctx, void (*pfunc)(bool done)) { ctx->report_progress = pfunc; } char * gcli_get_apibase(struct gcli_ctx *ctx) { if (!ctx->apibase) ctx->apibase = ctx->get_apibase(ctx); return ctx->apibase; } char * gcli_get_token(struct gcli_ctx *ctx) { return ctx->get_token(ctx); } char * gcli_get_authheader(struct gcli_ctx *ctx) { char *hdr = NULL; char *token = gcli_get_token(ctx); if (token && gcli_forge(ctx)->make_authheader) { hdr = gcli_forge(ctx)->make_authheader(ctx, token); } gcli_clear_ptr(&token); return hdr; } bool gcli_be_verbose(struct gcli_ctx *ctx) { return ctx->verbosity == GCLI_VERBOSITY_VERBOSE; } bool gcli_be_quiet(struct gcli_ctx *ctx) { return ctx->verbosity == GCLI_VERBOSITY_QUIET; } int gcli_getverbosity(struct gcli_ctx *ctx) { return ctx->verbosity; } void gcli_setverbosity(struct gcli_ctx *ctx, int v) { ctx->verbosity = v; } void gcli_warn(struct gcli_ctx *ctx, char const *fmt, ...) { if (!gcli_be_verbose(ctx)) return; fputs("warning: ", stderr); va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fprintf(stderr, ": %s\n", strerror(errno)); } void gcli_warnx(struct gcli_ctx *ctx, char const *fmt, ...) { if (!gcli_be_verbose(ctx)) return; fputs("warning: ", stderr); va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fputc('\n', stderr); } gcli-2.9.1/src/curl.c000066400000000000000000000470611507017207500143550ustar00rootroot00000000000000/* * Copyright 2021-2025 Nico Sonack * Copyright 2022 Aritra Sarkar * * 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 #include #include /* Hack for NetBSD's and Oracle Solaris broken isalnum implementation */ #if defined(__NetBSD__) || (defined(__SVR4) && defined(__sun)) # ifdef isalnum # undef isalnum # endif # define isalnum gcli_curl_isalnum /* TODO: this is fucked in case we are working on an EBCDIC machine * (wtf are you doing anyways?) */ static int gcli_curl_isalnum(char const c) { return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || ('0' <= c && c <= '9'); } #endif /* __NetBSD and Oracle Solaris */ /* XXX move to gcli_ctx destructor */ void gcli_curl_ctx_destroy(struct gcli_ctx *ctx) { if (ctx->curl) curl_easy_cleanup(ctx->curl); ctx->curl = NULL; gcli_clear_ptr(&ctx->curl_useragent); } /* Ensures a clean cURL handle. Call this whenever you wanna use the * ctx->curl */ static int gcli_curl_ensure(struct gcli_ctx *ctx) { if (ctx->curl) { curl_easy_reset(ctx->curl); } else { ctx->curl = curl_easy_init(); if (!ctx->curl) return gcli_error(ctx, "failed to initialise curl context"); } if (!ctx->curl_useragent) { curl_version_info_data const *ver; ver = curl_version_info(CURLVERSION_NOW); ctx->curl_useragent = gcli_asprintf("curl/%s", ver->version); } return 0; } /* Check the given curl code for an OK result. If not, print an * appropriate error message and exit */ static int gcli_curl_check_api_error(struct gcli_ctx *ctx, CURLcode code, char const *url, struct gcli_fetch_buffer *const result) { long status_code = 0; if (code != CURLE_OK) { return gcli_error(ctx, "request to %s failed: curl error: %s", url, curl_easy_strerror(code)); } curl_easy_getinfo(ctx->curl, CURLINFO_RESPONSE_CODE, &status_code); if (status_code >= 300L) { return gcli_error(ctx, "request to %s failed with code %ld: API error: %s", url, status_code, gcli_forge(ctx)->get_api_error_string(ctx, result)); } return 0; } /* Callback for writing data into the struct gcli_fetch_buffer passed by * calling routines */ static size_t fetch_write_callback(char *in, size_t size, size_t nmemb, void *data) { /* the user may have passed null indicating that we do not care * about the result body of the request. */ if (data) { struct gcli_fetch_buffer *out = data; out->data = realloc(out->data, out->length + size * nmemb); memcpy(&(out->data[out->length]), in, size * nmemb); out->length += size * nmemb; } return size * nmemb; } /* Plain HTTP get request. * * pagination_next returns the next url to query for paged results. * Results are placed into the struct gcli_fetch_buffer. */ int gcli_fetch(struct gcli_ctx *ctx, char const *url, char **const pagination_next, struct gcli_fetch_buffer *out) { return gcli_fetch_with_method(ctx, "GET", url, NULL, pagination_next, out); } static int gcli_report_progress(void *_ctx, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { struct gcli_ctx *ctx = _ctx; (void) dltotal; (void) dlnow; (void) ultotal; (void) ulnow; /* not done */ ctx->report_progress(false); return 0; } /* Check the given url for a successful query */ int gcli_curl_test_success(struct gcli_ctx *ctx, char const *url) { CURLcode ret; struct gcli_fetch_buffer buffer = {0}; long status_code; bool is_success = true; int rc = 0; if ((rc = gcli_curl_ensure(ctx)) < 0) return rc; curl_easy_setopt(ctx->curl, CURLOPT_URL, url); curl_easy_setopt(ctx->curl, CURLOPT_BUFFERSIZE, 102400L); curl_easy_setopt(ctx->curl, CURLOPT_NOPROGRESS, 1L); curl_easy_setopt(ctx->curl, CURLOPT_MAXREDIRS, 50L); curl_easy_setopt(ctx->curl, CURLOPT_USERAGENT, ctx->curl_useragent); #if defined(CURL_HTTP_VERSION_2TLS) curl_easy_setopt( ctx->curl, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS); #endif curl_easy_setopt(ctx->curl, CURLOPT_TCP_KEEPALIVE, 1L); curl_easy_setopt(ctx->curl, CURLOPT_WRITEDATA, &buffer); curl_easy_setopt(ctx->curl, CURLOPT_WRITEFUNCTION, fetch_write_callback); curl_easy_setopt(ctx->curl, CURLOPT_FAILONERROR, 0L); curl_easy_setopt(ctx->curl, CURLOPT_FOLLOWLOCATION, 1L); if (ctx->report_progress) { curl_easy_setopt(ctx->curl, CURLOPT_XFERINFOFUNCTION, gcli_report_progress); curl_easy_setopt(ctx->curl, CURLOPT_XFERINFODATA, ctx); curl_easy_setopt(ctx->curl, CURLOPT_NOPROGRESS, 0L); } ret = curl_easy_perform(ctx->curl); if (ret != CURLE_OK) { is_success = false; } else { curl_easy_getinfo(ctx->curl, CURLINFO_RESPONSE_CODE, &status_code); if (status_code >= 300L) is_success = false; } if (ctx->report_progress) ctx->report_progress(true); gcli_fetch_buffer_free(&buffer); return is_success; } /* Perform a GET request to the given URL and print the results to the * STREAM. * * content_type may be NULL. */ int gcli_curl(struct gcli_ctx *ctx, FILE *stream, char const *url, char const *content_type) { CURLcode ret; struct curl_slist *headers; struct gcli_fetch_buffer buffer = {0}; char *auth_header = NULL; int rc = 0; headers = NULL; if ((rc = gcli_curl_ensure(ctx)) < 0) return rc; if (content_type) headers = curl_slist_append(headers, content_type); auth_header = gcli_get_authheader(ctx); if (auth_header) headers = curl_slist_append(headers, auth_header); curl_easy_setopt(ctx->curl, CURLOPT_URL, url); curl_easy_setopt(ctx->curl, CURLOPT_BUFFERSIZE, 102400L); curl_easy_setopt(ctx->curl, CURLOPT_NOPROGRESS, 1L); curl_easy_setopt(ctx->curl, CURLOPT_MAXREDIRS, 50L); curl_easy_setopt(ctx->curl, CURLOPT_FTP_SKIP_PASV_IP, 1L); curl_easy_setopt(ctx->curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(ctx->curl, CURLOPT_USERAGENT, ctx->curl_useragent); #if defined(CURL_HTTP_VERSION_2TLS) curl_easy_setopt( ctx->curl, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS); #endif curl_easy_setopt(ctx->curl, CURLOPT_TCP_KEEPALIVE, 1L); curl_easy_setopt(ctx->curl, CURLOPT_WRITEDATA, &buffer); curl_easy_setopt(ctx->curl, CURLOPT_WRITEFUNCTION, fetch_write_callback); curl_easy_setopt(ctx->curl, CURLOPT_FAILONERROR, 0L); curl_easy_setopt(ctx->curl, CURLOPT_FOLLOWLOCATION, 1L); if (ctx->report_progress) { curl_easy_setopt(ctx->curl, CURLOPT_XFERINFOFUNCTION, gcli_report_progress); curl_easy_setopt(ctx->curl, CURLOPT_XFERINFODATA, ctx); curl_easy_setopt(ctx->curl, CURLOPT_NOPROGRESS, 0L); } ret = curl_easy_perform(ctx->curl); rc = gcli_curl_check_api_error(ctx, ret, url, &buffer); if (ctx->report_progress) ctx->report_progress(true); if (rc == 0) fwrite(buffer.data, 1, buffer.length, stream); gcli_fetch_buffer_free(&buffer); curl_slist_free_all(headers); gcli_clear_ptr(&auth_header); return rc; } /* Callback to extract the link header for pagination handling. */ static size_t fetch_header_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { char **out = userdata; size_t sz = size * nmemb; gcli_sv buffer = gcli_sv_from_parts(ptr, sz); gcli_sv header_name = gcli_sv_chop_until(&buffer, ':'); /* Despite what the documentation says, this header is called * "link" not "Link". Webdev ftw /sarc */ if (gcli_sv_eq_to(header_name, "link")) { buffer.data += 1; buffer.length -= 1; buffer = gcli_sv_trim_front(buffer); *out = gcli_strndup(buffer.data, buffer.length); } return sz; } /* Parse the link http header for pagination */ static char * parse_link_header(char *_header) { gcli_sv header = SV(_header); gcli_sv entry = {0}; /* Iterate through the comma-separated list of link relations */ while ((entry = gcli_sv_chop_until(&header, ',')).length > 0) { entry = gcli_sv_trim(entry); /* the entries have semicolon-separated fields like so: * ; rel=\"next\" * * This chops off the url and then looks at the rest. * * We're making lots of assumptions about the input data here * without sanity checking it. If it fails, we will know. Most * likely a segfault. */ gcli_sv almost_url = gcli_sv_chop_until(&entry, ';'); if (gcli_sv_eq_to(entry, "; rel=\"next\"")) { /* Skip the triangle brackets around the url */ almost_url.data += 1; almost_url.length -= 2; almost_url = gcli_sv_trim(almost_url); return gcli_sv_to_cstr(almost_url); } /* skip the comma if we have enough data */ if (header.length > 0) { header.length -= 1; header.data += 1; } } return NULL; } /* Perform a HTTP Request with the given method to the url * * - data may be NULL. * - pagination_next may be NULL. * * Results are placed in the gcli_fetch_buffer. * * All requests will be done with authorization through the * gcli_config_get_authheader function. * * If pagination_next is non-null a URL that can be queried for more * data (pagination) is placed into it. If there is no more data, it * will be set to NULL. */ int gcli_fetch_with_method( struct gcli_ctx *ctx, char const *method, /* HTTP method. e.g. POST, GET, DELETE etc. */ char const *url, /* Endpoint */ char const *data, /* Form data */ char **const pagination_next, /* Next URL for pagination */ struct gcli_fetch_buffer *const out) /* output buffer */ { CURLcode ret; struct curl_slist *headers; struct gcli_fetch_buffer tmp = {0}; /* used for error codes when out is NULL */ struct gcli_fetch_buffer *buf = NULL; char *link_header = NULL; int rc = 0; if ((rc = gcli_curl_ensure(ctx)) < 0) return rc; char *auth_header = gcli_get_authheader(ctx); if (gcli_be_verbose(ctx)) fprintf(stderr, "info: cURL request %s %s...\n", method, url); headers = NULL; headers = curl_slist_append( headers, "Accept: application/vnd.github.v3+json"); headers = curl_slist_append( headers, "Content-Type: application/json"); if (auth_header) headers = curl_slist_append(headers, auth_header); /* Only clear the output buffer if we have a pointer to it. If the * user is not interested in the result we use a temporary buffer * for proper error reporting. */ if (out) { *out = (struct gcli_fetch_buffer) {0}; buf = out; } else { buf = &tmp; } curl_easy_setopt(ctx->curl, CURLOPT_URL, url); if (data) curl_easy_setopt(ctx->curl, CURLOPT_POSTFIELDS, data); curl_easy_setopt(ctx->curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(ctx->curl, CURLOPT_USERAGENT, ctx->curl_useragent); curl_easy_setopt(ctx->curl, CURLOPT_CUSTOMREQUEST, method); curl_easy_setopt(ctx->curl, CURLOPT_TCP_KEEPALIVE, 1L); curl_easy_setopt(ctx->curl, CURLOPT_WRITEDATA, buf); curl_easy_setopt(ctx->curl, CURLOPT_WRITEFUNCTION, fetch_write_callback); curl_easy_setopt(ctx->curl, CURLOPT_FAILONERROR, 0L); curl_easy_setopt(ctx->curl, CURLOPT_HEADERFUNCTION, fetch_header_callback); curl_easy_setopt(ctx->curl, CURLOPT_HEADERDATA, &link_header); curl_easy_setopt(ctx->curl, CURLOPT_FOLLOWLOCATION, 1L); if (ctx->report_progress) { curl_easy_setopt(ctx->curl, CURLOPT_XFERINFOFUNCTION, gcli_report_progress); curl_easy_setopt(ctx->curl, CURLOPT_XFERINFODATA, ctx); curl_easy_setopt(ctx->curl, CURLOPT_NOPROGRESS, 0L); } ret = curl_easy_perform(ctx->curl); rc = gcli_curl_check_api_error(ctx, ret, url, buf); if (ctx->report_progress) ctx->report_progress(true); /* only parse these headers and continue if there was no error */ if (rc == 0) { if (link_header && pagination_next) *pagination_next = parse_link_header(link_header); } else if (out) { /* error happened and we have an output buffer */ gcli_fetch_buffer_free(out); } gcli_clear_ptr(&link_header); curl_slist_free_all(headers); headers = NULL; /* if the user is not interested in the result, free the temporary * buffer */ if (!out) gcli_fetch_buffer_free(&tmp); gcli_clear_ptr(&auth_header); return rc; } /* Perform a POST request to the given URL and upload the buffer to it. * * Results are placed in out. * * content_type may not be NULL. */ int gcli_post_upload(struct gcli_ctx *ctx, char const *url, char const *content_type, void *buffer, size_t const buffer_size, struct gcli_fetch_buffer *const out) { CURLcode ret; struct curl_slist *headers; int rc = 0; char *auth_header, *contenttype_header, *contentsize_header; if ((rc = gcli_curl_ensure(ctx)) < 0) return rc; auth_header = gcli_get_authheader(ctx); contenttype_header = gcli_asprintf("Content-Type: %s", content_type); contentsize_header = gcli_asprintf("Content-Length: %zu", buffer_size); if (gcli_be_verbose(ctx)) fprintf(stderr, "info: cURL upload POST %s...\n", url); headers = NULL; headers = curl_slist_append( headers, "Accept: application/vnd.github.v3+json"); if (auth_header) headers = curl_slist_append(headers, auth_header); headers = curl_slist_append(headers, contenttype_header); headers = curl_slist_append(headers, contentsize_header); curl_easy_setopt(ctx->curl, CURLOPT_URL, url); curl_easy_setopt(ctx->curl, CURLOPT_POST, 1L); curl_easy_setopt(ctx->curl, CURLOPT_POSTFIELDS, buffer); curl_easy_setopt(ctx->curl, CURLOPT_POSTFIELDSIZE, (long)buffer_size); curl_easy_setopt(ctx->curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(ctx->curl, CURLOPT_USERAGENT, ctx->curl_useragent); curl_easy_setopt(ctx->curl, CURLOPT_WRITEDATA, out); curl_easy_setopt(ctx->curl, CURLOPT_WRITEFUNCTION, fetch_write_callback); if (ctx->report_progress) { curl_easy_setopt(ctx->curl, CURLOPT_XFERINFOFUNCTION, gcli_report_progress); curl_easy_setopt(ctx->curl, CURLOPT_XFERINFODATA, ctx); curl_easy_setopt(ctx->curl, CURLOPT_NOPROGRESS, 0L); } ret = curl_easy_perform(ctx->curl); rc = gcli_curl_check_api_error(ctx, ret, url, out); if (ctx->report_progress) ctx->report_progress(true); curl_slist_free_all(headers); headers = NULL; gcli_clear_ptr(&auth_header); gcli_clear_ptr(&contentsize_header); gcli_clear_ptr(&contenttype_header); return rc; } /** gcli_gitea_upload_attachment: * * Upload the given file to the given url. This is gitea-specific * code. */ int gcli_curl_gitea_upload_attachment(struct gcli_ctx *ctx, char const *url, char const *filename, struct gcli_fetch_buffer *const out) { CURLcode ret; curl_mime *mime; curl_mimepart *contentpart; struct curl_slist *headers; int rc = 0; char *auth_header; if ((rc = gcli_curl_ensure(ctx)) < 0) return rc; auth_header = gcli_get_authheader(ctx); if (gcli_be_verbose(ctx)) fprintf(stderr, "info: cURL upload POST %s...\n", url); headers = NULL; headers = curl_slist_append( headers, "Accept: application/json"); if (auth_header) headers = curl_slist_append(headers, auth_header); /* The docs say we should be using this mime thing. */ mime = curl_mime_init(ctx->curl); contentpart = curl_mime_addpart(mime); /* Attach the file. It will be read when curl_easy_perform is * called. This allows us to upload large files without reading or * mapping them into memory in one chunk. */ curl_mime_name(contentpart, "attachment"); ret = curl_mime_filedata(contentpart, filename); if (ret != CURLE_OK) { errx(1, "error: could not set attachment for upload: %s", curl_easy_strerror(ret)); } curl_easy_setopt(ctx->curl, CURLOPT_URL, url); curl_easy_setopt(ctx->curl, CURLOPT_MIMEPOST, mime); curl_easy_setopt(ctx->curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(ctx->curl, CURLOPT_WRITEDATA, out); curl_easy_setopt(ctx->curl, CURLOPT_WRITEFUNCTION, fetch_write_callback); if (ctx->report_progress) { curl_easy_setopt(ctx->curl, CURLOPT_XFERINFOFUNCTION, gcli_report_progress); curl_easy_setopt(ctx->curl, CURLOPT_XFERINFODATA, ctx); curl_easy_setopt(ctx->curl, CURLOPT_NOPROGRESS, 0L); } ret = curl_easy_perform(ctx->curl); rc = gcli_curl_check_api_error(ctx, ret, url, out); if (ctx->report_progress) ctx->report_progress(true); /* Cleanup */ curl_slist_free_all(headers); headers = NULL; curl_mime_free(mime); gcli_clear_ptr(&auth_header); return rc; } gcli_sv gcli_urlencode_sv(gcli_sv const _input) { size_t input_len; size_t output_len; size_t i; char *output; char *input; input = _input.data; input_len = _input.length; output = calloc(1, 3 * input_len + 1); output_len = 0; for (i = 0; i < input_len; ++i) { if (!isalnum(input[i]) && input[i] != '-' && input[i] != '_') { unsigned val = (input[i] & 0xFF); snprintf(output + output_len, 4, "%%%2.2X", val); output_len += 3; } else { output[output_len++] = input[i]; } } return gcli_sv_from_parts(output, output_len); } char * gcli_urlencode(char const *input) { gcli_sv encoded = gcli_urlencode_sv(SV((char *)input)); return encoded.data; } char * gcli_urldecode(struct gcli_ctx *ctx, char const *input) { char *curlresult, *result; if (gcli_curl_ensure(ctx) < 0) return NULL; curlresult = curl_easy_unescape(ctx->curl, input, 0, NULL); if (!curlresult) { gcli_error(ctx, "could not urldecode"); return NULL; } result = strdup(curlresult); curl_free(curlresult); return result; } /* Convenience function for fetching lists. * * listptr must be a double-pointer (pointer to a pointer to the start * of the array). e.g. * * struct foolist { struct foo *foos; size_t foos_size; } *out = ...; * * listptr = &out->foos; * listsize = &out->foos_size; * * If max is -1 then everything will be fetched. */ int gcli_fetch_list(struct gcli_ctx *ctx, char *url, struct gcli_fetch_list_ctx *fl) { char *next_url = NULL; int rc; do { struct gcli_fetch_buffer buffer = {0}; rc = gcli_fetch(ctx, url, &next_url, &buffer); if (rc == 0) { struct json_stream stream = {0}; json_open_buffer(&stream, buffer.data, buffer.length); rc = fl->parse(ctx, &stream, fl->listp, fl->sizep); if (fl->filter) fl->filter(fl->listp, fl->sizep, fl->userdata); json_close(&stream); } gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&url); if (rc < 0) break; } while ((url = next_url) && (fl->max == -1 || (int)(*fl->sizep) < fl->max)); gcli_clear_ptr(&next_url); return rc; } void gcli_fetch_buffer_free(struct gcli_fetch_buffer *const buffer) { if (!buffer) return; gcli_clear_ptr(&buffer->data); buffer->length = 0; } gcli-2.9.1/src/date_time.c000066400000000000000000000103161507017207500153340ustar00rootroot00000000000000/* * Copyright 2023-2025 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 #include int gcli_normalize_date(struct gcli_ctx *ctx, int fmt, char const *const input, char *output, size_t const output_size) { struct tm tm_buf = {0}; struct tm *utm_buf; char *endptr; time_t utctime; char const *sfmt; switch (fmt) { case DATEFMT_ISO8601: sfmt = "%Y-%m-%dT%H:%M:%SZ"; assert(output_size == 21); break; case DATEFMT_GITLAB: sfmt = "%Y%m%d"; assert(output_size == 9); break; default: return gcli_error(ctx, "bad date format"); } /* Parse input time */ endptr = strptime(input, "%Y-%m-%d", &tm_buf); if (endptr == NULL || *endptr != '\0') return gcli_error(ctx, "date »%s« is invalid: want YYYY-MM-DD", input); /* Convert to UTC: Really, we should be using the _r versions of * these functions for thread-safety but since gcli doesn't do * multithreading (except for inside libcurl) we do not need to be * worried about the storage behind the pointer returned by gmtime * to be altered by another thread. */ utctime = mktime(&tm_buf); utm_buf = gmtime(&utctime); /* Format the output string - now in UTC */ strftime(output, output_size, sfmt, utm_buf); return 0; } int gcli_parse_iso8601_date_time(struct gcli_ctx *ctx, char const *const input, time_t *const out) { char *endptr = NULL, *oldtz = NULL; time_t offset = 0; struct tm tm_buf = {0}; endptr = strptime(input, "%Y-%m-%dT%H:%M:%S", &tm_buf); /* TZ offset */ if (endptr && (*endptr == '+' || *endptr == '-')) { int hours = 0, minutes = 0, rc = 0; rc = sscanf(endptr, "%d:%d", &hours, &minutes); if (rc == 0) { return gcli_error(ctx, "failed to parse timezone offset"); } offset = (time_t)(3600 * hours + 60 * minutes); endptr = NULL; } if (endptr && *endptr != '.' && *endptr != 'Z') { return gcli_error(ctx, "failed to parse ISO8601 timestamp \"%s\": %s", input, strerror(errno)); } /* Thanks, POSIX, for this ugly pile of rubbish! */ { oldtz = getenv("TZ"); if (oldtz) oldtz = strdup(oldtz); /* TODO error handling */ setenv("TZ", "UTC", 1); tzset(); *out = mktime(&tm_buf) - offset; if (oldtz) { setenv("TZ", oldtz, 1); gcli_clear_ptr(&oldtz); } else { unsetenv("TZ"); } tzset(); } return 0; } int gcli_format_as_localtime(struct gcli_ctx *ctx, time_t timestamp, char **out) { char tmp[sizeof "YYYY-MMM-DD HH:MM:SS"] = {0}; struct tm tm_buf = {0}; size_t rc = 0; /* if the timestamp is 0 we assume it is unset. */ if (timestamp == 0) { *out = strdup("N/A"); return 0; } rc = strftime(tmp, sizeof tmp, "%Y-%b-%d %H:%M:%S", localtime_r(×tamp, &tm_buf)); if (rc + 1 != sizeof tmp) return gcli_error(ctx, "error formatting time stamp"); *out = strdup(tmp); return 0; } gcli-2.9.1/src/diffutil.c000066400000000000000000000557041507017207500152210ustar00rootroot00000000000000/* * Copyright 2023-2025 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 struct token; static bool is_patch_separator(struct token const *line); int gcli_diff_parser_from_buffer(char const *buf, size_t buf_size, char const *filename, struct gcli_diff_parser *out) { out->buf = out->hd = buf; out->buf_size = buf_size; out->filename = filename; out->col = out->row = 1; return 0; } int gcli_diff_parser_from_file(FILE *f, char const *filename, struct gcli_diff_parser *out) { long len = 0; char *buf; if (fseek(f, 0, SEEK_END) < 0) return -1; if ((len = ftell(f)) < 0) return -1; if (fseek(f, 0, SEEK_SET) < 0) return -1; buf = malloc(len + 1); buf[len] = '\0'; fread(buf, len, 1, f); out->buf_needs_free = true; return gcli_diff_parser_from_buffer(buf, len, filename, out); } static char const * gcli_strrstr(char const *const haystack, char const *const needle) { char const *hd = haystack; size_t const needle_len = strlen(needle); if (strstr(haystack, needle) == NULL) return NULL; for (;;) { char const *const tmp = strstr(hd, needle); if (tmp == NULL) return hd - needle_len; hd = tmp + needle_len; } } /* Remove a possibly parsed patch trailer included in the last hunk */ static int fixup_last_diff_in_patch(struct gcli_patch *patch) { /* find the last diff in the patch */ struct gcli_diff *d = TAILQ_LAST(&patch->diffs, gcli_diffs); if (d == NULL) return 0; /* if we found a diff grab the last hunk */ struct gcli_diff_hunk *h = TAILQ_LAST(&d->hunks, gcli_diff_hunks); assert(h); /* Find last occurence of \n--\n */ char const *const last = gcli_strrstr(h->body, "\n--\n"); if (last == NULL) return 0; size_t const offset = last - h->body; /* silly const dance */ h->body[offset + 1] = '\0'; return 0; } int gcli_parse_patch(struct gcli_diff_parser *parser, struct gcli_patch *out) { if (gcli_patch_parse_prelude(parser, out) < 0) return -1; TAILQ_INIT(&out->diffs); /* TODO cleanup */ while (parser->hd[0] == 'd') { struct gcli_diff *d = calloc(1, sizeof(*d)); if (gcli_parse_diff(parser, d) < 0) return -1; TAILQ_INSERT_TAIL(&out->diffs, d, next); } return fixup_last_diff_in_patch(out); } struct token { char const *start; char const *end; }; static inline int token_len(struct token const *const t) { return t->end - t->start; } static int nextline(struct gcli_diff_parser *parser, struct token *out) { out->start = parser->hd; if (*out->start == '\0') return -1; out->end = strchr(out->start, '\n'); if (out->end == NULL) out->end = parser->buf + parser->buf_size - 1; return 0; } static int read_commit_hash_from_separator(struct token const *line, struct gcli_patch *out) { char const *end_of_hash, *start_of_hash; start_of_hash = strchr(line->start, ' '); if (!start_of_hash) return -1; start_of_hash += 1; end_of_hash = strchr(start_of_hash, ' '); if (!end_of_hash) return -1; out->commit_hash = gcli_strndup(start_of_hash, end_of_hash - start_of_hash); return 0; } int gcli_patch_parse_prelude(struct gcli_diff_parser *parser, struct gcli_patch *out) { assert(out->prelude == NULL); char const *prelude_begin = parser->hd; for (;;) { struct token line = {0}; if (nextline(parser, &line) < 0) break; size_t const line_len = token_len(&line); if (line_len > 5 && strncmp(line.start, "diff ", 5) == 0) break; if (!out->commit_hash) { if (is_patch_separator(&line)) { int rc; rc = read_commit_hash_from_separator(&line, out); if (rc < 0) return rc; } } parser->hd = line.end + 1; parser->col = 1; parser->row += 1; } size_t const prelude_len = parser->hd - prelude_begin; out->prelude = calloc(prelude_len + 1, 1); memcpy(out->prelude, prelude_begin, prelude_len); return 0; } static int readfilename(struct token *line, char **filename) { struct token fname = {0}; fname.start = fname.end = line->start; int escapes = 0; char *outbuf; for (;;) { fname.end = strchr(fname.end, ' '); if (fname.end > line->end) { fname.end = line->end; break; } if (!fname.end || *(fname.end - 1) != '\\') break; fname.end += 1; /* skip over space */ escapes++; } if (fname.end == NULL) fname.end = line->end; outbuf = *filename = calloc(token_len(&fname) - escapes + 1, 1); for (;;) { char const *chunk_end = strchr(fname.start, ' '); if (chunk_end > fname.end) chunk_end = fname.end; strncat(outbuf, fname.start, chunk_end - fname.start); fname.start = chunk_end + 1; if (fname.start >= fname.end) break; } line->start = fname.start; return 0; } static int read_number(struct token *t, int base, int *out) { char *endptr = NULL; *out = strtol(t->start, &endptr, base); if (endptr == t->start) return -1; t->start = endptr; return 0; } static int expect_prefix(struct token *t, char const *const prefix) { size_t const prefix_len = strlen(prefix); if (token_len(t) < (int)prefix_len) return -1; if (strncmp(t->start, prefix, prefix_len)) return -1; t->start += prefix_len; return 0; } /* Git uses a patch separator in the format-patch code that ALWAYS * looks something like: * * "From long-ass-commit-hash-here Mon Sep 17 00:00:00 2001\n" * * We will 'abuse' this fact here. In fact git itself uses this to * separate commits in an e-mailed patch series. */ static bool is_patch_separator(struct token const *line) { size_t const line_len = token_len(line); char const prefix[] = "From "; char const suffix[] = " Mon Sep 17 00:00:00 2001\n"; if (line_len < sizeof(prefix) - 1) return false; if (line_len < sizeof(suffix) - 1) return false; bool const prefix_matches = memcmp(line->start, prefix, sizeof(prefix) - 1) == 0; if (!prefix_matches) return false; /* The commit hash is always 40 chars wide - but only when * using SHA1 commit hashes (version 0 format). * * When using version 1 object format, hashes are SHA256 * and thus 64 characters long. */ bool const suffix_matches = memcmp(line->start + (line_len - (sizeof(suffix) - 2)), suffix, sizeof(suffix) - 1) == 0; return suffix_matches; } static int parse_hunk_range_info(struct gcli_diff_parser *parser, struct gcli_diff_hunk *out) { struct token line = {0}; if (parser->hd[0] != '@') return -1; if (nextline(parser, &line) < 0) return -1; if (expect_prefix(&line, "@@ -") < 0) return -1; if (read_number(&line, 10, &out->range_r_start) < 0) return -1; char const delim_r = *line.start; if (delim_r == ',') { line.start += 1; if (read_number(&line, 10, &out->range_r_length) < 0) return -1; } else if (delim_r != ' ') { return -1; } if (expect_prefix(&line, " +") < 0) return -1; if (read_number(&line, 10, &out->range_a_start) < 0) return -1; char const delim_a = *line.start; if (delim_a == ',') { line.start += 1; if (read_number(&line, 10, &out->range_a_length) < 0) return -1; } else if (delim_a != ' ') { return -1; } if (expect_prefix(&line, " @@") < 0) return -1; /* in case of range context info there must be a space */ if (token_len(&line)) { if (*line.start++ != ' ') return -1; } out->context_info = calloc(token_len(&line) + 1, 1); strncat(out->context_info, line.start, token_len(&line)); parser->hd = line.end + 1; parser->diff_line_offset += 1; return 0; } static int parse_diff_header(struct gcli_diff_parser *parser, struct gcli_diff *out) { char const hunk_marker[] = "diff --git "; struct token line = {0}; if (nextline(parser, &line) < 0) return -1; size_t const line_len = token_len(&line); if (line_len < 5) return -1; if (strncmp(line.start, hunk_marker, sizeof(hunk_marker) - 1)) return -1; line.start += sizeof(hunk_marker) - 1; if (line.start[0] != 'a' || line.start[1] != '/') return -1; /* @@@ bounds check! */ line.start += 2; if (readfilename(&line, &out->file_a) < 0) return -1; if (line.start[0] != 'b' || line.start[1] != '/') return -1; line.start += 2; if (readfilename(&line, &out->file_b) < 0) return -1; if (line.start < line.end) return -1; parser->hd = line.end; if (*parser->hd++ != '\n') return -1; return 0; } static int parse_diff_index_line(struct gcli_diff_parser *parser, struct gcli_diff *out) { struct token line = {0}; char const *tl; if (nextline(parser, &line) < 0) return -1; if (expect_prefix(&line, "index ") < 0) return -1; tl = strchr(line.start, '.'); if (tl == NULL) return -1; out->hash_a = calloc(tl - line.start + 1, 1); memcpy(out->hash_a, line.start, tl - line.start); line.start = tl; if (line.start[0] != '.' || line.start[1] != '.') return -1; line.start += 2; tl = strchr(line.start, ' '); if (tl == NULL) return -1; if (tl > line.end) tl = line.end; out->hash_b = calloc(tl - line.start + 1, 1); memcpy(out->hash_b, line.start, tl - line.start); line.start = tl; if (line.start[0] == ' ') { line.start += 1; size_t const fmode_len = token_len(&line); out->file_mode = calloc(fmode_len + 1, 1); memcpy(out->file_mode, line.start, fmode_len); parser->hd = line.start + fmode_len; if (*parser->hd++ != '\n') return -1; } else { if (line.start[0] != '\n') return -1; parser->hd = line.start + 1; } return 0; } static int read_hunk_body(struct gcli_diff_parser *parser, struct gcli_diff_hunk *hunk) { struct token buf = {0}; size_t buf_len; buf.start = parser->hd; buf.end = parser->hd; hunk->diff_line_offset = parser->diff_line_offset; for (;;) { struct token line = {0}; if (parser->hd[0] == '\0') break; if (nextline(parser, &line) < 0) return -1; if (strncmp(line.start, "diff", 4) == 0) break; if (strncmp(line.start, "@@", 2) == 0) break; if (line.start[0] == 'F' && is_patch_separator(&line)) break; /* If it is a comment, don't count this line into the absolute diff * offset of the hunk */ if (line.start[0] == ' ' || line.start[0] == '+' || line.start[0] == '-' || line.start[0] == '\\') { parser->diff_line_offset += 1; } buf.end = line.end + 1; parser->hd = line.end + 1; } buf_len = token_len(&buf); hunk->body = calloc(buf_len + 1, 1); strncpy(hunk->body, buf.start, buf_len); return 0; } /* Parse the additions- or removals file name */ static int parse_hunk_a_or_r_file(struct gcli_diff_parser *parser, char c, char **out) { struct token line = {0}; size_t linelen; if (nextline(parser, &line) < 0) return -1; if (expect_prefix(&line, "--- ") == 0) { if (token_len(&line) >= 2 && line.start[0] == c && line.start[1] == '/') { line.start += 2; } else if (line.start[0] != '/') { return -1; } } else if (expect_prefix(&line, "+++ ") == 0) { if (token_len(&line) >= 2 && line.start[0] != c && line.start[1] != '/') { return -1; } line.start += 2; } else { return -1; } linelen = token_len(&line); *out = calloc(linelen + 1, 1); memcpy(*out, line.start, linelen); parser->hd = line.end + 1; return 0; } static int try_parse_new_file_mode(struct gcli_diff_parser *parser, struct gcli_diff *out) { struct token line = {0}; char const fmode_prefix[] = "new file mode "; if (nextline(parser, &line) < 0) return -1; /* Don't fail this, might be the index line */ if (expect_prefix(&line, fmode_prefix) < 0) return 0; if (read_number(&line, 8, &out->new_file_mode) < 0) return -1; if (token_len(&line)) return -1; parser->hd = line.end + 1; return 0; } int gcli_parse_diff(struct gcli_diff_parser *parser, struct gcli_diff *out) { if (parse_diff_header(parser, out) < 0) return -1; if (try_parse_new_file_mode(parser, out) < 0) return -1; if (parse_diff_index_line(parser, out) < 0) return -1; if (parse_hunk_a_or_r_file(parser, 'a', &out->r_file) < 0) return -1; if (parse_hunk_a_or_r_file(parser, 'b', &out->a_file) < 0) return -1; parser->diff_line_offset = 0; TAILQ_INIT(&out->hunks); while (parser->hd[0] == '@') { struct gcli_diff_hunk *hunk = calloc(1, sizeof(*hunk)); if (parse_hunk_range_info(parser, hunk) < 0) { gcli_clear_ptr(&hunk); return -1; } if (read_hunk_body(parser, hunk) < 0) { gcli_clear_ptr(&hunk); return -1; } TAILQ_INSERT_TAIL(&out->hunks, hunk, next); } return 0; } static int patch_series_read_prelude(struct gcli_diff_parser *parser, struct gcli_patch_series *series) { char const *prelude_begin = parser->hd; for (;;) { struct token line = {0}; if (nextline(parser, &line) < 0) break; if (is_patch_separator(&line)) break; parser->hd = line.end + 1; parser->col = 1; parser->row += 1; } size_t const prelude_len = parser->hd - prelude_begin; series->prelude = calloc(prelude_len + 1, 1); memcpy(series->prelude, prelude_begin, prelude_len); return 0; } int gcli_parse_patch_series(struct gcli_diff_parser *parser, struct gcli_patch_series *series) { TAILQ_INIT(&series->patches); if (patch_series_read_prelude(parser, series) < 0) return -1; while (parser->hd[0] != '\0') { struct gcli_patch *p = calloc(1, sizeof(*p)); TAILQ_INSERT_TAIL(&series->patches, p, next); if (gcli_parse_patch(parser, p) < 0) return -1; } return 0; } void gcli_free_diff_hunk(struct gcli_diff_hunk *hunk) { gcli_clear_ptr(&hunk->context_info); gcli_clear_ptr(&hunk->body); } void gcli_free_diff(struct gcli_diff *diff) { gcli_clear_ptr(&diff->file_a); gcli_clear_ptr(&diff->file_b); gcli_clear_ptr(&diff->hash_a); gcli_clear_ptr(&diff->hash_b); gcli_clear_ptr(&diff->file_mode); gcli_clear_ptr(&diff->r_file); gcli_clear_ptr(&diff->a_file); struct gcli_diff_hunk *h = TAILQ_FIRST(&diff->hunks); while (h) { struct gcli_diff_hunk *n = TAILQ_NEXT(h, next); gcli_free_diff_hunk(h); gcli_clear_ptr(&h); h = n; } TAILQ_INIT(&diff->hunks); } void gcli_free_patch(struct gcli_patch *patch) { struct gcli_diff *d, *n; gcli_clear_ptr(&patch->prelude); d = TAILQ_FIRST(&patch->diffs); while (d) { n = TAILQ_NEXT(d, next); gcli_free_diff(d); gcli_clear_ptr(&d); d = n; } TAILQ_INIT(&patch->diffs); } void gcli_free_diff_parser(struct gcli_diff_parser *parser) { if (parser->buf_needs_free) gcli_clear_ptr(&parser->buf); memset(parser, 0, sizeof(*parser)); } /******************************************************************** * Comment Extraction *******************************************************************/ struct hunk_line_info { int patched_line; int original_line; }; struct comment_read_ctx { struct gcli_diff const *diff; struct gcli_diff_hunk const *hunk; struct gcli_diff_comments *comments; char const *front; struct hunk_line_info line_info; int diff_line_offset; /* Offset of the comment within the current diff */ bool last_line_is_new; }; static struct gcli_diff_comment * make_comment(struct comment_read_ctx *ctx, char *text, struct hunk_line_info const *line_info, int diff_line_offset) { struct gcli_diff_comment *comment = calloc(1, sizeof(*comment)); comment->after.filename = strdup(ctx->diff->file_b); comment->after.start_row = line_info->patched_line; comment->after.end_row = line_info->patched_line; comment->before.filename = strdup(ctx->diff->file_a); comment->before.start_row = line_info->original_line; comment->before.end_row = line_info->original_line; comment->comment = text; comment->diff_line_offset = diff_line_offset; /* If the diff has an associated patch use its commit hash. otherwise fallback * to the hash in the diff header. This may happen when we only parsed a * diff and not a patch and extracted comments from it. */ if (ctx->diff->patch && ctx->diff->patch->commit_hash) comment->commit_hash = strdup(ctx->diff->patch->commit_hash); else comment->commit_hash = strdup(ctx->diff->hash_b); return comment; } static int read_comment_unprefixed(struct comment_read_ctx *ctx) { char const *start = ctx->front; struct hunk_line_info const line = ctx->line_info; int const diff_line_offset = ctx->diff_line_offset; size_t comment_len = 0; struct gcli_diff_comment *cmt; char *text; for (;;) { char c = *ctx->front; if (c == ' ' || c == '+' || c == '-' || c == '{') break; else if (c == '\0') /* invalid: comment at end of hunk */ return -1; ctx->front = strchr(ctx->front, '\n'); if (ctx->front == NULL) break; ctx->diff_line_offset += 1; ctx->front += 1; } if (ctx->front) comment_len = ctx->front - start; else comment_len = strlen(start); text = calloc(comment_len + 1, 1); memcpy(text, start, comment_len); cmt = make_comment(ctx, text, &line, diff_line_offset); TAILQ_INSERT_TAIL(ctx->comments, cmt, next); return 0; } static int read_comment_prefixed(struct comment_read_ctx *ctx) { char const *start = ctx->front; struct hunk_line_info const line = ctx->line_info; int const diff_line_offset = ctx->diff_line_offset; size_t comment_len = 0; struct gcli_diff_comment *cmt; char *text; for (;;) { char const *c = ctx->front; if (*c != '>') { if (*c == ' ' || *c == '+' || *c == '-' || *c == '{') break; else if (*c == '\0') /* invalid: comment at end of hunk */ return -1; } ctx->front = strchr(c, '\n'); if (ctx->front == NULL) break; /* Skip the prefix chars */ size_t const prefix_len = c[1] == ' ' ? 2 : 1; comment_len += ctx->front - (c + prefix_len) + 1; ctx->diff_line_offset += 1; ctx->front += 1; } text = calloc(comment_len + 1, 1); while (start < ctx->front) { char const *line_end = strchr(start, '\n'); size_t const prefix_len = start[1] == ' ' ? 2 : 1; strncat(text, start + prefix_len, line_end - (start + prefix_len) + 1); start = line_end + 1; } cmt = make_comment(ctx, text, &line, diff_line_offset); TAILQ_INSERT_TAIL(ctx->comments, cmt, next); return 0; } static int read_comment(struct comment_read_ctx *ctx) { if (strncmp(ctx->front, "> ", 2) == 0) return read_comment_prefixed(ctx); else return read_comment_unprefixed(ctx); } static int gcli_hunk_get_comments(struct gcli_diff const *diff, struct gcli_diff_hunk const *hunk, struct gcli_diff_comments *out) { struct comment_read_ctx ctx = { .diff = diff, .hunk = hunk, .comments = out, .front = hunk->body, .line_info = { .patched_line = hunk->range_a_start, .original_line = hunk->range_r_start, }, .diff_line_offset = hunk->diff_line_offset, .last_line_is_new = false, }; char const *range_start = NULL; bool in_comment = false, /* whether we are reading lines referring to a comment */ in_multiline_comment = false, /* whether the current comment is a multiline comment */ is_first_line = true; /* whether this is the first diff line of a comment */ for (;;) { struct gcli_diff_comment *c = TAILQ_LAST(out, gcli_diff_comments); char const hd = *ctx.front; switch (hd) { case '\0': break; case '+': case ' ': case '-': case '\\': ctx.diff_line_offset += 1; ctx.last_line_is_new = hd == '+'; if (hd == '+' || hd == ' ') ctx.line_info.patched_line += 1; if (hd == '-' || hd == ' ') ctx.line_info.original_line += 1; if (c && !c->diff_text && !in_multiline_comment) { char const *end = strchr(ctx.front, '\n'); if (end == NULL) end = ctx.front + strlen(ctx.front); end += 1; c->diff_text = calloc((end - ctx.front) + 1, 1); memcpy(c->diff_text, ctx.front, end - ctx.front); c->start_is_in_new = c->end_is_in_new = hd == '+'; } if (c && in_comment && in_multiline_comment) { c->end_is_in_new = ctx.last_line_is_new; if (is_first_line) c->start_is_in_new = ctx.last_line_is_new; } is_first_line = false; break; case '{': if (!c || c->diff_text) return -1; range_start = ctx.front; in_multiline_comment = true; break; case '}': { assert(range_start != NULL); if (!c || !in_comment || !in_multiline_comment) return -1; /* no comment to refer to */ range_start += 2; size_t const len = ctx.front - range_start; c->diff_text = calloc(len + 1, 1); memcpy(c->diff_text, range_start, len); /* FIXME: add deletion-only and addition-only detection here */ if (c->after.end_row != ctx.line_info.patched_line) c->after.end_row = ctx.line_info.patched_line - 1; if (c->before.end_row != ctx.line_info.original_line) c->before.end_row = ctx.line_info.original_line - 1; in_comment = false; } break; case '\n': break; /* skip - not comment */ default: { /* comment */ if (read_comment(&ctx) < 0) return -1; in_comment = true; in_multiline_comment = false; is_first_line = true; continue; } break; } if ((ctx.front = strchr(ctx.front, '\n')) == NULL) break; ctx.front += 1; } return 0; } static int gcli_diff_get_comments(struct gcli_diff const *diff, struct gcli_diff_comments *out) { struct gcli_diff_hunk *hunk; TAILQ_FOREACH(hunk, &diff->hunks, next) { if (gcli_hunk_get_comments(diff, hunk, out) < 0) return -1; } return 0; } int gcli_patch_get_comments(struct gcli_patch const *patch, struct gcli_diff_comments *out) { struct gcli_diff const *diff; TAILQ_FOREACH(diff, &patch->diffs, next) { if (gcli_diff_get_comments(diff, out) < 0) return -1; } return 0; } int gcli_patch_series_get_comments(struct gcli_patch_series const *series, struct gcli_diff_comments *out) { struct gcli_patch const *patch; TAILQ_INIT(out); TAILQ_FOREACH(patch, &series->patches, next) { if (gcli_patch_get_comments(patch, out) < 0) return -1; } return 0; } void gcli_free_patch_series(struct gcli_patch_series *series) { struct gcli_patch *p = TAILQ_FIRST(&series->patches); while (p) { struct gcli_patch *n = TAILQ_NEXT(p, next); gcli_free_patch(p); gcli_clear_ptr(&p); p = n; } TAILQ_INIT(&series->patches); } gcli-2.9.1/src/forges.c000066400000000000000000000415221507017207500146710ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static struct gcli_forge_descriptor const github_forge_descriptor = { /* Comments */ .get_comment = github_get_comment, .get_issue_comments = github_get_comments, .get_pull_comments = github_get_comments, .perform_submit_comment = github_perform_submit_comment, /* Forks */ .fork_create = github_fork_create, .get_forks = github_get_forks, /* Issues */ .get_issue_summary = github_get_issue_summary, .search_issues = github_issues_search, .issue_add_labels = github_issue_add_labels, .issue_assign = github_issue_assign, .issue_clear_milestone = github_issue_clear_milestone, .issue_close = github_issue_close, .issue_remove_labels = github_issue_remove_labels, .issue_reopen = github_issue_reopen, .issue_set_milestone = github_issue_set_milestone, .issue_set_title = github_issue_set_title, .issue_set_op = github_issue_set_op, .perform_submit_issue = github_perform_submit_issue, .issue_quirks = GCLI_ISSUE_QUIRKS_PROD_COMP | GCLI_ISSUE_QUIRKS_URL, /* Milestones */ .create_milestone = github_create_milestone, .delete_milestone = github_delete_milestone, .get_milestone = github_get_milestone, .get_milestone_issues = github_milestone_get_issues, .get_milestones = github_get_milestones, .milestone_set_duedate = github_milestone_set_duedate, /* Pull requests */ .get_pull = github_get_pull, .get_pull_checks = github_pull_get_checks, .get_pull_commits = github_get_pull_commits, .search_pulls = github_search_pulls, .perform_submit_pull = github_perform_submit_pull, .pull_add_reviewer = github_pull_add_reviewer, .pull_assign = github_issue_assign, .pull_close = github_pull_close, .pull_get_diff = github_pull_get_diff, .pull_get_patch = github_pull_get_patch, .pull_merge = github_pull_merge, .pull_reopen = github_pull_reopen, .pull_set_title = github_pull_set_title, .pull_checkout = github_pull_checkout, /* HACK: Here we can use the same functions as with issues because * PRs are the same as issues on Github and the functions have the * same types/arguments */ .pull_add_labels = github_issue_add_labels, .pull_clear_milestone = github_issue_clear_milestone, .pull_remove_labels = github_issue_remove_labels, .pull_set_milestone = github_issue_set_milestone, .pull_create_review = github_pull_create_review, .pull_get_reviews = github_pull_get_reviews, .pull_get_review_threads = github_pull_get_review_threads, .create_release = github_create_release, .delete_release = github_delete_release, .get_releases = github_get_releases, /* Labels */ .create_label = github_create_label, .delete_label = github_delete_label, .get_labels = github_get_labels, .get_label = github_get_label, .label_set_title = github_label_set_title, .label_set_description = github_label_set_description, .label_set_colour = github_label_set_colour, /* Repos */ .get_repos = github_get_repos, .repo_create = github_repo_create, .repo_delete = github_repo_delete, .repo_set_visibility = github_repo_set_visibility, /* SSH Key management */ .add_sshkey = github_add_sshkey, .delete_sshkey = github_delete_sshkey, .get_sshkeys = github_get_sshkeys, /* Notifications */ .get_notifications = github_get_notifications, .notification_mark_as_read = github_notification_mark_as_read, /* Internal stuff */ .get_api_error_string = github_api_error_string, .make_authheader = github_make_authheader, .user_object_key = "login", /* Quirks */ .milestone_quirks = GCLI_MILESTONE_QUIRKS_EXPIRED | GCLI_MILESTONE_QUIRKS_DUEDATE | GCLI_MILESTONE_QUIRKS_PULLS, .pull_summary_quirks = GCLI_PRS_QUIRK_COVERAGE | GCLI_PRS_QUIRK_AUTOMERGE, /* automerge field seems broken */ }; static struct gcli_forge_descriptor const gitlab_forge_descriptor = { /* Comments */ .get_comment = gitlab_get_comment, .get_issue_comments = gitlab_get_issue_comments, .get_pull_comments = gitlab_get_mr_comments, .perform_submit_comment = gitlab_perform_submit_comment, /* Forks */ .fork_create = gitlab_fork_create, .get_forks = gitlab_get_forks, /* Issues */ .get_issue_summary = gitlab_get_issue_summary, .search_issues = gitlab_issues_search, .issue_add_labels = gitlab_issue_add_labels, .issue_assign = gitlab_issue_assign, .issue_clear_milestone = gitlab_issue_clear_milestone, .issue_close = gitlab_issue_close, .issue_remove_labels = gitlab_issue_remove_labels, .issue_reopen = gitlab_issue_reopen, .issue_set_milestone = gitlab_issue_set_milestone, .issue_set_title = gitlab_issue_set_title, .issue_set_op = gitlab_issue_set_op, .perform_submit_issue = gitlab_perform_submit_issue, .issue_quirks = GCLI_ISSUE_QUIRKS_PROD_COMP | GCLI_ISSUE_QUIRKS_URL, /* Milestones */ .create_milestone = gitlab_create_milestone, .delete_milestone = gitlab_delete_milestone, .get_milestone = gitlab_get_milestone, .get_milestone_issues = gitlab_milestone_get_issues, .get_milestones = gitlab_get_milestones, .milestone_set_duedate = gitlab_milestone_set_duedate, /* Pull requests */ .get_pull = gitlab_get_pull, .get_pull_checks = (gcli_get_pull_checks_cb)gitlab_get_mr_pipelines, .get_pull_commits = gitlab_get_pull_commits, .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, .pull_assign = gitlab_mr_add_assignee, .pull_clear_milestone = gitlab_mr_clear_milestone, .pull_close = gitlab_mr_close, .pull_create_review = gitlab_mr_create_review, .pull_get_diff = gitlab_mr_get_diff, .pull_get_patch = gitlab_mr_get_patch, .pull_merge = gitlab_mr_merge, .pull_remove_labels = gitlab_mr_remove_labels, .pull_reopen = gitlab_mr_reopen, .pull_set_milestone = gitlab_mr_set_milestone, .pull_set_title = gitlab_mr_set_title, .pull_checkout = gitlab_mr_checkout, /* Releases */ .create_release = gitlab_create_release, .delete_release = gitlab_delete_release, .get_releases = gitlab_get_releases, /* Labels */ .create_label = gitlab_create_label, .delete_label = gitlab_delete_label, .get_labels = gitlab_get_labels, .get_label = gitlab_get_label, .label_set_title = gitlab_label_set_title, .label_set_description = gitlab_label_set_description, .label_set_colour = gitlab_label_set_colour, /* Repos */ .get_repos = gitlab_get_repos, .repo_create = gitlab_repo_create, .repo_delete = gitlab_repo_delete, .repo_set_visibility = gitlab_repo_set_visibility, /* SSH Key management */ .add_sshkey = gitlab_add_sshkey, .delete_sshkey = gitlab_delete_sshkey, .get_sshkeys = gitlab_get_sshkeys, /* Notifications */ .get_notifications = gitlab_get_notifications, .notification_mark_as_read = gitlab_notification_mark_as_read, /* Internal stuff */ .get_api_error_string = gitlab_api_error_string, .make_authheader = gitlab_make_authheader, .user_object_key = "username", /* Quirks */ .milestone_quirks = GCLI_MILESTONE_QUIRKS_NISSUES, .pull_summary_quirks = GCLI_PRS_QUIRK_ADDDEL | GCLI_PRS_QUIRK_COMMITS | GCLI_PRS_QUIRK_CHANGES | GCLI_PRS_QUIRK_MERGED, }; static struct gcli_forge_descriptor const gitea_forge_descriptor = { /* Comments */ .get_comment = gitea_get_comment, .get_issue_comments = gitea_get_comments, .get_pull_comments = gitea_get_comments, .perform_submit_comment = gitea_perform_submit_comment, /* Forks */ .fork_create = gitea_fork_create, .get_forks = gitea_get_forks, /* Issues */ .get_issue_summary = gitea_get_issue_summary, .search_issues = gitea_issues_search, .issue_add_labels = gitea_issue_add_labels, .issue_assign = gitea_issue_assign, .issue_clear_milestone = gitea_issue_clear_milestone, .issue_close = gitea_issue_close, .issue_remove_labels = gitea_issue_remove_labels, .issue_reopen = gitea_issue_reopen, .issue_set_milestone = gitea_issue_set_milestone, .issue_set_title = gitea_issue_set_title, .issue_set_op = gitea_issue_set_op, .perform_submit_issue = gitea_submit_issue, .issue_quirks = GCLI_ISSUE_QUIRKS_PROD_COMP | GCLI_ISSUE_QUIRKS_URL, /* Milestones */ .create_milestone = gitea_create_milestone, .delete_milestone = gitea_delete_milestone, .get_milestone = gitea_get_milestone, .get_milestone_issues = gitea_milestone_get_issues, .get_milestones = gitea_get_milestones, .milestone_set_duedate = gitea_milestone_set_duedate, /* Pull requests */ .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, .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, .pull_assign = gitea_pull_assign, .pull_clear_milestone = gitea_pull_clear_milestone, .pull_close = gitea_pull_close, .pull_get_diff = gitea_pull_get_diff, .pull_get_patch = gitea_pull_get_patch, .pull_merge = gitea_pull_merge, .pull_remove_labels = gitea_issue_remove_labels, .pull_reopen = gitea_pull_reopen, .pull_set_milestone = gitea_pull_set_milestone, .pull_set_title = gitea_pull_set_title, .pull_get_reviews = gitea_pull_get_reviews, /* Releases */ .create_release = gitea_create_release, .delete_release = gitea_delete_release, .get_releases = gitea_get_releases, /* Labels */ .create_label = gitea_create_label, .delete_label = gitea_delete_label, .get_labels = gitea_get_labels, .get_label = gitea_get_label, .label_set_title = gitea_label_set_title, .label_set_description = gitea_label_set_description, .label_set_colour = gitea_label_set_colour, /* Repos */ .get_repos = gitea_get_repos, .repo_create = gitea_repo_create, .repo_delete = gitea_repo_delete, .repo_set_visibility = gitea_repo_set_visibility, /* SSH Key management */ .add_sshkey = gitea_add_sshkey, .delete_sshkey = gitea_delete_sshkey, .get_sshkeys = gitea_get_sshkeys, /* Notifications */ .get_notifications = gitea_get_notifications, .notification_mark_as_read = gitea_notification_mark_as_read, /* Internal stuff */ .make_authheader = gitea_make_authheader, .get_api_error_string = github_api_error_string, /* hack! */ .user_object_key = "username", /* Quirks */ .milestone_quirks = GCLI_MILESTONE_QUIRKS_EXPIRED | GCLI_MILESTONE_QUIRKS_PULLS, .pull_summary_quirks = GCLI_PRS_QUIRK_COMMITS | GCLI_PRS_QUIRK_ADDDEL | GCLI_PRS_QUIRK_AUTOMERGE | GCLI_PRS_QUIRK_DRAFT | GCLI_PRS_QUIRK_CHANGES | GCLI_PRS_QUIRK_COVERAGE, .pull_quirks = GCLI_PULL_QUIRK_AUTOMERGE, /* uses Github Backend with GraphQL * which doesn't work with gitea */ }; static struct gcli_forge_descriptor const bugzilla_forge_descriptor = { /* Issues */ .search_issues = bugzilla_get_bugs, .get_issue_summary = bugzilla_get_bug, .get_issue_comments = bugzilla_bug_get_comments, .get_comment = bugzilla_bug_get_comment, .get_issue_attachments = bugzilla_bug_get_attachments, .perform_submit_issue = bugzilla_bug_submit, .issue_quirks = GCLI_ISSUE_QUIRKS_COMMENTS | GCLI_ISSUE_QUIRKS_LOCKED, .perform_submit_comment = bugzilla_submit_comment, .attachment_get_content = bugzilla_attachment_get_content, /* Internal stuff */ .make_authheader = bugzilla_make_authheader, .get_api_error_string = bugzilla_api_error_string, .user_object_key = "---dummy---", }; struct gcli_forge_descriptor const * gcli_forge(struct gcli_ctx *ctx) { switch (ctx->get_forge_type(ctx)) { case GCLI_FORGE_GITHUB: return &github_forge_descriptor; case GCLI_FORGE_GITLAB: return &gitlab_forge_descriptor; case GCLI_FORGE_GITEA: return &gitea_forge_descriptor; case GCLI_FORGE_BUGZILLA: return &bugzilla_forge_descriptor; default: errx(1, "error: cannot determine forge type. try forcing an account " "with -a, specifying -t or create a .gcli file."); } return NULL; } gcli-2.9.1/src/forks.c000066400000000000000000000042761507017207500145350ustar00rootroot00000000000000/* * Copyright 2021-2025 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 int gcli_get_forks(struct gcli_ctx *ctx, struct gcli_path const *const path, int const max, struct gcli_fork_list *const out) { gcli_null_check_call(get_forks, ctx, path, max, out); } int gcli_fork_create(struct gcli_ctx *ctx, struct gcli_path const *const repo_path, char const *const in) { gcli_null_check_call(fork_create, ctx, repo_path, in); } void gcli_fork_free(struct gcli_fork *fork) { gcli_clear_ptr(&fork->full_name); gcli_clear_ptr(&fork->owner); } void gcli_forks_free(struct gcli_fork_list *const list) { for (size_t i = 0; i < list->forks_size; ++i) { gcli_fork_free(&list->forks[i]); } gcli_clear_ptr(&list->forks); list->forks_size = 0; } gcli-2.9.1/src/gcli.c000066400000000000000000000043671507017207500143300ustar00rootroot00000000000000/* * Copyright 2023-2025 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 char const * gcli_init(struct gcli_ctx **ctx, gcli_forge_type (*get_forge_type)(struct gcli_ctx *), char *(*get_token)(struct gcli_ctx *), char *(*get_apibase)(struct gcli_ctx *)) { *ctx = calloc(1, sizeof (struct gcli_ctx)); if (!(*ctx)) return strerror(errno); (*ctx)->get_forge_type = get_forge_type; (*ctx)->get_token = get_token; (*ctx)->get_apibase = get_apibase; (*ctx)->apibase = NULL; return NULL; } void gcli_destroy(struct gcli_ctx **ctx) { if (ctx && *ctx) { gcli_clear_ptr(&(*ctx)->apibase); gcli_clear_ptr(ctx); /* TODO: other deinit stuff? */ } } char const * gcli_get_error(struct gcli_ctx *ctx) { if (ctx->last_error) return ctx->last_error; else return "No error"; } void gcli_clear_ptr(void *ptr) { void **_ptr = ptr; free(*_ptr); *_ptr = NULL; } gcli-2.9.1/src/gitea/000077500000000000000000000000001507017207500143255ustar00rootroot00000000000000gcli-2.9.1/src/gitea/comments.c000066400000000000000000000041261507017207500163210ustar00rootroot00000000000000/* * Copyright 2022 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 int gitea_get_comments(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, struct gcli_comment_list *const out) { return github_get_comments(ctx, issue_path, out); } int gitea_perform_submit_comment(struct gcli_ctx *ctx, struct gcli_submit_comment_opts const *const opts) { return github_perform_submit_comment(ctx, opts); } int gitea_get_comment(struct gcli_ctx *ctx, struct gcli_path const *const target, enum comment_target_type target_type, gcli_id const comment_id, struct gcli_comment *out) { return github_get_comment(ctx, target, target_type, comment_id, out); } gcli-2.9.1/src/gitea/config.c000066400000000000000000000030631507017207500157400ustar00rootroot00000000000000/* * Copyright 2022-2025 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 char * gitea_make_authheader(struct gcli_ctx *ctx, char const *token) { (void) ctx; return gcli_asprintf("Authorization: token %s", token); } gcli-2.9.1/src/gitea/forks.c000066400000000000000000000034751507017207500156260ustar00rootroot00000000000000/* * Copyright 2021,2022 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 int gitea_get_forks(struct gcli_ctx *ctx, struct gcli_path const *const path, int const max, struct gcli_fork_list *const out) { return github_get_forks(ctx, path, max, out); } int gitea_fork_create(struct gcli_ctx *ctx, struct gcli_path const *const repo_path, char const *_in) { return github_fork_create(ctx, repo_path, _in); } gcli-2.9.1/src/gitea/issues.c000066400000000000000000000236601507017207500160130ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include #include #include #include #include #include #include #include int gitea_issue_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char **url, char const *const suffix_fmt, ...) { char *suffix = NULL; int rc = 0; va_list vp; va_start(vp, suffix_fmt); suffix = gcli_vasprintf(suffix_fmt, vp); va_end(vp); switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner = NULL, *e_repo = NULL; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *url = gcli_asprintf("%s/repos/%s/%s/issues/%"PRIid"%s", gcli_get_apibase(ctx), e_owner, e_repo, path->as_default.id, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_URL: { *url = gcli_asprintf("%s%s", path->as_url, suffix); } break; default: { rc = gcli_error(ctx, "unsupported path kind for Gitea issues"); } break; } gcli_clear_ptr(&suffix); return rc; } static int gitea_issues_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const suffix, char **url) { return gitea_repo_make_url(ctx, path, url, "/issues%s", suffix ? suffix : ""); } int gitea_issues_search(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue_fetch_details const *details, int const max, struct gcli_issue_list *const out) { char *url = NULL, *suffix = NULL; int rc = 0; struct gcli_fetch_list_ctx fl = { .listp = &out->issues, .sizep = &out->issues_size, .parse = (parsefn)(parse_github_issues), .max = max, }; gcli_url_options_append(&suffix, "type", "issues"); gcli_url_options_append(&suffix, "state", details->all ? "all" : "open"); gcli_url_options_append(&suffix, "milestones", details->milestone); gcli_url_options_append(&suffix, "created_by", details->author); gcli_url_options_append(&suffix, "labels", details->label); gcli_url_options_append(&suffix, "assigned_by", details->assignee); gcli_url_options_append(&suffix, "q", details->search_term); rc = gitea_issues_make_url(ctx, path, suffix, &url); gcli_clear_ptr(&suffix); if (rc < 0) return rc; return gcli_fetch_list(ctx, url, &fl); } int gitea_get_issue_summary(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue *const out) { return github_get_issue_summary(ctx, path, out); } int 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); } static int gitea_issue_patch_state(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const state) { char *url = NULL, *payload = NULL; struct gcli_jsongen gen = {0}; int rc = 0; rc = gitea_issue_make_url(ctx, path, &url, ""); if (rc < 0) return rc; gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "state"); gcli_jsongen_string(&gen, state); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "PATCH", url, payload, NULL, NULL); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } /* Gitea has closed, Github has close ... go figure */ int gitea_issue_close(struct gcli_ctx *ctx, struct gcli_path const *const path) { return gitea_issue_patch_state(ctx, path, "closed"); } int gitea_issue_reopen(struct gcli_ctx *ctx, struct gcli_path const *const path) { return gitea_issue_patch_state(ctx, path, "open"); } int gitea_issue_assign(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, char const *const assignee) { char *url = NULL, *payload = NULL; struct gcli_jsongen gen = {0}; int rc = 0; /* Generate URL */ rc = gitea_issue_make_url(ctx, issue_path, &url, ""); if (rc < 0) return rc; /* Generate payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "assignees"); gcli_jsongen_begin_array(&gen); gcli_jsongen_string(&gen, assignee); gcli_jsongen_end_array(&gen); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "PATCH", url, payload, NULL, NULL); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } /* Return the stringified id of the given label */ static char * get_id_of_label(char const *label_name, struct gcli_label_list const *const list) { for (size_t i = 0; i < list->labels_size; ++i) if (strcmp(list->labels[i].name, label_name) == 0) return gcli_asprintf("%"PRIid, list->labels[i].id); return NULL; } static void free_id_list(char *list[], size_t const list_size) { for (size_t i = 0; i < list_size; ++i) { gcli_clear_ptr(&list[i]); } gcli_clear_ptr(&list); } static char ** label_names_to_ids(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const names[], size_t const names_size) { struct gcli_label_list list = {0}; char **ids = NULL; size_t ids_size = 0; gitea_get_labels(ctx, path, -1, &list); for (size_t i = 0; i < names_size; ++i) { char *const label_id = get_id_of_label(names[i], &list); if (!label_id) { free_id_list(ids, ids_size); ids = NULL; gcli_error(ctx, "no such label '%s'", names[i]); goto out; } ids = realloc(ids, sizeof(*ids) * (ids_size +1)); ids[ids_size++] = label_id; } out: gcli_free_labels(&list); return ids; } int gitea_issue_add_labels(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const labels[], size_t const labels_size) { char *payload = NULL, *url = NULL; struct gcli_jsongen gen = {0}; int rc = 0; /* First, convert to ids */ char **ids = label_names_to_ids(ctx, path, labels, labels_size); if (!ids) return -1; rc = gitea_issue_make_url(ctx, path, &url, "/labels"); if (rc < 0) { free_id_list(ids, labels_size); return rc; } /* Construct json payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "labels"); gcli_jsongen_begin_array(&gen); for (size_t i = 0; i < labels_size; ++i) { gcli_jsongen_string(&gen, ids[i]); } gcli_jsongen_end_array(&gen); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); free_id_list(ids, labels_size); rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, NULL); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } int gitea_issue_remove_labels(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const labels[], size_t const labels_size) { int rc = 0; /* Unfortunately the gitea api does not give us an endpoint to * delete labels from an issue in bulk. So, just iterate over the * given labels and delete them one after another. */ char **ids = label_names_to_ids(ctx, path, labels, labels_size); if (!ids) return -1; for (size_t i = 0; i < labels_size; ++i) { char *url = NULL; rc = gitea_issue_make_url(ctx, path, &url, "/labels/%s", ids[i]); if (rc < 0) break; rc = gcli_fetch_with_method(ctx, "DELETE", url, NULL, NULL, NULL); gcli_clear_ptr(&url); if (rc < 0) break; } free_id_list(ids, labels_size); return rc; } int gitea_issue_set_milestone(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, gcli_id const milestone) { return github_issue_set_milestone(ctx, issue_path, milestone); } int gitea_issue_clear_milestone(struct gcli_ctx *ctx, struct gcli_path const *const issue_path) { return github_issue_set_milestone(ctx, issue_path, 0); } int gitea_issue_set_title(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, char const *const new_title) { return github_issue_set_title(ctx, issue_path, new_title); } int gitea_issue_set_op(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const new_op) { return github_issue_set_op(ctx, path, new_op); } gcli-2.9.1/src/gitea/labels.c000066400000000000000000000153601507017207500157400ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include /* Recycle the GitHub parser for labels */ #include int gitea_get_labels(struct gcli_ctx *ctx, struct gcli_path const *const path, int max, struct gcli_label_list *const list) { return github_get_labels(ctx, path, max, list); } int gitea_create_label(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_label *const label) { return github_create_label(ctx, path, label); } /* Resolve the label name to an ID. */ static int gitea_get_label_id(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const label_name, gcli_id *const id) { struct gcli_label_list list = {0}; int rc = 0; *id = 0; rc = gitea_get_labels(ctx, path, -1, &list); if (rc < 0) return rc; /* Search for the id */ for (size_t i = 0; i < list.labels_size; ++i) { if (strcmp(list.labels[i].name, label_name) == 0) { *id = list.labels[i].id; rc = 0; goto done; } } /* not found */ rc = gcli_error(ctx, "%s: no such label", label_name); done: gcli_free_labels(&list); return rc; } static int gitea_label_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char **url, char const *const fmt, ...) { char *suffix = NULL; int rc = 0; va_list vp; va_start(vp, fmt); suffix = gcli_vasprintf(fmt, vp); va_end(vp); switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner = NULL, *e_repo = NULL; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *url = gcli_asprintf("%s/repos/%s/%s/labels/%"PRIid"%s", gcli_get_apibase(ctx), e_owner, e_repo, path->as_default.id, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_NAMED: { struct gcli_path repo_path = {0}; char *e_owner, *e_repo; gcli_id id = 0; /* prepare the path to the repository */ repo_path.kind = GCLI_PATH_DEFAULT; repo_path.as_default.owner = path->as_named.owner; repo_path.as_default.repo = path->as_named.repo; /* resolve the id */ rc = gitea_get_label_id(ctx, &repo_path, path->as_named.id, &id); if (rc < 0) goto done; /* now make the actual URL */ e_owner = gcli_urlencode(path->as_named.owner); e_repo = gcli_urlencode(path->as_named.repo); *url = gcli_asprintf("%s/repos/%s/%s/labels/%"PRIid"%s", gcli_get_apibase(ctx), e_owner, e_repo, id, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_URL: { *url = gcli_asprintf("%s%s", path->as_url, suffix); } break; default: { rc = gcli_error(ctx, "unsupported path kind for Gitea issues"); } break; } done: gcli_clear_ptr(&suffix); return rc; } int gitea_delete_label(struct gcli_ctx *ctx, struct gcli_path const *const path) { char *url = NULL; int rc = 0; /* DELETE /repos/{owner}/{repo}/labels/{} */ rc = gitea_label_make_url(ctx, path, &url, ""); if (rc == 0) { rc = gcli_fetch_with_method(ctx, "DELETE", url, NULL, NULL, NULL); } gcli_clear_ptr(&url); return rc; } int gitea_get_label(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_label *const out) { int rc = 0; char *url = NULL; struct gcli_fetch_buffer buffer = {0}; rc = gitea_label_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc == 0) { struct json_stream stream = {0}; json_open_buffer(&stream, buffer.data, buffer.length); parse_github_label(ctx, &stream, out); json_close(&stream); } gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&url); return rc; } static int gitea_label_update_property(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const propname, char const *const val) { char *url = NULL, *payload = NULL; int rc = 0; struct gcli_jsongen gen = {0}; /* generate URL */ rc = gitea_label_make_url(ctx, path, &url, ""); if (rc < 0) return rc; /* generate payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, propname); gcli_jsongen_string(&gen, val); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); /* perform request */ rc = gcli_fetch_with_method(ctx, "PATCH", url, payload, NULL, NULL); gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } int gitea_label_set_title(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const new_name) { return gitea_label_update_property(ctx, path, "name", new_name); } int gitea_label_set_description(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const description) { return gitea_label_update_property( ctx, path, "description", description); } int gitea_label_set_colour(struct gcli_ctx *ctx, struct gcli_path const *const path, uint32_t const colour) { char *colour_string = NULL; int rc = 0; colour_string = gcli_asprintf("#%06X", colour & 0xFFFFFF); rc = gitea_label_update_property(ctx, path, "color", colour_string); gcli_clear_ptr(&colour_string); return rc; } gcli-2.9.1/src/gitea/milestones.c000066400000000000000000000114111507017207500166510ustar00rootroot00000000000000/* * Copyright 2023-2025 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 #include #include int gitea_milestone_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char **url, char const *const suffix_fmt, ...) { char *suffix = NULL; int rc = 0; va_list vp; va_start(vp, suffix_fmt); suffix = gcli_vasprintf(suffix_fmt, vp); va_end(vp); switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner, *e_repo; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *url = gcli_asprintf("%s/repos/%s/%s/milestones/%"PRIid"%s", gcli_get_apibase(ctx), e_owner, e_repo, path->as_default.id, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_URL: { *url = gcli_asprintf("%s%s", path->as_url, suffix); } break; default: { rc = gcli_error(ctx, "unsupported path kind for milestones"); } break; } gcli_clear_ptr(&suffix); return rc; } int gitea_get_milestones(struct gcli_ctx *ctx, struct gcli_path const *const path, int const max, struct gcli_milestone_list *const out) { char *url = NULL; int rc = 0; struct gcli_fetch_list_ctx fl = { .listp = &out->milestones, .sizep = &out->milestones_size, .max = max, .parse = (parsefn)(parse_gitea_milestones), }; rc = gitea_repo_make_url(ctx, path, &url, "/milestones"); if (rc < 0) return rc; return gcli_fetch_list(ctx, url, &fl); } int gitea_get_milestone(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_milestone *const out) { char *url; int rc = 0; struct gcli_fetch_buffer buffer = {0}; rc = gitea_milestone_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc == 0) { struct json_stream stream = {0}; json_open_buffer(&stream, buffer.data, buffer.length); parse_gitea_milestone(ctx, &stream, out); json_close(&stream); } gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&url); return rc; } int gitea_create_milestone(struct gcli_ctx *ctx, struct gcli_path const *repo, struct gcli_milestone_create_args const *args) { return github_create_milestone(ctx, repo, args); } int gitea_milestone_get_issues(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue_list *const out) { char *url = NULL; int rc = 0; if (path->kind != GCLI_PATH_DEFAULT) return gcli_error(ctx, "unsupported path kind for getting issues of Gitea milestone"); rc = gitea_repo_make_url(ctx, path, &url, "/issues?state=all&milestones=%"PRIid, path->as_default.id); if (rc < 0) return rc; return github_fetch_issues(ctx, url, -1, out); } int gitea_delete_milestone(struct gcli_ctx *ctx, struct gcli_path const *const path) { return github_delete_milestone(ctx, path); } int gitea_milestone_set_duedate(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const date) { return github_milestone_set_duedate(ctx, path, date); } gcli-2.9.1/src/gitea/pulls.c000066400000000000000000000213761507017207500156410ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include #include #include #include #include int gitea_pull_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char **url, char const *const suffix_fmt, ...) { char *suffix = NULL; int rc = 0; va_list vp; va_start(vp, suffix_fmt); suffix = gcli_vasprintf(suffix_fmt, vp); va_end(vp); switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner = NULL, *e_repo = NULL; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *url = gcli_asprintf("%s/repos/%s/%s/pulls/%"PRIid"%s", gcli_get_apibase(ctx), e_owner, e_repo, path->as_default.id, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_URL: { *url = gcli_asprintf("%s%s", path->as_url, suffix); } break; default: { rc = gcli_error(ctx, "unsupported path kind for Gitea pulls"); } break; } gcli_clear_ptr(&suffix); return rc; } static int gitea_pulls_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const suffix, char **url) { return gitea_repo_make_url(ctx, path, url, "/issues%s", suffix ? suffix : ""); } int gitea_search_pulls(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_pull_fetch_details const *details, int const max, struct gcli_pull_list *const out) { char *url = NULL, *suffix = NULL; int rc = 0; struct gcli_fetch_list_ctx fl = { .listp = &out->pulls, .sizep = &out->pulls_size, .parse = (parsefn)(parse_github_pulls), .max = max, }; gcli_url_options_append(&suffix, "type", "pulls"); gcli_url_options_append(&suffix, "state", details->all ? "all" : "open"); gcli_url_options_append(&suffix, "milestones", details->milestone); gcli_url_options_append(&suffix, "created_by", details->author); gcli_url_options_append(&suffix, "labels", details->label); gcli_url_options_append(&suffix, "q", details->search_term); rc = gitea_pulls_make_url(ctx, path, suffix, &url); gcli_clear_ptr(&suffix); if (rc < 0) return rc; return gcli_fetch_list(ctx, url, &fl); } int gitea_get_pull(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_pull *const out) { int const rc = github_get_pull(ctx, path, out); if (rc == 0) { /* fix for https://gitlab.com/herrhotzenplotz/gcli/issues/222: * * Sometimes the "reviewers" array contains a single NULL value * that means that this pull request is to be reviewed by the * owners of the repository. */ for (size_t i = 0; i < out->reviewers_size; ++i) { if (out->reviewers[i] == NULL) out->reviewers[i] = strdup("Owners"); } } return rc; } int gitea_get_pull_commits(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_commit_list *const out) { return github_get_pull_commits(ctx, path, out); } int gitea_pull_submit(struct gcli_ctx *ctx, struct gcli_submit_pull_options *opts) { gcli_warnx(ctx, "In case the following process errors out, see: " "https://github.com/go-gitea/gitea/issues/20175"); return github_perform_submit_pull(ctx, opts); } int gitea_pull_merge(struct gcli_ctx *ctx, struct gcli_path const *const path, enum gcli_merge_flags const flags) { bool const delete_branch = flags & GCLI_PULL_MERGE_DELETEHEAD; bool const squash = flags & GCLI_PULL_MERGE_SQUASH; char *url = NULL, *payload = NULL; struct gcli_jsongen gen = {0}; int rc = 0; /* Generate URL */ rc = gitea_pull_make_url(ctx, path, &url, "/merge"); if (rc < 0) return rc; /* Generate payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "Do"); gcli_jsongen_string(&gen, squash ? "squash" : "merge"); gcli_jsongen_objmember(&gen, "delete_branch_after_merge"); gcli_jsongen_bool(&gen, delete_branch); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, NULL); gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } static int gitea_pulls_patch_state(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *state) { char *url = NULL; char *data = NULL; int rc = 0; rc = gitea_pull_make_url(ctx, path, &url, ""); if (rc < 0) return rc; data = gcli_asprintf("{ \"state\": \"%s\"}", state); rc = gcli_fetch_with_method(ctx, "PATCH", url, data, NULL, NULL); gcli_clear_ptr(&data); gcli_clear_ptr(&url); return rc; } int gitea_pull_close(struct gcli_ctx *ctx, struct gcli_path const *const path) { return gitea_pulls_patch_state(ctx, path, "closed"); } int gitea_pull_reopen(struct gcli_ctx *ctx, struct gcli_path const *const path) { return gitea_pulls_patch_state(ctx, path, "open"); } int gitea_pull_get_patch(struct gcli_ctx *ctx, FILE *const stream, struct gcli_path const *const path) { char *url = NULL; int rc = 0; rc = gitea_pull_make_url(ctx, path, &url, ".patch"); if (rc < 0) return rc; rc = gcli_curl(ctx, stream, url, NULL); gcli_clear_ptr(&url); return rc; } int gitea_pull_get_diff(struct gcli_ctx *ctx, FILE *const stream, struct gcli_path const *const path) { char *url = NULL; int rc = 0; rc = gitea_pull_make_url(ctx, path, &url, ".diff"); if (rc < 0) return rc; rc = gcli_curl(ctx, stream, url, NULL); gcli_clear_ptr(&url); return rc; } int gitea_pull_get_checks(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_pull_checks_list *out) { (void) ctx; (void) path; (void) out; return gcli_error(ctx, "Pull Request checks are not available on Gitea"); } int gitea_pull_set_milestone(struct gcli_ctx *ctx, struct gcli_path const *const pull_path, gcli_id milestone_id) { return github_issue_set_milestone(ctx, pull_path, milestone_id); } int gitea_pull_clear_milestone(struct gcli_ctx *ctx, struct gcli_path const *const pull_path) { /* NOTE: The github routine for clearing issues sets the milestone * to null (not the integer zero). However this does not work in * the case of Gitea which clear the milestone by setting it to * the integer value zero. */ return github_issue_set_milestone(ctx, pull_path, 0); } int gitea_pull_add_reviewer(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *username) { return github_pull_add_reviewer(ctx, path, username); } int gitea_pull_assign(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *username) { return gitea_issue_assign(ctx, path, username); } int gitea_pull_set_title(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const title) { return github_pull_set_title(ctx, path, title); } int gitea_pull_get_reviews(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_pull_reviews *out) { return github_pull_get_reviews(ctx, path, out); } gcli-2.9.1/src/gitea/releases.c000066400000000000000000000113101507017207500162700ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include int gitea_get_releases(struct gcli_ctx *ctx, struct gcli_path const *const repo_path, int const max, struct gcli_release_list *const list) { return github_get_releases(ctx, repo_path, max, list); } static void gitea_parse_release(struct gcli_ctx *ctx, struct gcli_fetch_buffer const *const buffer, struct gcli_release *const out) { struct json_stream stream = {0}; json_open_buffer(&stream, buffer->data, buffer->length); parse_github_release(ctx, &stream, out); json_close(&stream); } static int gitea_upload_release_asset(struct gcli_ctx *ctx, char *const url, struct gcli_release_asset_upload const asset) { char *e_assetname = NULL; char *request = NULL; struct gcli_fetch_buffer buffer = {0}; int rc = 0; e_assetname = gcli_urlencode(asset.name); request = gcli_asprintf("%s?name=%s", url, e_assetname); rc = gcli_curl_gitea_upload_attachment(ctx, request, asset.path, &buffer); gcli_clear_ptr(&request); gcli_clear_ptr(&e_assetname); gcli_fetch_buffer_free(&buffer); return rc; } int gitea_create_release(struct gcli_ctx *ctx, struct gcli_create_release_args const *release) { char *payload = NULL, *upload_url = NULL, *url = NULL; struct gcli_fetch_buffer buffer = {0}; struct gcli_jsongen gen = {0}; struct gcli_release response = {0}; int rc = 0; /* Payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "tag_name"); gcli_jsongen_string(&gen, release->tag); gcli_jsongen_objmember(&gen, "draft"); gcli_jsongen_bool(&gen, release->draft); gcli_jsongen_objmember(&gen, "prerelease"); gcli_jsongen_bool(&gen, release->prerelease); if (release->body) { gcli_jsongen_objmember(&gen, "body"); gcli_jsongen_string(&gen, release->body); } if (release->commitish) { gcli_jsongen_objmember(&gen, "target_commitish"); gcli_jsongen_string(&gen, release->commitish); } if (release->name) { gcli_jsongen_objmember(&gen, "name"); gcli_jsongen_string(&gen, release->name); } } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); /* Generate URL */ rc = gitea_repo_make_url(ctx, &release->repo_path, &url, "/releases"); if (rc < 0) goto out; rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, &buffer); if (rc < 0) goto out; gitea_parse_release(ctx, &buffer, &response); /* create asset upload url */ rc = gitea_repo_make_url(ctx, &release->repo_path, &upload_url, "/releases/%s/assets", response.id); if (rc < 0) goto out; for (size_t i = 0; i < release->assets_size; ++i) { printf("INFO : Uploading asset %s...\n", release->assets[i].path); rc = gitea_upload_release_asset(ctx, upload_url, release->assets[i]); if (rc < 0) break; } gcli_release_free(&response); out: gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&upload_url); gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } int gitea_delete_release(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *id) { return github_delete_release(ctx, path, id); } gcli-2.9.1/src/gitea/repos.c000066400000000000000000000103261507017207500156230ustar00rootroot00000000000000/* * Copyright 2022-2025 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 int gitea_get_repos(struct gcli_ctx *ctx, char const *owner, int const max, struct gcli_repo_list *const list) { return github_get_repos(ctx, owner, max, list); } int gitea_get_own_repos(struct gcli_ctx *ctx, int const max, struct gcli_repo_list *const list) { return github_get_own_repos(ctx, max, list); } int gitea_repo_create(struct gcli_ctx *ctx, struct gcli_repo_create_options const *options, struct gcli_repo *const out) { return github_repo_create(ctx, options, out); } int gitea_repo_delete(struct gcli_ctx *ctx, struct gcli_path const *const path) { return github_repo_delete(ctx, path); } /* Unlike Github and Gitlab, Gitea only supports private or non-private * (thus public) repositories. Separate implementation required. */ int gitea_repo_set_visibility(struct gcli_ctx *ctx, struct gcli_path const *const path, gcli_repo_visibility vis) { char *url; bool is_private; char *payload; int rc; switch (vis) { case GCLI_REPO_VISIBILITY_PRIVATE: is_private = true; break; case GCLI_REPO_VISIBILITY_PUBLIC: is_private = false; break; default: assert(false && "Invalid visibility"); return gcli_error(ctx, "bad or unsupported visibility level for Gitea"); } rc = gitea_repo_make_url(ctx, path, &url, ""); if (rc < 0) return rc; payload = gcli_asprintf("{ \"private\": %s }", is_private ? "true" : "false"); rc = gcli_fetch_with_method(ctx, "PATCH", url, payload, NULL, NULL); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } int gitea_repo_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char **url, char const *const fmt, ...) { char *suffix = NULL; int rc = 0; va_list vp; va_start(vp, fmt); suffix = gcli_vasprintf(fmt, vp); va_end(vp); switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner, *e_repo; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *url = gcli_asprintf("%s/repos/%s/%s%s", gcli_get_apibase(ctx), e_owner, e_repo, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_NAMED: { char *e_owner, *e_repo; e_owner = gcli_urlencode(path->as_named.owner); e_repo = gcli_urlencode(path->as_named.repo); *url = gcli_asprintf("%s/repos/%s/%s%s", gcli_get_apibase(ctx), e_owner, e_repo, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_URL: { *url = gcli_asprintf("%s%s", path->as_url, suffix); } break; default: { rc = gcli_error(ctx, "unsupported path kind for Gitea repo"); } break; } gcli_clear_ptr(&suffix); return rc; } gcli-2.9.1/src/gitea/sshkeys.c000066400000000000000000000040511507017207500161620ustar00rootroot00000000000000/* * Copyright 2023 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 /* NOTE(Nico): The APIs of all the three forges we currently implement * are the same. Thus, we just call into the gitlab * implementation. Yes, this looks absurd. */ #include int gitea_get_sshkeys(struct gcli_ctx *ctx, struct gcli_sshkey_list *list) { return gitlab_get_sshkeys(ctx, list); } int gitea_add_sshkey(struct gcli_ctx *ctx, char const *const title, char const *const pubkey, struct gcli_sshkey *const out) { return gitlab_add_sshkey(ctx, title, pubkey, out); } int gitea_delete_sshkey(struct gcli_ctx *ctx, gcli_id const id) { return gitlab_delete_sshkey(ctx, id); } gcli-2.9.1/src/gitea/status.c000066400000000000000000000040611507017207500160150ustar00rootroot00000000000000/* * Copyright 2023-2025 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 int gitea_get_notifications(struct gcli_ctx *ctx, int const max, struct gcli_notification_list *const out) { char *url = NULL; struct gcli_fetch_list_ctx fl = { .listp = &out->notifications, .sizep = &out->notifications_size, .parse = (parsefn)(parse_gitea_notifications), .max = max, }; url = gcli_asprintf("%s/notifications", gcli_get_apibase(ctx)); return gcli_fetch_list(ctx, url, &fl); } int gitea_notification_mark_as_read(struct gcli_ctx *ctx, char const *id) { return github_notification_mark_as_read(ctx, id); } gcli-2.9.1/src/github/000077500000000000000000000000001507017207500145165ustar00rootroot00000000000000gcli-2.9.1/src/github/api.c000066400000000000000000000035171507017207500154410ustar00rootroot00000000000000/* * Copyright 2021-2025 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 char const * github_api_error_string(struct gcli_ctx *ctx, struct gcli_fetch_buffer *const buf) { struct json_stream stream = {0}; int rc; char *msg; json_open_buffer(&stream, buf->data, buf->length); rc = parse_github_get_error(ctx, &stream, &msg); json_close(&stream); if (rc < 0) return strdup("no message: failed to parse error response"); else return msg; } gcli-2.9.1/src/github/checkout.c000066400000000000000000000052371507017207500164760ustar00rootroot00000000000000/* * Copyright 2025 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 int github_pull_checkout(struct gcli_ctx *ctx, char const *const remote, struct gcli_path const *const path) { /* FIXME: this is more than not ideal! */ char *remote_ref, *local_ref, *refspec; int rc; pid_t pid; gcli_id pr_id; if (path->kind != GCLI_PATH_DEFAULT) return gcli_error(ctx, "unsupported path kind for checkout"); pr_id = path->as_default.id; remote_ref = gcli_asprintf("refs/pull/%"PRIid"/head", pr_id); local_ref = gcli_asprintf("github/pr/%"PRIid, pr_id); refspec = gcli_asprintf("%s:%s", remote_ref, local_ref); pid = fork(); if (pid < 0) return gcli_error(ctx, "could not fork"); if (pid == 0) { rc = execlp("git", "git", "fetch", remote, refspec, NULL); if (rc < 0) exit(EXIT_FAILURE); /* NOTREACHED */ } rc = gcli_wait_proc_ok(ctx, pid); if (rc < 0) return rc; gcli_clear_ptr(&remote_ref); gcli_clear_ptr(&refspec); pid = fork(); if (pid < 0) return gcli_error(ctx, "could not fork"); if (pid == 0) { rc = execlp("git", "git", "checkout", "--track", local_ref, NULL); if (rc < 0) exit(EXIT_FAILURE); /* NOTREACHED */ } rc = gcli_wait_proc_ok(ctx, pid); gcli_clear_ptr(&local_ref); return rc; } gcli-2.9.1/src/github/checks.c000066400000000000000000000063261507017207500161310ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include #include #include #include #include int github_get_checks(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *ref, int const max, struct github_check_list *const out) { struct gcli_fetch_buffer buffer = {0}; struct gcli_path norm_path = {0}; char *url = NULL, *next_url = NULL; int rc = 0; assert(out); /* we must normalise these paths to default paths. otherwise we'd get * bugs when a URL path to some item is passed in. */ rc = github_path_normalise(ctx, path, &norm_path); if (rc < 0) return rc; rc = github_repo_make_url(ctx, path, &url, "/commits/%s/check-runs", ref); if (rc < 0) return rc; do { rc = gcli_fetch(ctx, url, &next_url, &buffer); if (rc == 0) { struct json_stream stream = {0}; json_open_buffer(&stream, buffer.data, buffer.length); parse_github_checks(ctx, &stream, out); json_close(&stream); } gcli_clear_ptr(&url); gcli_fetch_buffer_free(&buffer); if (rc < 0) break; } while ((url = next_url) && ((int)(out->checks_size) < max || max < 0)); /* TODO: don't leak list on error */ gcli_clear_ptr(&next_url); gcli_path_free(&norm_path); return rc; } void gcli_github_check_free(struct gcli_github_check *check) { gcli_clear_ptr(&check->name); gcli_clear_ptr(&check->status); gcli_clear_ptr(&check->conclusion); gcli_clear_ptr(&check->started_at); gcli_clear_ptr(&check->completed_at); } void github_free_checks(struct github_check_list *const list) { for (size_t i = 0; i < list->checks_size; ++i) { gcli_github_check_free(&list->checks[i]); } gcli_clear_ptr(&list->checks); list->checks_size = 0; } gcli-2.9.1/src/github/comments.c000066400000000000000000000112001507017207500165010ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include #include int github_perform_submit_comment(struct gcli_ctx *ctx, struct gcli_submit_comment_opts const *const opts) { int rc = 0; struct gcli_jsongen gen = {0}; char *payload = NULL, *url = NULL; rc = github_issue_make_url(ctx, &opts->target, &url, "/comments"); if (rc < 0) return rc; gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "body"); gcli_jsongen_string(&gen, opts->message); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, NULL); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } int github_fetch_comments(struct gcli_ctx *ctx, char *url, struct gcli_comment_list *const out) { struct gcli_fetch_list_ctx fl = { .listp = &out->comments, .sizep = &out->comments_size, .parse = (parsefn)parse_github_comments, .max = -1, }; return gcli_fetch_list(ctx, url, &fl); } int github_get_comments(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_comment_list *const out) { char *url = NULL; int rc = 0; struct gcli_path const *p = path; struct gcli_path norm_path = {0}; /* This routine can be called with paths to PRs too. However, * when called on a PR we get its review comments. We don't * want that. Return its issue comments (which is the * regular discussion) instead. * For this purpose forcefully convert the path to a default * style path and derive the correct url (to the underlying issue) * from that default path. */ if (path->kind != GCLI_PATH_DEFAULT) { rc = github_path_normalise(ctx, path, &norm_path); if (rc < 0) return rc; p = &norm_path; } /* issue URL from normalised path */ rc = github_issue_make_url(ctx, p, &url, "/comments"); if (rc < 0) return rc; rc = github_fetch_comments(ctx, url, out); /* free normalised path if it was used */ if (p == &norm_path) gcli_path_free(&norm_path); return rc; } static int github_fetch_comment(struct gcli_ctx *ctx, char const *const url, struct gcli_comment *const out) { struct gcli_fetch_buffer buffer = {0}; struct json_stream stream = {0}; int rc = 0; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc < 0) return rc; json_open_buffer(&stream, buffer.data, buffer.length); rc = parse_github_comment(ctx, &stream, out); json_close(&stream); gcli_fetch_buffer_free(&buffer); return rc; } int github_get_comment(struct gcli_ctx *ctx, struct gcli_path const *const target, enum comment_target_type target_type, gcli_id comment_id, struct gcli_comment *out) { char *url = NULL; int rc = 0; (void) target_type; /* target type and id ignored as pull requests are issues on GitHub */ rc = github_repo_make_url(ctx, target, &url, "/issues/comments/%"PRIid, comment_id); if (rc < 0) return rc; rc = github_fetch_comment(ctx, url, out); gcli_clear_ptr(&url); return rc; } gcli-2.9.1/src/github/config.c000066400000000000000000000030641507017207500161320ustar00rootroot00000000000000/* * Copyright 2021-2025 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 char * github_make_authheader(struct gcli_ctx *ctx, char const *token) { (void) ctx; return gcli_asprintf("Authorization: token %s", token); } gcli-2.9.1/src/github/forks.c000066400000000000000000000056601507017207500160150ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include int github_get_forks(struct gcli_ctx *ctx, struct gcli_path const *const path, int const max, struct gcli_fork_list *const list) { char *url = NULL; int rc = 0; struct gcli_fetch_list_ctx fl = { .listp = &list->forks, .sizep = &list->forks_size, .max = max, .parse = (parsefn)(parse_github_forks), }; rc = github_repo_make_url(ctx, path, &url, "/forks"); if (rc < 0) return rc; return gcli_fetch_list(ctx, url, &fl); } int github_fork_create(struct gcli_ctx *ctx, struct gcli_path const *const repo_path, char const *const in) { char *url = NULL; char *post_data = NULL; int rc = 0; bool is_org = false; if (in) { char *e_in = gcli_urlencode(in); rc = github_user_is_org(ctx, e_in); gcli_clear_ptr(&e_in); if (rc < 0) return rc; is_org = rc; } rc = github_repo_make_url(ctx, repo_path, &url, "/forks"); if (rc < 0) return rc; if (is_org) { struct gcli_jsongen gen = {0}; gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "organization"); gcli_jsongen_string(&gen, in); } gcli_jsongen_end_object(&gen); post_data = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); } rc = gcli_fetch_with_method(ctx, "POST", url, post_data, NULL, NULL); gcli_clear_ptr(&url); gcli_clear_ptr(&post_data); return rc; } gcli-2.9.1/src/github/gists.c000066400000000000000000000152111507017207500160130ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include #include /* /!\ Before changing this, see comment in gists.h /!\ */ int parse_github_gist_files_idiot_hack(struct gcli_ctx *ctx, json_stream *stream, struct gcli_gist *const gist) { (void) ctx; enum json_type next = JSON_NULL; gist->files = NULL; gist->files_size = 0; if (json_next(stream) != JSON_OBJECT) return gcli_error(ctx, "expected Gist Files Object"); while ((next = json_next(stream)) == JSON_STRING) { gist->files = realloc(gist->files, sizeof(*gist->files) * (gist->files_size + 1)); struct gcli_gist_file *it = &gist->files[gist->files_size++]; if (parse_github_gist_file(ctx, stream, it) < 0) return -1; } if (next != JSON_OBJECT_END) return gcli_error(ctx, "unclosed Gist Files Object"); return 0; } int gcli_get_gists(struct gcli_ctx *ctx, char const *user, int const max, struct gcli_gist_list *const list) { char *url = NULL; struct gcli_fetch_list_ctx fl = { .listp = &list->gists, .sizep = &list->gists_size, .parse = (parsefn)(parse_github_gists), .max = max, }; if (user) url = gcli_asprintf("%s/users/%s/gists", gcli_get_apibase(ctx), user); else url = gcli_asprintf("%s/gists", gcli_get_apibase(ctx)); return gcli_fetch_list(ctx, url, &fl); } int gcli_get_gist(struct gcli_ctx *ctx, char const *gist_id, struct gcli_gist *out) { char *url = NULL; struct gcli_fetch_buffer buffer = {0}; int rc = 0; url = gcli_asprintf("%s/gists/%s", gcli_get_apibase(ctx), gist_id); rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc == 0) { struct json_stream stream = {0}; json_open_buffer(&stream, buffer.data, buffer.length); json_set_streaming(&stream, 1); parse_github_gist(ctx, &stream, out); json_close(&stream); } gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&url); return rc; } #define READ_SZ 4096 static char * read_file(FILE *f) { size_t size = 0; char *out = NULL; while (!feof(f) && !ferror(f)) { out = realloc(out, size + READ_SZ); size_t bytes_read = fread(out + size, 1, READ_SZ, f); if (bytes_read == 0) break; size += bytes_read; } if (out) { out = realloc(out, size + 1); out[size] = '\0'; } if (ferror(f)) { gcli_clear_ptr(&out); out = NULL; } return out; } int gcli_create_gist(struct gcli_ctx *ctx, struct gcli_new_gist opts) { char *content = NULL; char *post_data = NULL; char *url = NULL; int rc = 0; struct gcli_fetch_buffer buffer = {0}; struct gcli_jsongen gen = {0}; /* Read in the file content. this may come from stdin this we don't know * the size in advance. */ content = read_file(opts.file); if (content == NULL) return gcli_error(ctx, "failed to read from input file"); /* This API is documented very badly. In fact, I dug up how you're * supposed to do this from * https://github.com/phadej/github/blob/master/src/GitHub/Data/Gists.hs * * From this we can infer that we're supposed to create a JSON * object like so: * * { * "description": "foobar", * "public": true, * "files": { * "barf.exe": { * "content": "#!/bin/sh\necho This file cannot be run in DOS mode" * } * } * } */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "description"); gcli_jsongen_string(&gen, opts.gist_description); gcli_jsongen_objmember(&gen, "public"); gcli_jsongen_bool(&gen, true); gcli_jsongen_objmember(&gen, "files"); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, opts.file_name); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "content"); gcli_jsongen_string(&gen, content); } gcli_jsongen_end_object(&gen); } gcli_jsongen_end_object(&gen); } gcli_jsongen_end_object(&gen); post_data = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); /* Generate URL */ url = gcli_asprintf("%s/gists", gcli_get_apibase(ctx)); /* Perferm fetch */ rc = gcli_fetch_with_method(ctx, "POST", url, post_data, NULL, &buffer); gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&content); gcli_clear_ptr(&url); gcli_clear_ptr(&post_data); return rc; } int gcli_delete_gist(struct gcli_ctx *ctx, char const *gist_id) { char *url = NULL; struct gcli_fetch_buffer buffer = {0}; int rc = 0; url = gcli_asprintf("%s/gists/%s", gcli_get_apibase(ctx), gist_id); rc = gcli_fetch_with_method(ctx, "DELETE", url, NULL, NULL, &buffer); gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&url); return rc; } void gcli_gist_free(struct gcli_gist *g) { gcli_clear_ptr(&g->id); gcli_clear_ptr(&g->owner); gcli_clear_ptr(&g->url); gcli_clear_ptr(&g->date); gcli_clear_ptr(&g->git_pull_url); gcli_clear_ptr(&g->description); for (size_t j = 0; j < g->files_size; ++j) { gcli_clear_ptr(&g->files[j].filename); gcli_clear_ptr(&g->files[j].language); gcli_clear_ptr(&g->files[j].url); gcli_clear_ptr(&g->files[j].type); } gcli_clear_ptr(&g->files); memset(g, 0, sizeof(*g)); } void gcli_gists_free(struct gcli_gist_list *const list) { for (size_t i = 0; i < list->gists_size; ++i) gcli_gist_free(&list->gists[i]); gcli_clear_ptr(&list->gists); list->gists_size = 0; } gcli-2.9.1/src/github/issues.c000066400000000000000000000412531507017207500162020ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include #include #include #include /* TODO: Remove this function once we use linked lists for storing * issues. * * This is an ugly hack caused by the sillyness of the Github API that * treats Pull Requests as issues and reports them to us when we * request issues. This function nukes them from the list, readjusts * the allocation size and fixes the reported list size. */ static void github_hack_fixup_issues_that_are_actually_pulls(struct gcli_issue **list, size_t *size, void *_data) { (void) _data; for (size_t i = *size; i > 0; --i) { if ((*list)[i-1].is_pr) { struct gcli_issue *l = *list; /* len = 7, i = 5, to move = 7 - 5 = 2 * 0 1 2 3 4 5 6 * | x | x | x | x | X | x | x | */ gcli_issue_free(&l[i-1]); memmove(&l[i-1], &l[i], sizeof(*l) * (*size - i)); *list = realloc(l, (--(*size)) * sizeof(*l)); } } } int github_issue_make_url(struct gcli_ctx *const ctx, struct gcli_path const *const path, char **const url, char const *const fmt, ...) { int rc = 0; va_list vp; char *suffix = NULL; va_start(vp, fmt); suffix = gcli_vasprintf(fmt, vp); va_end(vp); switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner, *e_repo; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *url = gcli_asprintf("%s/repos/%s/%s/issues/%"PRIid"%s", gcli_get_apibase(ctx), e_owner, e_repo, path->as_default.id, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_URL: { *url = gcli_asprintf("%s%s", path->as_url, suffix); } break; default: { rc = gcli_error(ctx, "unsupported path kind"); } break; } gcli_clear_ptr(&suffix); return rc; } int github_fetch_issues(struct gcli_ctx *ctx, char *url, int const max, struct gcli_issue_list *const out) { struct gcli_fetch_list_ctx fl = { .listp = &out->issues, .sizep = &out->issues_size, .parse = (parsefn)(parse_github_issues), .filter = (filterfn)(github_hack_fixup_issues_that_are_actually_pulls), .max = max, }; return gcli_fetch_list(ctx, url, &fl); } static int get_milestone_id(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *milestone_name, gcli_id *out) { int rc = 0; struct gcli_milestone_list list = {0}; rc = github_get_milestones(ctx, path, -1, &list); if (rc < 0) return rc; rc = gcli_error(ctx, "%s: no such milestone", milestone_name); for (size_t i = 0; i < list.milestones_size; ++i) { if (strcmp(list.milestones[i].title, milestone_name) == 0) { *out = list.milestones[i].id; rc = 0; break; } } gcli_free_milestones(&list); return rc; } static int parse_github_milestone(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *milestone, gcli_id *out) { char *endptr = NULL; size_t const m_len = strlen(milestone); /* first try parsing as a milestone ID, if it isn't one, * go looking for a similarly named milestone */ *out = strtoull(milestone, &endptr, 10); if (endptr == milestone + m_len) return 0; return get_milestone_id(ctx, path, milestone, out); } /* Search issues with a search term */ static int search_issues(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue_fetch_details const *details, int const max, struct gcli_issue_list *const out) { char *url = NULL, *query_string = NULL, *e_query_string = NULL, *milestone = NULL, *author = NULL, *label = NULL, *assignee = NULL; int rc = 0; struct gcli_fetch_buffer buffer = {0}; struct json_stream stream = {0}; (void) max; /* Search only works with default paths */ if (path->kind != GCLI_PATH_DEFAULT) return gcli_error(ctx, "unsupported path kind for issue search"); /* Encode the various fields */ if (details->milestone) milestone = gcli_asprintf("milestone:%s", details->milestone); if (details->author) author = gcli_asprintf("author:%s", details->author); if (details->label) label = gcli_asprintf("label:%s", details->label); if (details->assignee) assignee = gcli_asprintf("assignee:%s", details->assignee); query_string = gcli_asprintf("repo:%s/%s is:issue%s %s %s %s %s %s", path->as_default.owner, path->as_default.repo, details->all ? "" : " is:open", milestone ? milestone : "", author ? author : "", label ? label : "", assignee ? assignee : "", details->search_term); e_query_string = gcli_urlencode(query_string); url = gcli_asprintf("%s/search/issues?q=%s", gcli_get_apibase(ctx), e_query_string); gcli_clear_ptr(&milestone); gcli_clear_ptr(&author); gcli_clear_ptr(&label); gcli_clear_ptr(&assignee); gcli_clear_ptr(&query_string); gcli_clear_ptr(&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_issue_search_result(ctx, &stream, out); json_close(&stream); gcli_fetch_buffer_free(&buffer); error_fetch: gcli_clear_ptr(&url); return rc; } /** Routine for generating a URL for getting issues of a repository given its * path. */ static int github_issues_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const suffix, char **out) { int rc = 0; switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner, *e_repo; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *out = gcli_asprintf("%s/repos/%s/%s/issues%s", gcli_get_apibase(ctx), e_owner, e_repo, suffix ? suffix : ""); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_URL: { *out = gcli_asprintf("%s%s", path->as_url, suffix ? suffix : ""); } break; default: { rc = gcli_error(ctx, "unsupported path kind for issue list"); } break; } return rc; } /* Optimised routine for issues without a search term */ static int get_issues(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue_fetch_details const *details, int const max, struct gcli_issue_list *const out) { char *url = NULL, *suffix = NULL; int rc = 0; if (details->milestone) { gcli_id milestone_id; rc = parse_github_milestone(ctx, path, details->milestone, &milestone_id); if (rc < 0) return rc; gcli_url_options_appendf(&suffix, "milestone", "%"PRIid, milestone_id); } gcli_url_options_append(&suffix, "creator", details->author); gcli_url_options_append(&suffix, "labels", details->label); gcli_url_options_append(&suffix, "assignee", details->assignee); gcli_url_options_append(&suffix, "state", details->all ? "all" : "open"); rc = github_issues_make_url(ctx, path, suffix, &url); gcli_clear_ptr(&suffix); if (rc < 0) return rc; return github_fetch_issues(ctx, url, max, out); } int github_issues_search(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue_fetch_details const *details, int const max, struct gcli_issue_list *const out) { if (details->search_term) return search_issues(ctx, path, details, max, out); else return get_issues(ctx, path, details, max, out); } int github_fetch_issue(struct gcli_ctx *const ctx, char *const url, struct gcli_issue *const out) { struct gcli_fetch_buffer buffer = {0}; struct json_stream parser = {0}; int rc = 0; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc == 0) { json_open_buffer(&parser, buffer.data, buffer.length); json_set_streaming(&parser, true); parse_github_issue(ctx, &parser, out); json_close(&parser); } gcli_fetch_buffer_free(&buffer); return rc; } int github_get_issue_summary(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue *const out) { char *url = NULL; int rc = 0; rc = github_issue_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = github_fetch_issue(ctx, url, out); gcli_clear_ptr(&url); return rc; } static int github_issue_patch_state(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const state) { char *url = NULL, *payload = NULL; int rc = 0; rc = github_issue_make_url(ctx, path, &url, ""); if (rc < 0) return rc; payload = gcli_asprintf("{ \"state\": \"%s\"}", state); rc = gcli_fetch_with_method(ctx, "PATCH", url, payload, NULL, NULL); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } int github_issue_close(struct gcli_ctx *ctx, struct gcli_path const *const path) { return github_issue_patch_state(ctx, path, "closed"); } int github_issue_reopen(struct gcli_ctx *ctx, struct gcli_path const *const path) { return github_issue_patch_state(ctx, path, "open"); } int 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 */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "title"); gcli_jsongen_string(&gen, opts->title); /* Body can be omitted and is NULL in that case */ if (opts->body) { gcli_jsongen_objmember(&gen, "body"); gcli_jsongen_string(&gen, opts->body); } } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); /* Generate URL */ e_owner = gcli_urlencode(opts->owner); e_repo = gcli_urlencode(opts->repo); url = gcli_asprintf("%s/repos/%s/%s/issues", gcli_get_apibase(ctx), e_owner, e_repo); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); /* 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); } gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } int github_issue_assign(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *assignee) { struct gcli_jsongen gen = {0}; char *url = NULL, *payload = NULL; int rc = 0; /* Generate URL */ rc = github_issue_make_url(ctx, path, &url, "/assignees"); if (rc < 0) return rc; /* Generate Payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "assignees"); gcli_jsongen_begin_array(&gen); gcli_jsongen_string(&gen, assignee); gcli_jsongen_end_array(&gen); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, NULL); gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } int github_issue_add_labels(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, char const *const labels[], size_t const labels_size) { char *data = NULL, *url = NULL; int rc = 0; struct gcli_jsongen gen = {0}; assert(labels_size > 0); rc = github_issue_make_url(ctx, issue_path, &url, "/labels"); if (rc < 0) return rc; gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "labels"); gcli_jsongen_begin_array(&gen); for (size_t i = 0; i < labels_size; ++i) { gcli_jsongen_string(&gen, labels[i]); } gcli_jsongen_end_array(&gen); } gcli_jsongen_end_object(&gen); data = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "POST", url, data, NULL, NULL); gcli_clear_ptr(&url); gcli_clear_ptr(&data); return rc; } int github_issue_remove_labels(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const labels[], size_t const labels_size) { char *url = NULL, *e_label = NULL; int rc = 0; if (labels_size != 1) { return gcli_error(ctx, "GitHub only supports removing labels from " "issues one by one."); } e_label = gcli_urlencode(labels[0]); rc = github_issue_make_url(ctx, path, &url, "/labels/%s", e_label); if (rc == 0) rc = gcli_fetch_with_method(ctx, "DELETE", url, NULL, NULL, NULL); gcli_clear_ptr(&url); gcli_clear_ptr(&e_label); return rc; } int github_issue_set_milestone(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, gcli_id const milestone) { char *url, *body; int rc; rc = github_issue_make_url(ctx, issue_path, &url, ""); if (rc < 0) return rc; body = gcli_asprintf("{ \"milestone\": %"PRIid" }", milestone); rc = gcli_fetch_with_method(ctx, "PATCH", url, body, NULL, NULL); gcli_clear_ptr(&body); gcli_clear_ptr(&url); return rc; } int github_issue_clear_milestone(struct gcli_ctx *ctx, struct gcli_path const *const path) { char *url = NULL; char const *payload = NULL; int rc; rc = github_issue_make_url(ctx, path, &url, ""); if (rc < 0) return rc; payload = "{ \"milestone\": null }"; rc = gcli_fetch_with_method(ctx, "PATCH", url, payload, NULL, NULL); gcli_clear_ptr(&url); return rc; } int github_issue_set_title(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, char const *const new_title) { char *url, *payload; struct gcli_jsongen gen = {0}; int rc; /* Generate url */ rc = github_issue_make_url(ctx, issue_path, &url, ""); if (rc < 0) return rc; /* Generate payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "title"); gcli_jsongen_string(&gen, new_title); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "PATCH", url, payload, NULL, NULL); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } int github_issue_set_op(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const new_op) { char *url, *payload; struct gcli_jsongen gen = {0}; int rc; /* Generate url */ rc = github_issue_make_url(ctx, path, &url, ""); if (rc < 0) return rc; /* Generate payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "body"); gcli_jsongen_string(&gen, new_op); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "PATCH", url, payload, NULL, NULL); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } gcli-2.9.1/src/github/labels.c000066400000000000000000000157351507017207500161370ustar00rootroot00000000000000/* * Copyright 2022-2025 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 int github_get_labels(struct gcli_ctx *ctx, struct gcli_path const *const path, int const max, struct gcli_label_list *const out) { char *url = NULL; int rc = 0; struct gcli_fetch_list_ctx fl = { .listp = &out->labels, .sizep= &out->labels_size, .parse = (parsefn)(parse_github_labels), .max = max, }; *out = (struct gcli_label_list) {0}; rc = github_repo_make_url(ctx, path, &url, "/labels"); if (rc < 0) return rc; return gcli_fetch_list(ctx, url, &fl); } int github_create_label(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_label *const label) { char *url = NULL, *payload = NULL, *colour = NULL; int rc = 0; struct gcli_fetch_buffer buffer = {0}; struct gcli_jsongen gen = {0}; struct json_stream stream = {0}; /* Generate URL: /repos/{owner}/{repo}/labels */ rc = github_repo_make_url(ctx, path, &url, "/labels"); if (rc < 0) return rc; /* Generate payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "name"); gcli_jsongen_string(&gen, label->name); gcli_jsongen_objmember(&gen, "description"); gcli_jsongen_string(&gen, label->description); colour = gcli_asprintf("%06X", label->colour & 0xFFFFFF); gcli_jsongen_objmember(&gen, "color"); gcli_jsongen_string(&gen, colour); gcli_clear_ptr(&colour); colour = NULL; } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, &buffer); if (rc == 0) { json_open_buffer(&stream, buffer.data, buffer.length); parse_github_label(ctx, &stream, label); json_close(&stream); } gcli_clear_ptr(&url); gcli_clear_ptr(&payload); gcli_fetch_buffer_free(&buffer); return rc; } static int github_label_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char **url, char const *const fmt, ...) { int rc = 0; va_list vp; char *suffix = NULL; va_start(vp, fmt); suffix = gcli_vasprintf(fmt, vp); va_end(vp); /* TODO: add support for ID-based label adressing which in turn * would resolve the id to a name. Yes it's inefficient but it's * what GitHub wants us to do. */ switch (path->kind) { case GCLI_PATH_NAMED: { char *e_owner, *e_repo, *e_label; e_owner = gcli_urlencode(path->as_named.owner); e_repo = gcli_urlencode(path->as_named.repo); e_label = gcli_urlencode(path->as_named.id); *url = gcli_asprintf("%s/repos/%s/%s/labels/%s%s", gcli_get_apibase(ctx), e_owner, e_repo, e_label, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); gcli_clear_ptr(&e_label); } break; case GCLI_PATH_URL: { *url = gcli_asprintf("%s%s", path->as_url, suffix); } break; default: { rc = gcli_error(ctx, "unsupported path kind for GitHub labels"); } break; } gcli_clear_ptr(&suffix); return rc; } int github_delete_label(struct gcli_ctx *ctx, struct gcli_path const *const path) { char *url = NULL; int rc = 0; /* DELETE /repos/{owner}/{repo}/labels/{name} */ rc = github_label_make_url(ctx, path, &url, ""); if (rc == 0) rc = gcli_fetch_with_method(ctx, "DELETE", url, NULL, NULL, NULL); gcli_clear_ptr(&url); return rc; } int github_get_label(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_label *const out) { int rc = 0; char *url = NULL; struct gcli_fetch_buffer buffer = {0}; rc = github_label_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc == 0) { struct json_stream stream = {0}; json_open_buffer(&stream, buffer.data, buffer.length); parse_github_label(ctx, &stream, out); json_close(&stream); } gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&url); return rc; } static int github_label_update_property(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const propname, char const *const val) { char *url = NULL, *payload = NULL; int rc = 0; struct gcli_jsongen gen = {0}; /* generate URL */ rc = github_label_make_url(ctx, path, &url, ""); if (rc < 0) return rc; /* generate payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, propname); gcli_jsongen_string(&gen, val); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); /* perform request */ rc = gcli_fetch_with_method(ctx, "PATCH", url, payload, NULL, NULL); gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } int github_label_set_title(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const new_name) { return github_label_update_property(ctx, path, "new_name", new_name); } int github_label_set_description(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const new_description) { return github_label_update_property( ctx, path, "description", new_description); } int github_label_set_colour(struct gcli_ctx *ctx, struct gcli_path const *const path, uint32_t const colour) { char *colour_string = NULL; int rc = 0; colour_string = gcli_asprintf("%06X", colour & 0xFFFFFF); rc = github_label_update_property(ctx, path, "color", colour_string); gcli_clear_ptr(&colour_string); return rc; } gcli-2.9.1/src/github/milestones.c000066400000000000000000000157151507017207500170550ustar00rootroot00000000000000/* * Copyright 2023-2025 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 #include #include #include #include #include #include #include /* Given a repository path make the url to milestones */ static int github_milestones_make_url(struct gcli_ctx *const ctx, struct gcli_path const *const path, char const *const suffix, char **url) { int rc = 0; switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner, *e_repo; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *url = gcli_asprintf("%s/repos/%s/%s/milestones%s", gcli_get_apibase(ctx), e_owner, e_repo, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_URL: { *url = gcli_asprintf("%s/milestones%s", path->as_url, suffix); } break; default: { rc = gcli_error(ctx, "unsupported path kind for milestones"); } break; } return rc; } int github_get_milestones(struct gcli_ctx *ctx, struct gcli_path const *const path, int const max, struct gcli_milestone_list *const out) { char *url; int rc; struct gcli_fetch_list_ctx fl = { .listp = &out->milestones, .sizep = &out->milestones_size, .parse = (parsefn)parse_github_milestones, .max = max, }; rc = github_milestones_make_url(ctx, path, "", &url); if (rc < 0) return rc; return gcli_fetch_list(ctx, url, &fl); } int github_milestone_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char **url, char const *const suffix_fmt, ...) { char *suffix = NULL; int rc = 0; va_list vp; va_start(vp, suffix_fmt); suffix = gcli_vasprintf(suffix_fmt, vp); va_end(vp); switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner, *e_repo; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *url = gcli_asprintf("%s/repos/%s/%s/milestones/%"PRIid"%s", gcli_get_apibase(ctx), e_owner, e_repo, path->as_default.id, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_URL: { *url = gcli_asprintf("%s%s", path->as_url, suffix); } break; default: { rc = gcli_error(ctx, "unsupported path kind for milestones"); } break; } gcli_clear_ptr(&suffix); return rc; } int github_get_milestone(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_milestone *const out) { char *url; struct gcli_fetch_buffer buffer = {0}; int rc = 0; rc = github_milestone_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc == 0) { struct json_stream stream = {0}; json_open_buffer(&stream, buffer.data, buffer.length); parse_github_milestone(ctx, &stream, out); json_close(&stream); } gcli_clear_ptr(&url); gcli_fetch_buffer_free(&buffer); return rc; } int github_milestone_get_issues(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue_list *const out) { char *url = NULL; int rc = 0; if (path->kind != GCLI_PATH_DEFAULT) return gcli_error(ctx, "unsupported path type getting milestone's issues"); rc = github_repo_make_url(ctx, path, &url, "/issues?milestone=%"PRIid"&state=all", path->as_default.id); if (rc < 0) return rc; return github_fetch_issues(ctx, url, -1, out); } int github_create_milestone(struct gcli_ctx *ctx, struct gcli_path const *repo, struct gcli_milestone_create_args const *args) { char *url, *payload; int rc = 0; struct gcli_jsongen gen = {0}; /* build url */ rc = github_repo_make_url(ctx, repo, &url, "/milestones"); if (rc < 0) return rc; /* generate payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "title"); gcli_jsongen_string(&gen, args->title); if (args->description) { gcli_jsongen_objmember(&gen, "description"); gcli_jsongen_string(&gen, args->description); } } 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); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } int github_delete_milestone(struct gcli_ctx *ctx, struct gcli_path const *const path) { char *url = NULL; int rc = 0; rc = github_milestone_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = gcli_fetch_with_method(ctx, "DELETE", url, NULL, NULL, NULL); gcli_clear_ptr(&url); return rc; } int github_milestone_set_duedate(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const date) { char *url = NULL, *payload = NULL, norm_date[21] = {0}; int rc = 0; rc = gcli_normalize_date(ctx, DATEFMT_ISO8601, date, norm_date, sizeof norm_date); if (rc < 0) return rc; rc = github_milestone_make_url(ctx, path, &url, ""); if (rc < 0) return rc; payload = gcli_asprintf("{ \"due_on\": \"%s\"}", norm_date); rc = gcli_fetch_with_method(ctx, "PATCH", url, payload, NULL, NULL); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } gcli-2.9.1/src/github/path.c000066400000000000000000000062541507017207500156250ustar00rootroot00000000000000/* * Copyright 2025 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 static int split_url(struct gcli_ctx *ctx, char const *const url, struct gcli_path *const path) { char const *repo = NULL, *owner = NULL, *id = NULL, *id_end = NULL; char *endptr = NULL; /* search for begin of owner */ owner = strstr(url, "/repos/"); if (owner == NULL) goto bad; owner += sizeof("/repos/") - 1; /* search for begin of repo */ repo = strchr(owner, '/'); if (repo == NULL) goto bad; /* if we found the repository, copy out the owner */ path->as_default.owner = gcli_strndup(owner, repo - owner); repo += 1; /* the type comes now, copy out the repo name first */ id = strchr(repo, '/'); if (id == NULL) goto bad; path->as_default.repo = gcli_strndup(repo, id - repo); id += 1; id = strchr(id, '/'); if (id == NULL) goto done; id += 1; id_end = strchr(id, '/'); if (id_end == NULL) id_end = id + strlen(id); path->as_default.id = strtoul(id, &endptr, 10); if (endptr != id_end) path->as_default.id = 0; done: return 0; bad: return gcli_error(ctx, "bad Github URL path: %s", url); } /* Helpers for converting a Github forge URL to a path */ int github_path_normalise(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_path *const norm_path) { norm_path->kind = GCLI_PATH_DEFAULT; switch (path->kind) { case GCLI_PATH_DEFAULT: { norm_path->as_default.owner = strdup(path->as_default.owner); norm_path->as_default.repo = strdup(path->as_default.repo); norm_path->as_default.id = path->as_default.id; } break; case GCLI_PATH_URL: { return split_url(ctx, path->as_url, norm_path); } break; default: { return gcli_error( ctx, "github_path_normalise: cannot normalise path of type %d", path->kind); } } return 0; } gcli-2.9.1/src/github/pulls.c000066400000000000000000000543161507017207500160320ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include #include #include #include #include #include #include #include int github_pull_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char **url, char const *const fmt, ...) { int rc = 0; char *suffix = NULL; va_list vp; va_start(vp, fmt); suffix = gcli_vasprintf(fmt, vp); va_end(vp); switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner, *e_repo; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *url = gcli_asprintf("%s/repos/%s/%s/pulls/%"PRIid"%s", gcli_get_apibase(ctx), e_owner, e_repo, path->as_default.id, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_URL: { *url = gcli_asprintf("%s%s", path->as_url, suffix); } break; default: { rc = gcli_error(ctx, "unsupported path type for gitlab merge request"); } break; } gcli_clear_ptr(&suffix); return rc; } /* The following function is a hack around the stupidity of the Github * REST API. With Github's REST API it is impossible to explicitly * request a list of pull requests filtered by a given author. I guess * what you're supposed to be doing is to do the filtering * yourself. * * What this function does is to go through the list of pull requests * and removes the ones that are not authored by the given * username. It then shrinks the allocation size of the list such that * we don't confuse the malloc allocator. Really, it shouldn't change * the actual storage and instead just record the new size of the * allocation. */ static bool pull_has_label(struct gcli_pull const *p, char const *const label) { for (size_t i = 0; i < p->labels_size; ++i) { if (strcmp(p->labels[i], label) == 0) return true; } return false; } static void github_pulls_filter(struct gcli_pull **listp, size_t *sizep, struct gcli_pull_fetch_details const *details) { for (size_t i = *sizep; i > 0; --i) { struct gcli_pull *pulls = *listp; struct gcli_pull *pull = &pulls[i-1]; bool should_remove = false; if (details->author && strcmp(details->author, pull->author)) should_remove = true; if (details->label && !pull_has_label(pull, details->label)) should_remove = true; if (details->milestone && pull->milestone && strcmp(pull->milestone, details->milestone)) should_remove = true; if (should_remove) { gcli_pull_free(pull); memmove(pull, &pulls[i], sizeof(*pulls) * (*sizep - i)); *listp = realloc(pulls, sizeof(*pulls) * (--(*sizep))); } } } static int github_fetch_pulls(struct gcli_ctx *ctx, char *url, struct gcli_pull_fetch_details const *details, int max, struct gcli_pull_list *const list) { struct gcli_fetch_list_ctx fl = { .listp = &list->pulls, .sizep = &list->pulls_size, .parse = (parsefn)(parse_github_pulls), .filter = (filterfn)(github_pulls_filter), .userdata = details, .max = max, }; return gcli_fetch_list(ctx, url, &fl); } static int search_pulls(struct gcli_ctx *ctx, struct gcli_path const *const path, 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 (path->kind != GCLI_PATH_DEFAULT) return gcli_error(ctx, "unsupported path kind for searching " "with search term on GitHub"); if (details->milestone) milestone = gcli_asprintf("milestone:%s", details->milestone); if (details->author) author = gcli_asprintf("author:%s", details->author); if (details->label) label = gcli_asprintf("label:%s", details->label); query_string = gcli_asprintf("repo:%s/%s is:pull-request%s %s %s %s %s", path->as_default.owner, path->as_default.repo, details->all ? "" : " is:open", milestone ? milestone : "", author ? author : "", label ? label : "", details->search_term); e_query_string = gcli_urlencode(query_string); url = gcli_asprintf("%s/search/issues?q=%s", gcli_get_apibase(ctx), e_query_string); gcli_clear_ptr(&milestone); gcli_clear_ptr(&author); gcli_clear_ptr(&label); gcli_clear_ptr(&query_string); gcli_clear_ptr(&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); gcli_fetch_buffer_free(&buffer); error_fetch: gcli_clear_ptr(&url); return rc; } static int list_pulls(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_pull_fetch_details const *const details, int const max, struct gcli_pull_list *const list) { char *url = NULL; int rc = 0; rc = github_repo_make_url(ctx, path, &url, "/pulls?state=%s", details->all ? "all" : "open"); if (rc < 0) return rc; return github_fetch_pulls(ctx, url, details, max, list); } int github_search_pulls(struct gcli_ctx *ctx, struct gcli_path const *const path, 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, path, details, max, list); else return list_pulls(ctx, path, details, max, list); } int github_pull_get_patch(struct gcli_ctx *ctx, FILE *stream, struct gcli_path const *const path) { char *url = NULL; int rc = 0; rc = github_pull_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = gcli_curl(ctx, stream, url, "Accept: application/vnd.github.v3.patch"); gcli_clear_ptr(&url); return rc; } int github_print_get_patch(struct gcli_ctx *ctx, FILE *stream, char const *owner, char const *repo, gcli_id pr_number) { char *url = NULL; char *e_owner = NULL; char *e_repo = NULL; int rc = 0; e_owner = gcli_urlencode(owner); e_repo = gcli_urlencode(repo); url = gcli_asprintf( "%s/repos/%s/%s/pulls/%"PRIid, gcli_get_apibase(ctx), e_owner, e_repo, pr_number); rc = gcli_curl(ctx, stream, url, "Accept: application/vnd.github.v3.patch"); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); gcli_clear_ptr(&url); return rc; } int github_pull_get_diff(struct gcli_ctx *ctx, FILE *stream, struct gcli_path const *const path) { char *url = NULL; int rc = 0; rc = github_pull_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = gcli_curl(ctx, stream, url, "Accept: application/vnd.github.v3.diff"); gcli_clear_ptr(&url); return rc; } static int github_pull_delete_head_branch(struct gcli_ctx *ctx, struct gcli_path const *const path) { struct gcli_pull pull = {0}; char *url = NULL; char const *head_branch; int rc = 0; rc = github_get_pull(ctx, path, &pull); if (rc < 0) return rc; head_branch = strchr(pull.head_label, ':'); head_branch++; rc = github_repo_make_url(ctx, path, &url, "/git/refs/heads/%s", head_branch); if (rc < 0) goto err_make_url; rc = gcli_fetch_with_method(ctx, "DELETE", url, NULL, NULL, NULL); gcli_clear_ptr(&url); err_make_url: gcli_pull_free(&pull); return rc; } int github_pull_merge(struct gcli_ctx *ctx, struct gcli_path const *const path, enum gcli_merge_flags const flags) { bool const delete_source = flags & GCLI_PULL_MERGE_DELETEHEAD; bool const squash = flags & GCLI_PULL_MERGE_SQUASH; char *url = NULL; int rc = 0; rc = github_pull_make_url(ctx, path, &url, "/merge?merge_method=%s", squash ? "squash" : "merge"); if (rc < 0) return rc; rc = gcli_fetch_with_method(ctx, "PUT", url, "{}", NULL, NULL); if (rc == 0 && delete_source) rc = github_pull_delete_head_branch(ctx, path); gcli_clear_ptr(&url); return rc; } static int github_pull_patch_state(struct gcli_ctx *const ctx, struct gcli_path const *const path, char const *const new_state) { char *url = NULL, *payload = NULL; int rc = 0; struct gcli_jsongen gen = {0}; /* Generate URL */ rc = github_pull_make_url(ctx, path, &url, ""); if (rc < 0) return rc; /* Generate payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "state"); gcli_jsongen_string(&gen, new_state); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "PATCH", url, payload, NULL, NULL); gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } int github_pull_close(struct gcli_ctx *ctx, struct gcli_path const *const path) { return github_pull_patch_state(ctx, path, "closed"); } int github_pull_reopen(struct gcli_ctx *ctx, struct gcli_path const *const path) { return github_pull_patch_state(ctx, path, "open"); } static int github_pull_set_automerge(struct gcli_ctx *const ctx, char const *const node_id) { char *url, *query, *payload; int rc; char const *const fmt = "mutation updateAutomergeState {\n" " enablePullRequestAutoMerge(input: {\n" " pullRequestId: \"%s\",\n" " mergeMethod: MERGE\n" " }) {\n" " clientMutationId\n" " }\n" "}\n"; struct gcli_jsongen gen = {0}; query = gcli_asprintf(fmt, node_id); gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "query"); gcli_jsongen_string(&gen, query); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); gcli_clear_ptr(&query); url = gcli_asprintf("%s/graphql", gcli_get_apibase(ctx)); rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, NULL); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } static int github_pull_add_reviewers(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const *users, size_t users_size) { int rc = 0; char *url, *payload; struct gcli_jsongen gen = {0}; /* /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers */ rc = github_pull_make_url(ctx, path, &url, "/requested_reviewers"); if (rc < 0) return rc; /* 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 */ gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } int github_perform_submit_pull(struct gcli_ctx *ctx, struct gcli_submit_pull_options *opts) { char *url = NULL, *payload = NULL; struct gcli_fetch_buffer buffer = {0}; struct gcli_jsongen gen = {0}; int rc = 0; rc = github_repo_make_url(ctx, &opts->target_repo, &url, "/pulls"); if (rc < 0) return rc; gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "head"); gcli_jsongen_string(&gen, opts->from); gcli_jsongen_objmember(&gen, "base"); gcli_jsongen_string(&gen, opts->target_branch); gcli_jsongen_objmember(&gen, "title"); gcli_jsongen_string(&gen, opts->title); /* Body is optional and will be NULL if unset */ if (opts->body) { gcli_jsongen_objmember(&gen, "body"); gcli_jsongen_string(&gen, opts->body); } } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, &buffer); /* 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) && (opts->target_repo.kind == GCLI_PATH_DEFAULT)) { struct json_stream json = {0}; struct gcli_pull pull = {0}; struct gcli_path target_pull_path = {0}; json_open_buffer(&json, buffer.data, buffer.length); parse_github_pull(ctx, &json, &pull); target_pull_path = opts->target_repo; target_pull_path.as_default.id = pull.id; if (opts->labels_size) { rc = github_issue_add_labels( ctx, &target_pull_path, (char const *const *)opts->labels, opts->labels_size); } if (rc == 0 && opts->reviewers_size) { rc = github_pull_add_reviewers( ctx, &target_pull_path, (char const *const *)opts->reviewers, opts->reviewers_size); } if (rc == 0 && opts->automerge) { /* pull.id is the global pull request ID */ rc = github_pull_set_automerge(ctx, pull.node_id); } gcli_pull_free(&pull); json_close(&json); } gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } static void filter_commit_short_sha(struct gcli_commit **listp, size_t *sizep, void *_data) { (void) _data; for (size_t i = 0; i < *sizep; ++i) (*listp)[i].sha = gcli_strndup((*listp)[i].long_sha, 8); } int github_get_pull_commits(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_commit_list *const out) { char *url = NULL; int rc = 0; struct gcli_fetch_list_ctx fl = { .listp = &out->commits, .sizep = &out->commits_size, .max = -1, .parse = (parsefn)(parse_github_commits), .filter = (filterfn)(filter_commit_short_sha), }; rc = github_pull_make_url(ctx, path, &url, "/commits"); if (rc < 0) return rc; return gcli_fetch_list(ctx, url, &fl); } int github_get_pull(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_pull *const out) { int rc = 0; struct gcli_fetch_buffer buffer = {0}; char *url = NULL; rc = github_pull_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc == 0) { struct json_stream stream = {0}; json_open_buffer(&stream, buffer.data, buffer.length); parse_github_pull(ctx, &stream, out); json_close(&stream); } gcli_clear_ptr(&url); gcli_fetch_buffer_free(&buffer); return rc; } int github_pull_get_checks(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_pull_checks_list *out) { int rc = 0; char refname[64] = {0}; struct gcli_path norm_path = {0}; struct gcli_path const *p = path; if (path->kind != GCLI_PATH_DEFAULT) { rc = github_path_normalise(ctx, path, &norm_path); if (rc < 0) return rc; p = &norm_path; } /* This is kind of a hack, but it works! * Yes, even a few months later I agree that this is a hack. */ snprintf(refname, sizeof refname, "refs%%2Fpull%%2F%"PRIid"%%2Fhead", p->as_default.id); rc = github_get_checks(ctx, p, refname, -1, (struct github_check_list *)out); /* clean up normalised path if it has been normalised */ if (p == &norm_path) { gcli_path_free(&norm_path); p = NULL; } return rc; } int github_pull_add_reviewer(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *username) { return github_pull_add_reviewers(ctx, path, &username, 1); } int github_pull_set_title(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *new_title) { char *url, *payload; int rc; struct gcli_jsongen gen = {0}; /* Generate the url */ rc = github_pull_make_url(ctx, path, &url, ""); if (rc < 0) return rc; /* Generate the payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "title"); gcli_jsongen_string(&gen, new_title); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); /* perform request */ rc = gcli_fetch_with_method(ctx, "PATCH", url, payload, NULL, NULL); /* Cleanup */ gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } int github_pull_create_review(struct gcli_ctx *ctx, struct gcli_pull_create_review_details const *details) { int rc = 0; char *url = NULL, *payload = NULL; struct gcli_jsongen gen = {0}; /* lookup table for translating enum to string */ char const *const state_string[] = { [GCLI_REVIEW_ACCEPT_CHANGES] = "APPROVE", [GCLI_REVIEW_REQUEST_CHANGES] = "REQUEST_CHANGES", [GCLI_REVIEW_COMMENT] = "COMMENT", }; rc = github_pull_make_url(ctx, &details->path, &url, "/reviews"); if (rc < 0) return rc; if (gcli_jsongen_init(&gen) < 0) { rc = gcli_error(ctx, "failed to init JSON generator"); goto bail; } gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "body"); gcli_jsongen_string(&gen, details->body); gcli_jsongen_objmember(&gen, "event"); gcli_jsongen_string(&gen, state_string[details->review_state]); gcli_jsongen_objmember(&gen, "comments"); gcli_jsongen_begin_array(&gen); { struct gcli_diff_comment *comment; TAILQ_FOREACH(comment, &details->comments, next) { gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "path"); gcli_jsongen_string(&gen, comment->after.filename); gcli_jsongen_objmember(&gen, "body"); gcli_jsongen_string(&gen, comment->comment); gcli_jsongen_objmember(&gen, "line"); gcli_jsongen_number(&gen, comment->after.end_row); if (comment->after.start_row != comment->after.end_row) { gcli_jsongen_objmember(&gen, "start_line"); gcli_jsongen_number(&gen, comment->after.start_row); } } gcli_jsongen_end_object(&gen); } } gcli_jsongen_end_array(&gen); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, NULL); gcli_jsongen_free(&gen); bail: gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } int github_pull_get_reviews(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_pull_reviews *out) { char *url; int rc = 0; struct gcli_fetch_list_ctx fl = { .listp = &out->reviews, .sizep = &out->reviews_size, .parse = (parsefn)(parse_github_pull_reviews), }; rc = github_pull_make_url(ctx, path, &url, "/reviews"); if (rc < 0) return rc; return gcli_fetch_list(ctx, url, &fl); } struct gcli_pull_review_thread * find_thread(struct gcli_pull_review_thread *list, gcli_id const comment_id) { struct gcli_pull_review_comment *c; TAILQ_FOREACH(c, list, next) { if (c->id == comment_id) return &c->replies; } return NULL; } static int threadify_comments(struct gcli_ctx *ctx, struct gcli_pull_review_comments *list, struct gcli_pull_review_thread *out) { struct gcli_pull_review_comment *c, *c1; (void) ctx; TAILQ_INIT(out); /* init the replies and push root-comments */ for (size_t i = 0; i < list->comments_size; ++i) { c = calloc(1, sizeof *c); memcpy(c, list->comments + i, sizeof *c); TAILQ_INIT(&c->replies); TAILQ_INSERT_TAIL(out, c, next); } /* clear list to make dumb stuff obvious */ gcli_clear_ptr(&list->comments); list->comments_size = 0; /* now iterate through comment list and push the replies as needed */ c = TAILQ_FIRST(out); while (c) { struct gcli_pull_review_thread *thd = out; c1 = TAILQ_NEXT(c, next); /* root comments */ if (!c->in_reply_to) { c = c1; continue; } thd = find_thread(out, c->in_reply_to); /* check for API bugs */ if (thd == NULL) { return gcli_error( ctx, "encountered bad comment id reference: %"PRIid " in comment id %"PRIid, c->in_reply_to, c->id); } TAILQ_REMOVE(out, c, next); TAILQ_INSERT_TAIL(thd, c, next); c = c1; } return 0; } int github_pull_get_review_threads(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_pull_review_thread *out) { char *url; int rc = 0; // @@@ handle releasing memory! struct gcli_pull_review_comments list = {0}; struct gcli_fetch_list_ctx fl = { .listp = &list.comments, .sizep = &list.comments_size, .parse = (parsefn)(parse_github_pull_review_comments), }; rc = github_pull_make_url(ctx, path, &url, "/comments"); if (rc < 0) return rc; rc = gcli_fetch_list(ctx, url, &fl); if (rc < 0) { gcli_pull_review_comments_free(&list); return rc; } rc = threadify_comments(ctx, &list, out); return rc; } gcli-2.9.1/src/github/releases.c000066400000000000000000000131661507017207500164740ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include #include #include int github_get_releases(struct gcli_ctx *ctx, struct gcli_path const *const path, int const max, struct gcli_release_list *const list) { char *url = NULL; int rc = 0; struct gcli_fetch_list_ctx fl = { .listp = &list->releases, .sizep = &list->releases_size, .max = max, .parse = (parsefn)(parse_github_releases), }; *list = (struct gcli_release_list) {0}; rc = github_repo_make_url(ctx, path, &url, "/releases"); if (rc < 0) return rc; return gcli_fetch_list(ctx, url, &fl); } static void github_parse_single_release(struct gcli_ctx *ctx, struct gcli_fetch_buffer buffer, struct gcli_release *const out) { struct json_stream stream = {0}; json_open_buffer(&stream, buffer.data, buffer.length); json_set_streaming(&stream, 1); parse_github_release(ctx, &stream, out); json_close(&stream); } static int github_get_upload_url(struct gcli_ctx *ctx, struct gcli_release *const it, char **out) { char *delim = strchr(it->upload_url, '{'); if (delim == NULL) return gcli_error(ctx, "GitHub API returned an invalid upload url"); size_t len = delim - it->upload_url; *out = gcli_strndup(it->upload_url, len); return 0; } static int github_upload_release_asset(struct gcli_ctx *ctx, char const *url, struct gcli_release_asset_upload const asset) { char *req = NULL; gcli_sv file_content = {0}; struct gcli_fetch_buffer buffer = {0}; int rc = 0; file_content.length = gcli_read_file(asset.path, &file_content.data); if (file_content.length == 0) return -1; /* TODO: URL escape this */ req = gcli_asprintf("%s?name=%s", url, asset.name); rc = gcli_post_upload( ctx, req, "application/octet-stream", /* HACK */ file_content.data, file_content.length, &buffer); gcli_clear_ptr(&req); gcli_fetch_buffer_free(&buffer); return rc; } int github_create_release(struct gcli_ctx *ctx, struct gcli_create_release_args const *release) { char *url = NULL, *upload_url = NULL, *payload = NULL; int rc = 0; struct gcli_fetch_buffer buffer = {0}; struct gcli_jsongen gen = {0}; struct gcli_release response = {0}; /* Payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "tag_name"); gcli_jsongen_string(&gen, release->tag); gcli_jsongen_objmember(&gen, "draft"); gcli_jsongen_bool(&gen, release->draft); gcli_jsongen_objmember(&gen, "prerelease"); gcli_jsongen_bool(&gen, release->prerelease); if (release->body) { gcli_jsongen_objmember(&gen, "body"); gcli_jsongen_string(&gen, release->body); } if (release->commitish) { gcli_jsongen_objmember(&gen, "target_commitish"); gcli_jsongen_string(&gen, release->commitish); } if (release->name) { gcli_jsongen_objmember(&gen, "name"); gcli_jsongen_string(&gen, release->name); } } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = github_repo_make_url(ctx, &release->repo_path, &url, "/releases"); if (rc < 0) goto out; rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, &buffer); if (rc < 0) goto out; github_parse_single_release(ctx, buffer, &response); rc = github_get_upload_url(ctx, &response, &upload_url); if (rc < 0) goto out; for (size_t i = 0; i < release->assets_size; ++i) { printf("INFO : Uploading asset %s...\n", release->assets[i].path); rc = github_upload_release_asset(ctx, upload_url, release->assets[i]); if (rc < 0) break; } out: gcli_release_free(&response); gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&upload_url); gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } int github_delete_release(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const id) { char *url = NULL; int rc = 0; rc = github_repo_make_url(ctx, path, &url, "/releases/%s", id); if (rc < 0) return rc; rc = gcli_fetch_with_method(ctx, "DELETE", url, NULL, NULL, NULL); gcli_clear_ptr(&url); return rc; } gcli-2.9.1/src/github/repos.c000066400000000000000000000154211507017207500160150ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include #include int github_repo_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char **url, char const *const fmt, ...) { char *suffix = NULL; int rc = 0; va_list vp; va_start(vp, fmt); suffix = gcli_vasprintf(fmt, vp); va_end(vp); switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner, *e_repo; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *url = gcli_asprintf("%s/repos/%s/%s%s", gcli_get_apibase(ctx), e_owner, e_repo, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_NAMED: { char *e_owner, *e_repo; e_owner = gcli_urlencode(path->as_named.owner); e_repo = gcli_urlencode(path->as_named.repo); *url = gcli_asprintf("%s/repos/%s/%s%s", gcli_get_apibase(ctx), e_owner, e_repo, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; default: { rc = gcli_error(ctx, "unsupported path kind for GitHub repository"); } break; } gcli_clear_ptr(&suffix); return rc; } /* Github is a little stupid in that it distinguishes * organizations and users. This kludge checks, whether the * param is a user or an actual organization. */ int github_user_is_org(struct gcli_ctx *ctx, char const *e_owner) { char *url = gcli_asprintf("%s/users/%s", gcli_get_apibase(ctx), e_owner); int const rc = gcli_curl_test_success(ctx, url); gcli_clear_ptr(&url); /* 0 = failed, 1 = success, -1 = error (just like a BOOL in Win32 * /sarc). But to make the name of the function make sense, reverse * non-negative return values (failure means user *is* an org); * negative return to indiciate error is preserved */ return rc < 0 ? rc : !rc; } int github_get_repos(struct gcli_ctx *ctx, char const *owner, int const max, struct gcli_repo_list *const list) { char *url = NULL, *e_owner = NULL; int rc = 0; struct gcli_fetch_list_ctx lf = { .listp = &list->repos, .sizep = &list->repos_size, .max = max, .parse = (parsefn)(parse_github_repos), }; e_owner = gcli_urlencode(owner); rc = github_user_is_org(ctx, e_owner); if (rc < 0) return rc; if (!rc) { /* it is a user */ url = gcli_asprintf("%s/users/%s/repos", gcli_get_apibase(ctx), e_owner); } else { /* this is an actual organization */ url = gcli_asprintf("%s/orgs/%s/repos", gcli_get_apibase(ctx), e_owner); } gcli_clear_ptr(&e_owner); return gcli_fetch_list(ctx, url, &lf); } int github_get_own_repos(struct gcli_ctx *ctx, int const max, struct gcli_repo_list *const list) { char *url = NULL; struct gcli_fetch_list_ctx fl = { .listp = &list->repos, .sizep = &list->repos_size, .max = max, .parse = (parsefn)(parse_github_repos), }; url = gcli_asprintf("%s/user/repos", gcli_get_apibase(ctx)); return gcli_fetch_list(ctx, url, &fl); } int github_repo_delete(struct gcli_ctx *ctx, struct gcli_path const *const path) { char *url = NULL; int rc = 0; rc = github_repo_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = gcli_fetch_with_method(ctx, "DELETE", url, NULL, NULL, NULL); gcli_clear_ptr(&url); return rc; } int github_repo_create(struct gcli_ctx *ctx, struct gcli_repo_create_options const *options, struct gcli_repo *const out) { char *url, *payload; struct gcli_fetch_buffer buffer = {0}; struct gcli_jsongen gen = {0}; struct json_stream stream = {0}; int rc = 0; /* Request preparation */ url = gcli_asprintf("%s/user/repos", gcli_get_apibase(ctx)); /* Construct payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "name"); gcli_jsongen_string(&gen, options->name); gcli_jsongen_objmember(&gen, "description"); gcli_jsongen_string(&gen, options->description); gcli_jsongen_objmember(&gen, "private"); gcli_jsongen_bool(&gen, options->private); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); /* Fetch and parse result */ rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, out ? &buffer : NULL); if (rc == 0 && out) { json_open_buffer(&stream, buffer.data, buffer.length); parse_github_repo(ctx, &stream, out); json_close(&stream); } /* Cleanup */ gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } int github_repo_set_visibility(struct gcli_ctx *ctx, struct gcli_path const *const path, gcli_repo_visibility vis) { char *url; char const *vis_str; char *payload; int rc; switch (vis) { case GCLI_REPO_VISIBILITY_PRIVATE: vis_str = "private"; break; case GCLI_REPO_VISIBILITY_PUBLIC: vis_str = "public"; break; default: assert(false && "Invalid visibility"); return gcli_error(ctx, "bad visibility level"); } rc = github_repo_make_url(ctx, path, &url, ""); if (rc < 0) return rc; payload = gcli_asprintf("{ \"visibility\": \"%s\" }", vis_str); rc = gcli_fetch_with_method(ctx, "PATCH", url, payload, NULL, NULL); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } gcli-2.9.1/src/github/sshkeys.c000066400000000000000000000036751507017207500163660ustar00rootroot00000000000000/* * Copyright 2023 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 /* NOTE(Nico): This looks strange because it reuses the Gitlab code * because the APIs are the same. */ #include int github_get_sshkeys(struct gcli_ctx *ctx, struct gcli_sshkey_list *out) { return gitlab_get_sshkeys(ctx, out); } int github_add_sshkey(struct gcli_ctx *ctx, char const *const title, char const *const pubkey, struct gcli_sshkey *out) { return gitlab_add_sshkey(ctx, title, pubkey, out); } int github_delete_sshkey(struct gcli_ctx *ctx, gcli_id const id) { return gitlab_delete_sshkey(ctx, id); } gcli-2.9.1/src/github/status.c000066400000000000000000000044301507017207500162060ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include int github_get_notifications(struct gcli_ctx *ctx, int const max, struct gcli_notification_list *const out) { char *url = NULL; struct gcli_fetch_list_ctx fl = { .listp = &out->notifications, .sizep = &out->notifications_size, .parse = (parsefn)(parse_github_notifications), .max = max, }; url = gcli_asprintf("%s/notifications", gcli_get_apibase(ctx)); return gcli_fetch_list(ctx, url, &fl); } int github_notification_mark_as_read(struct gcli_ctx *ctx, char const *id) { char *url = NULL; int rc = 0; url = gcli_asprintf( "%s/notifications/threads/%s", gcli_get_apibase(ctx), id); rc = gcli_fetch_with_method(ctx, "PATCH", url, NULL, NULL, NULL); gcli_clear_ptr(&url); return rc; } gcli-2.9.1/src/gitlab/000077500000000000000000000000001507017207500144765ustar00rootroot00000000000000gcli-2.9.1/src/gitlab/api.c000066400000000000000000000065631507017207500154250ustar00rootroot00000000000000/* * Copyright 2021-2025 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 char const * gitlab_api_error_string(struct gcli_ctx *ctx, struct gcli_fetch_buffer *const buf) { char *msg = NULL; int rc; struct json_stream stream = {0}; struct gitlab_error_data error_data = {0}; json_open_buffer(&stream, buf->data, buf->length); rc = parse_gitlab_get_error(ctx, &stream, &error_data); json_close(&stream); /* Extract a most useful error message */ if (error_data.error_description) { msg = strdup(error_data.error_description); } else if (error_data.message) { msg = strdup(error_data.message); } else if (error_data.error) { msg = strdup(error_data.error); } else { msg = NULL; } gcli_clear_ptr(&error_data.error_description); gcli_clear_ptr(&error_data.message); gcli_clear_ptr(&error_data.error); if (rc < 0 || msg == NULL) { gcli_clear_ptr(&msg); if (gcli_be_verbose(ctx)) { return gcli_asprintf("Could not parse Gitlab error response. " "The response was:\n\n%.*s\n", (int)buf->length, buf->data); } else { return strdup("no error message: failed to parse error response. " "Please run the gcli query with verbose mode again."); } } else { return msg; } } int gitlab_user_id(struct gcli_ctx *ctx, char const *user_name) { struct gcli_fetch_buffer buffer = {0}; struct json_stream stream = {0}; char *url = NULL; char *e_username; long uid = -1; int rc; e_username = gcli_urlencode(user_name); url = gcli_asprintf("%s/users?username=%s", gcli_get_apibase(ctx), e_username); uid = gcli_fetch(ctx, url, NULL, &buffer); if (uid == 0) { json_open_buffer(&stream, buffer.data, buffer.length); json_set_streaming(&stream, 1); uid = rc = gcli_json_advance(ctx, &stream, "[{s", "id"); if (rc == 0) { rc = get_long(ctx, &stream, &uid); json_close(&stream); } } gcli_clear_ptr(&e_username); gcli_clear_ptr(&url); gcli_fetch_buffer_free(&buffer); return uid; } gcli-2.9.1/src/gitlab/checkout.c000066400000000000000000000054641507017207500164600ustar00rootroot00000000000000/* * Copyright 2025 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 int gitlab_mr_checkout(struct gcli_ctx *ctx, char const *const remote, struct gcli_path const *const path) { /* FIXME: this is more than not ideal! */ char *remote_ref, *local_ref, *refspec; gcli_id pr_id; int rc; pid_t pid; if (path->kind != GCLI_PATH_DEFAULT) return gcli_error(ctx, "unsupported path kind for checkout"); pr_id = path->as_default.id; if (path->kind != GCLI_PATH_DEFAULT) return gcli_error(ctx, "unsupported path kind for MR checkout"); pr_id = path->as_default.id; remote_ref = gcli_asprintf("merge-requests/%"PRIid"/head", pr_id); local_ref = gcli_asprintf("gitlab/mr/%"PRIid, pr_id); refspec = gcli_asprintf("%s:%s", remote_ref, local_ref); pid = fork(); if (pid < 0) return gcli_error(ctx, "could not fork"); if (pid == 0) { rc = execlp("git", "git", "fetch", remote, refspec, NULL); if (rc < 0) exit(EXIT_FAILURE); /* NOTREACHED */ } rc = gcli_wait_proc_ok(ctx, pid); if (rc < 0) return rc; gcli_clear_ptr(&remote_ref); gcli_clear_ptr(&refspec); pid = fork(); if (pid < 0) return gcli_error(ctx, "could not fork"); if (pid == 0) { rc = execlp("git", "git", "checkout", "--track", local_ref, NULL); if (rc < 0) exit(EXIT_FAILURE); /* NOTREACHED */ } rc = gcli_wait_proc_ok(ctx, pid); gcli_clear_ptr(&local_ref); return rc; } gcli-2.9.1/src/gitlab/comments.c000066400000000000000000000117551507017207500165000ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include int gitlab_perform_submit_comment(struct gcli_ctx *ctx, struct gcli_submit_comment_opts const *const opts) { char *url = NULL, *payload = NULL; struct gcli_jsongen gen = {0}; int rc = 0; switch (opts->target_type) { case ISSUE_COMMENT: rc = gitlab_issue_make_url(ctx, &opts->target, &url, "/notes"); break; case PR_COMMENT: rc = gitlab_mr_make_url(ctx, &opts->target, &url, "/notes"); break; } if (rc < 0) return rc; gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "body"); gcli_jsongen_string(&gen, opts->message); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, NULL); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } static void reverse_comment_list(struct gcli_comment_list *const list) { struct gcli_comment *reversed = calloc(list->comments_size, sizeof *list->comments); for (size_t i = 0; i < list->comments_size; ++i) reversed[i] = list->comments[list->comments_size - i - 1]; gcli_clear_ptr(&list->comments); list->comments = reversed; } int gitlab_fetch_comments(struct gcli_ctx *ctx, char *url, struct gcli_comment_list *const out) { struct gcli_fetch_list_ctx fl = { .listp = &out->comments, .sizep = &out->comments_size, .parse = (parsefn)parse_gitlab_comments, .max = -1, }; /* Comments in the resulting list are in reverse on Gitlab * (most recent is first). */ int const rc = gcli_fetch_list(ctx, url, &fl); if (rc == 0) reverse_comment_list(out); return rc; } int gitlab_get_mr_comments(struct gcli_ctx *ctx, struct gcli_path const *const mr_path, struct gcli_comment_list *const out) { char *url = NULL; int rc = 0; rc = gitlab_mr_make_url(ctx, mr_path, &url, "/notes"); if (rc < 0) return rc; return gitlab_fetch_comments(ctx, url, out); } int gitlab_get_issue_comments(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, struct gcli_comment_list *const out) { char *url = NULL; int rc = 0; rc = gitlab_issue_make_url(ctx, issue_path, &url, "/notes"); if (rc < 0) return rc; return gitlab_fetch_comments(ctx, url, out); } static int gitlab_fetch_comment(struct gcli_ctx *ctx, char const *const url, struct gcli_comment *const out) { int rc = 0; struct gcli_fetch_buffer buffer = {0}; struct json_stream stream = {0}; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc < 0) return rc; json_open_buffer(&stream, buffer.data, buffer.length); rc = parse_gitlab_comment(ctx, &stream, out); json_close(&stream); gcli_fetch_buffer_free(&buffer); return rc; } int gitlab_get_comment(struct gcli_ctx *ctx, struct gcli_path const *const target, enum comment_target_type const target_type, gcli_id const comment_id, struct gcli_comment *const out) { char *url = NULL; int rc = 0; switch (target_type) { case ISSUE_COMMENT: rc = gitlab_issue_make_url(ctx, target, &url, "/notes/%"PRIid, comment_id); break; case PR_COMMENT: rc = gitlab_mr_make_url(ctx, target, &url, "/notes/%"PRIid, comment_id); break; } if (rc < 0) return rc; rc = gitlab_fetch_comment(ctx, url, out); gcli_clear_ptr(&url); return rc; } gcli-2.9.1/src/gitlab/config.c000066400000000000000000000030561507017207500161130ustar00rootroot00000000000000/* * Copyright 2021-2025 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 char * gitlab_make_authheader(struct gcli_ctx *ctx, char const *token) { (void) ctx; return gcli_asprintf("PRIVATE-TOKEN: %s", token); } gcli-2.9.1/src/gitlab/forks.c000066400000000000000000000053661507017207500160000ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include int gitlab_get_forks(struct gcli_ctx *ctx, struct gcli_path const *const path, int const max, struct gcli_fork_list *const list) { char *url = NULL; int rc = 0; struct gcli_fetch_list_ctx fl = { .listp = &list->forks, .sizep = &list->forks_size, .parse = (parsefn)parse_gitlab_forks, .max = max, }; rc = gitlab_repo_make_url(ctx, path, &url, "/forks"); if (rc < 0) return rc; return gcli_fetch_list(ctx, url, &fl); } int gitlab_fork_create(struct gcli_ctx *ctx, struct gcli_path const *const repo_path, char const *const in) { char *url = NULL; char *post_data = NULL; int rc = 0; rc = gitlab_repo_make_url(ctx, repo_path, &url, "/fork"); if (rc < 0) return rc; if (in) { struct gcli_jsongen gen = {0}; gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "namespace_path"); gcli_jsongen_string(&gen, in); } gcli_jsongen_end_object(&gen); post_data = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); } rc = gcli_fetch_with_method(ctx, "POST", url, post_data, NULL, NULL); gcli_clear_ptr(&url); gcli_clear_ptr(&post_data); return rc; } gcli-2.9.1/src/gitlab/issues.c000066400000000000000000000314501507017207500161600ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include #include #include #include #include int gitlab_issue_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char **url, char const *const suffix_fmt, ...) { char *suffix = NULL; int rc = 0; va_list vp; va_start(vp, suffix_fmt); suffix = gcli_vasprintf(suffix_fmt, vp); va_end(vp); switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner, *e_repo; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *url = gcli_asprintf("%s/projects/%s%%2F%s/issues/%"PRIid"%s", gcli_get_apibase(ctx), e_owner, e_repo, path->as_default.id, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_PID_ID: { *url = gcli_asprintf("%s/projects/%"PRIid"/issues/%"PRIid"%s", gcli_get_apibase(ctx), path->as_pid_id.project_id, path->as_pid_id.id, suffix); } break; case GCLI_PATH_URL: { *url = gcli_asprintf("%s%s", path->as_url, suffix); } break; default: { rc = gcli_error(ctx, "unsupported path type for gitlab issue"); } break; } gcli_clear_ptr(&suffix); return rc; } /** Given the url fetch issues */ int gitlab_fetch_issues(struct gcli_ctx *ctx, char *url, int const max, struct gcli_issue_list *const out) { struct gcli_fetch_list_ctx fl = { .listp = &out->issues, .sizep = &out->issues_size, .max = max, .parse = (parsefn)(parse_gitlab_issues), }; return gcli_fetch_list(ctx, url, &fl); } static int gitlab_issues_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const suffix, char **url) { int rc = 0; rc = gitlab_repo_make_url(ctx, path, url, "/issues%s", suffix ? suffix : ""); return rc; } int gitlab_issues_search(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue_fetch_details const *details, int const max, struct gcli_issue_list *const out) { char *url = NULL, *suffix = NULL; int rc = 0; gcli_url_options_append(&suffix, "state", details->all ? NULL : "opened"); gcli_url_options_append(&suffix, "author_username", details->author); gcli_url_options_append(&suffix, "labels", details->label); gcli_url_options_append(&suffix, "milestone", details->milestone); gcli_url_options_append(&suffix, "assignee_username", details->assignee); gcli_url_options_append(&suffix, "search", details->search_term); rc = gitlab_issues_make_url(ctx, path, suffix, &url); gcli_clear_ptr(&suffix); if (rc < 0) return rc; return gitlab_fetch_issues(ctx, url, max, out); } int gitlab_fetch_issue(struct gcli_ctx *ctx, char *url, struct gcli_issue *out) { int rc = 0; struct gcli_fetch_buffer buffer = {0}; struct json_stream parser = {0}; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc == 0) { json_open_buffer(&parser, buffer.data, buffer.length); json_set_streaming(&parser, true); parse_gitlab_issue(ctx, &parser, out); json_close(&parser); } gcli_fetch_buffer_free(&buffer); return rc; } int gitlab_get_issue_summary(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue *const out) { char *url = NULL; int rc = 0; rc = gitlab_issue_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = gitlab_fetch_issue(ctx, url, out); gcli_clear_ptr(&url); return rc; } static int gitlab_issue_patch_state(struct gcli_ctx *const ctx, struct gcli_path const *const path, char const *const new_state) { char *url = NULL, *payload = NULL; struct gcli_jsongen gen = {0}; int rc = 0; /* Generate URL */ rc = gitlab_issue_make_url(ctx, path, &url, ""); if (rc < 0) return rc; /* Generate payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "state_event"); gcli_jsongen_string(&gen, new_state); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "PUT", url, payload, NULL, NULL); gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } int gitlab_issue_close(struct gcli_ctx *ctx, struct gcli_path const *const path) { return gitlab_issue_patch_state(ctx, path, "close"); } int gitlab_issue_reopen(struct gcli_ctx *ctx, struct gcli_path const *const path) { return gitlab_issue_patch_state(ctx, path, "reopen"); } int 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; 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); gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "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) { gcli_jsongen_objmember(&gen, "description"); gcli_jsongen_string(&gen, opts->body); } } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); url = gcli_asprintf("%s/projects/%s%%2F%s/issues", gcli_get_apibase(ctx), e_owner, e_repo); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); 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); } gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } int gitlab_issue_assign(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, char const *assignee) { char *url = NULL, *payload = NULL; struct gcli_jsongen gen = {0}; int assignee_uid = -1; int rc = 0; assignee_uid = gitlab_user_id(ctx, assignee); if (assignee_uid < 0) return assignee_uid; /* Generate URL */ rc = gitlab_issue_make_url(ctx, issue_path, &url, ""); if (rc < 0) return rc; /* Generate payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "assignee_ids"); gcli_jsongen_begin_array(&gen); gcli_jsongen_number(&gen, assignee_uid); gcli_jsongen_end_array(&gen); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "PUT", url, payload, NULL, NULL); gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } static int gitlab_issues_update_labels(struct gcli_ctx *const ctx, struct gcli_path const *const path, char const *const labels[], size_t const labels_size, char const *const what) { char *url = NULL, *payload = NULL, *label_list = NULL; struct gcli_jsongen gen = {0}; int rc = 0; /* Generate URL */ rc = gitlab_issue_make_url(ctx, path, &url, ""); if (rc < 0) return rc; /* Generate payload. For some reason Gitlab expects us to put a * comma-separated list of issues into a JSON string. Figures...*/ label_list = gcli_join_with(labels, labels_size, ","); gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, what); gcli_jsongen_string(&gen, label_list); } gcli_jsongen_end_object(&gen); gcli_clear_ptr(&label_list); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "PUT", url, payload, NULL, NULL); gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } int gitlab_issue_add_labels(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const labels[], size_t const labels_size) { return gitlab_issues_update_labels(ctx, path, labels, labels_size, "add_labels"); } int gitlab_issue_remove_labels(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const labels[], size_t const labels_size) { return gitlab_issues_update_labels(ctx, path, labels, labels_size, "remove_labels"); } int gitlab_issue_set_milestone(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, gcli_id const milestone) { char *url; int rc = 0; rc = gitlab_issue_make_url(ctx, issue_path, &url, "?milestone_url=%"PRIid, milestone); if (rc < 0) return rc; rc = gcli_fetch_with_method(ctx, "PUT", url, NULL, NULL, NULL); gcli_clear_ptr(&url); return rc; } int gitlab_issue_clear_milestone(struct gcli_ctx *ctx, struct gcli_path const *const issue_path) { char *url; char const *payload; int rc; /* The Gitlab API says: * * milestone_id: The global ID of a milestone to assign the * issue to. Set to 0 or provide an empty value to unassign a * milestone. * * However, the documentation is plain wrong and trying to set it * to zero does absolutely nothing. What do you expect from an * enterprise quality product?! Certainly not this kind of spanish * inquisition. Fear and surprise. That's the Gitlab API in a * nutshell.*/ rc = gitlab_issue_make_url(ctx, issue_path, &url, ""); if (rc < 0) return rc; payload = "{ \"milestone_id\": null }"; rc = gcli_fetch_with_method(ctx, "PUT", url, payload, NULL, NULL); gcli_clear_ptr(&url); return rc; } int gitlab_issue_set_title(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, char const *const new_title) { char *url, *payload; struct gcli_jsongen gen = {0}; int rc; /* Generate url */ rc = gitlab_issue_make_url(ctx, issue_path, &url, ""); if (rc < 0) return rc; /* Generate payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "title"); gcli_jsongen_string(&gen, new_title); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "PUT", url, payload, NULL, NULL); gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } int gitlab_issue_set_op(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const new_op) { char *url, *payload; struct gcli_jsongen gen = {0}; int rc; /* Generate url */ rc = gitlab_issue_make_url(ctx, path, &url, ""); if (rc < 0) return rc; /* Generate payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "description"); gcli_jsongen_string(&gen, new_op); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "PUT", url, payload, NULL, NULL); gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } gcli-2.9.1/src/gitlab/labels.c000066400000000000000000000156031507017207500161110ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include int gitlab_get_labels(struct gcli_ctx *ctx, struct gcli_path const *const path, int const max, struct gcli_label_list *const out) { char *url = NULL; int rc = 0; struct gcli_fetch_list_ctx fl = { .listp = &out->labels, .sizep = &out->labels_size, .max = max, .parse = (parsefn)(parse_gitlab_labels), }; *out = (struct gcli_label_list) {0}; rc = gitlab_repo_make_url(ctx, path, &url, "/labels"); if (rc < 0) return rc; return gcli_fetch_list(ctx, url, &fl); } int gitlab_create_label(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_label *const label) { char *url = NULL, *payload = NULL; int rc = 0; struct gcli_fetch_buffer buffer = {0}; struct gcli_jsongen gen = {0}; struct json_stream stream = {0}; /* Generate URL */ rc = gitlab_repo_make_url(ctx, path, &url, "/labels"); if (rc < 0) return rc; /* Generate payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { char *colour_string = NULL; gcli_jsongen_objmember(&gen, "name"); gcli_jsongen_string(&gen, label->name); colour_string = gcli_asprintf("#%06X", label->colour & 0xFFFFFF); gcli_jsongen_objmember(&gen, "color"); gcli_jsongen_string(&gen, colour_string); gcli_clear_ptr(&colour_string); gcli_jsongen_objmember(&gen, "description"); gcli_jsongen_string(&gen, label->description); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, &buffer); if (rc == 0) { json_open_buffer(&stream, buffer.data, buffer.length); json_set_streaming(&stream, 1); parse_gitlab_label(ctx, &stream, label); json_close(&stream); } gcli_clear_ptr(&payload); gcli_clear_ptr(&url); gcli_fetch_buffer_free(&buffer); return rc; } static int gitlab_label_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char **url, char const *const fmt, ...) { int rc = 0; va_list vp; char *suffix = NULL; va_start(vp, fmt); suffix = gcli_vasprintf(fmt, vp); va_end(vp); switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner, *e_repo; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *url = gcli_asprintf("%s/projects/%s%%2F%s/labels/%"PRIid"%s", gcli_get_apibase(ctx), e_owner, e_repo, path->as_default.id, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_NAMED: { char *e_owner, *e_repo, *e_label; e_owner = gcli_urlencode(path->as_named.owner); e_repo = gcli_urlencode(path->as_named.repo); e_label = gcli_urlencode(path->as_named.id); *url = gcli_asprintf("%s/projects/%s%%2F%s/labels/%s%s", gcli_get_apibase(ctx), e_owner, e_repo, e_label, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); gcli_clear_ptr(&e_label); } break; case GCLI_PATH_URL: { *url = gcli_asprintf("%s%s", path->as_url, suffix); } break; default: { rc = gcli_error(ctx, "unsupported path kind for GitLab labels"); } break; } gcli_clear_ptr(&suffix); return rc; } int gitlab_delete_label(struct gcli_ctx *ctx, struct gcli_path const *const path) { char *url = NULL; int rc = 0; rc = gitlab_label_make_url(ctx, path, &url, ""); if (rc == 0) rc = gcli_fetch_with_method(ctx, "DELETE", url, NULL, NULL, NULL); gcli_clear_ptr(&url); return rc; } int gitlab_get_label(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_label *const out) { char *url; int rc = 0; struct gcli_fetch_buffer buffer = {0}; struct json_stream parser = {0}; rc = gitlab_label_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc == 0) { json_open_buffer(&parser, buffer.data, buffer.length); json_set_streaming(&parser, true); parse_gitlab_label(ctx, &parser, out); json_close(&parser); } gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&url); return rc; } static int gitlab_label_update_property(struct gcli_ctx *ctx, struct gcli_path const *path, char const *const propname, char const *const val) { char *e_val = NULL, *url = NULL; int rc = 0; e_val = gcli_urlencode(val); rc = gitlab_label_make_url(ctx, path, &url, "?%s=%s", propname, e_val); if (rc == 0) { rc = gcli_fetch_with_method(ctx, "PUT", url, NULL, NULL, NULL); } gcli_clear_ptr(&url); gcli_clear_ptr(&e_val); return rc; } int gitlab_label_set_title(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const new_name) { return gitlab_label_update_property(ctx, path, "new_name", new_name); } int gitlab_label_set_description(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const new_description) { return gitlab_label_update_property( ctx, path, "description", new_description); } int gitlab_label_set_colour(struct gcli_ctx *ctx, struct gcli_path const *const path, uint32_t const colour) { char *colour_string = NULL; int rc = 0; colour_string = gcli_asprintf("#%06X", colour & 0xFFFFFF); rc = gitlab_label_update_property(ctx, path, "color", colour_string); gcli_clear_ptr(&colour_string); return rc; } gcli-2.9.1/src/gitlab/merge_requests.c000066400000000000000000000776431507017207500177150ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include #include #include #include #include #include /* for nanosleep */ #include /* Workaround because gitlab doesn't give us an explicit field for * this. */ static void gitlab_mrs_fixup(struct gcli_pull_list *const list) { for (size_t i = 0; i < list->pulls_size; ++i) { list->pulls[i].merged = !strcmp(list->pulls[i].state, "merged"); } } int gitlab_mr_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char **url, char const *const suffix_fmt, ...) { int rc = 0; char *suffix = NULL; va_list vp; va_start(vp, suffix_fmt); suffix = gcli_vasprintf(suffix_fmt, vp); va_end(vp); switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner, *e_repo; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *url = gcli_asprintf("%s/projects/%s%%2F%s/merge_requests/%"PRIid"%s", gcli_get_apibase(ctx), e_owner, e_repo, path->as_default.id, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_PID_ID: { *url = gcli_asprintf("%s/projects/%"PRIid"/merge_requests/%"PRIid"%s", gcli_get_apibase(ctx), path->as_pid_id.project_id, path->as_pid_id.id, suffix); } break; case GCLI_PATH_URL: { *url = gcli_asprintf("%s%s", path->as_url, suffix); } break; default: { rc = gcli_error(ctx, "unsupported path type for gitlab merge request"); } break; } gcli_clear_ptr(&suffix); return rc; } int gitlab_fetch_mrs(struct gcli_ctx *ctx, char *url, int const max, struct gcli_pull_list *const list) { int rc = 0; struct gcli_fetch_list_ctx fl = { .listp = &list->pulls, .sizep = &list->pulls_size, .max = max, .parse = (parsefn)(parse_gitlab_mrs), }; rc = gcli_fetch_list(ctx, url, &fl); /* TODO: don't leak the list on error */ if (rc == 0) gitlab_mrs_fixup(list); return rc; } static int gitlab_mrs_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const suffix, char **url) { int rc = 0; rc = gitlab_repo_make_url(ctx, path, url, "/merge_requests%s", suffix ? suffix : ""); return rc; } int gitlab_get_mrs(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_pull_fetch_details const *const details, int const max, struct gcli_pull_list *const list) { int rc = 0; char *url = NULL, *suffix = NULL; gcli_url_options_append(&suffix, "state", details->all ? NULL : "opened"); gcli_url_options_append(&suffix, "author_username", details->author); gcli_url_options_append(&suffix, "labels", details->label); gcli_url_options_append(&suffix, "milestone", details->milestone); gcli_url_options_append(&suffix, "search", details->search_term); rc = gitlab_mrs_make_url(ctx, path, suffix, &url); gcli_clear_ptr(&suffix); if (rc < 0) return rc; return gitlab_fetch_mrs(ctx, url, max, list); } static void gitlab_free_diff(struct gitlab_diff *diff) { gcli_clear_ptr(&diff->diff); gcli_clear_ptr(&diff->old_path); gcli_clear_ptr(&diff->new_path); gcli_clear_ptr(&diff->a_mode); gcli_clear_ptr(&diff->b_mode); memset(diff, 0, sizeof(*diff)); } static void gitlab_free_diffs(struct gitlab_diff_list *list) { for (size_t i = 0; i < list->diffs_size; ++i) { gitlab_free_diff(&list->diffs[i]); } gcli_clear_ptr(&list->diffs); list->diffs_size = 0; } static void gitlab_make_commit_diff(struct gcli_commit const *const commit, struct gitlab_diff const *const diff, char const *const prev_commit_sha, FILE *const out) { fprintf(out, "diff --git a/%s b/%s\n", diff->old_path, diff->new_path); if (diff->new_file) { fprintf(out, "new file mode %s\n", diff->b_mode); fprintf(out, "index 0000000..%s\n", commit->sha); } else { fprintf(out, "index %s..%s %s\n", prev_commit_sha, commit->sha, diff->b_mode); } fprintf(out, "--- %s%s\n", diff->new_file ? "" : "a/", diff->new_file ? "/dev/null" : diff->old_path); fprintf(out, "+++ %s%s\n", diff->deleted_file ? "" : "b/", diff->deleted_file ? "/dev/null" : diff->new_path); fputs(diff->diff, out); } static int gitlab_make_commit_patch(struct gcli_ctx *ctx, FILE *stream, struct gcli_path const *const repo_path, char const *const prev_commit_sha, struct gcli_commit const *const commit) { char *url; int rc; struct gitlab_diff_list list = {0}; struct gcli_fetch_list_ctx fl = { .listp = &list.diffs, .sizep = &list.diffs_size, .max = -1, .parse = (parsefn)(parse_gitlab_diffs), }; /* /projects/:id/repository/commits/:sha/diff */ rc = gitlab_repo_make_url(ctx, repo_path, &url, "/repository/commits/%s/diff", commit->sha); if (rc < 0) return rc; rc = gcli_fetch_list(ctx, url, &fl); if (rc < 0) { gcli_clear_ptr(&url); return rc; } fprintf(stream, "From %s Mon Sep 17 00:00:00 2001\n", commit->long_sha); fprintf(stream, "From: %s <%s>\n", commit->author, commit->email); fprintf(stream, "Date: %s\n", commit->date); fprintf(stream, "Subject: %s\n\n", commit->message); for (size_t i = 0; i < list.diffs_size; ++i) { gitlab_make_commit_diff(commit, &list.diffs[i], prev_commit_sha, stream); } fprintf(stream, "--\n2.42.2\n\n\n"); gitlab_free_diffs(&list); return rc; } int gitlab_mr_get_patch(struct gcli_ctx *ctx, FILE *stream, struct gcli_path const *const path) { int rc = 0; struct gcli_pull pull = {0}; struct gcli_commit_list commits = {0}; char const *prev_commit_sha; char *base_sha_short; rc = gitlab_get_pull(ctx, path, &pull); if (rc < 0) goto err_get_pull; rc = gitlab_get_pull_commits(ctx, path, &commits); if (rc < 0) goto err_get_commit_list; base_sha_short = gcli_strndup(pull.base_sha, 8); prev_commit_sha = base_sha_short; for (size_t i = commits.commits_size; i > 0; --i) { rc = gitlab_make_commit_patch(ctx, stream, path, prev_commit_sha, &commits.commits[i - 1]); if (rc < 0) goto err_make_commit_patch; prev_commit_sha = commits.commits[i - 1].sha; } err_make_commit_patch: gcli_clear_ptr(&base_sha_short); gcli_commits_free(&commits); err_get_commit_list: gcli_pull_free(&pull); err_get_pull: return rc; } static int gitlab_mr_get_diff_versions(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gitlab_mr_version_list *const out) { char *url = NULL; int rc = 0; struct gcli_fetch_list_ctx fl_ctx = { .listp = &out->versions, .sizep = &out->versions_size, .max = -1, .parse = (parsefn)parse_gitlab_mr_version_list, }; rc = gitlab_mr_make_url(ctx, path, &url, "/versions"); if (rc < 0) return rc; return gcli_fetch_list(ctx, url, &fl_ctx); } static int gitlab_mr_get_diff_version(struct gcli_ctx *ctx, struct gcli_path const *const path, gcli_id const version_id, struct gitlab_diff_list *const out) { char *url = NULL; int rc = 0; struct gcli_fetch_buffer buffer = {0}; rc = gitlab_mr_make_url(ctx, path, &url, "/versions/%"PRIid, version_id); if (rc < 0) return rc; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc == 0) { struct json_stream stream = {0}; json_open_buffer(&stream, buffer.data, buffer.length); rc = parse_gitlab_mr_version_diffs(ctx, &stream, out); json_close(&stream); } gcli_clear_ptr(&url); gcli_clear_ptr(&buffer.data); return rc; } int gitlab_mr_get_diff(struct gcli_ctx *ctx, FILE *stream, struct gcli_path const *const path) { int rc; struct gitlab_diff_list diff_list = {0}; struct gitlab_mr_version const *version; struct gitlab_mr_version_list version_list = {0}; /* Grab a list of diff versions available for this MR. * The list is sorted numerically decending. Thus we need * to just grab the very first version in the array and use * it. */ rc = gitlab_mr_get_diff_versions(ctx, path, &version_list); if (rc < 0) goto err_get_mr_diff_version; if (!version_list.versions_size) { rc = gcli_error(ctx, "no diffs available for the merge request"); goto err_get_mr_diff_version; } version = &version_list.versions[0]; rc = gitlab_mr_get_diff_version(ctx, path, version->id, &diff_list); if (rc < 0) goto err_get_diffs; fprintf(stream, "GCLI: Below is metadata for this diff. Do not remove or alter\n"); fprintf(stream, "GCLI: in case you're using this for a review.\n"); fprintf(stream, "GCLI: base_sha %s\n", version->base_commit); fprintf(stream, "GCLI: start_sha %s\n", version->start_commit); fprintf(stream, "GCLI: head_sha %s\n", version->head_commit); for (size_t i = 0; i < diff_list.diffs_size; ++i) { struct gitlab_diff const *const diff = &diff_list.diffs[i]; fprintf(stream, "diff --git a/%s b/%s\n", diff->old_path, diff->new_path); if (diff->new_file) { fprintf(stream, "new file mode %s\n", diff->b_mode); fprintf(stream, "index 0000000..%s\n", version->head_commit); } else { fprintf(stream, "index %s..%s %s\n", version->base_commit, version->head_commit, diff->b_mode); } fprintf(stream, "--- %s%s\n", diff->new_file ? "" : "a/", diff->new_file ? "/dev/null" : diff->old_path); fprintf(stream, "+++ %s%s\n", diff->deleted_file ? "" : "b/", diff->deleted_file ? "/dev/null" : diff->new_path); fputs(diff->diff, stream); } gitlab_free_diffs(&diff_list); err_get_diffs: gitlab_mr_version_list_free(&version_list); err_get_mr_diff_version: return rc; } int gitlab_mr_set_automerge(struct gcli_ctx *const ctx, struct gcli_path const *const path) { char *url; int rc; rc = gitlab_mr_make_url( ctx, path, &url, "/merge?merge_when_pipeline_succeeds=true"); if (rc < 0) return rc; rc = gcli_fetch_with_method(ctx, "PUT", url, NULL, NULL, NULL); gcli_clear_ptr(&url); return rc; } int gitlab_mr_merge(struct gcli_ctx *ctx, struct gcli_path const *const path, enum gcli_merge_flags const flags) { bool const delete_source = flags & GCLI_PULL_MERGE_DELETEHEAD; bool const squash = flags & GCLI_PULL_MERGE_SQUASH; char *url = NULL; char const *data = "{}"; int rc = 0; struct gcli_fetch_buffer buffer = {0}; /* PUT /projects/:id/merge_requests/:merge_request_iid/merge */ rc = gitlab_mr_make_url(ctx, path, &url, "/merge?squash=%s" "&should_remove_source_branch=%s", squash ? "true" : "false", delete_source ? "true" : "false"); if (rc < 0) return rc; rc = gcli_fetch_with_method(ctx, "PUT", url, data, NULL, &buffer); gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&url); return rc; } int gitlab_get_pull(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_pull *const out) { struct gcli_fetch_buffer buffer = {0}; char *url = NULL; int rc = 0; /* GET /projects/:id/merge_requests/:merge_request_iid */ rc = gitlab_mr_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc == 0) { struct json_stream stream = {0}; json_open_buffer(&stream, buffer.data, buffer.length); parse_gitlab_mr(ctx, &stream, out); json_close(&stream); } gcli_clear_ptr(&url); gcli_fetch_buffer_free(&buffer); return rc; } int gitlab_get_pull_commits(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_commit_list *const out) { char *url = NULL; int rc = 0; struct gcli_fetch_list_ctx fl = { .listp = &out->commits, .sizep = &out->commits_size, .max = -1, .parse = (parsefn)(parse_gitlab_commits), }; /* GET /projects/:id/merge_requests/:merge_request_iid/commits */ rc = gitlab_mr_make_url(ctx, path, &url, "/commits"); if (rc < 0) return rc; return gcli_fetch_list(ctx, url, &fl); } static int gitlab_mr_patch_state(struct gcli_ctx *const ctx, struct gcli_path const *const path, char const *const new_state) { char *url = NULL, *payload = NULL; struct gcli_jsongen gen = {0}; int rc = 0; /* Generate URL */ rc = gitlab_mr_make_url(ctx, path, &url, ""); if (rc < 0) return rc; /* Generate payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "state_event"); gcli_jsongen_string(&gen, new_state); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "PUT", url, payload, NULL, NULL); gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } int gitlab_mr_close(struct gcli_ctx *ctx, struct gcli_path const *const path) { return gitlab_mr_patch_state(ctx, path, "close"); } int gitlab_mr_reopen(struct gcli_ctx *ctx, struct gcli_path const *const path) { return gitlab_mr_patch_state(ctx, path, "reopen"); } /* This routine is a workaround for a Gitlab bug: * * https://gitlab.com/gitlab-org/gitlab/-/issues/353984 * * This is a race condition because something in the creation of a merge request * is being handled asynchronously. See the above link for more details. * * TL;DR: We need to wait until the »merge_status« field of the MR is set to * »can_be_merged«. This is indicated by the mergable field becoming true. */ static int gitlab_mr_wait_until_mergeable(struct gcli_ctx *ctx, struct gcli_path const *const path) { char *url; int rc = 0; struct timespec const ts = { .tv_sec = 1, .tv_nsec = 0 }; rc = gitlab_mr_make_url(ctx, path, &url, ""); if (rc < 0) return rc; for (;;) { bool is_mergeable; struct gcli_fetch_buffer buffer = {0}; struct json_stream stream = {0}; struct gcli_pull pull = {0}; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc < 0) break; json_open_buffer(&stream, buffer.data, buffer.length); rc = parse_gitlab_mr(ctx, &stream, &pull); json_close(&stream); /* FIXME: this doesn't quite cut it when the PR has no commits in it. * In that case this will turn into an infinite loop. */ is_mergeable = pull.mergeable; gcli_pull_free(&pull); gcli_fetch_buffer_free(&buffer); if (is_mergeable) break; /* sort of a hack: wait for a second until the next request goes out */ nanosleep(&ts, NULL); } gcli_clear_ptr(&url); return rc; } int 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 * better for both github and gitlab. */ char *source_branch = NULL, *source_owner = NULL, *payload = NULL, *url = NULL; char const *target_branch = NULL; int rc = 0; struct gcli_fetch_buffer buffer = {0}; struct gcli_jsongen gen = {0}; struct gcli_repo target = {0}; /* generate url */ rc = gitlab_repo_make_url(ctx, &opts->target_repo, &url, "/merge_requests"); if (rc < 0) return rc; target_branch = opts->target_branch; 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'"); *source_branch++ = '\0'; /* Figure out the project id */ rc = gitlab_get_repo(ctx, &opts->target_repo, &target); if (rc < 0) return rc; /* generate payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "source_branch"); gcli_jsongen_string(&gen, source_branch); gcli_jsongen_objmember(&gen, "target_branch"); gcli_jsongen_string(&gen, target_branch); gcli_jsongen_objmember(&gen, "title"); gcli_jsongen_string(&gen, opts->title); /* description is optional and will be NULL if unset */ if (opts->body) { gcli_jsongen_objmember(&gen, "description"); gcli_jsongen_string(&gen, opts->body); } 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"); gcli_jsongen_begin_array(&gen); for (size_t i = 0; i < opts->labels_size; ++i) 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); gcli_jsongen_free(&gen); gcli_repo_free(&target); /* perform request */ rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, &buffer); /* if that succeeded and the user wants automerge, parse the result and * set the automerge flag. */ if (rc == 0 && opts->automerge && opts->target_repo.kind == GCLI_PATH_DEFAULT) { struct json_stream stream = {0}; struct gcli_pull pull = {0}; struct gcli_path target_mr_path = opts->target_repo; json_open_buffer(&stream, buffer.data, buffer.length); rc = parse_gitlab_mr(ctx, &stream, &pull); json_close(&stream); target_mr_path.as_default.id = pull.number; if (rc < 0) goto out; rc = gitlab_mr_wait_until_mergeable(ctx, &target_mr_path); if (rc < 0) goto out; rc = gitlab_mr_set_automerge(ctx, &target_mr_path); out: gcli_pull_free(&pull); } /* cleanup */ gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&source_owner); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } static int gitlab_mr_update_labels(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const labels[], size_t const labels_size, char const *const update_action) { char *url = NULL, *payload = NULL, *list = NULL; struct gcli_jsongen gen = {0}; int rc = 0; /* Generate URL */ rc = gitlab_mr_make_url(ctx, path, &url, ""); if (rc < 0) return rc; /* Generate payload */ list = gcli_join_with(labels, labels_size, ","); gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, update_action); gcli_jsongen_string(&gen, list); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); gcli_clear_ptr(&list); rc = gcli_fetch_with_method(ctx, "PUT", url, payload, NULL, NULL); gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } int gitlab_mr_add_labels(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const labels[], size_t const labels_size) { return gitlab_mr_update_labels(ctx, path, labels, labels_size, "add_labels"); } int gitlab_mr_remove_labels(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const labels[], size_t const labels_size) { return gitlab_mr_update_labels(ctx, path, labels, labels_size, "remove_labels"); } int gitlab_mr_set_milestone(struct gcli_ctx *ctx, struct gcli_path const *const mr_path, gcli_id milestone_id) { char *url = NULL, *payload = NULL; struct gcli_jsongen gen = {0}; int rc = 0; /* Generate URL */ rc = gitlab_mr_make_url(ctx, mr_path, &url, ""); if (rc < 0) return rc; /* Generate Payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "milestone_id"); gcli_jsongen_id(&gen, milestone_id); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); rc = gcli_fetch_with_method(ctx, "PUT", url, payload, NULL, NULL); gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } int gitlab_mr_clear_milestone(struct gcli_ctx *ctx, struct gcli_path const *const mr_path) { /* GitLab's REST API docs state: * * The global ID of a milestone to assign the merge request * to. Set to 0 or provide an empty value to unassign a * milestone. */ return gitlab_mr_set_milestone(ctx, mr_path, 0); } /* helper typedef to shorten down the type of this function pointer */ typedef int (*user_id_list_parser)( struct gcli_ctx *, struct json_stream *, struct gitlab_user_id_list *out); /* generic helper function for extracting user id lists from a merge request */ static int gitlab_mr_get_user_id_list( struct gcli_ctx *ctx, struct gcli_path const *const path, user_id_list_parser parser, struct gitlab_user_id_list *const out) { char *url = NULL; int rc = 0; struct gcli_fetch_buffer buffer = {0}; rc = gitlab_mr_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc == 0) { struct json_stream stream = {0}; json_open_buffer(&stream, buffer.data, buffer.length); /* use generic parser to extract data */ rc = parser(ctx, &stream, out); json_close(&stream); } gcli_clear_ptr(&url); gcli_fetch_buffer_free(&buffer); return rc; } static void gitlab_user_id_list_free(struct gitlab_user_id_list *const list) { gcli_clear_ptr(&list->users); list->users_size = 0; } /* helper function for adding a user to a list of users (e.g. * assignees or reviewers */ static int gitlab_mr_add_user_id( struct gcli_ctx *ctx, struct gcli_path const *const path, /* path to the MR */ user_id_list_parser const parser, /* parser for extracting user ids */ char const *const field_name, /* field in the POST payload */ char const *username) { char *url, *payload; int uid, rc = 0; struct gitlab_user_id_list list = {0}; struct gcli_jsongen gen = {0}; /* Fetch list of already existing users */ rc = gitlab_mr_get_user_id_list(ctx, path, parser, &list); if (rc < 0) goto bail_get_reviewers; /* Resolve user id from user name */ uid = gitlab_user_id(ctx, username); if (uid < 0) goto bail_resolve_user_id; /* Start generating payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, field_name); gcli_jsongen_begin_array(&gen); { for (size_t i = 0; i < list.users_size; ++i) gcli_jsongen_number(&gen, list.users[i]); /* Push new user id into list of user ids */ gcli_jsongen_number(&gen, uid); } gcli_jsongen_end_array(&gen); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); /* generate URL */ rc = gitlab_mr_make_url(ctx, path, &url, ""); if (rc == 0) { rc = gcli_fetch_with_method(ctx, "PUT", url, payload, NULL, NULL); } gcli_clear_ptr(&url); gcli_clear_ptr(&payload); bail_resolve_user_id: gitlab_user_id_list_free(&list); bail_get_reviewers: return rc; } int gitlab_mr_add_reviewer(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const username) { return gitlab_mr_add_user_id(ctx, path, parse_gitlab_reviewer_ids, "reviewer_ids", username); } int gitlab_mr_add_assignee(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const username) { return gitlab_mr_add_user_id(ctx, path, parse_gitlab_assignee_ids, "assignee_ids", username); } int gitlab_mr_set_title(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const new_title) { char *url, *payload; struct gcli_jsongen gen = {0}; int rc = 0; /* Generate url * * PUT /projects/:id/merge_requests/:merge_request_iid */ rc = gitlab_mr_make_url(ctx, path, &url, ""); if (rc < 0) return rc; /* Generate payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "title"); gcli_jsongen_string(&gen, new_title); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); /* perform request */ rc = gcli_fetch_with_method(ctx, "PUT", url, payload, NULL, NULL); /* clean up */ gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } /* Compute the SHA1 message digest of the given input string. Returns * NULL on failure or the hexadecimal representation of the digest on * success */ static char * digest_sha1(char const *const string) { int success = 0; unsigned char hash_data[20] = {0}; unsigned int hash_data_len = sizeof(hash_data); size_t const result_size = 41; char *result = calloc(result_size, 1); success = EVP_Digest(string, strlen(string), hash_data, &hash_data_len, EVP_sha1(), NULL); if (!success) return NULL; for (size_t i = 0; i < sizeof(hash_data); ++i) { snprintf(result + 2*i, result_size - 2*i, "%02x", hash_data[i]); } return result; } static int line_code(struct gcli_ctx *ctx, struct gcli_jsongen *const gen, char const *filename, int const old, int const new) { char tmp[128] = {0}; char *sha_digest; sha_digest = digest_sha1(filename); if (!sha_digest) return gcli_error(ctx, "failed to produce SHA1 digest of filename"); snprintf(tmp, sizeof(tmp), "%s_%d_%d", sha_digest, old, new); gcli_jsongen_string(gen, tmp); gcli_clear_ptr(&sha_digest); return 0; } static int post_diff_comment(struct gcli_ctx *ctx, struct gcli_pull_create_review_details const *details, struct gcli_diff_comment const *comment) { char *url, *payload; char const *base_sha, *start_sha, *head_sha; int rc = 0; struct gcli_jsongen gen = {0}; if ((base_sha = gcli_pull_get_meta_by_key(details, "base_sha")) == NULL) return gcli_error(ctx, "no base_sha in meta"); if ((start_sha = gcli_pull_get_meta_by_key(details, "start_sha")) == NULL) return gcli_error(ctx, "no start_sha in meta"); if ((head_sha = gcli_pull_get_meta_by_key(details, "head_sha")) == NULL) return gcli_error(ctx, "no head_sha in meta"); /* /projects/:id/merge_requests/:merge_request_iid/discussions */ rc = gitlab_mr_make_url(ctx, &details->path, &url, "/discussions"); if (rc < 0) return rc; /* Generate payload */ if (gcli_jsongen_init(&gen) < 0) goto err_jsongen_init; gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "body"); gcli_jsongen_string(&gen, comment->comment); gcli_jsongen_objmember(&gen, "commit_id"); gcli_jsongen_string(&gen, comment->commit_hash); gcli_jsongen_objmember(&gen, "position"); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "position_type"); gcli_jsongen_string(&gen, "text"); gcli_jsongen_objmember(&gen, "base_sha"); gcli_jsongen_string(&gen, base_sha); gcli_jsongen_objmember(&gen, "start_sha"); gcli_jsongen_string(&gen, start_sha); gcli_jsongen_objmember(&gen, "head_sha"); gcli_jsongen_string(&gen, head_sha); gcli_jsongen_objmember(&gen, "new_path"); gcli_jsongen_string(&gen, comment->after.filename); gcli_jsongen_objmember(&gen, "old_path"); gcli_jsongen_string(&gen, comment->before.filename); gcli_jsongen_objmember(&gen, "new_line"); gcli_jsongen_number(&gen, comment->after.start_row); gcli_jsongen_objmember(&gen, "line_range"); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "start"); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "type"); gcli_jsongen_string(&gen, comment->start_is_in_new ? "new" : "old"); gcli_jsongen_objmember(&gen, "line_code"); line_code(ctx, &gen, comment->after.filename, comment->before.start_row, comment->after.start_row); } gcli_jsongen_end_object(&gen); gcli_jsongen_objmember(&gen, "end"); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "type"); gcli_jsongen_string(&gen, comment->end_is_in_new ? "new" : "old"); gcli_jsongen_objmember(&gen, "line_code"); line_code(ctx, &gen, comment->after.filename, comment->before.end_row, comment->after.end_row); } gcli_jsongen_end_object(&gen); } gcli_jsongen_end_object(&gen); } gcli_jsongen_end_object(&gen); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, NULL); gcli_clear_ptr(&payload); err_jsongen_init: gcli_clear_ptr(&url); return rc; } int gitlab_mr_create_review(struct gcli_ctx *ctx, struct gcli_pull_create_review_details const *const details) { int rc; struct gcli_diff_comment const *comment; TAILQ_FOREACH(comment, &details->comments, next) { rc = post_diff_comment(ctx, details, comment); if (rc < 0) return rc; } /* Abort on failure */ if (rc < 0) return rc; /* Check whether we wish to submit a general comment */ if (details->body && *details->body) { struct gcli_submit_comment_opts opts = { .target = details->path, .target_type = PR_COMMENT, .message = details->body, }; rc = gitlab_perform_submit_comment(ctx, &opts); if (rc < 0) return rc; } /* Check whether to approve or unapprove the MR */ switch (details->review_state) { case GCLI_REVIEW_ACCEPT_CHANGES: rc = gitlab_mr_approve(ctx, &details->path); break; case GCLI_REVIEW_REQUEST_CHANGES: rc = gitlab_mr_unapprove(ctx, &details->path); break; default: /* commenting only implies no change to the merge request */ break; } return rc; } void gitlab_mr_version_free(struct gitlab_mr_version *version) { gcli_clear_ptr(&version->base_commit); gcli_clear_ptr(&version->start_commit); gcli_clear_ptr(&version->head_commit); } void gitlab_mr_version_list_free(struct gitlab_mr_version_list *list) { for (size_t i = 0; i < list->versions_size; ++i) { gitlab_mr_version_free(&list->versions[i]); } gcli_clear_ptr(&list->versions); list->versions_size = 0; } static int gitlab_mr_request_update_approval(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const action) { int rc = 0; char *url = NULL; rc = gitlab_mr_make_url(ctx, path, &url, "/%s", action); if (rc < 0) return rc; rc = gcli_fetch_with_method(ctx, "POST", url, "{}", NULL, NULL); gcli_clear_ptr(&url); return rc; } int gitlab_mr_approve(struct gcli_ctx *ctx, struct gcli_path const *const path) { return gitlab_mr_request_update_approval(ctx, path, "approve"); } int gitlab_mr_unapprove(struct gcli_ctx *ctx, struct gcli_path const *const path) { return gitlab_mr_request_update_approval(ctx, path, "unapprove"); } gcli-2.9.1/src/gitlab/milestones.c000066400000000000000000000136661507017207500170400ustar00rootroot00000000000000/* * Copyright 2023-2025 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 #include #include #include #include #include #include #include #include int gitlab_get_milestones(struct gcli_ctx *ctx, struct gcli_path const *const path, int max, struct gcli_milestone_list *const out) { char *url = NULL; int rc = 0; struct gcli_fetch_list_ctx fl = { .listp = &out->milestones, .sizep = &out->milestones_size, .max = max, .parse = (parsefn)(parse_gitlab_milestones), }; rc = gitlab_repo_make_url( ctx, path, &url, "/milestones?include_ancestors=true"); if (rc < 0) return rc; return gcli_fetch_list(ctx, url, &fl); } int gitlab_milestone_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char **url, char const *const suffix_fmt, ...) { char *suffix = NULL; int rc = 0; va_list vp; va_start(vp, suffix_fmt); suffix = gcli_vasprintf(suffix_fmt, vp); va_end(vp); switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner = NULL, *e_repo = NULL; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *url = gcli_asprintf("%s/projects/%s%%2F%s/milestones/%"PRIid"%s", gcli_get_apibase(ctx), e_owner, e_repo, path->as_default.id, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_URL: { *url = gcli_asprintf("%s%s", path->as_url, suffix); } break; default: { rc = gcli_error(ctx, "bad path kind for gitlab milestone"); } break; } gcli_clear_ptr(&suffix); return rc; } int gitlab_get_milestone(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_milestone *const out) { char *url; struct gcli_fetch_buffer buffer = {0}; struct json_stream stream = {0}; int rc = 0; rc = gitlab_milestone_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc == 0) { json_open_buffer(&stream, buffer.data, buffer.length); parse_gitlab_milestone(ctx, &stream, out); json_close(&stream); } gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&url); return rc; } int gitlab_milestone_get_issues(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue_list *const out) { char *url = NULL; int rc = 0; rc = gitlab_milestone_make_url(ctx, path, &url, "/issues"); if (rc < 0) return rc; return gitlab_fetch_issues(ctx, url, -1, out);; } int gitlab_create_milestone(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_milestone_create_args const *args) { char *url = NULL, *payload = NULL; int rc = 0; struct gcli_jsongen gen = {0}; /* build url */ rc = gitlab_repo_make_url(ctx, path, &url, "/milestones"); if (rc < 0) return rc; /* build payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "title"); gcli_jsongen_string(&gen, args->title); if (args->description) { gcli_jsongen_objmember(&gen, "description"); gcli_jsongen_string(&gen, args->description); } } 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); gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } int gitlab_delete_milestone(struct gcli_ctx *ctx, struct gcli_path const *const path) { char *url = NULL; int rc = 0; rc = gitlab_milestone_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = gcli_fetch_with_method(ctx, "DELETE", url, NULL, NULL, NULL); gcli_clear_ptr(&url); return rc; } int gitlab_milestone_set_duedate(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const date) { char *url = NULL, norm_date[9] = {0}; int rc = 0; rc = gcli_normalize_date(ctx, DATEFMT_GITLAB, date, norm_date, sizeof norm_date); if (rc < 0) return rc; rc = gitlab_milestone_make_url(ctx, path, &url, "?due_date=%s", norm_date); if (rc < 0) return rc; rc = gcli_fetch_with_method(ctx, "PUT", url, "", NULL, NULL); gcli_clear_ptr(&url); return rc; } gcli-2.9.1/src/gitlab/pipelines.c000066400000000000000000000225361507017207500166420ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include #include #include #include #include #include static int fetch_pipelines(struct gcli_ctx *ctx, char *url, int const max, struct gitlab_pipeline_list *const list) { struct gcli_fetch_list_ctx fl = { .listp = &list->pipelines, .sizep = &list->pipelines_size, .max = max, .parse = (parsefn)(parse_gitlab_pipelines), }; return gcli_fetch_list(ctx, url, &fl); } int gitlab_get_pipelines(struct gcli_ctx *ctx, struct gcli_path const *const path, int const max, struct gitlab_pipeline_list *const list) { char *url = NULL; int rc = 0; rc = gitlab_repo_make_url(ctx, path, &url, "/pipelines"); if (rc < 0) return rc; return fetch_pipelines(ctx, url, max, list); } static int gitlab_pipeline_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char **url, char const *const suffix_fmt, ...) { char *suffix = NULL; int rc = 0; va_list vp; va_start(vp, suffix_fmt); suffix = gcli_vasprintf(suffix_fmt, vp); va_end(vp); switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner, *e_repo = NULL; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *url = gcli_asprintf("%s/projects/%s%%2F%s/pipelines/%"PRIid"%s", gcli_get_apibase(ctx), e_owner, e_repo, path->as_default.id, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_URL: { *url = gcli_asprintf("%s%s", path->as_url, suffix); } break; default: { rc = gcli_error(ctx, "unsupported path type for gitlab pipelines"); } break; } gcli_clear_ptr(&suffix); return rc; } int gitlab_get_pipeline(struct gcli_ctx *ctx, struct gcli_path const *const pipeline_path, struct gitlab_pipeline *out) { char *url = NULL; int rc = 0; struct gcli_fetch_buffer buffer = {0}; struct json_stream stream = {0}; rc = gitlab_pipeline_make_url(ctx, pipeline_path, &url, ""); if (rc < 0) return rc; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc == 0) { json_open_buffer(&stream, buffer.data, buffer.length); rc = parse_gitlab_pipeline(ctx, &stream, out); json_close(&stream); gcli_fetch_buffer_free(&buffer); } gcli_clear_ptr(&url); return rc; } int gitlab_get_mr_pipelines(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gitlab_pipeline_list *const list) { char *url = NULL; int rc = 0; rc = gitlab_mr_make_url(ctx, path, &url, "/pipelines"); if (rc < 0) return rc; /* fetch everything */ return fetch_pipelines(ctx, url, -1, list); } void gitlab_pipeline_free(struct gitlab_pipeline *pipeline) { gcli_clear_ptr(&pipeline->status); gcli_clear_ptr(&pipeline->ref); gcli_clear_ptr(&pipeline->sha); gcli_clear_ptr(&pipeline->source); gcli_clear_ptr(&pipeline->web_url); } void gitlab_pipelines_free(struct gitlab_pipeline_list *const list) { for (size_t i = 0; i < list->pipelines_size; ++i) { gitlab_pipeline_free(&list->pipelines[i]); } gcli_clear_ptr(&list->pipelines); list->pipelines_size = 0; } int gitlab_get_pipeline_jobs(struct gcli_ctx *ctx, struct gcli_path const *const pipeline_path, int const max, struct gitlab_job_list *const out) { char *url = NULL; int rc = 0; struct gcli_fetch_list_ctx fl = { .listp = &out->jobs, .sizep = &out->jobs_size, .max = max, .parse = (parsefn)(parse_gitlab_jobs), }; rc = gitlab_pipeline_make_url(ctx, pipeline_path, &url, "/jobs"); if (rc < 0) return rc; return gcli_fetch_list(ctx, url, &fl); } int gitlab_get_pipeline_children(struct gcli_ctx *ctx, struct gcli_path const *const pipeline_path, int count, struct gitlab_pipeline_list *out) { char *url = NULL; int rc = 0; struct gcli_fetch_list_ctx fl = { .listp = &out->pipelines, .sizep = &out->pipelines_size, .max = count, .parse = (parsefn)(parse_gitlab_pipeline_children), }; rc = gitlab_pipeline_make_url(ctx, pipeline_path, &url, "/bridges"); if (rc < 0) return rc; return gcli_fetch_list(ctx, url, &fl); } void gitlab_free_job(struct gitlab_job *const job) { gcli_clear_ptr(&job->status); gcli_clear_ptr(&job->stage); gcli_clear_ptr(&job->name); gcli_clear_ptr(&job->ref); gcli_clear_ptr(&job->runner_name); gcli_clear_ptr(&job->runner_description); gcli_clear_ptr(&job->web_url); } void gitlab_free_jobs(struct gitlab_job_list *list) { for (size_t i = 0; i < list->jobs_size; ++i) gitlab_free_job(&list->jobs[i]); gcli_clear_ptr(&list->jobs); list->jobs_size = 0; } static int gitlab_job_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char **url, char const *const suffix_fmt, ...) { char *suffix = NULL; int rc = 0; va_list vp; va_start(vp, suffix_fmt); suffix = gcli_vasprintf(suffix_fmt, vp); va_end(vp); switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner, *e_repo = NULL; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *url = gcli_asprintf("%s/projects/%s%%2F%s/jobs/%"PRIid"%s", gcli_get_apibase(ctx), e_owner, e_repo, path->as_default.id, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_URL: { *url = gcli_asprintf("%s%s", path->as_url, suffix); } break; default: { rc = gcli_error(ctx, "unsupported path type for gitlab jobs"); } break; } gcli_clear_ptr(&suffix); return rc; } int gitlab_job_get_log(struct gcli_ctx *ctx, struct gcli_path const *const job_path, FILE *stream) { char *url = NULL; int rc = 0; rc = gitlab_job_make_url(ctx, job_path, &url, "/trace"); if (rc < 0) return rc; rc = gcli_curl(ctx, stream, url, NULL); gcli_clear_ptr(&url); return rc; } int gitlab_get_job(struct gcli_ctx *ctx, struct gcli_path const *const job_path, struct gitlab_job *const out) { struct gcli_fetch_buffer buffer = {0}; char *url = NULL; int rc = 0; rc = gitlab_job_make_url(ctx, job_path, &url, ""); if (rc < 0) return rc; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc == 0) { struct json_stream stream = {0}; json_open_buffer(&stream, buffer.data, buffer.length); json_set_streaming(&stream, 1); parse_gitlab_job(ctx, &stream, out); json_close(&stream); } gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&url); return rc; } int gitlab_job_cancel(struct gcli_ctx *ctx, struct gcli_path const *const path) { char *url = NULL; int rc = 0; rc = gitlab_job_make_url(ctx, path, &url, "/cancel"); if (rc < 0) return rc; rc = gcli_fetch_with_method(ctx, "POST", url, NULL, NULL, NULL); gcli_clear_ptr(&url); return rc; } int gitlab_job_retry(struct gcli_ctx *ctx, struct gcli_path const *const path) { int rc = 0; char *url = NULL; rc = gitlab_job_make_url(ctx, path, &url, "/retry"); if (rc < 0) return rc; rc = gcli_fetch_with_method(ctx, "POST", url, NULL, NULL, NULL); gcli_clear_ptr(&url); return rc; } int gitlab_job_download_artifacts(struct gcli_ctx *ctx, struct gcli_path const *const job_path, char const *const outfile) { FILE *f = NULL; char *url = NULL; int rc = 0; f = fopen(outfile, "wb"); if (f == NULL) rc = gcli_error(ctx, "failed to open output file %s", outfile); if (rc == 0) rc = gitlab_job_make_url(ctx, job_path, &url, "/artifacts"); if (rc == 0) rc = gcli_curl(ctx, f, url, "application/zip"); if (f) fclose(f); gcli_clear_ptr(&url); return rc; } gcli-2.9.1/src/gitlab/releases.c000066400000000000000000000111651507017207500164510ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include #include #include static void fixup_asset_name(struct gcli_ctx *ctx, struct gcli_release_asset *const asset) { if (!asset->name) asset->name = gcli_urldecode(ctx, strrchr(asset->url, '/') + 1); } void gitlab_fixup_release_assets(struct gcli_ctx *ctx, struct gcli_release *const release) { for (size_t i = 0; i < release->assets_size; ++i) fixup_asset_name(ctx, &release->assets[i]); } static void fixup_release_asset_names(struct gcli_ctx *ctx, struct gcli_release_list *list) { /* Iterate over releases */ for (size_t i = 0; i < list->releases_size; ++i) gitlab_fixup_release_assets(ctx, &list->releases[i]); } int gitlab_get_releases(struct gcli_ctx *ctx, struct gcli_path const *const repo_path, int const max, struct gcli_release_list *const list) { char *url = NULL; int rc = 0; struct gcli_fetch_list_ctx fl = { .listp = &list->releases, .sizep = &list->releases_size, .max = max, .parse = (parsefn)(parse_gitlab_releases), }; *list = (struct gcli_release_list) {0}; rc = gitlab_repo_make_url(ctx, repo_path, &url, "/releases"); if (rc < 0) return rc; rc = gcli_fetch_list(ctx, url, &fl); if (rc == 0) fixup_release_asset_names(ctx, list); return rc; } int gitlab_create_release(struct gcli_ctx *ctx, struct gcli_create_release_args const *release) { char *url = NULL, *payload = NULL; struct gcli_jsongen gen = {0}; int rc = 0; /* Warnings because unsupported on gitlab */ if (release->prerelease) gcli_warnx(ctx, "prereleases are not supported on GitLab, option ignored"); if (release->draft) gcli_warnx(ctx, "draft releases are not supported on GitLab, option ignored"); if (release->assets_size) gcli_warnx(ctx, "GitLab release asset uploads are not yet supported"); /* Payload generation */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "tag_name"); gcli_jsongen_string(&gen, release->tag); if (release->body) { gcli_jsongen_objmember(&gen, "description"); gcli_jsongen_string(&gen, release->body); } if (release->commitish) { gcli_jsongen_objmember(&gen, "ref"); gcli_jsongen_string(&gen, release->commitish); } if (release->name) { gcli_jsongen_objmember(&gen, "name"); gcli_jsongen_string(&gen, release->name); } } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); /* Generate URL */ rc = gitlab_repo_make_url(ctx, &release->repo_path, &url, "/releases"); /* perform request */ if (rc == 0) { rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, NULL); } gcli_clear_ptr(&url); gcli_clear_ptr(&payload); return rc; } int gitlab_delete_release(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const id) { char *url = NULL; int rc = 0; rc = gitlab_repo_make_url(ctx, path, &url, "/releases/%s", id); if (rc < 0) return rc; rc = gcli_fetch_with_method(ctx, "DELETE", url, NULL, NULL, NULL); gcli_clear_ptr(&url); return rc; } gcli-2.9.1/src/gitlab/repos.c000066400000000000000000000151471507017207500160020ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include int gitlab_get_repo(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_repo *const out) { /* GET /projects/:id */ char *url = NULL; struct gcli_fetch_buffer buffer = {0}; struct json_stream stream = {0}; int rc; rc = gitlab_repo_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = gcli_fetch(ctx, url, NULL, &buffer); if (rc == 0) { json_open_buffer(&stream, buffer.data, buffer.length); parse_gitlab_repo(ctx, &stream, out); json_close(&stream); } gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&url); return rc; } static void gitlab_repos_fixup_missing_visibility(struct gcli_repo_list *const list) { static char const *const public = "public"; /* Gitlab does not return a visibility field in the repo object on * unauthenticated API requests. We fix up the missing field here * assuming that the repository must be public. */ for (size_t i = 0; i < list->repos_size; ++i) { if (!list->repos[i].visibility) list->repos[i].visibility = strdup(public); } } int gitlab_get_repos(struct gcli_ctx *ctx, char const *owner, int const max, struct gcli_repo_list *const list) { char *url = NULL; char *e_owner = NULL; int rc = 0; struct gcli_fetch_list_ctx fl = { .listp = &list->repos, .sizep = &list->repos_size, .parse = (parsefn)(parse_gitlab_repos), .max = max, }; e_owner = gcli_urlencode(owner); url = gcli_asprintf("%s/users/%s/projects", gcli_get_apibase(ctx), e_owner); gcli_clear_ptr(&e_owner); rc = gcli_fetch_list(ctx, url, &fl); if (rc == 0) gitlab_repos_fixup_missing_visibility(list); return rc; } int gitlab_repo_delete(struct gcli_ctx *ctx, struct gcli_path const *const path) { char *url = NULL; int rc = 0; rc = gitlab_repo_make_url(ctx, path, &url, ""); if (rc < 0) return rc; rc = gcli_fetch_with_method(ctx, "DELETE", url, NULL, NULL, NULL); gcli_clear_ptr(&url); return rc; } int gitlab_repo_create(struct gcli_ctx *ctx, struct gcli_repo_create_options const *options, struct gcli_repo *out) { char *url, *payload; struct gcli_fetch_buffer buffer = {0}; struct gcli_jsongen gen = {0}; int rc; struct json_stream stream = {0}; /* Request preparation */ url = gcli_asprintf("%s/projects", gcli_get_apibase(ctx)); gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "name"); gcli_jsongen_string(&gen, options->name); gcli_jsongen_objmember(&gen, "description"); gcli_jsongen_string(&gen, options->description); gcli_jsongen_objmember(&gen, "visibility"); gcli_jsongen_string(&gen, options->private ? "private" : "public"); } gcli_jsongen_end_object(&gen); payload = gcli_jsongen_to_string(&gen); gcli_jsongen_free(&gen); /* Fetch and parse result */ rc = gcli_fetch_with_method(ctx, "POST", url, payload, NULL, out ? &buffer : NULL); if (rc == 0 && out) { json_open_buffer(&stream, buffer.data, buffer.length); parse_gitlab_repo(ctx, &stream, out); json_close(&stream); } gcli_fetch_buffer_free(&buffer); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } int gitlab_repo_set_visibility(struct gcli_ctx *ctx, struct gcli_path const *const path, gcli_repo_visibility vis) { char *url; char const *vis_str; char *payload; int rc; switch (vis) { case GCLI_REPO_VISIBILITY_PRIVATE: vis_str = "private"; break; case GCLI_REPO_VISIBILITY_PUBLIC: vis_str = "public"; break; default: assert(false && "Invalid visibility"); return gcli_error(ctx, "bad visibility level"); } rc = gitlab_repo_make_url(ctx, path, &url, ""); if (rc < 0) return rc; payload = gcli_asprintf("{ \"visibility\": \"%s\" }", vis_str); rc = gcli_fetch_with_method(ctx, "PUT", url, payload, NULL, NULL); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } int gitlab_repo_make_url(struct gcli_ctx *ctx, struct gcli_path const *const path, char **url, char const *const suffix_fmt, ...) { char *suffix = NULL; int rc = 0; va_list vp; va_start(vp, suffix_fmt); suffix = gcli_vasprintf(suffix_fmt, vp); va_end(vp); switch (path->kind) { case GCLI_PATH_DEFAULT: { char *e_owner, *e_repo = NULL; e_owner = gcli_urlencode(path->as_default.owner); e_repo = gcli_urlencode(path->as_default.repo); *url = gcli_asprintf("%s/projects/%s%%2F%s%s", gcli_get_apibase(ctx), e_owner, e_repo, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_NAMED: { char *e_owner, *e_repo = NULL; e_owner = gcli_urlencode(path->as_named.owner); e_repo = gcli_urlencode(path->as_named.repo); *url = gcli_asprintf("%s/projects/%s%%2F%s%s", gcli_get_apibase(ctx), e_owner, e_repo, suffix); gcli_clear_ptr(&e_owner); gcli_clear_ptr(&e_repo); } break; case GCLI_PATH_URL: { *url = gcli_asprintf("%s%s", path->as_url, suffix); } break; default: { rc = gcli_error(ctx, "unsupported path type for gitlab repos"); } break; } gcli_clear_ptr(&suffix); return rc; } gcli-2.9.1/src/gitlab/snippets.c000066400000000000000000000061541507017207500165150ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include void gcli_gitlab_snippet_free(struct gcli_gitlab_snippet *snippet) { gcli_clear_ptr(&snippet->title); gcli_clear_ptr(&snippet->filename); gcli_clear_ptr(&snippet->date); gcli_clear_ptr(&snippet->author); gcli_clear_ptr(&snippet->visibility); gcli_clear_ptr(&snippet->raw_url); } void gcli_snippets_free(struct gcli_gitlab_snippet_list *const list) { for (size_t i = 0; i < list->snippets_size; ++i) { gcli_gitlab_snippet_free(&list->snippets[i]); } gcli_clear_ptr(&list->snippets); list->snippets_size = 0; } int gcli_snippets_get(struct gcli_ctx *ctx, int const max, struct gcli_gitlab_snippet_list *const out) { char *url = NULL; struct gcli_fetch_list_ctx fl = { .listp = &out->snippets, .sizep = &out->snippets_size, .max = max, .parse = (parsefn)(parse_gitlab_snippets), }; *out = (struct gcli_gitlab_snippet_list) {0}; url = gcli_asprintf("%s/snippets", gcli_get_apibase(ctx)); return gcli_fetch_list(ctx, url, &fl); } int gcli_snippet_delete(struct gcli_ctx *ctx, char const *snippet_id) { int rc = 0; char *url; url = gcli_asprintf("%s/snippets/%s", gcli_get_apibase(ctx), snippet_id); rc = gcli_fetch_with_method(ctx, "DELETE", url, NULL, NULL, NULL); gcli_clear_ptr(&url); return rc; } int gcli_snippet_get(struct gcli_ctx *ctx, char const *snippet_id, FILE *stream) { int rc = 0; char *url = gcli_asprintf("%s/snippets/%s/raw", gcli_get_apibase(ctx), snippet_id); rc = gcli_curl(ctx, stream, url, NULL); gcli_clear_ptr(&url); return rc; } gcli-2.9.1/src/gitlab/sshkeys.c000066400000000000000000000064041507017207500163370ustar00rootroot00000000000000/* * Copyright 2023-2025 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 HAVE_CONFIG_H #include #endif /* HAVE_CONFIG_H */ #include #include #include #include #include #include int gitlab_get_sshkeys(struct gcli_ctx *ctx, struct gcli_sshkey_list *list) { char *url; struct gcli_fetch_list_ctx fl = { .listp = &list->keys, .sizep = &list->keys_size, .max = -1, .parse = (parsefn)(parse_gitlab_sshkeys), }; *list = (struct gcli_sshkey_list) {0}; url = gcli_asprintf("%s/user/keys", gcli_get_apibase(ctx)); return gcli_fetch_list(ctx, url, &fl); } int gitlab_add_sshkey(struct gcli_ctx *ctx, char const *const title, char const *const pubkey, struct gcli_sshkey *const out) { char *url = NULL, *payload = NULL; int rc = 0; struct gcli_fetch_buffer buf = {0}; struct gcli_jsongen gen = {0}; /* generate url */ url = gcli_asprintf("%s/user/keys", gcli_get_apibase(ctx)); /* Prepare payload */ gcli_jsongen_init(&gen); gcli_jsongen_begin_object(&gen); { gcli_jsongen_objmember(&gen, "title"); gcli_jsongen_string(&gen, title); gcli_jsongen_objmember(&gen, "key"); gcli_jsongen_string(&gen, pubkey); } 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, &buf); if (rc == 0 && out) { struct json_stream stream = {0}; json_open_buffer(&stream, buf.data, buf.length); parse_gitlab_sshkey(ctx, &stream, out); json_close(&stream); } gcli_fetch_buffer_free(&buf); gcli_clear_ptr(&payload); gcli_clear_ptr(&url); return rc; } int gitlab_delete_sshkey(struct gcli_ctx *ctx, gcli_id id) { char *url; int rc = 0; url = gcli_asprintf("%s/user/keys/%"PRIid, gcli_get_apibase(ctx), id); rc = gcli_fetch_with_method(ctx, "DELETE", url, NULL, NULL, NULL); gcli_clear_ptr(&url); return rc; } gcli-2.9.1/src/gitlab/status.c000066400000000000000000000044361507017207500161740ustar00rootroot00000000000000/* * Copyright 2022-2025 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 #include #include int gitlab_get_notifications(struct gcli_ctx *ctx, int const max, struct gcli_notification_list *const out) { char *url = NULL; struct gcli_fetch_list_ctx fl = { .listp = &out->notifications, .sizep = &out->notifications_size, .parse = (parsefn)(parse_gitlab_todos), .max = max, }; url = gcli_asprintf("%s/todos", gcli_get_apibase(ctx)); return gcli_fetch_list(ctx, url, &fl); } int gitlab_notification_mark_as_read(struct gcli_ctx *ctx, char const *id) { char *url = NULL; int rc = 0; url = gcli_asprintf("%s/todos/%s/mark_as_done", gcli_get_apibase(ctx), id); rc = gcli_fetch_with_method(ctx, "POST", url, NULL, NULL, NULL); gcli_clear_ptr(&url); return rc; } gcli-2.9.1/src/issues.c000066400000000000000000000117001507017207500147120ustar00rootroot00000000000000/* * Copyright 2021-2025 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 void gcli_issue_free(struct gcli_issue *const it) { gcli_clear_ptr(&it->product); gcli_clear_ptr(&it->component); gcli_clear_ptr(&it->author); gcli_clear_ptr(&it->state); gcli_clear_ptr(&it->body); gcli_clear_ptr(&it->url); gcli_clear_ptr(&it->title); gcli_clear_ptr(&it->web_url); for (size_t i = 0; i < it->labels_size; ++i) gcli_clear_ptr(&it->labels[i]); gcli_clear_ptr(&it->labels); for (size_t i = 0; i < it->assignees_size; ++i) gcli_clear_ptr(&it->assignees[i]); gcli_clear_ptr(&it->assignees); gcli_clear_ptr(&it->milestone); } void gcli_issues_free(struct gcli_issue_list *const list) { for (size_t i = 0; i < list->issues_size; ++i) gcli_issue_free(&list->issues[i]); gcli_clear_ptr(&list->issues); list->issues_size = 0; } int gcli_issues_search(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue_fetch_details const *details, int const max, struct gcli_issue_list *const out) { gcli_null_check_call(search_issues, ctx, path, details, max, out); } int gcli_get_issue(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_issue *const out) { gcli_null_check_call(get_issue_summary, ctx, path, out); } int gcli_issue_close(struct gcli_ctx *ctx, struct gcli_path const *const path) { gcli_null_check_call(issue_close, ctx, path); } int gcli_issue_reopen(struct gcli_ctx *ctx, struct gcli_path const *const path) { gcli_null_check_call(issue_reopen, ctx, path); } int gcli_issue_submit(struct gcli_ctx *ctx, struct gcli_submit_issue_options *opts) { gcli_null_check_call(perform_submit_issue, ctx, opts, NULL); } int gcli_issue_assign(struct gcli_ctx *ctx, struct gcli_path const *issue_path, char const *assignee) { gcli_null_check_call(issue_assign, ctx, issue_path, assignee); } int gcli_issue_add_labels(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, char const *const labels[], size_t const labels_size) { gcli_null_check_call(issue_add_labels, ctx, issue_path, labels, labels_size); } int gcli_issue_remove_labels(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, char const *const labels[], size_t const labels_size) { gcli_null_check_call(issue_remove_labels, ctx, issue_path, labels, labels_size); } int gcli_issue_set_milestone(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, int const milestone) { gcli_null_check_call(issue_set_milestone, ctx, issue_path, milestone); } int gcli_issue_clear_milestone(struct gcli_ctx *ctx, struct gcli_path const *const issue_path) { gcli_null_check_call(issue_clear_milestone, ctx, issue_path); } int gcli_issue_set_title(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, char const *new_title) { gcli_null_check_call(issue_set_title, ctx, issue_path, new_title); } int gcli_issue_get_attachments(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, struct gcli_attachment_list *out) { gcli_null_check_call(get_issue_attachments, ctx, issue_path, out); } int gcli_issue_set_op(struct gcli_ctx *ctx, struct gcli_path const *const issue_path, char const *new_op) { gcli_null_check_call(issue_set_op, ctx, issue_path, new_op); } gcli-2.9.1/src/json_gen.c000066400000000000000000000152071507017207500152070ustar00rootroot00000000000000/* * Copyright 2023-2025 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 #include #include static void grow_buffer(struct gcli_jsongen *gen) { gen->buffer_capacity *= 2; gen->buffer = realloc(gen->buffer, gen->buffer_capacity); } int gcli_jsongen_init(struct gcli_jsongen *gen) { /* This will allocate a 32 byte buffer. We can optimise * this for better allocation speed by analysing some statistics * of how long usually the generated buffers are. */ memset(gen, 0, sizeof(*gen)); gen->buffer_capacity = 16; grow_buffer(gen); gen->first_elem = true; return 0; } void gcli_jsongen_free(struct gcli_jsongen *gen) { gcli_clear_ptr(&gen->buffer); gen->buffer_size = 0; gen->buffer_capacity = 0; gen->scopes_size = 0; } char * gcli_jsongen_to_string(struct gcli_jsongen *gen) { char *buf = calloc(gen->buffer_size + 1, 1); return memcpy(buf, gen->buffer, gen->buffer_size); } static void fit(struct gcli_jsongen *gen, size_t const n_chars) { while (gen->buffer_capacity - gen->buffer_size < n_chars) grow_buffer(gen); } static int push_scope(struct gcli_jsongen *gen, int const scope) { if (gen->scopes_size >= (sizeof(gen->scopes) / sizeof(*gen->scopes))) return -1; gen->scopes[gen->scopes_size++] = scope; return 0; } static int pop_scope(struct gcli_jsongen *gen) { if (gen->scopes_size == 0) return -1; return gen->scopes[--gen->scopes_size]; } static bool is_array_or_object_scope(struct gcli_jsongen *gen) { return !!gen->scopes_size; } static void append_str(struct gcli_jsongen *gen, char const *str) { size_t const len = strlen(str); fit(gen, len); memcpy(gen->buffer + gen->buffer_size, str, len); gen->buffer_size += len; } static void put_comma_if_needed(struct gcli_jsongen *gen) { if (!gen->await_object_value && !gen->first_elem && is_array_or_object_scope(gen)) append_str(gen, ", "); gen->first_elem = false; } static bool is_object_scope(struct gcli_jsongen *gen) { if (gen->scopes_size == 0) return false; return gen->scopes[gen->scopes_size - 1] == GCLI_JSONGEN_OBJECT; } int gcli_jsongen_begin_object(struct gcli_jsongen *gen) { /* Cannot put a json object into a json object key */ if (is_object_scope(gen) && !gen->await_object_value) { assert(0 && "attempt to use json object as object key"); return -1; } put_comma_if_needed(gen); if (push_scope(gen, GCLI_JSONGEN_OBJECT) < 0) return -1; append_str(gen, "{"); gen->first_elem = true; return 0; } int gcli_jsongen_end_object(struct gcli_jsongen *gen) { if (pop_scope(gen) != GCLI_JSONGEN_OBJECT) { assert(0 && "unbalanced json scopes"); return -1; } append_str(gen, "}"); gen->await_object_value = false; gen->first_elem = false; return 0; } int gcli_jsongen_begin_array(struct gcli_jsongen *gen) { /* Cannot put a json array into a json object key */ if (is_object_scope(gen) && !gen->await_object_value) { assert(0 && "attempt to use array as object key"); return -1; } put_comma_if_needed(gen); if (push_scope(gen, GCLI_JSONGEN_ARRAY) < 0) return -1; append_str(gen, "["); gen->first_elem = true; return 0; } int gcli_jsongen_end_array(struct gcli_jsongen *gen) { if (pop_scope(gen) != GCLI_JSONGEN_ARRAY) { assert(0 && "unbalanced json scopes"); return -1; } append_str(gen, "]"); gen->await_object_value = false; gen->first_elem = false; return 0; } static void append_vstrf(struct gcli_jsongen *gen, char const *const fmt, va_list vp) { va_list vp_copy; size_t len; va_copy(vp_copy, vp); len = vsnprintf(NULL, 0, fmt, vp_copy); fit(gen, len + 1); vsnprintf(gen->buffer + gen->buffer_size, len + 1, fmt, vp); gen->buffer_size += len; } static void append_strf(struct gcli_jsongen *gen, char const *const fmt, ...) { va_list ap; va_start(ap, fmt); append_vstrf(gen, fmt, ap); va_end(ap); } int gcli_jsongen_objmember(struct gcli_jsongen *gen, char const *const key) { if (!is_object_scope(gen)) { assert(0 && "gcli_jsongen_objmember called outside object scope"); return -1; } put_comma_if_needed(gen); char *e_key = gcli_json_escape_cstr(key); append_strf(gen, "\"%s\": ", e_key); gen->first_elem = false; gen->await_object_value = true; gcli_clear_ptr(&e_key); return 0; } int gcli_jsongen_number(struct gcli_jsongen *gen, long long const number) { put_comma_if_needed(gen); append_strf(gen, "%lld", number); gen->await_object_value = false; gen->first_elem = false; return 0; } int gcli_jsongen_id(struct gcli_jsongen *gen, gcli_id const id) { put_comma_if_needed(gen); append_strf(gen, "%"PRIid, id); gen->await_object_value = false; gen->first_elem = false; return 0; } int gcli_jsongen_bool(struct gcli_jsongen *gen, bool const value) { put_comma_if_needed(gen); append_strf(gen, "%s", value ? "true" : "false"); gen->await_object_value = false; gen->first_elem = false; return 0; } int gcli_jsongen_string(struct gcli_jsongen *gen, char const *value) { put_comma_if_needed(gen); char *e_value = gcli_json_escape_cstr(value); append_strf(gen, "\"%s\"", e_value); gen->await_object_value = false; gen->first_elem = false; gcli_clear_ptr(&e_value); return 0; } int gcli_jsongen_null(struct gcli_jsongen *gen) { put_comma_if_needed(gen); append_str(gen, "null"); gen->await_object_value = false; gen->first_elem = false; return 0; } gcli-2.9.1/src/json_util.c000066400000000000000000000346001507017207500154110ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include #include #include int get_int_(struct gcli_ctx *ctx, json_stream *const input, int *out, char const *where) { if (json_next(input) != JSON_NUMBER) return gcli_error(ctx, "unexpected non-integer field in %s", where); *out = json_get_number(input); return 0; } int get_id_(struct gcli_ctx *ctx, json_stream *const input, gcli_id *out, char const *where) { if (json_next(input) != JSON_NUMBER) return gcli_error(ctx, "unexpected non-integer ID field in %s", where); *out = json_get_number(input); return 0; } int get_long_(struct gcli_ctx *ctx, json_stream *const input, long *out, char const *where) { if (json_next(input) != JSON_NUMBER) return gcli_error(ctx, "unexpected non-integer field in %s", where); *out = json_get_number(input); return 0; } int get_size_t_(struct gcli_ctx *ctx, json_stream *const input, size_t *out, char const *where) { if (json_next(input) != JSON_NUMBER) return gcli_error(ctx, "unexpected non-integer field in %s", where); *out = json_get_number(input); return 0; } int get_double_(struct gcli_ctx *ctx, json_stream *const input, double *out, char const *where) { enum json_type type = json_next(input); /* This is dumb but it fixes a couple of weirdnesses of the API */ if (type == JSON_NULL) { *out = 0; return 0; } if (type == JSON_NUMBER) { *out = json_get_number(input); return 0; } return gcli_error(ctx, "unexpected non-double field in %s", where); } int get_string_(struct gcli_ctx *ctx, json_stream *const input, char **out, char const *where) { enum json_type const type = json_next(input); if (type == JSON_NULL) { *out = strdup(""); return 0; } if (type != JSON_STRING) return gcli_error(ctx, "unexpected non-string field in %s", where); size_t len; char const *it = json_get_string(input, &len); if (!it) *out = strdup(""); else *out = gcli_strndup(it, len); return 0; } int get_bool_(struct gcli_ctx *ctx,json_stream *const input, bool *out, char const *where) { enum json_type value_type = json_next(input); if (value_type == JSON_TRUE) { *out = true; return 0; } else if (value_type == JSON_FALSE || value_type == JSON_NULL) { // HACK *out = false; return 0; } return gcli_error(ctx, "unexpected non-boolean value in %s", where); } int get_bool_relaxed_(struct gcli_ctx *ctx,json_stream *const input, bool *out, char const *where) { enum json_type value_type = json_next(input); if (value_type == JSON_TRUE) { *out = true; return 0; } else if (value_type == JSON_FALSE || value_type == JSON_NULL) { // HACK *out = false; return 0; } else if (value_type == JSON_NUMBER) { *out = json_get_number(input) != 0.0; return 0; } return gcli_error(ctx, "unexpected non-boolean value in %s", where); } int get_user_(struct gcli_ctx *ctx, json_stream *const input, char **out, char const *where) { if (json_next(input) != JSON_OBJECT) return gcli_error(ctx, "%s: user field is not an object", where); char const *expected_key = gcli_forge(ctx)->user_object_key; while (json_next(input) == JSON_STRING) { size_t len = 0; char const *key = json_get_string(input, &len); if (strncmp(expected_key, key, len) == 0) { if (json_next(input) != JSON_STRING) return gcli_error(ctx, "%s: login isn't a string", where); char const *tmp = json_get_string(input, &len); *out = gcli_strndup(tmp, len); } else { json_next(input); } } return 0; } static struct { char c; char const *with; } json_escape_table[] = { { .c = '\n', .with = "\\n" }, { .c = '\t', .with = "\\t" }, { .c = '\r', .with = "\\r" }, { .c = '\\', .with = "\\\\" }, { .c = '"' , .with = "\\\"" }, }; gcli_sv gcli_json_escape(gcli_sv const it) { gcli_sv result = {0}; result.data = calloc(2 * it.length + 1, 1); if (!result.data) err(1, "malloc"); for (size_t i = 0; i < it.length; ++i) { for (size_t c = 0; c < ARRAY_SIZE(json_escape_table); ++c) { if (json_escape_table[c].c == it.data[i]) { size_t const len = strlen(json_escape_table[c].with); memcpy(result.data + result.length, json_escape_table[c].with, len); result.length += len; goto next; } } memcpy(result.data + result.length, it.data + i, 1); result.length += 1; next: continue; } return result; } int get_sv_(struct gcli_ctx *ctx, json_stream *const input, gcli_sv *out, char const *where) { enum json_type type = json_next(input); if (type == JSON_NULL) { *out = SV_NULL; return 0; } if (type != JSON_STRING) return gcli_error(ctx, "unexpected non-string field in %s", where); size_t len; char const *it = json_get_string(input, &len); char *copy = gcli_strndup(it, len); *out = SV(copy); return 0; } int get_label_(struct gcli_ctx *ctx, json_stream *const input, char const **out, char const *where) { if (json_next(input) != JSON_OBJECT) return gcli_error(ctx, "%s: label field is not an object", where); while (json_next(input) == JSON_STRING) { size_t len = 0; char const *key = json_get_string(input, &len); if (strncmp("name", key, len) == 0) { if (json_next(input) != JSON_STRING) return gcli_error(ctx, "%s: name of the label is not a string", where); *out = json_get_string(input, &len); *out = gcli_strndup(*out, len); } else { json_next(input); } } return 0; } int gcli_json_advance(struct gcli_ctx *ctx, json_stream *const stream, char const *fmt, ...) { va_list ap; va_start(ap, fmt); while (*fmt) { switch (*fmt++) { case '[': { if (json_next(stream) != JSON_ARRAY) return gcli_error(ctx, "expected array begin"); } break; case '{': { if (json_next(stream) != JSON_OBJECT) return gcli_error(ctx, "expected array begin"); } break; case 's': { if (json_next(stream) != JSON_STRING) return gcli_error(ctx, "expected string"); char *it = va_arg(ap, char *); size_t len = 0; char const *other = json_get_string(stream, &len); if (strncmp(it, other, len)) return gcli_error(ctx, "string unmatched"); } break; case ']': { if (json_next(stream) != JSON_ARRAY_END) return gcli_error(ctx, "expected array end"); } break; case '}': { if (json_next(stream) != JSON_OBJECT_END) return gcli_error(ctx, "expected object end"); } break; case 'i': { if (json_next(stream) != JSON_NUMBER) return gcli_error(ctx, "expected integer"); } break; } } va_end(ap); return 0; } int get_parse_int_(struct gcli_ctx *ctx, json_stream *const input, long *out, char const *function) { char *endptr = NULL; char *string; int rc = get_string_(ctx, input, &string, function); if (rc < 0) return rc; *out = strtol(string, &endptr, 10); if (endptr != string + strlen(string)) return gcli_error(ctx, "%s: cannot parse %s as integer", function, string); return 0; } int get_github_style_colour(struct gcli_ctx *ctx, json_stream *const input, uint32_t *out) { char *colour_str; char *endptr = NULL; int rc; rc = get_string(ctx, input, &colour_str); if (rc < 0) return rc; unsigned long colour = strtoul(colour_str, &endptr, 16); if (endptr != colour_str + strlen(colour_str)) return gcli_error(ctx, "%s: bad colour code returned by API", colour_str); gcli_clear_ptr(&colour_str); *out = ((uint32_t)(colour)) << 8; return 0; } int get_gitlab_style_colour(struct gcli_ctx *ctx, json_stream *const input, uint32_t *out) { char *colour; char *endptr = NULL; long code = 0; int rc = 0; rc = get_string(ctx, input, &colour); if (rc < 0) return rc; code = strtol(colour + 1, &endptr, 16); if (endptr != (colour + 1 + strlen(colour + 1))) return gcli_error(ctx, "%s: invalid colour code"); gcli_clear_ptr(&colour); *out = ((uint32_t)(code) << 8); return 0; } int get_gitea_visibility(struct gcli_ctx *ctx, json_stream *const input, char **out) { bool is_private; int rc = get_bool(ctx, input, &is_private); if (rc < 0) return rc; *out = strdup(is_private ? "private" : "public"); return 0; } int get_gitlab_can_be_merged(struct gcli_ctx *ctx, json_stream *const input, bool *out) { gcli_sv tmp; int rc = 0; rc = get_sv(ctx, input, &tmp); if (rc < 0) return rc; *out = gcli_sv_eq_to(tmp, "can_be_merged"); gcli_clear_ptr(&tmp.data); return rc; } int get_github_is_pr(struct gcli_ctx *ctx, json_stream *input, int *out) { enum json_type next = json_peek(input); (void) ctx; if (next == JSON_NULL) json_next(input); else SKIP_OBJECT_VALUE(input); *out = (next == JSON_OBJECT); return 0; } int get_int_to_sv_(struct gcli_ctx *ctx, json_stream *input, gcli_sv *out, char const *function) { int rc, val; rc = get_int_(ctx, input, &val, function); if (rc < 0) return rc; *out = gcli_sv_fmt("%d", val); return 0; } int get_github_notification_target_type(struct gcli_ctx *ctx, json_stream *input, enum gcli_notification_target_type *out) { gcli_sv tmp; int rc = 0; rc = get_sv(ctx, input, &tmp); if (rc < 0) return rc; if (gcli_sv_eq_to(tmp, "Issue")) { *out = GCLI_NOTIFICATION_TARGET_ISSUE; } else if (gcli_sv_eq_to(tmp, "PullRequest")) { *out = GCLI_NOTIFICATION_TARGET_PULL_REQUEST; } else if (gcli_sv_eq_to(tmp, "Release")) { *out = GCLI_NOTIFICATION_TARGET_RELEASE; } else { rc = gcli_error( ctx, "bad github notification target type: "SV_FMT, SV_ARGS(tmp)); } gcli_clear_ptr(&tmp.data); return rc; } int get_gitlab_notification_target_type(struct gcli_ctx *ctx, json_stream *input, enum gcli_notification_target_type *out) { gcli_sv tmp; int rc = 0; rc = get_sv(ctx, input, &tmp); if (rc < 0) return rc; if (gcli_sv_eq_to(tmp, "Issue")) { *out = GCLI_NOTIFICATION_TARGET_ISSUE; } else if (gcli_sv_eq_to(tmp, "MergeRequest")) { *out = GCLI_NOTIFICATION_TARGET_PULL_REQUEST; } else if (gcli_sv_eq_to(tmp, "Commit")) { *out = GCLI_NOTIFICATION_TARGET_COMMIT; } else if (gcli_sv_eq_to(tmp, "Epic")) { *out = GCLI_NOTIFICATION_TARGET_EPIC; } else { rc = gcli_error( ctx, "bad github notification target type: "SV_FMT, SV_ARGS(tmp)); } gcli_clear_ptr(&tmp.data); return rc; } int get_gitea_notification_target_type(struct gcli_ctx *ctx, json_stream *input, enum gcli_notification_target_type *out) { gcli_sv tmp; int rc = 0; rc = get_sv(ctx, input, &tmp); if (rc < 0) return rc; if (gcli_sv_eq_to(tmp, "Issue")) { *out = GCLI_NOTIFICATION_TARGET_ISSUE; } else if (gcli_sv_eq_to(tmp, "Pull")) { *out = GCLI_NOTIFICATION_TARGET_PULL_REQUEST; } else if (gcli_sv_eq_to(tmp, "Commit")) { *out = GCLI_NOTIFICATION_TARGET_COMMIT; } else if (gcli_sv_eq_to(tmp, "Repository")) { *out = GCLI_NOTIFICATION_TARGET_REPOSITORY; } else { rc = gcli_error( ctx, "bad github notification target type: "SV_FMT, SV_ARGS(tmp)); } gcli_clear_ptr(&tmp.data); return rc; } int get_iso8601_time_(struct gcli_ctx *ctx, json_stream *input, time_t *out, char const *where) { char *copy; char const *it; enum json_type type; size_t len; int rc = 0; type = json_next(input); if (type == JSON_NULL) { *out = 0; return 0; } if (type != JSON_STRING) return gcli_error(ctx, "unexpected non-string field in %s", where); it = json_get_string(input, &len); copy = gcli_strndup(it, len); rc = gcli_parse_iso8601_date_time(ctx, copy, out); gcli_clear_ptr(©); return rc; } int get_url_path_(struct gcli_ctx *ctx, struct json_stream *input, struct gcli_path *out, char const *function) { char *copy; char const *it; enum json_type type; size_t len; int rc = 0; type = json_next(input); if (type == JSON_NULL) { *out = (struct gcli_path){0}; return 0; } if (type != JSON_STRING) return gcli_error(ctx, "unexpected non-string field in %s", function); it = json_get_string(input, &len); copy = gcli_strndup(it, len); out->kind = GCLI_PATH_URL; out->as_url = copy; return rc; } int get_gitlab_notification_target_(struct gcli_ctx *ctx, struct json_stream *input, struct gcli_path *out, char const *where) { if (json_next(input) != JSON_OBJECT) return gcli_error(ctx, "%s: notification target is not an object", where); out->kind = GCLI_PATH_PID_ID; while (json_next(input) == JSON_STRING) { size_t len = 0; char const *key = json_get_string(input, &len); if (strncmp("iid", key, len) == 0) { if (json_next(input) != JSON_NUMBER) { return gcli_error( ctx, "%s: iid of the target is not a number", where); } out->as_pid_id.id = (gcli_id) json_get_number(input); } else if (strncmp("project_id", key, len) == 0) { if (json_next(input) != JSON_NUMBER) { return gcli_error( ctx, "%s: project_id of the target is not a number", where); } out->as_pid_id.project_id = (gcli_id) json_get_number(input); } else { SKIP_OBJECT_VALUE(input); } } return 0; } gcli-2.9.1/src/labels.c000066400000000000000000000060411507017207500146430ustar00rootroot00000000000000/* * Copyright 2022-2025 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 int gcli_get_labels(struct gcli_ctx *ctx, struct gcli_path const *const path, int const max, struct gcli_label_list *const out) { gcli_null_check_call(get_labels, ctx, path, max, out); } void gcli_free_label(struct gcli_label *const label) { gcli_clear_ptr(&label->name); gcli_clear_ptr(&label->description); } void gcli_free_labels(struct gcli_label_list *const list) { for (size_t i = 0; i < list->labels_size; ++i) gcli_free_label(&list->labels[i]); gcli_clear_ptr(&list->labels); list->labels_size = 0; } int gcli_create_label(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_label *const label) { gcli_null_check_call(create_label, ctx, path, label); } int gcli_delete_label(struct gcli_ctx *ctx, struct gcli_path const *const path) { gcli_null_check_call(delete_label, ctx, path); } int gcli_get_label(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_label *out) { gcli_null_check_call(get_label, ctx, path, out); } int gcli_label_set_title(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const new_name) { gcli_null_check_call(label_set_title, ctx, path, new_name); } int gcli_label_set_description(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const new_description) { gcli_null_check_call(label_set_description, ctx, path, new_description); } int gcli_label_set_colour(struct gcli_ctx *ctx, struct gcli_path const *const path, uint32_t const colour) { gcli_null_check_call(label_set_colour, ctx, path, colour); } gcli-2.9.1/src/milestones.c000066400000000000000000000060771507017207500155740ustar00rootroot00000000000000/* * Copyright 2023-2025 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 int gcli_get_milestones(struct gcli_ctx *ctx, struct gcli_path const *const path, int const max, struct gcli_milestone_list *const out) { gcli_null_check_call(get_milestones, ctx, path, max, out); } int gcli_get_milestone(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_milestone *const out) { gcli_null_check_call(get_milestone, ctx, path, out); } int gcli_create_milestone(struct gcli_ctx *ctx, struct gcli_path const *const repo, struct gcli_milestone_create_args const *args) { gcli_null_check_call(create_milestone, ctx, repo, args); } int gcli_delete_milestone(struct gcli_ctx *ctx, struct gcli_path const *const path) { gcli_null_check_call(delete_milestone, ctx, path); } void gcli_free_milestone(struct gcli_milestone *const it) { gcli_clear_ptr(&it->title); gcli_clear_ptr(&it->state); gcli_clear_ptr(&it->description); gcli_clear_ptr(&it->web_url); } void gcli_free_milestones(struct gcli_milestone_list *const it) { for (size_t i = 0; i < it->milestones_size; ++i) gcli_free_milestone(&it->milestones[i]); gcli_clear_ptr(&it->milestones); it->milestones_size = 0; } int gcli_milestone_get_issues(struct gcli_ctx *ctx, struct gcli_path const *const milestone_path, struct gcli_issue_list *const out) { gcli_null_check_call(get_milestone_issues, ctx, milestone_path, out); } int gcli_milestone_set_duedate(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const date) { gcli_null_check_call(milestone_set_duedate, ctx, path, date); } gcli-2.9.1/src/nvlist.c000066400000000000000000000051601507017207500147210ustar00rootroot00000000000000/* * Copyright 2023-2025 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 int gcli_nvlist_init(struct gcli_nvlist *list) { TAILQ_INIT(list); return 0; } int gcli_nvlist_free(struct gcli_nvlist *list) { struct gcli_nvpair *p1, *p2; p1 = TAILQ_FIRST(list); while (p1 != NULL) { p2 = TAILQ_NEXT(p1, next); gcli_clear_ptr(&p1->key); gcli_clear_ptr(&p1->value); gcli_clear_ptr(&p1); p1 = p2; } TAILQ_INIT(list); return 0; } int gcli_nvlist_append(struct gcli_nvlist *list, char *key, char *value) { /* TODO: handle the case where a pair with an already existing * key is inserted. */ struct gcli_nvpair *pair = calloc(1, sizeof(*pair)); if (pair == NULL) return -1; pair->key = key; pair->value = value; TAILQ_INSERT_TAIL(list, pair, next); return 0; } char const * gcli_nvlist_find(struct gcli_nvlist const *list, char const *key) { struct gcli_nvpair const *pair; TAILQ_FOREACH(pair, list,next) { if (strcmp(pair->key, key) == 0) return pair->value; } return NULL; } char const * gcli_nvlist_find_or(struct gcli_nvlist const *list, char const *const key, char const *const alternative) { char const *const result = gcli_nvlist_find(list, key); if (result) return result; else return alternative; } gcli-2.9.1/src/path.c000066400000000000000000000036221507017207500143370ustar00rootroot00000000000000/* * Copyright 2025 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 void gcli_path_free(struct gcli_path *const path) { switch (path->kind) { case GCLI_PATH_DEFAULT: gcli_clear_ptr(&path->as_default.owner); gcli_clear_ptr(&path->as_default.repo); break; case GCLI_PATH_URL: gcli_clear_ptr(&path->as_url); break; case GCLI_PATH_BUGZILLA: gcli_clear_ptr(&path->as_bugzilla.product); gcli_clear_ptr(&path->as_bugzilla.component); break; case GCLI_PATH_ID: break; case GCLI_PATH_PID_ID: break; default: assert(0 && "unreachable"); } } gcli-2.9.1/src/pgen/000077500000000000000000000000001507017207500141655ustar00rootroot00000000000000gcli-2.9.1/src/pgen/dump_c.c000066400000000000000000000176311507017207500156100ustar00rootroot00000000000000/* * Copyright 2022 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 static void pregen_array_parser(struct objparser *p, struct objentry *it) { fprintf(outfile, "static int\n" "parse_%s_%s_array(struct gcli_ctx *ctx, struct json_stream *stream, " "%s%s *out)\n", p->name, it->name, p->is_struct ? "struct " : "", p->returntype); fprintf(outfile, "{\n"); fprintf(outfile, "\tint rc = 0;\n"); fprintf(outfile, "\tif (json_peek(stream) == JSON_NULL) {\n"); fprintf(outfile, "\t\tjson_next(stream);\n"); fprintf(outfile, "\t\tout->%s = NULL;\n", it->name); fprintf(outfile, "\t\tout->%s_size = 0;\n", it->name); fprintf(outfile, "\t\treturn 0;\n"); fprintf(outfile, "\t}\n\n"); fprintf(outfile, "\tif (json_next(stream) != JSON_ARRAY)\n"); fprintf(outfile, "\t\treturn gcli_error(ctx, \"expected array for %s array in %s\");\n\n", it->name, p->name); fprintf(outfile, "\twhile (json_peek(stream) != JSON_ARRAY_END) {\n"); fprintf(outfile, "\t\tout->%s = realloc(out->%s, sizeof(*out->%s) * (out->%s_size + 1));\n", it->name, it->name, it->name, it->name); fprintf(outfile, "\t\tmemset(&out->%s[out->%s_size], 0, sizeof(out->%s[out->%s_size]));\n", it->name, it->name, it->name, it->name); fprintf(outfile, "\t\trc = %s(ctx, stream, &out->%s[out->%s_size++]);\n", it->parser, it->name, it->name); fprintf(outfile, "\t\tif (rc < 0)\n\t\t\treturn rc;\n"); fprintf(outfile, "\t}\n\n"); fprintf(outfile, "\tif (json_next(stream) != JSON_ARRAY_END)\n"); fprintf(outfile, "\t\treturn gcli_error(ctx, \"unexpected element in array " "while parsing %s\");\n", p->name); fprintf(outfile, "\treturn 0;\n"); fprintf(outfile, "}\n\n"); } static void objparser_pregen_array_parsers(struct objparser *p) { for (struct objentry *it = p->entries; it; it = it->next) { if (it->kind == OBJENTRY_ARRAY) pregen_array_parser(p, it); } } static void objparser_dump_entries(struct objparser *p) { fprintf(outfile, "\twhile ((key_type = json_next(stream)) == JSON_STRING) {\n"); fprintf(outfile, "\t\tsize_t len;\n"); fprintf(outfile, "\t\tkey = json_get_string(stream, &len);\n"); for (struct objentry *it = p->entries; it; it = it->next) { fprintf(outfile, "\t\tif (strncmp(\"%s\", key, len) == 0) {\n", it->jsonname); if (it->kind == OBJENTRY_SIMPLE) { if (it->parser) { fprintf(outfile, "\t\t\tif (%s(ctx, stream, &out->%s) < 0)\n", it->parser, it->name); } else { fprintf(outfile, "\t\t\tif (get_%s(ctx, stream, &out->%s) < 0)\n", it->type, it->name); } fprintf(outfile, "\t\t\t\treturn -1;\n"); } else if (it->kind == OBJENTRY_ARRAY) { fprintf(outfile, "\t\t\tif (parse_%s_%s_array(ctx, stream, out) < 0)\n", p->name, it->name); fprintf(outfile, "\t\t\t\treturn -1;\n"); } else if (it->kind == OBJENTRY_CONTINUATION) { fprintf(outfile, "\t\t\tif (%s(ctx, stream, out) < 0)\n", it->parser); fprintf(outfile, "\t\t\t\treturn -1;\n"); } fprintf(outfile, "\t\t} else "); } fprintf(outfile, "\n\t\t\tSKIP_OBJECT_VALUE(stream);\n"); fprintf(outfile, "\t}\n"); } static void objparser_dump_select(struct objparser *p) { fprintf(outfile, "\twhile ((key_type = json_next(stream)) == JSON_STRING) {\n"); fprintf(outfile, "\t\tsize_t len;\n"); fprintf(outfile, "\t\tkey = json_get_string(stream, &len);\n"); fprintf(outfile, "\t\tif (strncmp(\"%s\", key, len) == 0) {\n", p->select.fieldname); fprintf(outfile, "\t\t\tif (get_%s(ctx, stream, out) < 0)\n", p->select.fieldtype); fprintf(outfile, "\t\t\t\treturn -1;\n"); fprintf(outfile, "\t\t} else "); fprintf(outfile, "\n\t\t\tSKIP_OBJECT_VALUE(stream);\n"); fprintf(outfile, "\t}\n"); } void objparser_dump_c(struct objparser *p) { objparser_pregen_array_parsers(p); fprintf(outfile, "int\n" "parse_%s(struct gcli_ctx *ctx, struct json_stream *stream, %s%s *out)\n", p->name, p->is_struct ? "struct " : "", p->returntype); fprintf(outfile, "{\n"); fprintf(outfile, "\tenum json_type key_type;\n"); fprintf(outfile, "\tconst char *key;\n\n"); fprintf(outfile, "\tif (json_next(stream) == JSON_NULL)\n"); fprintf(outfile, "\t\treturn 0;\n"); /* not ideal */ switch (p->kind) { case OBJPARSER_ENTRIES: objparser_dump_entries(p); break; case OBJPARSER_SELECT: objparser_dump_select(p); break; default: assert(0 && "unreached"); } fprintf(outfile, "\tif (key_type != JSON_OBJECT_END)\n"); fprintf(outfile, "\t\treturn gcli_error(ctx, \"unexpected object key type " "in parse_%s\");\n", p->name); fprintf(outfile, "\treturn 0;\n"); fprintf(outfile, "}\n\n"); } void arrayparser_dump_c(struct arrayparser *p) { fprintf(outfile, "int\n" "parse_%s(struct gcli_ctx *ctx, struct json_stream *stream, %s%s **out, " "size_t *out_size)\n", p->name, p->is_struct ? "struct " : "", p->returntype); fprintf(outfile, "{\n"); fprintf(outfile, "\tif (json_peek(stream) == JSON_NULL) {\n"); fprintf(outfile, "\t\tjson_next(stream);\n"); fprintf(outfile, "\t\t*out = NULL;\n"); fprintf(outfile, "\t\t*out_size = 0;\n"); fprintf(outfile, "\t\treturn 0;\n"); fprintf(outfile, "\t}\n\n"); fprintf(outfile, "\tif (json_next(stream) != JSON_ARRAY)\n"); fprintf(outfile, "\t\treturn gcli_error(ctx, \"Expected array of %s array in parse_%s\");\n\n", p->returntype, p->name); fprintf(outfile, "\twhile (json_peek(stream) != JSON_ARRAY_END) {\n"); fprintf(outfile, "\t\tint rc;\n"); fprintf(outfile, "\t\t%s%s *it;\n", p->is_struct ? "struct " : "", p->returntype); fprintf(outfile, "\t\t*out = realloc(*out, sizeof(**out) * (*out_size + 1));\n"); fprintf(outfile, "\t\tit = &(*out)[(*out_size)++];\n"); fprintf(outfile, "\t\tmemset(it, 0, sizeof(*it));\n"); fprintf(outfile, "\t\trc = %s(ctx, stream, it);\n", p->parser); fprintf(outfile, "\t\tif (rc < 0)\n"); fprintf(outfile, "\t\t\treturn rc;\n"); fprintf(outfile, "\t}\n\n"); fprintf(outfile, "\tif (json_next(stream) != JSON_ARRAY_END)\n"); fprintf(outfile, "\t\treturn gcli_error(ctx, \"unexpected element in array " "while parsing %s\");\n", p->name); fprintf(outfile, "\treturn 0;\n"); fprintf(outfile, "}\n\n"); } void include_dump_c(const char *file) { fprintf(outfile, "#include <%s>\n", file); } void header_dump_c(void) { fprintf(outfile, "#include \n"); fprintf(outfile, "#include \n"); fprintf(outfile, "#include \n"); fprintf(outfile, "#include \n"); fprintf(outfile, "#include \n"); fprintf(outfile, "#include \n"); fprintf(outfile, "#include <%.*s.h>\n", (int)(strlen(outfilename) - 2), outfilename); } gcli-2.9.1/src/pgen/dump_h.c000066400000000000000000000052261507017207500156120ustar00rootroot00000000000000/* * Copyright 2022 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 static int should_replace(char c) { return c == '_' || c == '/' || c == '.' || c == '-'; } static char * get_header_name(void) { size_t len; char *result; len = strlen(outfilename); result = calloc(len + 1, 1); for (size_t i = 0; i < len; ++i) { if (should_replace(outfilename[i])) result[i] = '_'; else result[i] = toupper(outfilename[i]); } return result; } void header_dump_h(void) { char *hname = get_header_name(); fprintf(outfile, "#ifndef %s\n", hname); fprintf(outfile, "#define %s\n\n", hname); fprintf(outfile, "#include \n"); free(hname); } void objparser_dump_h(struct objparser *p) { fprintf(outfile, "int parse_%s(struct gcli_ctx *ctx, struct json_stream *, %s%s *);\n", p->name, p->is_struct ? "struct " : "", p->returntype); } void include_dump_h(const char *file) { fprintf(outfile, "#include <%s>\n", file); } void footer_dump_h(void) { char *hname = get_header_name(); fprintf(outfile, "\n#endif /* %s */\n", hname); free(hname); } void arrayparser_dump_h(struct arrayparser *p) { fprintf(outfile, "int parse_%s(struct gcli_ctx *ctx, struct json_stream *, " "%s%s **out, size_t *out_size);\n", p->name, p->is_struct ? "struct " : "", p->returntype); } gcli-2.9.1/src/pgen/dump_plain.c000066400000000000000000000035011507017207500164600ustar00rootroot00000000000000/* * Copyright 2022 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 void objparser_dump_plain(struct objparser *p) { fprintf(outfile, "object parser: name = %s, type = %s\n", p->name, p->returntype); for (struct objentry *it = p->entries; it != NULL; it = it->next) { fprintf(outfile, " entry: kind = %s, jsonname = %s, name = %s, type = %s, " "parser = %s\n", it->kind == OBJENTRY_SIMPLE ? "simple" : "array", it->name, it->jsonname, it->type, it->parser); } } gcli-2.9.1/src/pgen/lexer.l000066400000000000000000000071721507017207500154700ustar00rootroot00000000000000/* * Copyright 2022 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 "y.tab.h" int yycol = 1, yyrow = 1; char *yyfile = NULL; void yyerror(const char *msg) { fprintf(stderr, "%s:%d:%d: error: %s\n", yyfile, yyrow, yycol, msg); exit(1); } int yywrap(void) { return 1; } %} %% \n { yycol = 1; yyrow += 1; } [ \t] { yycol += 1; } parser { yycol += yyleng; return PARSER; } include { yycol += yyleng; return INCLUDE; } is { yycol += yyleng; return IS; } object { yycol += yyleng; return OBJECT; } with { yycol += yyleng; return WITH; } as { yycol += yyleng; return AS; } use { yycol += yyleng; return USE; } array { yycol += yyleng; return ARRAY; } of { yycol += yyleng; return OF; } select { yycol += yyleng; return SELECT; } struct { yycol += yyleng; return STRUCT; } => { yycol += yyleng; return FATARROW; } "(" { yycol += yyleng; return OPAREN; } ")" { yycol += yyleng; return CPAREN; } ";" { yycol += yyleng; return SEMICOLON; } "," { yycol += yyleng; return COMMA; } [A-Za-z][A-Za-z0-9_*.]* { yycol += yyleng; yylval.ident.text = strdup(yytext); return IDENT; } \"[^\"]*\" { yycol += yyleng; yylval.strlit.text = strdup(yytext + 1); yylval.strlit.text[strlen(yytext + 1) - 1] = '\0'; return STRLIT; } . { yyerror("unrecognized character"); } %% gcli-2.9.1/src/pgen/parser.y000066400000000000000000000212571507017207500156620ustar00rootroot00000000000000/* * Copyright 2022-2025 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 FILE *outfile = NULL; char *outfilename = NULL; int dumptype = 0; /* Forward declaration. Is in generated lexer.c */ extern int yylex (void); static void objparser_dump(struct objparser *); static void arrayparser_dump(struct arrayparser *); static void include_dump(const char *); static void header_dump(void); static void footer_dump(void); %} %token PARSER IS OBJECT WITH AS USE FATARROW INCLUDE %token OPAREN CPAREN SEMICOLON ARRAY OF COMMA SELECT STRUCT %union { struct strlit strlit; struct ident ident; struct objentry objentry; struct objentry *objentries; struct objparser objparser; struct arrayparser arrayparser; } %token STRLIT %token IDENT %type obj_entry %type obj_entries %type objparser %type arrayparser; %% input: instruction input | ; instruction: objparser SEMICOLON { objparser_dump(&($1)); } | arrayparser SEMICOLON { arrayparser_dump(&($1)); } | INCLUDE STRLIT SEMICOLON { include_dump($2.text); } ; objparser: PARSER IDENT IS OBJECT OF IDENT WITH OPAREN obj_entries CPAREN { $$.kind = OBJPARSER_ENTRIES; $$.name = $2.text; $$.is_struct = false; $$.returntype = $6.text; $$.entries = $9; } | PARSER IDENT IS OBJECT OF STRUCT IDENT WITH OPAREN obj_entries CPAREN { $$.kind = OBJPARSER_ENTRIES; $$.name = $2.text; $$.is_struct = true; $$.returntype = $7.text; $$.entries = $10; } | PARSER IDENT IS OBJECT OF IDENT SELECT STRLIT AS IDENT { $$.kind = OBJPARSER_SELECT; $$.name = $2.text; $$.returntype = $6.text; $$.select.fieldname = $8.text; $$.select.fieldtype = $10.text; } ; arrayparser: PARSER IDENT IS ARRAY OF IDENT USE IDENT { $$.name = $2.text; $$.is_struct = false; $$.returntype = $6.text; $$.parser = $8.text; } | PARSER IDENT IS ARRAY OF STRUCT IDENT USE IDENT { $$.name = $2.text; $$.is_struct = true; $$.returntype = $7.text; $$.parser = $9.text; } ; obj_entries: obj_entries COMMA obj_entry { $$ = malloc(sizeof(*($$))); *($$) = $3; $$->next = $1; } | obj_entry { $$ = malloc(sizeof(*($$))); *($$) = $1; $$->next = NULL; } ; obj_entry: STRLIT FATARROW IDENT AS IDENT { $$.jsonname = $1.text; $$.kind = OBJENTRY_SIMPLE; $$.name = $3.text; $$.type = $5.text; $$.parser = NULL; } | STRLIT FATARROW IDENT AS IDENT USE IDENT { $$.jsonname = $1.text; $$.kind = OBJENTRY_SIMPLE; $$.name = $3.text; $$.type = $5.text; $$.parser = $7.text; } | STRLIT FATARROW IDENT AS ARRAY OF IDENT USE IDENT { $$.jsonname = $1.text; $$.kind = OBJENTRY_ARRAY; $$.name = $3.text; $$.type = $7.text; $$.parser = $9.text; } | STRLIT FATARROW USE IDENT { $$.jsonname = $1.text; $$.kind = OBJENTRY_CONTINUATION; $$.parser = $4.text; } ; %% #include #include #include #include extern FILE *yyin; extern char *yyfile; /******************************************************************************/ /* Table of functions to call when dumping various parts of the output * file */ struct { void (*dump_header)(void); void (*dump_footer)(void); void (*dump_objparser)(struct objparser *); void (*dump_arrayparser)(struct arrayparser *); void (*dump_include)(const char *); } dumpers[] = { [DUMP_PLAIN] = { .dump_objparser = objparser_dump_plain }, [DUMP_C] = { .dump_header = header_dump_c, .dump_objparser = objparser_dump_c, .dump_include = include_dump_c, .dump_arrayparser = arrayparser_dump_c, }, [DUMP_H] = { .dump_header = header_dump_h, .dump_objparser = objparser_dump_h, .dump_include = include_dump_h, .dump_footer = footer_dump_h, .dump_arrayparser = arrayparser_dump_h, } }; /* Helpers */ static void objparser_dump(struct objparser *p) { if (dumpers[dumptype].dump_objparser) dumpers[dumptype].dump_objparser(p); else yyerror("internal error: don't know how to dump an object parser"); } static void arrayparser_dump(struct arrayparser *p) { if (dumpers[dumptype].dump_arrayparser) dumpers[dumptype].dump_arrayparser(p); else yyerror("internal error: don't know how to dump an array parser"); } static void include_dump(const char *file) { if (dumpers[dumptype].dump_include) dumpers[dumptype].dump_include(file); } static void header_dump(void) { if (dumpers[dumptype].dump_header) dumpers[dumptype].dump_header(); } static void footer_dump(void) { if (dumpers[dumptype].dump_footer) dumpers[dumptype].dump_footer(); } /******************************************************************************/ static void usage(void) { fprintf(stderr, "usage: pgen [-v] [-o outputfile] [-t c|h|plain] [...]\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -v Print version and exit\n"); fprintf(stderr, " -o file Dump output into the given file\n"); fprintf(stderr, " -t type Type of the output. Can be either c, h or plain.\n"); } int main(int argc, char *argv[]) { int ch; while ((ch = getopt(argc, argv, "hvo:t:")) != -1) { switch (ch) { case 'o': { if (outfile) errx(1, "cannot specify -o more than once"); outfile = fopen(optarg, "w"); outfilename = optarg; } break; case 'v': { fprintf(stderr, "pgen version 0.1\n"); exit(0); } break; case 't': { if (strcmp(optarg, "plain") == 0) dumptype = DUMP_PLAIN; else if (strcmp(optarg, "c") == 0) dumptype = DUMP_C; else if (strcmp(optarg, "h") == 0) dumptype = DUMP_H; else errx(1, "invalid dump type %s", optarg); } break; case '?': case '-': default: usage(); exit(1); } } argc -= optind; argv += optind; if (!outfile) { outfile = stdout; outfilename = ""; } header_dump(); if (argc) { for (int i = 0; i < argc; ++i) { yyfile = argv[i]; yyin = fopen(argv[i], "r"); yyparse(); fclose(yyin); } } else { yyfile = ""; yyin = stdin; yyparse(); } footer_dump(); fclose(outfile); return 0; } gcli-2.9.1/src/port/000077500000000000000000000000001507017207500142205ustar00rootroot00000000000000gcli-2.9.1/src/port/err.c000066400000000000000000000034641507017207500151630ustar00rootroot00000000000000/* * Copyright 2021-2025 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 void err(int code, const char *fmt, ...) { va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fprintf(stderr, ": %s\n", strerror(errno)); exit(code); } void errx(int code, const char *fmt, ...) { va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fputc('\n', stderr); exit(code); } gcli-2.9.1/src/port/string.c000066400000000000000000000056001507017207500156730ustar00rootroot00000000000000/* * Copyright 2021-2025 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 char * gcli_vasprintf(char const *const fmt, va_list vp) { char tmp = 0, *result = NULL; size_t actual = 0; va_list vp_copy; va_copy(vp_copy, vp); actual = vsnprintf(&tmp, 1, fmt, vp_copy); result = calloc(1, actual + 1); if (!result) err(1, "calloc"); vsnprintf(result, actual + 1, fmt, vp); return result; } char * gcli_asprintf(const char *const fmt, ...) { char *result; va_list vp; va_start(vp, fmt); result = gcli_vasprintf(fmt, vp); va_end(vp); return result; } char * gcli_strndup(const char *it, size_t len) { char *result = NULL; char const *tmp = NULL; size_t actual = 0; if (!len) return NULL; tmp = it; while (tmp[actual++] && actual < len); result = calloc(1, actual + 1); memcpy(result, it, actual); return result; } char * gcli_join_with(char const *const items[], size_t const items_size, char const *sep) { char *buffer = NULL; size_t buffer_size = 0; size_t bufoff = 0; size_t sep_size = 0; sep_size = strlen(sep); /* this works because of the null terminator at the end */ for (size_t i = 0; i < items_size; ++i) { buffer_size += strlen(items[i]) + sep_size; } buffer = calloc(1, buffer_size); if (!buffer) return NULL; for (size_t i = 0; i < items_size; ++i) { size_t len = strlen(items[i]); memcpy(buffer + bufoff, items[i], len); if (i != items_size - 1) memcpy(&buffer[bufoff + len], sep, sep_size); bufoff += len + sep_size; } return buffer; } gcli-2.9.1/src/port/sv.c000066400000000000000000000077321507017207500150250ustar00rootroot00000000000000/* * Copyright 2021-2025 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 gcli_sv gcli_sv_append(gcli_sv this, gcli_sv const that) { /* Allocate one byte more as we're going to manually zero-terminate the result * down in get_message */ this.data = realloc(this.data, this.length + that.length + 1); memcpy(this.data + this.length, that.data, that.length); this.length += that.length; return this; } char * gcli_sv_to_cstr(gcli_sv it) { return gcli_strndup(it.data, it.length); } bool gcli_sv_eq_to(const gcli_sv this, const char *that) { size_t len = strlen(that); if (len != this.length) return false; return strncmp(this.data, that, len) == 0; } gcli_sv gcli_sv_fmt(const char *fmt, ...) { char tmp = 0; va_list vp; gcli_sv result = {0}; va_start(vp, fmt); result.length = vsnprintf(&tmp, 1, fmt, vp); va_end(vp); result.data = calloc(1, result.length + 1); va_start(vp, fmt); vsnprintf(result.data, result.length + 1, fmt, vp); va_end(vp); return result; } gcli_sv gcli_sv_trim_front(gcli_sv it) { if (it.length == 0) return it; // TODO: not utf-8 aware while (it.length > 0) { if (!isspace(*it.data)) break; it.data++; it.length--; } return it; } bool gcli_sv_has_prefix(gcli_sv it, const char *prefix) { size_t len = strlen(prefix); if (it.length < len) return false; return strncmp(it.data, prefix, len) == 0; } gcli_sv gcli_sv_chop_until(gcli_sv *it, char c) { gcli_sv result = *it; result.length = 0; while (it->length > 0) { if (*it->data == c) break; it->data++; it->length--; result.length++; } return result; } static gcli_sv gcli_sv_trim_end(gcli_sv it) { while (it.length > 0 && isspace(it.data[it.length - 1])) it.length--; return it; } gcli_sv gcli_sv_trim(gcli_sv it) { return gcli_sv_trim_front(gcli_sv_trim_end(it)); } gcli_sv gcli_sv_chop_to_last(gcli_sv *it, char sep) { gcli_sv result = *it; while (result.length) { if (result.data[--result.length] == sep) break; } it->length -= result.length; it->data += result.length; return result; } gcli_sv gcli_sv_strip_suffix(gcli_sv input, const char *suffix) { gcli_sv expected_suffix = SV((char *)suffix); if (input.length < expected_suffix.length) return input; gcli_sv actual_suffix = gcli_sv_from_parts( input.data + input.length - expected_suffix.length, expected_suffix.length); if (gcli_sv_eq(expected_suffix, actual_suffix)) input.length -= expected_suffix.length; return input; } bool gcli_sv_eq(gcli_sv this, gcli_sv that) { if (this.length != that.length) return false; return strncmp(this.data, that.data, this.length) == 0; } gcli-2.9.1/src/port/util.c000066400000000000000000000051101507017207500153360ustar00rootroot00000000000000/* * Copyright 2021-2025 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 bool gcli_yesno(const char *fmt, ...) { char tmp = 0; va_list vp; gcli_sv message = {0}; bool result = false; va_start(vp, fmt); message.length = vsnprintf(&tmp, 1, fmt, vp); va_end(vp); message.data = calloc(1, message.length + 1); va_start(vp, fmt); vsnprintf(message.data, message.length + 1, fmt, vp); va_end(vp); do { printf(SV_FMT" [yN] ", SV_ARGS(message)); char c = getchar(); if (c == 'y' || c == 'Y') { result = true; getchar(); break; } else if (c == 'n' || c == 'N') { getchar(); break; } else if (c == '\n') { break; } getchar(); // consume newline character } while (!feof(stdin)); free(message.data); return result; } int gcli_read_file(char const *path, char **buffer) { FILE *f; size_t len; int rc = 0; /* open file and determine length */ f = fopen(path, "r"); if (!f) return -1; if (fseek(f, 0, SEEK_END) < 0) goto err_seek; len = ftell(f); rewind(f); *buffer = malloc(len + 1); if (fread(*buffer, 1, len, f) != len) { rc = -1; goto err_read; } (*buffer)[len] = '\0'; rc = (int)(len); err_read: err_seek: fclose(f); return rc; } gcli-2.9.1/src/pulls.c000066400000000000000000000232141507017207500145410ustar00rootroot00000000000000/* * Copyright 2021-2025 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 #include void gcli_pulls_free(struct gcli_pull_list *const it) { for (size_t i = 0; i < it->pulls_size; ++i) gcli_pull_free(&it->pulls[i]); gcli_clear_ptr(&it->pulls); it->pulls_size = 0; } int gcli_search_pulls(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_pull_fetch_details const *const details, int const max, struct gcli_pull_list *const out) { gcli_null_check_call(search_pulls, ctx, path, details, max, out); } int gcli_pull_get_diff(struct gcli_ctx *ctx, FILE *stream, struct gcli_path const *const path) { gcli_null_check_call(pull_get_diff, ctx, stream, path); } int gcli_pull_get_commits(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_commit_list *const out) { gcli_null_check_call(get_pull_commits, ctx, path, out); } void gcli_commits_free(struct gcli_commit_list *list) { for (size_t i = 0; i < list->commits_size; ++i) { gcli_clear_ptr(&list->commits[i].sha); gcli_clear_ptr(&list->commits[i].long_sha); gcli_clear_ptr(&list->commits[i].message); gcli_clear_ptr(&list->commits[i].date); gcli_clear_ptr(&list->commits[i].author); gcli_clear_ptr(&list->commits[i].email); } gcli_clear_ptr(&list->commits); list->commits_size = 0; } void gcli_pull_free(struct gcli_pull *const it) { gcli_clear_ptr(&it->author); gcli_clear_ptr(&it->state); gcli_clear_ptr(&it->title); gcli_clear_ptr(&it->body); gcli_clear_ptr(&it->commits_link); gcli_clear_ptr(&it->head_label); gcli_clear_ptr(&it->base_label); gcli_clear_ptr(&it->head_sha); gcli_clear_ptr(&it->base_sha); gcli_clear_ptr(&it->start_sha); gcli_clear_ptr(&it->milestone); gcli_clear_ptr(&it->coverage); gcli_clear_ptr(&it->node_id); gcli_clear_ptr(&it->web_url); for (size_t i = 0; i < it->labels_size; ++i) gcli_clear_ptr(&it->labels[i]); gcli_clear_ptr(&it->labels); for (size_t i = 0; i < it->reviewers_size; ++i) gcli_clear_ptr(&it->reviewers[i]); gcli_clear_ptr(&it->reviewers); for (size_t i = 0; i < it->assignees_size; ++i) gcli_clear_ptr(&it->assignees[i]); gcli_clear_ptr(&it->assignees); } int gcli_get_pull(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_pull *const out) { gcli_null_check_call(get_pull, ctx, path, out); } int gcli_pull_get_checks(struct gcli_ctx *ctx, struct gcli_path const *const path, struct gcli_pull_checks_list *out) { gcli_null_check_call(get_pull_checks, ctx, path, out); } void gcli_pull_checks_free(struct gcli_pull_checks_list *list) { switch (list->forge_type) { case GCLI_FORGE_GITHUB: github_free_checks((struct github_check_list *)list); break; case GCLI_FORGE_GITLAB: gitlab_pipelines_free((struct gitlab_pipeline_list *)list); break; default: assert(0 && "unreachable"); } } int gcli_pull_submit(struct gcli_ctx *ctx, struct gcli_submit_pull_options *opts) { if (opts->automerge) { int const q = gcli_forge(ctx)->pull_quirks; if (q & GCLI_PULL_QUIRK_AUTOMERGE) return gcli_error(ctx, "forge does not support auto-merge"); } gcli_null_check_call(perform_submit_pull, ctx, opts); } int gcli_pull_merge(struct gcli_ctx *ctx, struct gcli_path const *const path, enum gcli_merge_flags flags) { gcli_null_check_call(pull_merge, ctx, path, flags); } int gcli_pull_close(struct gcli_ctx *ctx, struct gcli_path const *const path) { gcli_null_check_call(pull_close, ctx, path); } int gcli_pull_reopen(struct gcli_ctx *ctx, struct gcli_path const *const path) { gcli_null_check_call(pull_reopen, ctx, path); } int gcli_pull_add_labels(struct gcli_ctx *ctx, struct gcli_path const *const pull_path, char const *const labels[], size_t const labels_size) { gcli_null_check_call(pull_add_labels, ctx, pull_path, labels, labels_size); } int gcli_pull_remove_labels(struct gcli_ctx *ctx, struct gcli_path const *const pull_path, char const *const labels[], size_t const labels_size) { gcli_null_check_call(pull_remove_labels, ctx, pull_path, labels, labels_size); } int gcli_pull_set_milestone(struct gcli_ctx *ctx, struct gcli_path const *const pull_path, int milestone_id) { gcli_null_check_call(pull_set_milestone, ctx, pull_path, milestone_id); } int gcli_pull_clear_milestone(struct gcli_ctx *ctx, struct gcli_path const *const pull_path) { gcli_null_check_call(pull_clear_milestone, ctx, pull_path); } int gcli_pull_add_reviewer(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *username) { gcli_null_check_call(pull_add_reviewer, ctx, path, username); } int gcli_pull_assign(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const username) { gcli_null_check_call(pull_assign, ctx, path, username); } int gcli_pull_get_patch(struct gcli_ctx *ctx, FILE *out, struct gcli_path const *const path) { gcli_null_check_call(pull_get_patch, ctx, out, path); } int gcli_pull_set_title(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *new_title) { gcli_null_check_call(pull_set_title, ctx, path, new_title); } int gcli_pull_create_review(struct gcli_ctx *ctx, struct gcli_pull_create_review_details const *details) { gcli_null_check_call(pull_create_review, ctx, details); } char const * gcli_pull_get_meta_by_key(struct gcli_pull_create_review_details const *details, char const *key) { size_t const key_len = strlen(key); struct gcli_review_meta_line *l; TAILQ_FOREACH(l, &details->meta_lines, next) { if (strncmp(l->entry, key, key_len) == 0 && l->entry[key_len] == ' ') return l->entry + key_len + 1; } return NULL; } int gcli_pull_checkout(struct gcli_ctx *ctx, char const *const remote, struct gcli_path const *const pull_path) { gcli_null_check_call(pull_checkout, ctx, remote, pull_path); } int gcli_pull_get_reviews(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_pull_reviews *out) { gcli_null_check_call(pull_get_reviews, ctx, path, out); } void gcli_pull_reviews_free(struct gcli_pull_reviews *it) { struct gcli_pull_review *r; for (size_t i = 0; i < it->reviews_size; ++i) { r = &it->reviews[i]; gcli_clear_ptr(&r->author); gcli_clear_ptr(&r->state); gcli_clear_ptr(&r->body); } gcli_clear_ptr(&it->reviews); it->reviews_size = 0; } static void gcli_pull_review_comment_free(struct gcli_pull_review_comment *c) { gcli_clear_ptr(&c->author); gcli_clear_ptr(&c->body); gcli_clear_ptr(&c->diff_hunk); gcli_clear_ptr(&c->path); } void gcli_pull_review_comments_free(struct gcli_pull_review_comments *it) { for (size_t i = 0; i < it->comments_size; ++i) gcli_pull_review_comment_free(&it->comments[i]); gcli_clear_ptr(&it->comments); it->comments_size = 0; } int gcli_pull_get_review_threads(struct gcli_ctx *ctx, struct gcli_path const *path, struct gcli_pull_review_thread *out) { gcli_null_check_call(pull_get_review_threads, ctx, path, out); } void gcli_pull_review_thread_free(struct gcli_pull_review_thread *thd) { struct gcli_pull_review_comment *c, *c1; c = TAILQ_FIRST(thd); while (c != NULL) { c1 = TAILQ_NEXT(c, next); gcli_pull_review_comment_free(c); gcli_pull_review_thread_free(&c->replies); gcli_clear_ptr(&c); c = c1; } } int gcli_pull_approve(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const message) { struct gcli_pull_create_review_details details = {0}; details.path = *path; details.body = message; details.review_state = GCLI_PULL_APPROVED; return gcli_pull_create_review(ctx, &details); } int gcli_pull_unapprove(struct gcli_ctx *ctx, struct gcli_path const *const path, char const *const message) { struct gcli_pull_create_review_details details = {0}; details.path = *path; details.body = message; details.review_state = GCLI_PULL_UNAPPROVED; return gcli_pull_create_review(ctx, &details); } gcli-2.9.1/src/releases.c000066400000000000000000000060451507017207500152100ustar00rootroot00000000000000/* * Copyright 2021-2025 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 int gcli_get_releases(struct gcli_ctx *ctx, struct gcli_path const *const repo_path, int const max, struct gcli_release_list *const list) { gcli_null_check_call(get_releases, ctx, repo_path, max, list); } void gcli_release_free(struct gcli_release *release) { gcli_clear_ptr(&release->id); gcli_clear_ptr(&release->name); gcli_clear_ptr(&release->body); gcli_clear_ptr(&release->author); gcli_clear_ptr(&release->upload_url); for (size_t i = 0; i < release->assets_size; ++i) { gcli_clear_ptr(&release->assets[i].name); gcli_clear_ptr(&release->assets[i].url); } gcli_clear_ptr(&release->assets); } void gcli_free_releases(struct gcli_release_list *const list) { for (size_t i = 0; i < list->releases_size; ++i) { gcli_release_free(&list->releases[i]); } gcli_clear_ptr(&list->releases); list->releases_size = 0; } int gcli_create_release(struct gcli_ctx *ctx, struct gcli_create_release_args const *release) { gcli_null_check_call(create_release, ctx, release); } int gcli_release_push_asset(struct gcli_ctx *ctx, struct gcli_create_release_args *const release, struct gcli_release_asset_upload const asset) { if (release->assets_size == GCLI_RELEASE_MAX_ASSETS) return gcli_error(ctx, "too many assets"); release->assets[release->assets_size++] = asset; return 0; } int gcli_delete_release(struct gcli_ctx *ctx, struct gcli_path const *const repo_path, char const *const id) { gcli_null_check_call(delete_release, ctx, repo_path, id); } gcli-2.9.1/src/repos.c000066400000000000000000000050761507017207500145400ustar00rootroot00000000000000/* * Copyright 2021-2025 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 int gcli_get_repos(struct gcli_ctx *ctx, char const *owner, int const max, struct gcli_repo_list *const out) { gcli_null_check_call(get_repos, ctx, owner, max, out); } void gcli_repo_free(struct gcli_repo *it) { gcli_clear_ptr(&it->full_name); gcli_clear_ptr(&it->name); gcli_clear_ptr(&it->owner); gcli_clear_ptr(&it->visibility); memset(it, 0, sizeof(*it)); } void gcli_repos_free(struct gcli_repo_list *const list) { for (size_t i = 0; i < list->repos_size; ++i) { gcli_repo_free(&list->repos[i]); } gcli_clear_ptr(&list->repos); list->repos_size = 0; } int gcli_repo_delete(struct gcli_ctx *ctx, struct gcli_path const *const path) { gcli_null_check_call(repo_delete, ctx, path); } int gcli_repo_create(struct gcli_ctx *ctx, struct gcli_repo_create_options const *options, struct gcli_repo *out) { gcli_null_check_call(repo_create, ctx, options, out); } int gcli_repo_set_visibility(struct gcli_ctx *ctx, struct gcli_path const *const path, gcli_repo_visibility vis) { gcli_null_check_call(repo_set_visibility, ctx, path, vis); } gcli-2.9.1/src/sshkeys.c000066400000000000000000000047611507017207500151010ustar00rootroot00000000000000/* * Copyright 2023-2025 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. */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include int gcli_sshkeys_get_keys(struct gcli_ctx *ctx, struct gcli_sshkey_list *out) { gcli_null_check_call(get_sshkeys, ctx, out); } void gcli_sshkeys_free_keys(struct gcli_sshkey_list *list) { for (size_t i = 0; i < list->keys_size; ++i) { gcli_clear_ptr(&list->keys[i].title); gcli_clear_ptr(&list->keys[i].key); } gcli_clear_ptr(&list->keys); list->keys_size = 0; } int gcli_sshkeys_add_key(struct gcli_ctx *ctx, char const *title, char const *public_key_path, struct gcli_sshkey *out) { int rc; char *buffer; struct gcli_forge_descriptor const *const forge = gcli_forge(ctx); if (forge->add_sshkey == NULL) { return gcli_error(ctx, "ssh_add_key is not supported by this forge"); } rc = gcli_read_file(public_key_path, &buffer); if (rc < 0) return rc; rc = forge->add_sshkey(ctx, title, buffer, out); gcli_clear_ptr(&buffer); return rc; } int gcli_sshkeys_delete_key(struct gcli_ctx *ctx, gcli_id const id) { gcli_null_check_call(delete_sshkey, ctx, id); } gcli-2.9.1/src/status.c000066400000000000000000000062671507017207500147360ustar00rootroot00000000000000/* * Copyright 2022-2025 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 int gcli_get_notifications(struct gcli_ctx *ctx, int const max, struct gcli_notification_list *const out) { gcli_null_check_call(get_notifications, ctx, max, out); } void gcli_free_notification(struct gcli_notification *const notification) { gcli_clear_ptr(¬ification->id); gcli_clear_ptr(¬ification->title); gcli_clear_ptr(¬ification->reason); gcli_clear_ptr(¬ification->date); gcli_clear_ptr(¬ification->repository); gcli_path_free(¬ification->target); } void gcli_free_notifications(struct gcli_notification_list *list) { for (size_t i = 0; i < list->notifications_size; ++i) { gcli_free_notification(&list->notifications[i]); } gcli_clear_ptr(&list->notifications); list->notifications_size = 0; } int gcli_notification_mark_as_read(struct gcli_ctx *ctx, char const *id) { gcli_null_check_call(notification_mark_as_read, ctx, id); } static char const * notification_target_type_strings[MAX_GCLI_NOTIFICATION_TARGET] = { [GCLI_NOTIFICATION_TARGET_INVALID] = "Invalid", [GCLI_NOTIFICATION_TARGET_ISSUE] = "Issue", [GCLI_NOTIFICATION_TARGET_PULL_REQUEST] = "Pull Request", [GCLI_NOTIFICATION_TARGET_COMMIT] = "Commit", [GCLI_NOTIFICATION_TARGET_EPIC] = "Epic", [GCLI_NOTIFICATION_TARGET_REPOSITORY] = "Repository", [GCLI_NOTIFICATION_TARGET_RELEASE] = "Release", }; char const * gcli_notification_target_type_str(enum gcli_notification_target_type type) { if (type > MAX_GCLI_NOTIFICATION_TARGET) return NULL; return notification_target_type_strings[type]; } int gcli_notification_get_comments(struct gcli_ctx *ctx, struct gcli_notification const *const notification, struct gcli_comment_list *comments) { gcli_null_check_call(notification_get_comments, ctx, notification, comments); } gcli-2.9.1/src/url.c000066400000000000000000000070371507017207500142110ustar00rootroot00000000000000/* * Copyright 2025 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 int gcli_parse_url(char const *const input, struct gcli_url *out) { size_t n; char *buf, *hd, *tmp; hd = buf = strdup(input); /* check if we can find a scheme */ tmp = strstr(hd, "://"); if (tmp) { out->scheme = gcli_strndup(hd, tmp - hd); hd = tmp + 3; } /* now an optional user, terminated by an '@' */ tmp = strchr(hd, '@'); if (tmp) { out->user = gcli_strndup(hd, tmp - hd); hd = tmp + 1; } /* now the host, terminated by either a ':' or a '/' */ tmp = strpbrk(hd, ":/"); if (!tmp) { gcli_clear_ptr(&buf); gcli_url_free(out); return -1; } out->host = gcli_strndup(hd, tmp - hd); hd = tmp; /* if we found a ':', try and parse a port number! */ if (*hd++ == ':') { n = strspn(hd, "0123456789"); if (n) out->port = gcli_strndup(hd, n); /* skip over parsed characters */ hd += n; /* we should point at a '/' now */ if (*hd == '/') hd += 1; } out->path = strdup(hd); gcli_clear_ptr(&buf); return 0; } void gcli_url_free(struct gcli_url *url) { gcli_clear_ptr(&url->scheme); gcli_clear_ptr(&url->user); gcli_clear_ptr(&url->host); gcli_clear_ptr(&url->port); gcli_clear_ptr(&url->path); } void gcli_url_options_append(char **result, char const *const key, char const *const value) { char *e_key, *e_value, *kvp; size_t kvplen, resultlen; if (key == NULL || value == NULL) return; e_key = gcli_urlencode(key); e_value = gcli_urlencode(value); kvp = gcli_asprintf("%c%s=%s", *result ? '&' : '?', e_key, e_value); gcli_clear_ptr(&e_key); gcli_clear_ptr(&e_value); kvplen = strlen(kvp); resultlen = 0; if (*result) resultlen = strlen(*result); *result = realloc(*result, kvplen + resultlen + 1); memcpy(*result + resultlen, kvp, kvplen + 1); gcli_clear_ptr(&kvp); } void gcli_url_options_appendf(char **result, char const *const key, char const *const fmt, ...) { char *value; va_list vp; if (key == NULL || fmt == NULL) return; va_start(vp, fmt); value = gcli_vasprintf(fmt, vp); va_end(vp); gcli_url_options_append(result, key, value); gcli_clear_ptr(&value); } gcli-2.9.1/src/waitproc.c000066400000000000000000000040301507017207500152250ustar00rootroot00000000000000/* * 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 int gcli_wait_proc_ok(struct gcli_ctx *ctx, pid_t pid) { int status; if (waitpid(pid, &status, WEXITED) == -1) { return gcli_error(ctx, "failed to wait for child process: %s", strerror(errno)); } if (WIFEXITED(status)) { int exit_code = WEXITSTATUS(status); if (exit_code) { return gcli_error(ctx, "child exited with error code %d", WEXITSTATUS(status)); } return 0; } if (WIFSIGNALED(status)) { return gcli_error(ctx, "child exited due to signal %d", WTERMSIG(status)); } return gcli_error(ctx, "unknown child status"); } gcli-2.9.1/templates/000077500000000000000000000000001507017207500144435ustar00rootroot00000000000000gcli-2.9.1/templates/bugzilla/000077500000000000000000000000001507017207500162545ustar00rootroot00000000000000gcli-2.9.1/templates/bugzilla/api.t000066400000000000000000000001111507017207500172030ustar00rootroot00000000000000parser bugzilla_get_error is object of char* select "message" as string; gcli-2.9.1/templates/bugzilla/bugs.t000066400000000000000000000053301507017207500174020ustar00rootroot00000000000000include "gcli/comments.h"; include "gcli/issues.h"; include "gcli/bugzilla/bugs.h"; include "gcli/bugzilla/bugs-parser.h"; parser bugzilla_bug_creator is object of struct gcli_issue with ("real_name" => author as string); parser bugzilla_assigned_to_detail is object of struct gcli_issue with ("name" => use parse_bugzilla_assignee); parser bugzilla_bug_item is object of struct gcli_issue with ("id" => number as id, "summary" => title as string, "creation_time" => created_at as iso8601_time, "creator_detail" => use parse_bugzilla_bug_creator, "status" => state as string, "product" => product as string, "component" => component as string, "status" => state as string, "product" => product as string, "component" => component as string, "assigned_to_detail" => use parse_bugzilla_assigned_to_detail, "url" => url as string); parser bugzilla_bugs is object of struct gcli_issue_list with ("bugs" => issues as array of gcli_issue use parse_bugzilla_bug_item); parser bugzilla_comment is object of struct gcli_comment with ("id" => id as id, "text" => body as string, "creation_time" => date as iso8601_time, "creator" => author as string); parser bugzilla_comments_internal_skip_first is object of struct gcli_comment_list with ("comments" => use parse_bugzilla_comments_array_skip_first); parser bugzilla_comments is object of struct gcli_comment_list with ("bugs" => use parse_bugzilla_bug_comments_dictionary_skip_first); parser bugzilla_comment_text is object of char* select "text" as string; parser bugzilla_comments_internal_only_first is object of char* with ("comments" => use parse_bugzilla_comments_array_only_first); parser bugzilla_bug_op is object of char* with ("bugs" => use parse_bugzilla_bug_comments_dictionary_only_first); parser bugzilla_bug_attachments is object of struct gcli_attachment_list with ("bugs" => use parse_bugzilla_bug_attachments_dict); parser bugzilla_bug_attachment is object of struct gcli_attachment with ("id" => id as id, "summary" => summary as string, "file_name" => file_name as string, "creation_time" => created_at as iso8601_time, "creator" => author as string, "content_type" => content_type as string, "is_obsolete" => is_obsolete as bool_relaxed, "data" => data_base64 as string); parser bugzilla_bug_attachments_internal is 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; parser bugzilla_single_comment is object of struct gcli_comment with ("comments" => use parse_bugzilla_single_comments_array_only_first); gcli-2.9.1/templates/gitea/000077500000000000000000000000001507017207500155345ustar00rootroot00000000000000gcli-2.9.1/templates/gitea/milestones.t000066400000000000000000000010111507017207500200740ustar00rootroot00000000000000include "gcli/milestones.h"; parser gitea_milestone is object of struct gcli_milestone with ("id" => id as id, "title" => title as string, "created_at" => created_at as iso8601_time, "description" => description as string, "state" => state as string, "updated_at" => updated_at as iso8601_time, "open_issues" => open_issues as int, "due_on" => due_date as iso8601_time, "closed_issues" => closed_issues as int); parser gitea_milestones is array of struct gcli_milestone use parse_gitea_milestone; gcli-2.9.1/templates/gitea/status.t000066400000000000000000000012471507017207500172500ustar00rootroot00000000000000include "gcli/status.h"; parser gitea_notification_repository is object of struct gcli_notification with ("full_name" => repository as string); parser gitea_notification_status is object of struct gcli_notification with ("title" => title as string, "type" => type as gitea_notification_target_type, "url" => target as url_path); parser gitea_notification is object of struct gcli_notification with ("id" => id as int_to_string, "repository" => use parse_gitea_notification_repository, "subject" => use parse_gitea_notification_status, "updated_at" => date as string); parser gitea_notifications is array of struct gcli_notification use parse_gitea_notification; gcli-2.9.1/templates/github/000077500000000000000000000000001507017207500157255ustar00rootroot00000000000000gcli-2.9.1/templates/github/api.t000066400000000000000000000001071507017207500166610ustar00rootroot00000000000000parser github_get_error is object of char* select "message" as string; gcli-2.9.1/templates/github/checks.t000066400000000000000000000007541507017207500173600ustar00rootroot00000000000000include "gcli/github/checks.h"; parser github_check is object of struct gcli_github_check with ("name" => name as string, "status" => status as string, "conclusion" => conclusion as string, "started_at" => started_at as string, "completed_at" => completed_at as string, "id" => id as id); parser github_checks is object of struct github_check_list with ("check_runs" => checks as array of gcli_github_check use parse_github_check); gcli-2.9.1/templates/github/comments.t000066400000000000000000000004731507017207500177430ustar00rootroot00000000000000include "gcli/github/comments.h"; parser github_comment is object of struct gcli_comment with ("id" => id as id, "created_at" => date as iso8601_time, "body" => body as string, "user" => author as user); parser github_comments is array of struct gcli_comment use parse_github_comment; gcli-2.9.1/templates/github/forks.t000066400000000000000000000004561507017207500172430ustar00rootroot00000000000000include "gcli/forks.h"; parser github_fork is object of struct gcli_fork with ("full_name" => full_name as string, "owner" => owner as user, "created_at" => date as iso8601_time, "forks_count" => forks as int); parser github_forks is array of struct gcli_fork use parse_github_fork; gcli-2.9.1/templates/github/gists.t000066400000000000000000000013051507017207500172420ustar00rootroot00000000000000include "gcli/json_util.h"; include "gcli/github/gists.h"; parser github_gist_file is object of struct gcli_gist_file with ("filename" => filename as string, "language" => language as string, "raw_url" => url as string, "size" => size as size_t, "type" => type as string); parser github_gist is object of struct gcli_gist with ("owner" => owner as user, "html_url" => url as string, "id" => id as string, "created_at" => date as string, "git_pull_url" => git_pull_url as string, "description" => description as string, "files" => use parse_github_gist_files_idiot_hack); parser github_gists is array of struct gcli_gist use parse_github_gist; gcli-2.9.1/templates/github/issues.t000066400000000000000000000020541507017207500174260ustar00rootroot00000000000000include "gcli/issues.h"; include "gcli/labels.h"; include "templates/github/labels.h"; parser github_issue_milestone is object of struct gcli_issue with ("title" => milestone as string); parser github_issue is object of struct gcli_issue with ("title" => title as string, "state" => state as string, "body" => body as string, "created_at" => created_at as iso8601_time, "number" => number as id, "comments" => comments as int, "user" => author as user, "locked" => locked as bool, "labels" => labels as array of github_label use parse_github_label_text, "assignees" => assignees as array of char* use get_user, "pull_request" => is_pr as github_is_pr, "milestone" => use parse_github_issue_milestone, "html_url" => web_url as string); parser github_issues is array of struct gcli_issue use parse_github_issue; parser github_issue_search_result is object of struct gcli_issue_list with ("items" => issues as array of gcli_issue use parse_github_issue); gcli-2.9.1/templates/github/labels.t000066400000000000000000000006111507017207500173520ustar00rootroot00000000000000include "gcli/github/labels.h"; parser github_label_text is object of char* select "name" as string; parser github_label is object of struct gcli_label with ("id" => id as id, "name" => name as string, "description" => description as string, "color" => colour as github_style_colour); parser github_labels is array of struct gcli_label use parse_github_label; gcli-2.9.1/templates/github/milestones.t000066400000000000000000000010061507017207500202710ustar00rootroot00000000000000include "gcli/milestones.h"; parser github_milestone is object of struct gcli_milestone with ("number" => id as id, "title" => title as string, "created_at" => created_at as iso8601_time, "state" => state as string, "updated_at" => updated_at as iso8601_time, "description" => description as string, "open_issues" => open_issues as int, "closed_issues" => closed_issues as int, "html_url" => web_url as string); parser github_milestones is array of struct gcli_milestone use parse_github_milestone; gcli-2.9.1/templates/github/pulls.t000066400000000000000000000062221507017207500172530ustar00rootroot00000000000000include "gcli/pulls.h"; include "templates/github/labels.h"; parser github_commit_author_field is object of struct gcli_commit with ("name" => author as string, "email" => email as string, "date" => date as string); parser github_commit_commit_field is object of struct gcli_commit with ("message" => message as string, "author" => use parse_github_commit_author_field); parser github_commit is object of struct gcli_commit with ("sha" => long_sha as string, "commit" => use parse_github_commit_commit_field); parser github_commits is array of struct gcli_commit use parse_github_commit; parser github_pull_head is object of struct gcli_pull with ("sha" => head_sha as string, "label" => head_label as string); parser github_branch_label is object of struct gcli_pull with ("label" => base_label as string); parser github_pull_milestone is object of struct gcli_pull with ("title" => milestone as string); parser github_user is object of char* select "login" as string; parser github_pull is object of struct gcli_pull with ("title" => title as string, "state" => state as string, "body" => body as string, "created_at" => created_at as iso8601_time, "number" => number as id, "id" => id as id, "node_id" => node_id as string, "commits" => commits as int, "labels" => labels as array of github_label use parse_github_label_text, "comments" => comments as int, "additions" => additions as int, "deletions" => deletions as int, "changed_files" => changed_files as int, "merged_at" => merged as is_string, "mergeable" => mergeable as bool, "draft" => draft as bool, "user" => author as user, "head" => use parse_github_pull_head, "base" => use parse_github_branch_label, "milestone" => use parse_github_pull_milestone, "requested_reviewers" => reviewers as array of char* use parse_github_user, "assignees" => assignees as array of char* use parse_github_user, "html_url" => web_url as string); 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_pull_search_result is object of struct gcli_pull_list with ("items" => pulls as array of gcli_pull use parse_github_pull); parser github_pull_review is object of struct gcli_pull_review with ("id" => id as id, "user" => author as user, "submitted_at" => submitted_at as iso8601_time, "state" => state as string); parser github_pull_reviews is array of struct gcli_pull_review use parse_github_pull_review; parser github_pull_review_comment is object of struct gcli_pull_review_comment with ("user" => author as user, "id" => id as id, "created_at" => created_at as iso8601_time, "body" => body as string, "diff_hunk" => diff_hunk as string, "path" => path as string, "in_reply_to_id" => in_reply_to as id); parser github_pull_review_comments is array of struct gcli_pull_review_comment use parse_github_pull_review_comment; gcli-2.9.1/templates/github/releases.t000066400000000000000000000013331507017207500177150ustar00rootroot00000000000000include "gcli/releases.h"; parser github_release_asset is object of struct gcli_release_asset with ("browser_download_url" => url as string, "name" => name as string); parser github_release is object of struct gcli_release with ("name" => name as string, "body" => body as string, "id" => id as int_to_string, "author" => author as user, "created_at" => date as iso8601_time, "draft" => draft as bool, "prerelease" => prerelease as bool, "assets" => assets as array of gcli_release_asset use parse_github_release_asset, "upload_url" => upload_url as string); parser github_releases is array of struct gcli_release use parse_github_release; gcli-2.9.1/templates/github/repos.t000066400000000000000000000007521507017207500172460ustar00rootroot00000000000000include "gcli/github/repos.h"; include "gcli/gitea/repos.h"; parser github_repo is object of struct gcli_repo with ("id" => id as id, "full_name" => full_name as string, "name" => name as string, "owner" => owner as user, "created_at" => date as iso8601_time, "visibility" => visibility as string, "private" => visibility as gitea_visibility, "fork" => is_fork as bool); parser github_repos is array of struct gcli_repo use parse_github_repo; gcli-2.9.1/templates/github/status.t000066400000000000000000000013441507017207500174370ustar00rootroot00000000000000include "gcli/github/status.h"; parser github_notification_subject is object of struct gcli_notification with ("title" => title as string, "type" => type as github_notification_target_type, "url" => target as url_path); parser github_notification_repository is object of struct gcli_notification with ("full_name" => repository as string); parser github_notification is object of struct gcli_notification with ("updated_at" => date as string, "id" => id as string, "reason" => reason as string, "subject" => use parse_github_notification_subject, "repository" => use parse_github_notification_repository); parser github_notifications is array of struct gcli_notification use parse_github_notification; gcli-2.9.1/templates/gitlab/000077500000000000000000000000001507017207500157055ustar00rootroot00000000000000gcli-2.9.1/templates/gitlab/api.t000066400000000000000000000003301507017207500166370ustar00rootroot00000000000000include "gcli/gitlab/api.h"; parser gitlab_get_error is object of struct gitlab_error_data with ("error_description" => error_description as string, "error" => error as string, "message" => message as string); gcli-2.9.1/templates/gitlab/comments.t000066400000000000000000000004721507017207500177220ustar00rootroot00000000000000include "gcli/gitlab/comments.h"; parser gitlab_comment is object of struct gcli_comment with ("created_at" => date as iso8601_time, "body" => body as string, "author" => author as user, "id" => id as id); parser gitlab_comments is array of struct gcli_comment use parse_gitlab_comment; gcli-2.9.1/templates/gitlab/forks.t000066400000000000000000000007121507017207500172160ustar00rootroot00000000000000include "gcli/gitlab/forks.h"; parser gitlab_fork_namespace is object of struct gcli_fork with ("full_path" => owner as string); parser gitlab_fork is object of struct gcli_fork with ("path_with_namespace" => full_name as string, "namespace" => use parse_gitlab_fork_namespace, "created_at" => date as iso8601_time, "forks_count" => forks as int); parser gitlab_forks is array of struct gcli_fork use parse_gitlab_fork; gcli-2.9.1/templates/gitlab/issues.t000066400000000000000000000016711507017207500174120ustar00rootroot00000000000000include "gcli/gitlab/issues.h"; parser gitlab_user is object of char* select "username" as string; parser gitlab_issue_milestone is object of struct gcli_issue with ("title" => milestone as string); parser gitlab_issue is object of struct gcli_issue with ("title" => title as string, "state" => state as string, "description" => body as string, "created_at" => created_at as iso8601_time, "iid" => number as id, "user_notes_count" => comments as int, "author" => author as user, "discussion_locked" => locked as bool, "labels" => labels as array of char* use get_string, "assignees" => assignees as array of gitlab_user use parse_gitlab_user, "milestone" => use parse_gitlab_issue_milestone, "web_url" => web_url as string); parser gitlab_issues is array of struct gcli_issue use parse_gitlab_issue; gcli-2.9.1/templates/gitlab/labels.t000066400000000000000000000005021507017207500173310ustar00rootroot00000000000000include "gcli/gitlab/labels.h"; parser gitlab_label is object of struct gcli_label with ("name" => name as string, "description" => description as string, "color" => colour as gitlab_style_colour, "id" => id as id); parser gitlab_labels is array of struct gcli_label use parse_gitlab_label; gcli-2.9.1/templates/gitlab/merge_requests.t000066400000000000000000000066521507017207500211350ustar00rootroot00000000000000include "gcli/gitlab/merge_requests.h"; parser gitlab_mr_milestone is object of struct gcli_pull with ("title" => milestone as string); parser gitlab_mr_head_pipeline is object of struct gcli_pull with ("id" => head_pipeline_id as int, "coverage" => coverage as string); parser gitlab_user_name is object of char* select "username" as string; parser gitlab_diff_refs is object of struct gcli_pull with ("base_sha" => base_sha as string, "head_sha" => head_sha as string, "start_sha" => start_sha as string); parser gitlab_mr is object of struct gcli_pull with ("title" => title as string, "state" => state as string, "description" => body as string, "created_at" => created_at as iso8601_time, "iid" => number as id, "id" => id as id, "labels" => labels as array of char* use get_string, "user_notes_count" => comments as int, "merge_status" => mergeable as gitlab_can_be_merged, "draft" => draft as bool, "author" => author as user, "source_branch" => head_label as string, "target_branch" => base_label as string, "milestone" => use parse_gitlab_mr_milestone, "head_pipeline" => use parse_gitlab_mr_head_pipeline, "assignees" => assignees as array of char* use parse_gitlab_user_name, "reviewers" => reviewers as array of char* use parse_gitlab_user_name, "diff_refs" => use parse_gitlab_diff_refs, "web_url" => web_url as string, "merge_when_pipeline_succeeds" => automerge as bool); parser gitlab_mrs is array of struct gcli_pull use parse_gitlab_mr; parser gitlab_commit is object of struct gcli_commit with ("short_id" => sha as string, "id" => long_sha as string, "title" => message as string, "created_at" => date as string, "author_name" => author as string, "author_email" => email as string); parser gitlab_commits is array of struct gcli_commit use parse_gitlab_commit; parser gitlab_user_id is object of gcli_id select "id" as id; parser gitlab_reviewer_ids is object of struct gitlab_user_id_list with ("reviewers" => users as array of gcli_id use parse_gitlab_user_id); parser gitlab_assignee_ids is object of struct gitlab_user_id_list with ("assignees" => users as array of gcli_id use parse_gitlab_user_id); parser gitlab_diff is object of struct gitlab_diff with ("diff" => diff as string, "new_path" => new_path as string, "old_path" => old_path as string, "a_mode" => a_mode as string, "b_mode" => b_mode as string, "new_file" => new_file as bool, "renamed_file" => renamed_file as bool, "deleted_file" => deleted_file as bool); parser gitlab_diffs is array of struct gitlab_diff use parse_gitlab_diff; parser gitlab_mr_version is object of struct gitlab_mr_version with ("id" => id as id, "base_commit_sha" => base_commit as string, "head_commit_sha" => head_commit as string, "start_commit_sha" => start_commit as string); parser gitlab_mr_version_list is array of struct gitlab_mr_version use parse_gitlab_mr_version; parser gitlab_mr_version_diffs is object of struct gitlab_diff_list with ("diffs" => diffs as array of gitlab_diff use parse_gitlab_diff); gcli-2.9.1/templates/gitlab/milestones.t000066400000000000000000000007711507017207500202610ustar00rootroot00000000000000include "gcli/milestones.h"; parser gitlab_milestone is object of struct gcli_milestone with ("title" => title as string, "id" => id as id, "state" => state as string, "created_at" => created_at as iso8601_time, "description" => description as string, "updated_at" => updated_at as iso8601_time, "due_date" => due_date as iso8601_time, "expired" => expired as bool, "web_url" => web_url as string); parser gitlab_milestones is array of struct gcli_milestone use parse_gitlab_milestone; gcli-2.9.1/templates/gitlab/pipelines.t000066400000000000000000000027501507017207500200660ustar00rootroot00000000000000include "gcli/gitlab/pipelines.h"; parser gitlab_pipeline is object of struct gitlab_pipeline with ("status" => status as string, "created_at" => created_at as iso8601_time, "updated_at" => updated_at as iso8601_time, "ref" => ref as string, "sha" => sha as string, "source" => source as string, "id" => id as id, "web_url" => web_url as string); parser gitlab_pipelines is array of struct gitlab_pipeline use parse_gitlab_pipeline; parser gitlab_job_runner is object of struct gitlab_job with ("name" => runner_name as string, "description" => runner_description as string); parser gitlab_job is object of struct gitlab_job with ("status" => status as string, "stage" => stage as string, "name" => name as string, "ref" => ref as string, "created_at" => created_at as iso8601_time, "started_at" => started_at as iso8601_time, "finished_at" => finished_at as iso8601_time, "runner" => use parse_gitlab_job_runner, "duration" => duration as double, "id" => id as id, "coverage" => coverage as double, "web_url" => web_url as string); parser gitlab_jobs is array of struct gitlab_job use parse_gitlab_job; parser gitlab_pipeline_child is object of struct gitlab_pipeline with ("downstream_pipeline" => use parse_gitlab_pipeline, "name" => name as string); parser gitlab_pipeline_children is array of struct gitlab_pipeline use parse_gitlab_pipeline_child; gcli-2.9.1/templates/gitlab/releases.t000066400000000000000000000013601507017207500176750ustar00rootroot00000000000000include "gcli/gitlab/releases.h"; parser gitlab_release_asset is object of struct gcli_release_asset with ("url" => url as string); parser gitlab_release_assets is object of struct gcli_release with ("sources" => assets as array of gcli_release_asset use parse_gitlab_release_asset); parser gitlab_release is object of struct gcli_release with ("name" => name as string, "tag_name" => id as string, "description" => body as string, "assets" => use parse_gitlab_release_assets, "author" => author as user, "created_at" => date as iso8601_time, "upcoming_release" => prerelease as bool); parser gitlab_releases is array of struct gcli_release use parse_gitlab_release; gcli-2.9.1/templates/gitlab/repos.t000066400000000000000000000007301507017207500172220ustar00rootroot00000000000000include "gcli/gitlab/repos.h"; parser gitlab_repo is object of struct gcli_repo with ("path_with_namespace" => full_name as string, "name" => name as string, "owner" => owner as user, "created_at" => date as iso8601_time, "visibility" => visibility as string, "fork" => is_fork as bool, "id" => id as id); parser gitlab_repos is array of struct gcli_repo use parse_gitlab_repo; gcli-2.9.1/templates/gitlab/snippets.t000066400000000000000000000007221507017207500177400ustar00rootroot00000000000000include "gcli/json_util.h"; include "gcli/gitlab/snippets.h"; parser gitlab_snippet is object of struct gcli_gitlab_snippet with ("title" => title as string, "id" => id as id, "raw_url" => raw_url as string, "created_at" => date as string, "file_name" => filename as string, "author" => author as user, "visibility" => visibility as string); parser gitlab_snippets is array of struct gcli_gitlab_snippet use parse_gitlab_snippet; gcli-2.9.1/templates/gitlab/sshkeys.t000066400000000000000000000004371507017207500175670ustar00rootroot00000000000000include "gcli/sshkeys.h"; parser gitlab_sshkey is object of struct gcli_sshkey with ("title" => title as string, "id" => id as id, "key" => key as string, "created_at" => created_at as iso8601_time); parser gitlab_sshkeys is array of struct gcli_sshkey use parse_gitlab_sshkey; gcli-2.9.1/templates/gitlab/status.t000066400000000000000000000011331507017207500174130ustar00rootroot00000000000000include "gcli/gitlab/status.h"; parser gitlab_project is object of struct gcli_notification with ("path_with_namespace" => repository as string); parser gitlab_todo is object of struct gcli_notification with ("updated_at" => date as string, "action_name" => reason as string, "id" => id as int_to_string, "body" => title as string, "target_type" => type as gitlab_notification_target_type, "project" => use parse_gitlab_project, "target" => target as gitlab_notification_target); parser gitlab_todos is array of struct gcli_notification use parse_gitlab_todo; gcli-2.9.1/tests/000077500000000000000000000000001507017207500136075ustar00rootroot00000000000000gcli-2.9.1/tests/append-url.c000066400000000000000000000062071507017207500160270ustar00rootroot00000000000000/* * Copyright 2025 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 ATF_TC_WITHOUT_HEAD(sanity); ATF_TC_BODY(sanity, tc) { char *options = NULL; gcli_url_options_append(&options, NULL, NULL); ATF_CHECK_EQ(options, NULL); } ATF_TC_WITHOUT_HEAD(one_option); ATF_TC_BODY(one_option, tc) { char *options = NULL; gcli_url_options_append(&options, "foo", "bar"); ATF_REQUIRE(options != NULL); ATF_CHECK_STREQ(options, "?foo=bar"); free(options); } ATF_TC_WITHOUT_HEAD(two_options); ATF_TC_BODY(two_options, tc) { char *options = NULL; gcli_url_options_append(&options, "foo", "bar"); ATF_REQUIRE(options != NULL); ATF_CHECK_STREQ(options, "?foo=bar"); gcli_url_options_append(&options, "baz", "banana"); ATF_REQUIRE(options != NULL); ATF_CHECK_STREQ(options, "?foo=bar&baz=banana"); free(options); } ATF_TC_WITHOUT_HEAD(three_options_with_one_null); ATF_TC_BODY(three_options_with_one_null, tc) { char *options = NULL; gcli_url_options_append(&options, "foo", "bar"); ATF_REQUIRE(options != NULL); ATF_CHECK_STREQ(options, "?foo=bar"); gcli_url_options_append(&options, "peanut", NULL); ATF_REQUIRE(options != NULL); ATF_CHECK_STREQ(options, "?foo=bar"); gcli_url_options_append(&options, "baz", "banana"); ATF_REQUIRE(options != NULL); ATF_CHECK_STREQ(options, "?foo=bar&baz=banana"); free(options); } ATF_TC_WITHOUT_HEAD(urlencoded_options); ATF_TC_BODY(urlencoded_options, tc) { char *options = NULL; gcli_url_options_append(&options, "path", "foo/bar/baz"); ATF_REQUIRE(options != NULL); ATF_CHECK_STREQ(options, "?path=foo%2Fbar%2Fbaz"); free(options); } ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, sanity); ATF_TP_ADD_TC(tp, one_option); ATF_TP_ADD_TC(tp, two_options); ATF_TP_ADD_TC(tp, three_options_with_one_null); ATF_TP_ADD_TC(tp, urlencoded_options); return atf_no_error(); } gcli-2.9.1/tests/base64.c000066400000000000000000000006421507017207500150410ustar00rootroot00000000000000#include #include ATF_TC_WITHOUT_HEAD(simple_decode); ATF_TC_BODY(simple_decode, tc) { char const input[] = "aGVsbG8gd29ybGQ="; char output[sizeof("hello world")] = {0}; int rc = gcli_decode_base64(NULL, input, output, sizeof(output)); ATF_REQUIRE(rc == 0); ATF_CHECK_STREQ(output, "hello world"); } ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, simple_decode); return atf_no_error(); } gcli-2.9.1/tests/bugzilla-parse.c000066400000000000000000000117571507017207500167070ustar00rootroot00000000000000/* * Copyright 2023-2025 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 #include #include "gcli_tests.h" static gcli_forge_type get_bugzilla_forge_type(struct gcli_ctx *ctx) { (void) ctx; return GCLI_FORGE_BUGZILLA; } static struct gcli_ctx * test_context(void) { struct gcli_ctx *ctx; ATF_REQUIRE(gcli_init(&ctx, get_bugzilla_forge_type, NULL, NULL) == NULL); return ctx; } static FILE * open_sample(char const *const name) { FILE *r; char p[4096] = {0}; snprintf(p, sizeof p, "%s/samples/%s", TESTSRCDIR, name); r = fopen(p, "r"); return r; } ATF_TC_WITHOUT_HEAD(simple_bugzilla_issue); ATF_TC_BODY(simple_bugzilla_issue, tc) { struct gcli_issue_list list = {0}; struct gcli_issue const *issue; FILE *f; struct json_stream stream; struct gcli_ctx *ctx = test_context(); ATF_REQUIRE((f = open_sample("bugzilla_simple_bug.json"))); json_open_stream(&stream, f); ATF_REQUIRE(parse_bugzilla_bugs(ctx, &stream, &list) == 0); ATF_REQUIRE_EQ(list.issues_size, 1); issue = &list.issues[0]; ATF_CHECK_EQ(issue->number, 1); ATF_CHECK_STREQ(issue->title, "[aha] [scsi] Toshiba MK156FB scsi drive does not work with 2.0 kernel"); ATF_CHECK_EQ(issue->created_at, 779533801); ATF_CHECK_STREQ(issue->author, "Dave Evans"); ATF_CHECK_STREQ(issue->state, "Closed"); ATF_CHECK_STREQ(issue->product, "Base System"); ATF_CHECK_STREQ(issue->component, "kern"); json_close(&stream); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(bugzilla_comments); ATF_TC_BODY(bugzilla_comments, tc) { FILE *f; struct gcli_comment const *cmt = NULL; struct gcli_comment_list list = {0}; struct gcli_ctx *ctx = test_context(); struct json_stream stream; ATF_REQUIRE((f = open_sample("bugzilla_comments.json"))); json_open_stream(&stream, f); ATF_REQUIRE(parse_bugzilla_comments(ctx, &stream, &list) == 0); json_close(&stream); fclose(f); f = NULL; ATF_REQUIRE_EQ(list.comments_size, 1); cmt = &list.comments[0]; ATF_CHECK_EQ(cmt->id, 1285943); ATF_CHECK_STREQ(cmt->author, "zlei@FreeBSD.org"); ATF_CHECK_EQ(cmt->date, 1701105615); ATF_CHECK(cmt->body != NULL); gcli_comments_free(&list); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(bugzilla_attachments); ATF_TC_BODY(bugzilla_attachments, tc) { FILE *f = NULL; struct gcli_attachment const *it; struct gcli_attachment_list list = {0}; struct gcli_ctx *ctx = test_context(); struct json_stream stream = {0}; ATF_REQUIRE((f = open_sample("bugzilla_attachments.json"))); json_open_stream(&stream, f); ATF_REQUIRE(parse_bugzilla_bug_attachments(ctx, &stream, &list) == 0); ATF_CHECK(list.attachments_size == 2); it = list.attachments; ATF_CHECK_EQ(it->id, 246131); ATF_CHECK_EQ(it->is_obsolete, true); ATF_CHECK_STREQ(it->author, "nsonack@outlook.com"); ATF_CHECK_STREQ(it->content_type, "text/plain"); ATF_CHECK_EQ(it->created_at, 1699129151); ATF_CHECK_STREQ(it->file_name, "0001-devel-open62541-Update-to-version-1.3.8.patch"); ATF_CHECK_STREQ(it->summary, "Patch for updating the port"); it++; ATF_CHECK_EQ(it->id, 246910); ATF_CHECK_EQ(it->is_obsolete, false); ATF_CHECK_STREQ(it->author, "nsonack@outlook.com"); ATF_CHECK_STREQ(it->content_type, "text/plain"); ATF_CHECK_EQ(it->created_at, 1702055406); ATF_CHECK_STREQ(it->file_name, "0001-devel-open62541-Update-to-version-1.3.8.patch"); ATF_CHECK_STREQ(it->summary, "Patch v2 (now for version 1.3.9)"); gcli_attachments_free(&list); json_close(&stream); fclose(f); f = NULL; gcli_destroy(&ctx); } ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, simple_bugzilla_issue); ATF_TP_ADD_TC(tp, bugzilla_comments); ATF_TP_ADD_TC(tp, bugzilla_attachments); return atf_no_error(); } gcli-2.9.1/tests/difftests.c000066400000000000000000001017701507017207500157540ustar00rootroot00000000000000/* * Copyright 2023-2025 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 static FILE * open_sample(char const *const name) { FILE *r; char p[4096] = {0}; snprintf(p, sizeof p, "%s/samples/%s", TESTSRCDIR, name); ATF_REQUIRE((r = fopen(p, "r"))); return r; } ATF_TC_WITHOUT_HEAD(free_patch_cleans_up_properly); ATF_TC_BODY(free_patch_cleans_up_properly, tc) { struct gcli_patch patch = {0}; gcli_free_patch(&patch); ATF_CHECK(patch.prelude == NULL); ATF_CHECK(TAILQ_EMPTY(&patch.diffs)); } ATF_TC_WITHOUT_HEAD(patch_prelude); ATF_TC_BODY(patch_prelude, tc) { struct gcli_patch patch = {0}; struct gcli_diff_parser parser = {0}; char const *const fname = "01_diff_prelude.patch"; FILE *inf = open_sample("01_diff_prelude.patch"); ATF_REQUIRE(gcli_diff_parser_from_file(inf, fname, &parser) == 0); ATF_REQUIRE(gcli_patch_parse_prelude(&parser, &patch) == 0); char const expected_prelude[] = "From 47b40f51cae6cec9a3132f888fd66c21ecb687fa Mon Sep 17 00:00:00 2001\n" "From: Nico Sonack \n" "Date: Sun, 10 Oct 2021 12:23:11 +0200\n" "Subject: [PATCH] Start submission implementation\n" "\n" "---\n" " include/ghcli/pulls.h | 1 +\n" " src/ghcli.c | 55 +++++++++++++++++++++++++++++++++++++++++++\n" " src/pulls.c | 9 +++++++\n" " 3 files changed, 65 insertions(+)\n" "\n"; ATF_REQUIRE(patch.prelude != NULL); ATF_CHECK_STREQ(patch.prelude, expected_prelude); free(patch.prelude); gcli_free_diff_parser(&parser); } ATF_TC_WITHOUT_HEAD(empty_patch_should_not_fail); ATF_TC_BODY(empty_patch_should_not_fail, tc) { struct gcli_patch patch = {0}; struct gcli_diff_parser parser = {0}; char zeros[] = ""; ATF_REQUIRE(gcli_diff_parser_from_buffer(zeros, sizeof zeros, "zeros", &parser) == 0); ATF_REQUIRE(gcli_parse_patch(&parser, &patch) == 0); ATF_REQUIRE(patch.prelude != NULL); ATF_CHECK_STREQ(patch.prelude, ""); gcli_free_diff_parser(&parser); } ATF_TC_WITHOUT_HEAD(empty_hunk_should_not_fault); ATF_TC_BODY(empty_hunk_should_not_fault, tc) { struct gcli_diff diff = {0}; struct gcli_diff_parser parser = {0}; char input[] = ""; ATF_REQUIRE(gcli_diff_parser_from_buffer(input, sizeof input, "testinput", &parser) == 0); /* Expect this to error out because there is no diff --git marker */ ATF_REQUIRE(gcli_parse_diff(&parser, &diff) < 0); gcli_free_diff_parser(&parser); } ATF_TC_WITHOUT_HEAD(parse_simple_diff); ATF_TC_BODY(parse_simple_diff, tc) { struct gcli_diff diff = {0}; struct gcli_diff_parser parser = {0}; struct gcli_diff_hunk *hunk = NULL; char zeros[] = "diff --git a/README b/README\n" "index 8befdf0..d193b83 100644\n" "--- a/README\n" "+++ b/README\n" "@@ -3,3 +3,5 @@ This is just a placeholder\n" " Test test test\n" " \n" " foo\n" "+\n" "+Hello World\n"; ATF_REQUIRE(gcli_diff_parser_from_buffer(zeros, sizeof zeros, "zeros", &parser) == 0); ATF_REQUIRE(gcli_parse_diff(&parser, &diff) == 0); ATF_CHECK_STREQ(diff.file_a, "README"); ATF_CHECK_STREQ(diff.file_b, "README"); ATF_CHECK_STREQ(diff.hash_a, "8befdf0"); ATF_CHECK_STREQ(diff.hash_b, "d193b83"); ATF_CHECK_STREQ(diff.file_mode, "100644"); ATF_CHECK_STREQ(diff.r_file, "README"); ATF_CHECK_STREQ(diff.a_file, "README"); /* Complete parse */ ATF_CHECK(parser.hd[0] == '\0'); /* Check hunks */ hunk = TAILQ_FIRST(&diff.hunks); ATF_REQUIRE(hunk != NULL); ATF_CHECK(hunk->range_a_start == 3); ATF_CHECK(hunk->range_a_length == 5); ATF_CHECK(hunk->range_r_start == 3); ATF_CHECK(hunk->range_r_length == 3); ATF_CHECK(hunk->diff_line_offset == 1); ATF_CHECK_STREQ(hunk->context_info, "This is just a placeholder"); ATF_CHECK_STREQ(hunk->body, " Test test test\n" " \n" " foo\n" "+\n" "+Hello World\n"); /* This is the end of the list of hunks */ hunk = TAILQ_NEXT(hunk, next); ATF_CHECK(hunk == NULL); gcli_free_diff(&diff); gcli_free_diff_parser(&parser); } ATF_TC_WITHOUT_HEAD(diff_with_two_hunks); ATF_TC_BODY(diff_with_two_hunks, tp) { struct gcli_diff_parser parser = {0}; struct gcli_diff diff = {0}; char input[] = "diff --git a/README b/README\n" "index d193b83..21af54a 100644\n" "--- a/README\n" "+++ b/README\n" "@@ -1,3 +1,5 @@\n" "+Hunk 1\n" "+\n" " This is just a placeholder\n" " \n" " Test test test\n" "@@ -5,3 +7,5 @@ Test test test\n" " fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooobar\n" " \n" " Hello World\n" "+\n" "+Hunk 2\n" " \n"; ATF_REQUIRE(gcli_diff_parser_from_buffer(input, sizeof input, "", &parser) == 0); ATF_REQUIRE(gcli_parse_diff(&parser, &diff) == 0); ATF_CHECK_STREQ(diff.file_a, "README"); ATF_CHECK_STREQ(diff.file_b, "README"); ATF_CHECK_STREQ(diff.hash_a, "d193b83"); ATF_CHECK_STREQ(diff.hash_b, "21af54a"); ATF_CHECK_STREQ(diff.file_mode, "100644"); ATF_CHECK_STREQ(diff.r_file, "README"); ATF_CHECK_STREQ(diff.a_file, "README"); struct gcli_diff_hunk *h = NULL; /* First hunk of this diff */ h = TAILQ_FIRST(&diff.hunks); ATF_REQUIRE(h != NULL); ATF_CHECK(h->range_r_start == 1); ATF_CHECK(h->range_r_length == 3); ATF_CHECK(h->range_a_start == 1); ATF_CHECK(h->range_a_length == 5); ATF_CHECK(h->diff_line_offset == 1); ATF_CHECK_STREQ(h->context_info, ""); ATF_CHECK_STREQ(h->body, "+Hunk 1\n" "+\n" " This is just a placeholder\n" " \n" " Test test test\n"); /* Second hunk */ h = TAILQ_NEXT(h, next); ATF_REQUIRE(h != NULL); ATF_CHECK(h->range_r_start == 5); ATF_CHECK(h->range_r_length == 3); ATF_CHECK(h->range_a_start == 7); ATF_CHECK(h->range_a_length == 5); ATF_CHECK(h->diff_line_offset == 7); ATF_CHECK_STREQ(h->context_info, "Test test test"); ATF_CHECK_STREQ(h->body, " fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooobar\n" " \n" " Hello World\n" "+\n" "+Hunk 2\n" " \n"); /* This must be the end of the hunks */ h = TAILQ_NEXT(h, next); ATF_CHECK(h == NULL); gcli_free_diff(&diff); gcli_free_diff_parser(&parser); } ATF_TC_WITHOUT_HEAD(two_diffs_with_one_hunk_each); ATF_TC_BODY(two_diffs_with_one_hunk_each, tc) { char const diff_data[] = "diff --git a/README b/README\n" "index d193b83..ad32368 100644\n" "--- a/README\n" "+++ b/README\n" "@@ -1,3 +1,5 @@\n" "+Hunk 1\n" "+\n" " This is just a placeholder\n" " \n" " Test test test\n" "diff --git a/foo.json b/foo.json\n" "new file mode 100644\n" "index 0000000..3be9217\n" "--- /dev/null\n" "+++ b/foo.json\n" "@@ -0,0 +1 @@\n" "+wat\n"; struct gcli_patch patch = {0}; struct gcli_diff_parser parser = {0}; struct gcli_diff *diff; struct gcli_diff_hunk *hunk; ATF_REQUIRE(gcli_diff_parser_from_buffer( diff_data, sizeof(diff_data), "diff_data", &parser) == 0); ATF_REQUIRE(gcli_parse_patch(&parser, &patch) == 0); diff = TAILQ_FIRST(&patch.diffs); ATF_REQUIRE(diff != NULL); ATF_CHECK_STREQ(diff->file_a, "README"); ATF_CHECK_STREQ(diff->file_b, "README"); ATF_CHECK_STREQ(diff->hash_a, "d193b83"); ATF_CHECK_STREQ(diff->hash_b, "ad32368"); ATF_CHECK_STREQ(diff->file_mode, "100644"); ATF_CHECK_STREQ(diff->r_file, "README"); ATF_CHECK_STREQ(diff->a_file, "README"); ATF_CHECK(diff->new_file_mode == 0); hunk = TAILQ_FIRST(&diff->hunks); ATF_REQUIRE(hunk != NULL); ATF_CHECK_STREQ(hunk->context_info, ""); ATF_CHECK(hunk->range_r_start == 1); ATF_CHECK(hunk->range_r_length == 3); ATF_CHECK(hunk->range_a_start == 1); ATF_CHECK(hunk->range_a_length == 5); ATF_CHECK(hunk->diff_line_offset == 1); ATF_CHECK_STREQ(hunk->body, "+Hunk 1\n" "+\n" " This is just a placeholder\n" " \n" " Test test test\n"); hunk = TAILQ_NEXT(hunk, next); ATF_CHECK(hunk == NULL); /* last one in this list */ /* Second diff */ diff = TAILQ_NEXT(diff, next); ATF_REQUIRE(diff != NULL); ATF_CHECK_STREQ(diff->file_a, "foo.json"); ATF_CHECK_STREQ(diff->file_b, "foo.json"); ATF_CHECK_STREQ(diff->hash_a, "0000000"); ATF_CHECK_STREQ(diff->hash_b, "3be9217"); ATF_CHECK_STREQ(diff->r_file, "/dev/null"); ATF_CHECK_STREQ(diff->a_file, "foo.json"); ATF_CHECK(diff->new_file_mode == 0100644); hunk = TAILQ_FIRST(&diff->hunks); ATF_REQUIRE(hunk != NULL); ATF_CHECK(hunk->range_r_start == 0); ATF_CHECK(hunk->range_r_length == 0); ATF_CHECK(hunk->range_a_start == 1); ATF_CHECK(hunk->range_a_length == 0); ATF_CHECK(hunk->diff_line_offset == 1); ATF_CHECK_STREQ(hunk->body, "+wat\n"); /* This must be the last hunk in the diff */ hunk = TAILQ_NEXT(hunk, next); ATF_CHECK(hunk == NULL); gcli_free_patch(&patch); gcli_free_diff_parser(&parser); } ATF_TC_WITHOUT_HEAD(full_patch); ATF_TC_BODY(full_patch, tc) { struct gcli_patch patch = {0}; struct gcli_diff_parser parser = {0}; char const *const fname = "01_diff_prelude.patch"; FILE *inf = open_sample("01_diff_prelude.patch"); ATF_REQUIRE(gcli_diff_parser_from_file(inf, fname, &parser) == 0); ATF_REQUIRE(gcli_parse_patch(&parser, &patch) == 0); gcli_free_patch(&patch); gcli_free_diff_parser(&parser); } ATF_TC_WITHOUT_HEAD(simple_patch_with_comments); ATF_TC_BODY(simple_patch_with_comments, tc) { struct gcli_patch patch = {0}; struct gcli_diff_parser parser = {0}; struct gcli_diff_comments comments = {0}; char const *const fname = "simple_patch_with_comments.patch"; FILE *inf = open_sample(fname); ATF_REQUIRE(gcli_diff_parser_from_file(inf, fname, &parser) == 0); ATF_REQUIRE(gcli_parse_patch(&parser, &patch) == 0); TAILQ_INIT(&comments); ATF_REQUIRE(gcli_patch_get_comments(&patch, &comments) == 0); { struct gcli_diff_comment *comment = TAILQ_FIRST(&comments); ATF_REQUIRE(comment != NULL); ATF_CHECK_STREQ(comment->after.filename, "include/ghcli/pulls.h"); ATF_CHECK(comment->after.start_row == 60); ATF_CHECK(comment->diff_line_offset == 4); ATF_CHECK_STREQ(comment->comment, "This is a comment on line 60.\n"); ATF_CHECK_STREQ(comment->diff_text, "+void ghcli_pr_submit(const char *from, const char *to," " int in_draft);\n"); comment = TAILQ_NEXT(comment, next); ATF_CHECK(comment == NULL); } gcli_free_patch(&patch); gcli_free_diff_parser(&parser); } ATF_TC_WITHOUT_HEAD(diff_with_two_hunks_and_comments); ATF_TC_BODY(diff_with_two_hunks_and_comments, tc) { struct gcli_patch patch = {0}; struct gcli_diff_parser parser = {0}; struct gcli_diff_comments comments = {0}; char const input[] = "diff --git a/README b/README\n" "index d193b83..ad32368 100644\n" "--- a/README\n" "+++ b/README\n" "@@ -1,5 +1,6 @@\n" " line 1\n" " line 2\n" "+new line here\n" "This is the first comment\n" " line 3\n" " \n" " \n" "@@ -18,4 +19,5 @@\n" " \n" " line 19\n" " line 20\n" "This is the other comment\n" "+another addition right here\n" " line 21\n"; ATF_REQUIRE(gcli_diff_parser_from_buffer(input, sizeof(input), "input", &parser) == 0); ATF_REQUIRE(gcli_parse_patch(&parser, &patch) == 0); TAILQ_INIT(&comments); ATF_REQUIRE(gcli_patch_get_comments(&patch, &comments) == 0); { struct gcli_diff_comment *comment; comment = TAILQ_FIRST(&comments); ATF_REQUIRE(comment != NULL); ATF_CHECK_STREQ(comment->after.filename, "README"); ATF_CHECK_STREQ(comment->comment, "This is the first comment\n"); ATF_CHECK(comment->after.start_row == 4); ATF_CHECK(comment->diff_line_offset == 4); comment = TAILQ_NEXT(comment, next); ATF_REQUIRE(comment != NULL); ATF_CHECK_STREQ(comment->after.filename, "README"); ATF_CHECK_STREQ(comment->comment, "This is the other comment\n"); ATF_CHECK(comment->after.start_row == 22); ATF_CHECK(comment->diff_line_offset == 11); comment = TAILQ_NEXT(comment, next); ATF_REQUIRE(comment == NULL); } gcli_free_patch(&patch); gcli_free_diff_parser(&parser); } ATF_TC_WITHOUT_HEAD(patch_with_two_diffs_and_comments); ATF_TC_BODY(patch_with_two_diffs_and_comments, tc) { struct gcli_patch patch = {0}; struct gcli_diff_parser parser = {0}; struct gcli_diff_comments comments = {0}; char const input[] = "diff --git a/bar b/bar\n" "index 6c31faf..84b646b 100644\n" "--- a/bar\n" "+++ b/bar\n" "@@ -20,5 +20,5 @@ line 4\n" " \n" " \n" " \n" "I do not like this change.\n" "-line 5\n" "+line 69\n" " line 6\n" "diff --git a/foo b/foo\n" "index 9c2a709..d719e9c 100644\n" "--- a/foo\n" "+++ b/foo\n" "@@ -2,3 +2,12 @@ line 1\n" " line 2\n" " line 3\n" " line 4\n" "+\n" "+\n" "+\n" "+\n" "+\n" "This is horrible\n" "Get some help!\n" "+\n" "+\n" "+\n" "+This is a random line.\n"; ATF_REQUIRE(gcli_diff_parser_from_buffer(input, sizeof(input), "input", &parser) == 0); ATF_REQUIRE(gcli_parse_patch(&parser, &patch) == 0); TAILQ_INIT(&comments); ATF_REQUIRE(gcli_patch_get_comments(&patch, &comments) == 0); { struct gcli_diff_comment *c = NULL; /* First comment */ c = TAILQ_FIRST(&comments); ATF_REQUIRE(c != NULL); ATF_CHECK_STREQ(c->comment, "I do not like this change.\n"); ATF_CHECK_STREQ(c->after.filename, "bar"); ATF_CHECK_STREQ(c->before.filename, "bar"); ATF_CHECK(c->after.start_row == 23); ATF_CHECK(c->after.end_row == 23); ATF_CHECK(c->before.start_row == 23); ATF_CHECK(c->before.end_row == 23); ATF_CHECK(c->diff_line_offset == 4); /* Second comment */ c = TAILQ_NEXT(c, next); ATF_REQUIRE(c != NULL); ATF_CHECK_STREQ(c->after.filename, "foo"); ATF_CHECK_STREQ(c->comment, "This is horrible\nGet some help!\n"); ATF_CHECK(c->after.start_row == 10); ATF_CHECK(c->diff_line_offset == 9); /* End */ c = TAILQ_NEXT(c, next); ATF_CHECK(c == NULL); } } ATF_TC_WITHOUT_HEAD(single_diff_with_multiline_comment); ATF_TC_BODY(single_diff_with_multiline_comment, tc) { struct gcli_diff_parser parser = {0}; struct gcli_patch patch = {0}; struct gcli_diff_comments comments = {0}; struct gcli_diff_comment *c; char const input[] = "diff --git a/include/ghcli/pulls.h b/include/ghcli/pulls.h\n" "index 30a503cf..05d233eb 100644\n" "--- a/include/ghcli/pulls.h\n" "+++ b/include/ghcli/pulls.h\n" "@@ -57,5 +57,6 @@ int ghcli_get_prs(const char *org, const char *reponame, bool all, ghcli_pull *\n" " void ghcli_print_pr_table(FILE *stream, ghcli_pull *pulls, int pulls_size);\n" " void ghcli_print_pr_diff(FILE *stream, const char *org, const char *reponame, int pr_number);\n" " void ghcli_pr_summary(FILE *stream, const char *org, const char *reponame, int pr_number);\n" " \n" "This is a comment from line 61 to 62\n" "{\n" "+void ghcli_pr_submit(const char *from, const char *to, int in_draft);\n" " \n" "}\n" " #endif /* PULLS_H */\n"; ATF_REQUIRE(gcli_diff_parser_from_buffer(input, sizeof(input), "input", &parser) == 0); ATF_REQUIRE(gcli_parse_patch(&parser, &patch) == 0); TAILQ_INIT(&comments); ATF_REQUIRE(gcli_patch_get_comments(&patch, &comments) == 0); c = TAILQ_FIRST(&comments); ATF_REQUIRE(c != NULL); ATF_CHECK(c->after.start_row == 61); ATF_CHECK(c->after.end_row == 62); ATF_CHECK_STREQ(c->comment, "This is a comment from line 61 to 62\n"); c = TAILQ_NEXT(c, next); ATF_CHECK(c == NULL); gcli_free_patch(&patch); gcli_free_diff_parser(&parser); } ATF_TC_WITHOUT_HEAD(line_removals_offset_bug); ATF_TC_BODY(line_removals_offset_bug, tc) { struct gcli_diff_parser parser = {0}; struct gcli_patch patch = {0}; struct gcli_diff_comments comments = {0}; struct gcli_diff_comment *c; char const input[] = "diff --git a/include/ghcli/pulls.h b/include/ghcli/pulls.h\n" "index 30a503cf..05d233eb 100644\n" "--- a/include/ghcli/pulls.h\n" "+++ b/include/ghcli/pulls.h\n" "@@ -42,4 +42,3 @@ blah\n" " \n" "Test\n" "{\n" "-\n" "}\n" " \n" "Another comment\n" "{\n" " Failure should be here\n" "}\n"; ATF_REQUIRE(gcli_diff_parser_from_buffer(input, sizeof(input), "input", &parser) == 0); ATF_REQUIRE(gcli_parse_patch(&parser, &patch) == 0); TAILQ_INIT(&comments); ATF_REQUIRE(gcli_patch_get_comments(&patch, &comments) == 0); c = TAILQ_FIRST(&comments); ATF_REQUIRE(c != NULL); ATF_CHECK(c->after.start_row == 43); ATF_CHECK(c->after.end_row == 43); ATF_CHECK(c->diff_line_offset == 2); c = TAILQ_NEXT(c, next); ATF_REQUIRE(c != NULL); ATF_CHECK(c->after.start_row == 44); ATF_CHECK(c->after.end_row == 44); ATF_CHECK(c->diff_line_offset == 5); c = TAILQ_NEXT(c, next); ATF_CHECK(c == NULL); gcli_free_patch(&patch); gcli_free_diff_parser(&parser); } ATF_TC_WITHOUT_HEAD(leading_angle_bracket_are_removed_in_comments); ATF_TC_BODY(leading_angle_bracket_are_removed_in_comments, tc) { struct gcli_diff_parser parser = {0}; struct gcli_patch patch = {0}; struct gcli_diff_comments comments = {0}; struct gcli_diff_comment *c; char const input[] = "diff --git a/include/ghcli/pulls.h b/include/ghcli/pulls.h\n" "index 30a503cf..05d233eb 100644\n" "--- a/include/ghcli/pulls.h\n" "+++ b/include/ghcli/pulls.h\n" "@@ -57,5 +57,6 @@ int ghcli_get_prs(const char *org, const char *reponame, bool all, ghcli_pull *\n" " void ghcli_print_pr_table(FILE *stream, ghcli_pull *pulls, int pulls_size);\n" " void ghcli_print_pr_diff(FILE *stream, const char *org, const char *reponame, int pr_number);\n" " void ghcli_pr_summary(FILE *stream, const char *org, const char *reponame, int pr_number);\n" " \n" "> This is a comment on line 60.\n" ">\n" "> This comment extends over multiple lines.\n" "{\n" "+void ghcli_pr_submit(const char *from, const char *to, int in_draft);\n" " \n" "}\n" " #endif /* PULLS_H */\n"; ATF_REQUIRE(gcli_diff_parser_from_buffer(input, sizeof input, "input", &parser) == 0); ATF_REQUIRE(gcli_parse_patch(&parser, &patch) == 0); TAILQ_INIT(&comments); ATF_REQUIRE(gcli_patch_get_comments(&patch, &comments) == 0); c = TAILQ_FIRST(&comments); ATF_REQUIRE(c != NULL); ATF_CHECK_STREQ(c->comment, "This is a comment on line 60.\n" "\n" "This comment extends over multiple lines.\n"); ATF_CHECK_STREQ(c->diff_text, "+void ghcli_pr_submit(const char *from, const char *to, int in_draft);\n" " \n"); gcli_free_patch(&patch); gcli_free_diff_parser(&parser); } static void get_diff_comments(char const *const in, size_t const in_size, struct gcli_diff_comments *out) { struct gcli_diff_parser parser = {0}; struct gcli_patch patch = {0}; ATF_REQUIRE(gcli_diff_parser_from_buffer(in, in_size, "input", &parser) == 0); ATF_REQUIRE(gcli_parse_patch(&parser, &patch) == 0); TAILQ_INIT(out); ATF_REQUIRE(gcli_patch_get_comments(&patch, out) == 0); gcli_free_patch(&patch); gcli_free_diff_parser(&parser); } ATF_TC_WITHOUT_HEAD(old_and_new_are_set_correctly_in_patch); ATF_TC_BODY(old_and_new_are_set_correctly_in_patch, tc) { struct gcli_diff_comments comments = {0}; struct gcli_diff_comment *c; char const input[] = "diff --git a/include/ghcli/pulls.h b/include/ghcli/pulls.h\n" "index 30a503cf..05d233eb 100644\n" "--- a/include/ghcli/pulls.h\n" "+++ b/include/ghcli/pulls.h\n" "@@ -57,5 +57,6 @@ int ghcli_get_prs(const char *org, const char *reponame, bool all, ghcli_pull *\n" " void ghcli_print_pr_table(FILE *stream, ghcli_pull *pulls, int pulls_size);\n" " void ghcli_print_pr_diff(FILE *stream, const char *org, const char *reponame, int pr_number);\n" " void ghcli_pr_summary(FILE *stream, const char *org, const char *reponame, int pr_number);\n" " \n" "> This is a comment on line 60.\n" ">\n" "> This comment extends over multiple lines.\n" "{\n" "+void ghcli_pr_submit(const char *from, const char *to, int in_draft);\n" "}\n" " #endif /* PULLS_H */\n"; get_diff_comments(input, sizeof(input), &comments); c = TAILQ_FIRST(&comments); ATF_REQUIRE(c != NULL); ATF_CHECK(c->before.start_row == 61); ATF_CHECK(c->before.end_row == 61); ATF_CHECK(c->after.start_row == 61); ATF_CHECK(c->after.end_row == 61); ATF_CHECK(c->start_is_in_new == true); ATF_CHECK(c->end_is_in_new == true); } ATF_TC_WITHOUT_HEAD(new_and_old_with_both_deletions_and_additions); ATF_TC_BODY(new_and_old_with_both_deletions_and_additions, tc) { struct gcli_diff_comments comments = {0}; struct gcli_diff_comment *c; char const input[] = "diff --git a/include/ghcli/pulls.h b/include/ghcli/pulls.h\n" "index 30a503cf..05d233eb 100644\n" "--- a/README.md\n" "+++ b/README.md\n" "@@ -6,9 +6,8 @@ Das hier ist nur ein kurzer Test.\n" " Ich füge zum Test hier mal eine neue Zeile ein.\n" " \n" " \n" "> The hell?\n" "{\n" "-\n" "-\n" "-\n" "+This is just a change.\n" "+Across multiple lines.\n" "}\n" " \n" " \n" " This line belongs to a different commit.\n"; get_diff_comments(input, sizeof(input), &comments); c = TAILQ_FIRST(&comments); ATF_REQUIRE(c != NULL); ATF_CHECK(c->before.start_row == 9); ATF_CHECK(c->before.end_row == 11); ATF_CHECK(c->after.start_row == 9); ATF_CHECK(c->after.end_row == 10); ATF_CHECK(c->start_is_in_new == false); ATF_CHECK(c->end_is_in_new == true); } ATF_TC_WITHOUT_HEAD(comment_before_hunk_header); ATF_TC_BODY(comment_before_hunk_header, tc) { struct gcli_diff_parser parser = {0}; struct gcli_patch patch = {0}; struct gcli_diff_comments comments = {0}; char const input[] = "diff --git a/include/ghcli/pulls.h b/include/ghcli/pulls.h\n" "index 30a503cf..05d233eb 100644\n" "--- a/include/ghcli/pulls.h\n" "+++ b/include/ghcli/pulls.h\n" "@@ -57,5 +57,6 @@ int ghcli_get_prs(const char *org, const char *reponame, bool all, ghcli_pull *\n" " void ghcli_print_pr_table(FILE *stream, ghcli_pull *pulls, int pulls_size);\n" " void ghcli_print_pr_diff(FILE *stream, const char *org, const char *reponame, int pr_number);\n" " void ghcli_pr_summary(FILE *stream, const char *org, const char *reponame, int pr_number);\n" " \n" "> Comment here makes no sense whatsoever\n" "@@ -57,5 +57,6 @@ int ghcli_get_prs(const char *org, const char *reponame, bool all, ghcli_pull *\n"; ATF_REQUIRE(gcli_diff_parser_from_buffer(input, sizeof input, "input", &parser) == 0); ATF_REQUIRE(gcli_parse_patch(&parser, &patch) == 0); TAILQ_INIT(&comments); ATF_CHECK(gcli_patch_get_comments(&patch, &comments) < 0); } ATF_TC_WITHOUT_HEAD(simple_patch_series); ATF_TC_BODY(simple_patch_series, tc) { struct gcli_diff_comment *comment; struct gcli_diff_comments comments = {0}; struct gcli_diff_parser parser = {0}; struct gcli_patch *patch; struct gcli_patch_series series = {0}; char const *const fname = "simple_patch_series.patch"; FILE *inf = open_sample(fname); ATF_REQUIRE(gcli_diff_parser_from_file(inf, fname, &parser) == 0); ATF_REQUIRE(gcli_parse_patch_series(&parser, &series) == 0); ATF_REQUIRE((patch = TAILQ_FIRST(&series.patches))); ATF_CHECK_STREQ(patch->prelude, "From 361f83923b9924a3e8796b0ddf03f768e26a1236 Mon Sep 17 00:00:00 2001\n" "From: Nico Sonack \n" "Date: Sat, 16 Sep 2023 22:28:33 +0200\n" "Subject: [PATCH 1/2] Update README.md\n" "\n" "---\n" " README.md | 3 +++\n" " 1 file changed, 3 insertions(+)\n" "\n"); ATF_REQUIRE((patch = TAILQ_NEXT(patch, next))); ATF_CHECK_STREQ(patch->prelude, "From d9cbace712a92fdd0bac4f08b6d42e75069af363 Mon Sep 17 00:00:00 2001\n" "From: Nico Sonack \n" "Date: Wed, 20 Sep 2023 20:09:58 +0200\n" "Subject: [PATCH 2/2] Second commit\n" "\n" "This is the body of the commit.\n" "---\n" " README.md | 8 ++++++++\n" " 1 file changed, 8 insertions(+)\n" "\n"); ATF_CHECK((patch = TAILQ_NEXT(patch, next)) == NULL); TAILQ_INIT(&comments); ATF_REQUIRE(gcli_patch_series_get_comments(&series, &comments) == 0); ATF_REQUIRE((comment = TAILQ_FIRST(&comments))); ATF_CHECK_STREQ(comment->comment, "Why so much whitespace?\n"); ATF_CHECK_STREQ(comment->diff_text, "+\n+\n"); ATF_CHECK(comment->after.start_row == 4); ATF_CHECK(comment->after.end_row == 5); ATF_CHECK(comment->before.start_row == 4); ATF_CHECK(comment->before.end_row == 4); ATF_REQUIRE((comment = TAILQ_NEXT(comment, next))); ATF_CHECK_STREQ(comment->comment, "Why all this whitespace?\n"); ATF_CHECK_STREQ(comment->diff_text, "+\n+\n+\n+\n+\n+\n+\n"); ATF_CHECK(comment->after.start_row == 7); ATF_CHECK(comment->after.end_row == 13); ATF_CHECK(comment->before.start_row == 7); ATF_CHECK(comment->before.end_row == 7); } ATF_TC_WITHOUT_HEAD(patch_series_with_prelude); ATF_TC_BODY(patch_series_with_prelude, tc) { struct gcli_diff_parser parser = {0}; struct gcli_patch_series series = {0}; char const *const fname = "simple_patch_series.patch"; FILE *inf = open_sample(fname); ATF_REQUIRE(gcli_diff_parser_from_file(inf, fname, &parser) == 0); ATF_REQUIRE(gcli_parse_patch_series(&parser, &series) == 0); ATF_CHECK_STREQ(series.prelude, "GCLI: base_sha f00b4rc01dc0fee\n" "This is just a global comment.\n" "\n" "It should not end up in the patch prelude but in the patch series\n" "prelude.\n"); } /* Git object format version 1 is using SHA256 to hash objects. */ ATF_TC_WITHOUT_HEAD(patch_for_git_object_format_version_1); ATF_TC_BODY(patch_for_git_object_format_version_1, tc) { struct gcli_diff_parser parser = {0}; struct gcli_patch_series series = {0}; struct gcli_patch *patch; char const *const fname = "version_1_object_format.patch"; FILE *inf = open_sample(fname); ATF_REQUIRE(gcli_diff_parser_from_file(inf, fname, &parser) == 0); ATF_REQUIRE(gcli_parse_patch_series(&parser, &series) == 0); ATF_REQUIRE((patch = TAILQ_FIRST(&series.patches))); ATF_CHECK_STREQ( patch->commit_hash, "a4545b5e32af1be6ba8f41a80dc885ce6c34d36aa5958dfba05b79ffeef8a084"); } ATF_TC_WITHOUT_HEAD(multiline_change_with_comment); ATF_TC_BODY(multiline_change_with_comment, tc) { struct gcli_diff_parser parser = {0}; struct gcli_patch patch = {0}; struct gcli_diff_comments comments = {0}; struct gcli_diff_comment *comment; char const *const fname = "multiline_change_with_comment.diff"; FILE *inf = open_sample(fname); ATF_REQUIRE(gcli_diff_parser_from_file(inf, fname, &parser) == 0); ATF_REQUIRE(gcli_parse_patch(&parser, &patch) == 0); TAILQ_INIT(&comments); ATF_REQUIRE(gcli_patch_get_comments(&patch, &comments) == 0); ATF_REQUIRE((comment = TAILQ_FIRST(&comments))); ATF_CHECK(comment->before.start_row == 9); ATF_CHECK(comment->before.end_row == 11); ATF_CHECK(comment->after.start_row == 9); ATF_CHECK(comment->after.end_row == 10); } ATF_TC_WITHOUT_HEAD(bug_patch_series_fail_get_comments); ATF_TC_BODY(bug_patch_series_fail_get_comments, tc) { struct gcli_diff_parser parser = {0}; struct gcli_patch_series series = {0}; struct gcli_diff_comments comments = {0}; struct gcli_patch const *p = NULL; char const *const fname = "patch_series_fail_get_comments.patch"; FILE *inf = open_sample(fname); ATF_REQUIRE(gcli_diff_parser_from_file(inf, fname, &parser) == 0); ATF_REQUIRE(gcli_parse_patch_series(&parser, &series) == 0); p = TAILQ_FIRST(&series.patches); { struct gcli_diff const *d = TAILQ_FIRST(&p->diffs); struct gcli_diff_hunk const *h = TAILQ_FIRST(&d->hunks); ATF_CHECK_STREQ(h->body, " # README\n" " \n" " Das hier ist nur ein kurzer Test.\n" "Deine Mutter\n" "{\n" "+\n" "+\n" "+Ich füge zum Test hier mal eine neue Zeile ein.\n" "}\n"); ATF_CHECK(TAILQ_NEXT(h, next) == NULL); } p = TAILQ_NEXT(p, next); { struct gcli_diff const *d = TAILQ_FIRST(&p->diffs); struct gcli_diff_hunk const *h = TAILQ_FIRST(&d->hunks); ATF_CHECK_STREQ(h->body, " \n" " \n" " Ich füge zum Test hier mal eine neue Zeile ein.\n" "+\n" "+\n" "+\n" "+\n" "+\n" "+\n" "+\n" "Naja...\n" "{\n" "+This line belongs to a different commit.\n" "}\n"); ATF_CHECK(TAILQ_NEXT(h, next) == NULL); } p = TAILQ_NEXT(p, next); { struct gcli_diff const *d = TAILQ_FIRST(&p->diffs); struct gcli_diff_hunk const *h = TAILQ_FIRST(&d->hunks); ATF_CHECK_STREQ(h->body, " Ich füge zum Test hier mal eine neue Zeile ein.\n" " \n" " \n" "-\n" "-\n" "-\n" "+This is just a change.\n" "+Across multiple lines.\n" " \n" " \n" " This line belongs to a different commit.\n"); ATF_CHECK(TAILQ_NEXT(h, next) == NULL); } ATF_REQUIRE(gcli_patch_series_get_comments(&series, &comments) == 0); } ATF_TC_WITHOUT_HEAD(bug_short_hunk_range); ATF_TC_BODY(bug_short_hunk_range, tc) { struct gcli_diff_parser parser = {0}; struct gcli_patch patch = {0}; char const input[] = "diff --git a/foo b/foo\n" "index 30a503cf..05d233eb 100644\n" "--- a/foo\n" "+++ b/foo\n" "@@ -1 +1 @@\n" "-wat\n" "+banana\n"; ATF_REQUIRE(gcli_diff_parser_from_buffer(input, sizeof input, "input", &parser) == 0); ATF_REQUIRE(gcli_parse_patch(&parser, &patch) == 0); gcli_free_patch(&patch); gcli_free_diff_parser(&parser); } ATF_TC_WITHOUT_HEAD(bug_no_newline_at_end_of_file); ATF_TC_BODY(bug_no_newline_at_end_of_file, tc) { struct gcli_patch patch = {0}; struct gcli_diff_parser parser = {0}; struct gcli_diff_comments comments = {0}; struct gcli_diff_comment *comment = NULL; char const *const fname = "stuff_with_no_newline_in_diff.diff"; FILE *inf = open_sample(fname); ATF_REQUIRE(gcli_diff_parser_from_file(inf, fname, &parser) == 0); ATF_REQUIRE(gcli_parse_patch(&parser, &patch) == 0); TAILQ_INIT(&comments); ATF_REQUIRE(gcli_patch_get_comments(&patch, &comments) == 0); comment = TAILQ_FIRST(&comments); ATF_REQUIRE(comment); ATF_CHECK(comment->before.start_row == 1); ATF_CHECK(comment->before.end_row == 1); ATF_CHECK(comment->after.start_row == 1); ATF_CHECK(comment->after.end_row == 1); ATF_CHECK_STREQ(comment->comment, "This is a comment\n"); ATF_CHECK_STREQ(comment->diff_text, "-this is a test file\n" "+this is a test file\n" "\\ No newline at end of file\n"); gcli_free_patch(&patch); gcli_free_diff_parser(&parser); } ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, free_patch_cleans_up_properly); ATF_TP_ADD_TC(tp, patch_prelude); ATF_TP_ADD_TC(tp, empty_patch_should_not_fail); ATF_TP_ADD_TC(tp, parse_simple_diff); ATF_TP_ADD_TC(tp, empty_hunk_should_not_fault); ATF_TP_ADD_TC(tp, diff_with_two_hunks); ATF_TP_ADD_TC(tp, two_diffs_with_one_hunk_each); ATF_TP_ADD_TC(tp, full_patch); ATF_TP_ADD_TC(tp, simple_patch_with_comments); ATF_TP_ADD_TC(tp, diff_with_two_hunks_and_comments); ATF_TP_ADD_TC(tp, patch_with_two_diffs_and_comments); ATF_TP_ADD_TC(tp, single_diff_with_multiline_comment); ATF_TP_ADD_TC(tp, line_removals_offset_bug); ATF_TP_ADD_TC(tp, leading_angle_bracket_are_removed_in_comments); ATF_TP_ADD_TC(tp, comment_before_hunk_header); ATF_TP_ADD_TC(tp, simple_patch_series); ATF_TP_ADD_TC(tp, patch_series_with_prelude); ATF_TP_ADD_TC(tp, multiline_change_with_comment); ATF_TP_ADD_TC(tp, old_and_new_are_set_correctly_in_patch); ATF_TP_ADD_TC(tp, new_and_old_with_both_deletions_and_additions); ATF_TP_ADD_TC(tp, patch_for_git_object_format_version_1); ATF_TP_ADD_TC(tp, bug_patch_series_fail_get_comments); ATF_TP_ADD_TC(tp, bug_short_hunk_range); ATF_TP_ADD_TC(tp, bug_no_newline_at_end_of_file); return atf_no_error(); } gcli-2.9.1/tests/gcli_tests.h000066400000000000000000000032701507017207500161220ustar00rootroot00000000000000/* * Copyright 2023 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_TESTS_H #define GCLI_TESTS_H #if defined(HAVE_CONFIG_H) #include #endif /* HAVE_CONFIG_H */ #include /* Helper for making assertions on my custom string views. These * should get removed at some point */ #define ATF_CHECK_SV_EQTO(view, str) \ ATF_CHECK(sn_sv_eq_to((view), str)) #endif /* GCLI_TESTS_H */ gcli-2.9.1/tests/gitea-parse.c000066400000000000000000000027651507017207500161660ustar00rootroot00000000000000#include #include #include #include #include #include #include "gcli_tests.h" #include static gcli_forge_type get_gitea_forge_type(struct gcli_ctx *ctx) { (void) ctx; return GCLI_FORGE_GITEA; } static struct gcli_ctx * test_context(void) { struct gcli_ctx *ctx; ATF_REQUIRE(gcli_init(&ctx, get_gitea_forge_type, NULL, NULL) == NULL); return ctx; } static FILE * open_sample(char const *const name) { FILE *r; char p[4096] = {0}; snprintf(p, sizeof p, "%s/samples/%s", TESTSRCDIR, name); ATF_REQUIRE((r = fopen(p, "r"))); return r; } ATF_TC_WITHOUT_HEAD(gitea_simple_notification); ATF_TC_BODY(gitea_simple_notification, tc) { struct gcli_notification notification = {0}; FILE *sample; struct json_stream stream = {0}; struct gcli_ctx *ctx; ctx = test_context(); sample = open_sample("gitea_simple_notification.json"); json_open_stream(&stream, sample); ATF_REQUIRE(parse_gitea_notification(ctx, &stream, ¬ification) == 0); ATF_CHECK_STREQ(notification.id, "511579"); ATF_CHECK_STREQ(notification.title, "Remove register from C++ sources"); ATF_CHECK(notification.reason == NULL); ATF_CHECK_STREQ(notification.date, "2023-11-24T21:01:50Z"); ATF_CHECK_STREQ(notification.repository, "schilytools/schilytools"); fclose(sample); gcli_free_notification(¬ification); gcli_destroy(&ctx); } ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, gitea_simple_notification); return atf_no_error(); } gcli-2.9.1/tests/github-parse.c000066400000000000000000000260541507017207500163540ustar00rootroot00000000000000/* * Copyright 2023-2025 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 #include #include #include #include #include #include #include "gcli_tests.h" static gcli_forge_type get_github_forge_type(struct gcli_ctx *ctx) { (void) ctx; return GCLI_FORGE_GITHUB; } static struct gcli_ctx * test_context(void) { struct gcli_ctx *ctx; ATF_REQUIRE(gcli_init(&ctx, get_github_forge_type, NULL, NULL) == NULL); return ctx; } static FILE * open_sample(char const *const name) { FILE *r; char p[4096] = {0}; snprintf(p, sizeof p, "%s/samples/%s", TESTSRCDIR, name); r = fopen(p, "r"); return r; } ATF_TC_WITHOUT_HEAD(simple_github_issue); ATF_TC_BODY(simple_github_issue, tc) { struct gcli_issue issue = {0}; FILE *f; struct json_stream stream; struct gcli_ctx *ctx = test_context(); ATF_REQUIRE((f = open_sample("github_simple_issue.json"))); json_open_stream(&stream, f); ATF_REQUIRE(parse_github_issue(ctx, &stream, &issue) == 0); ATF_CHECK_EQ(issue.number, 115); ATF_CHECK_STREQ(issue.title, "consider removing FILE *out from printing functions"); ATF_CHECK_EQ(issue.created_at, 1647965170); ATF_CHECK_STREQ(issue.author, "herrhotzenplotz"); ATF_CHECK_STREQ(issue.state, "closed"); ATF_CHECK(issue.comments == 0); ATF_CHECK(issue.locked == false); ATF_CHECK_STREQ(issue.body, "We use these functions with ghcli only anyways. In " "the GUI stuff we use the datastructures returned by " "the api directly. And If we output, it is stdout " "everywhere.\n"); ATF_CHECK(issue.labels_size == 0); ATF_CHECK(issue.labels == NULL); ATF_CHECK(issue.assignees_size == 0); ATF_CHECK(issue.assignees == NULL); ATF_CHECK(issue.is_pr == 0); ATF_CHECK(!issue.milestone); json_close(&stream); gcli_issue_free(&issue); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(simple_github_pull); ATF_TC_BODY(simple_github_pull, tc) { struct gcli_pull pull = {0}; FILE *f; struct json_stream stream; struct gcli_ctx *ctx = test_context(); ATF_REQUIRE((f = open_sample("github_simple_pull.json"))); json_open_stream(&stream, f); ATF_REQUIRE(parse_github_pull(ctx, &stream, &pull) == 0); ATF_CHECK_STREQ(pull.author, "herrhotzenplotz"); ATF_CHECK_STREQ(pull.state, "closed"); ATF_CHECK_STREQ(pull.title, "mark notifications as read/done"); ATF_CHECK_STREQ(pull.body, "Fixes #99\n"); ATF_CHECK_EQ(pull.created_at, 1647955257); ATF_CHECK_STREQ(pull.head_label, "herrhotzenplotz:99"); ATF_CHECK_STREQ(pull.base_label, "herrhotzenplotz:trunk"); ATF_CHECK_STREQ(pull.head_sha, "a00f475af1e31d56c7a5839508a21e2b76a31e49"); ATF_CHECK(pull.milestone == NULL); ATF_CHECK(pull.id == 886044243); ATF_CHECK(pull.comments == 0); ATF_CHECK(pull.additions == 177); ATF_CHECK(pull.deletions == 82); ATF_CHECK(pull.commits == 6); ATF_CHECK(pull.changed_files == 13); ATF_CHECK(pull.labels == NULL); ATF_CHECK(pull.labels_size == 0); ATF_CHECK(pull.merged == true); ATF_CHECK(pull.mergeable == false); ATF_CHECK(pull.draft == false); json_close(&stream); gcli_pull_free(&pull); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(simple_github_label); ATF_TC_BODY(simple_github_label, tc) { struct gcli_label label = {0}; FILE *f; struct json_stream stream; struct gcli_ctx *ctx = test_context(); ATF_REQUIRE((f = open_sample("github_simple_label.json"))); json_open_stream(&stream, f); ATF_REQUIRE(parse_github_label(ctx, &stream, &label) == 0); ATF_CHECK(label.id == 3431203676); ATF_CHECK_STREQ(label.name, "bug"); ATF_CHECK_STREQ(label.description, "Something isn't working"); ATF_CHECK(label.colour == 0xd73a4a00); json_close(&stream); gcli_free_label(&label); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(simple_github_milestone); ATF_TC_BODY(simple_github_milestone, tc) { struct gcli_milestone milestone = {0}; FILE *f; struct json_stream stream; struct gcli_ctx *ctx = test_context(); ATF_REQUIRE((f = open_sample("github_simple_milestone.json"))); json_open_stream(&stream, f); ATF_REQUIRE(parse_github_milestone(ctx, &stream, &milestone) == 0); ATF_CHECK(milestone.id == 1); ATF_CHECK_STREQ(milestone.title, "Gitlab support"); ATF_CHECK_STREQ(milestone.state, "open"); ATF_CHECK_EQ(milestone.created_at, 1639465325); ATF_CHECK_STREQ(milestone.description, ""); ATF_CHECK_EQ(milestone.updated_at, 1639925383); ATF_CHECK_EQ(milestone.due_date, 0); ATF_CHECK(milestone.expired == false); ATF_CHECK(milestone.open_issues == 0); ATF_CHECK(milestone.closed_issues == 8); json_close(&stream); gcli_free_milestone(&milestone); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(simple_github_release); ATF_TC_BODY(simple_github_release, tc) { struct gcli_release release = {0}; FILE *f; struct json_stream stream; struct gcli_ctx *ctx = test_context(); ATF_REQUIRE((f = open_sample("github_simple_release.json"))); json_open_stream(&stream, f); ATF_REQUIRE(parse_github_release(ctx, &stream, &release) == 0); ATF_CHECK_STREQ(release.id, "116031718"); ATF_CHECK(release.assets_size == 0); ATF_CHECK(release.assets == NULL); ATF_CHECK_STREQ(release.name, "1.2.0"); ATF_CHECK_STREQ(release.body, "# Version 1.2.0\n\nThis is version 1.2.0 of gcli.\n\n## Notes\n\nPlease test and report bugs.\n\nYou can download autotoolized tarballs at: https://herrhotzenplotz.de/gcli/releases/gcli-1.2.0/\n\n## Bug Fixes\n\n- Fix compile error when providing --with-libcurl without any arguments\n- Fix memory leaks in string processing functions\n- Fix missing nul termination in read-file function\n- Fix segmentation fault when clearing the milestone of a PR on Gitea\n- Fix missing documentation for milestone action in issues and pulls\n- Set the 'merged' flag properly when showing Gitlab merge requests\n\n## New features\n\n- Add a config subcommand for managing ssh keys (see gcli-config(1))\n- Show number of comments/notes in list of issues and PRs\n- Add support for milestone management in pull requests\n"); ATF_CHECK_STREQ(release.author, "herrhotzenplotz"); ATF_CHECK_EQ(release.date, 1691739757); ATF_CHECK_STREQ(release.upload_url, "https://uploads.github.com/repos/herrhotzenplotz/gcli/releases/116031718/assets{?name,label}"); ATF_CHECK(release.draft == false); ATF_CHECK(release.prerelease == false); json_close(&stream); gcli_release_free(&release); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(simple_github_repo); ATF_TC_BODY(simple_github_repo, tc) { struct gcli_repo repo = {0}; FILE *f; struct json_stream stream; struct gcli_ctx *ctx = test_context(); ATF_REQUIRE((f = open_sample("github_simple_repo.json"))); json_open_stream(&stream, f); ATF_REQUIRE(parse_github_repo(ctx, &stream, &repo) == 0); ATF_CHECK(repo.id == 415015197); ATF_CHECK_STREQ(repo.full_name, "herrhotzenplotz/gcli"); ATF_CHECK_STREQ(repo.name, "gcli"); ATF_CHECK_STREQ(repo.owner, "herrhotzenplotz"); ATF_CHECK_EQ(repo.date, 1633702815); ATF_CHECK_STREQ(repo.visibility, "public"); ATF_CHECK(repo.is_fork == false); json_close(&stream); gcli_repo_free(&repo); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(simple_github_fork); ATF_TC_BODY(simple_github_fork, tc) { struct gcli_fork fork = {0}; FILE *f; struct json_stream stream; struct gcli_ctx *ctx = test_context(); ATF_REQUIRE((f = open_sample("github_simple_fork.json"))); json_open_stream(&stream, f); ATF_REQUIRE(parse_github_fork(ctx, &stream, &fork) == 0); ATF_CHECK_STREQ(fork.full_name, "gjnoonan/quick-lint-js"); ATF_CHECK_STREQ(fork.owner, "gjnoonan"); ATF_CHECK_EQ(fork.date, 1683783461); ATF_CHECK(fork.forks == 0); json_close(&stream); gcli_fork_free(&fork); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(simple_github_comment); ATF_TC_BODY(simple_github_comment, tc) { struct gcli_comment comment = {0}; FILE *f; struct json_stream stream; struct gcli_ctx *ctx = test_context(); ATF_REQUIRE((f = open_sample("github_simple_comment.json"))); json_open_stream(&stream, f); ATF_REQUIRE(parse_github_comment(ctx, &stream, &comment) == 0); ATF_CHECK(comment.id == 1424392601); ATF_CHECK_STREQ(comment.author, "herrhotzenplotz"); ATF_CHECK_EQ(comment.date, 1675957074); ATF_CHECK_STREQ(comment.body, "Hey,\n\nthe current trunk on Github might be a little outdated. I pushed the staging branch for version 1.0.0 from Gitlab to Github (cleanup-1.0). Could you try again with that branch and see if it still faults at the same place? If it does, please provide a full backtrace and if possible check with valgrind.\n"); json_close(&stream); gcli_comment_free(&comment); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(simple_github_check); ATF_TC_BODY(simple_github_check, tc) { struct gcli_github_check check = {0}; FILE *f; struct json_stream stream; struct gcli_ctx *ctx = test_context(); ATF_REQUIRE((f = open_sample("github_simple_check.json"))); json_open_stream(&stream, f); ATF_REQUIRE(parse_github_check(ctx, &stream, &check) == 0); ATF_CHECK_STREQ(check.name, "test Windows x86"); ATF_CHECK_STREQ(check.status, "completed"); ATF_CHECK_STREQ(check.conclusion, "success"); ATF_CHECK_STREQ(check.started_at, "2023-09-02T06:27:37Z"); ATF_CHECK_STREQ(check.completed_at, "2023-09-02T06:29:11Z"); ATF_CHECK(check.id == 16437184455); json_close(&stream); gcli_github_check_free(&check); gcli_destroy(&ctx); } ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, simple_github_issue); ATF_TP_ADD_TC(tp, simple_github_pull); ATF_TP_ADD_TC(tp, simple_github_label); ATF_TP_ADD_TC(tp, simple_github_milestone); ATF_TP_ADD_TC(tp, simple_github_release); ATF_TP_ADD_TC(tp, simple_github_repo); ATF_TP_ADD_TC(tp, simple_github_fork); ATF_TP_ADD_TC(tp, simple_github_comment); ATF_TP_ADD_TC(tp, simple_github_check); return atf_no_error(); } gcli-2.9.1/tests/gitlab-parse.c000066400000000000000000000273761507017207500163440ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gcli_tests.h" static gcli_forge_type get_gitlab_forge_type(struct gcli_ctx *ctx) { (void) ctx; return GCLI_FORGE_GITLAB; } static struct gcli_ctx * test_context(void) { struct gcli_ctx *ctx; ATF_REQUIRE(gcli_init(&ctx, get_gitlab_forge_type, NULL, NULL) == NULL); return ctx; } static FILE * open_sample(char const *const name) { FILE *r; char p[4096] = {0}; snprintf(p, sizeof p, "%s/samples/%s", TESTSRCDIR, name); ATF_REQUIRE((r = fopen(p, "r"))); return r; } ATF_TC_WITHOUT_HEAD(gitlab_simple_merge_request); ATF_TC_BODY(gitlab_simple_merge_request, tc) { struct json_stream stream = {0}; struct gcli_ctx *ctx = test_context(); FILE *f = open_sample("gitlab_simple_merge_request.json"); struct gcli_pull pull = {0}; json_open_stream(&stream, f); ATF_REQUIRE(parse_gitlab_mr(ctx, &stream, &pull) == 0); ATF_CHECK_STREQ(pull.author, "herrhotzenplotz"); ATF_CHECK_STREQ(pull.state, "merged"); ATF_CHECK_STREQ(pull.title, "Fix test suite"); ATF_CHECK_STREQ(pull.body, "This finally fixes the broken test suite"); ATF_CHECK_EQ(pull.created_at, 1693525070); ATF_CHECK(pull.commits_link == NULL); ATF_CHECK_STREQ(pull.head_label, "fix-test-suite"); ATF_CHECK_STREQ(pull.base_label, "trunk"); ATF_CHECK_STREQ(pull.head_sha, "3eab596a6806434e4a34bb19de12307ab1217af3"); ATF_CHECK(pull.milestone == NULL); ATF_CHECK(pull.id == 246912053); ATF_CHECK(pull.number == 216); ATF_CHECK(pull.comments == 0); // not supported ATF_CHECK(pull.additions == 0); // not supported ATF_CHECK(pull.deletions == 0); // not supported ATF_CHECK(pull.commits == 0); // not supported ATF_CHECK(pull.changed_files == 0); // not supported ATF_CHECK(pull.head_pipeline_id == 989409992); ATF_CHECK(pull.labels_size == 0); ATF_CHECK(pull.labels == NULL); ATF_CHECK(pull.merged == false); ATF_CHECK(pull.mergeable == true); ATF_CHECK(pull.draft == false); json_close(&stream); gcli_pull_free(&pull); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(gitlab_simple_issue); ATF_TC_BODY(gitlab_simple_issue, tc) { struct json_stream stream = {0}; struct gcli_ctx *ctx = test_context(); FILE *f = open_sample("gitlab_simple_issue.json"); struct gcli_issue issue = {0}; json_open_stream(&stream, f); ATF_REQUIRE(parse_gitlab_issue(ctx, &stream, &issue) == 0); ATF_CHECK(issue.number == 193); ATF_CHECK_STREQ(issue.title, "Make notifications API use a list struct containing both the ptr and size"); ATF_CHECK_EQ(issue.created_at, 1691952185); ATF_CHECK_STREQ(issue.author, "herrhotzenplotz"); ATF_CHECK_STREQ(issue.state, "closed"); ATF_CHECK(issue.comments == 2); ATF_CHECK(issue.locked == false); ATF_CHECK_STREQ(issue.body, "That would make some of the code much cleaner"); ATF_CHECK(issue.labels_size == 1); ATF_CHECK_STREQ(issue.labels[0], "good-first-issue"); ATF_CHECK(issue.assignees == NULL); ATF_CHECK(issue.assignees_size == 0); ATF_CHECK(issue.is_pr == 0); ATF_CHECK(issue.milestone == NULL); json_close(&stream); gcli_issue_free(&issue); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(gitlab_simple_label); ATF_TC_BODY(gitlab_simple_label, tc) { struct json_stream stream = {0}; struct gcli_ctx *ctx = test_context(); FILE *f = open_sample("gitlab_simple_label.json"); struct gcli_label label = {0}; json_open_stream(&stream, f); ATF_REQUIRE(parse_gitlab_label(ctx, &stream, &label) == 0); ATF_CHECK(label.id == 24376073); ATF_CHECK_STREQ(label.name, "bug"); ATF_CHECK_STREQ(label.description, "Something isn't working as expected"); ATF_CHECK(label.colour == 0xD73A4A00); json_close(&stream); gcli_free_label(&label); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(gitlab_simple_release); ATF_TC_BODY(gitlab_simple_release, tc) { struct json_stream stream = {0}; struct gcli_ctx *ctx = test_context(); FILE *f = open_sample("gitlab_simple_release.json"); struct gcli_release release = {0}; json_open_stream(&stream, f); ATF_REQUIRE(parse_gitlab_release(ctx, &stream, &release) == 0); /* NOTE(Nico): this silly hack is needed as the fixup is only * applied internally when you fetch the release list using the * public library API. */ gitlab_fixup_release_assets(ctx, &release); /* NOTE(Nico): on gitlab this is the tag name */ ATF_CHECK_STREQ(release.id, "1.2.0"); ATF_CHECK(release.assets_size == 4); { ATF_CHECK_STREQ(release.assets[0].name, "gcli-1.2.0.zip"); ATF_CHECK_STREQ(release.assets[0].url, "https://gitlab.com/herrhotzenplotz/gcli/-/archive/1.2.0/gcli-1.2.0.zip"); } { ATF_CHECK_STREQ(release.assets[1].name, "gcli-1.2.0.tar.gz"); ATF_CHECK_STREQ(release.assets[1].url, "https://gitlab.com/herrhotzenplotz/gcli/-/archive/1.2.0/gcli-1.2.0.tar.gz"); } { ATF_CHECK_STREQ(release.assets[2].name, "gcli-1.2.0.tar.bz2"); ATF_CHECK_STREQ(release.assets[2].url, "https://gitlab.com/herrhotzenplotz/gcli/-/archive/1.2.0/gcli-1.2.0.tar.bz2"); } { ATF_CHECK_STREQ(release.assets[3].name, "gcli-1.2.0.tar"); ATF_CHECK_STREQ(release.assets[3].url, "https://gitlab.com/herrhotzenplotz/gcli/-/archive/1.2.0/gcli-1.2.0.tar"); } ATF_CHECK_STREQ(release.name, "1.2.0"); ATF_CHECK_STREQ(release.body, "# Version 1.2.0\n\nThis is version 1.2.0 of gcli.\n\n## Notes\n\nPlease test and report bugs.\n\nYou can download autotoolized tarballs at: https://herrhotzenplotz.de/gcli/releases/gcli-1.2.0/\n\n## Bug Fixes\n\n- Fix compile error when providing --with-libcurl without any arguments\n- Fix memory leaks in string processing functions\n- Fix missing nul termination in read-file function\n- Fix segmentation fault when clearing the milestone of a PR on Gitea\n- Fix missing documentation for milestone action in issues and pulls\n- Set the 'merged' flag properly when showing Gitlab merge requests\n\n## New features\n\n- Add a config subcommand for managing ssh keys (see gcli-config(1))\n- Show number of comments/notes in list of issues and PRs\n- Add support for milestone management in pull requests\n"); ATF_CHECK_STREQ(release.author, "herrhotzenplotz"); ATF_CHECK_EQ(release.date, 1691740566); ATF_CHECK(release.upload_url == NULL); ATF_CHECK(release.draft == false); ATF_CHECK(release.prerelease == false); json_close(&stream); gcli_release_free(&release); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(gitlab_simple_fork); ATF_TC_BODY(gitlab_simple_fork, tc) { struct json_stream stream = {0}; struct gcli_ctx *ctx = test_context(); FILE *f = open_sample("gitlab_simple_fork.json"); struct gcli_fork fork = {0}; json_open_stream(&stream, f); ATF_REQUIRE(parse_gitlab_fork(ctx, &stream, &fork) == 0); ATF_CHECK_STREQ(fork.full_name, "gjnoonan/gcli"); ATF_CHECK_STREQ(fork.owner, "gjnoonan"); ATF_CHECK_EQ(fork.date, 1664718860); ATF_CHECK(fork.forks == 0); json_close(&stream); gcli_fork_free(&fork); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(gitlab_simple_milestone); ATF_TC_BODY(gitlab_simple_milestone, tc) { struct json_stream stream = {0}; struct gcli_ctx *ctx = test_context(); FILE *f = open_sample("gitlab_simple_milestone.json"); struct gcli_milestone milestone = {0}; json_open_stream(&stream, f); ATF_REQUIRE(parse_gitlab_milestone(ctx, &stream, &milestone) == 0); ATF_CHECK(milestone.id == 2975318); ATF_CHECK_STREQ(milestone.title, "Version 2"); ATF_CHECK_STREQ(milestone.state, "active"); ATF_CHECK_STREQ(milestone.description, "Things that need to be done for version 2"); ATF_CHECK_EQ(milestone.created_at, 1675624100); ATF_CHECK_EQ(milestone.due_date, 0); ATF_CHECK_EQ(milestone.updated_at, 1675624100); ATF_CHECK(milestone.expired == false); json_close(&stream); gcli_free_milestone(&milestone); gcli_destroy(&ctx); /* Ignore open issues and closed issues as they are * github/gitea-specific */ } ATF_TC_WITHOUT_HEAD(gitlab_simple_pipeline); ATF_TC_BODY(gitlab_simple_pipeline, tc) { struct json_stream stream = {0}; struct gcli_ctx *ctx = test_context(); FILE *f = open_sample("gitlab_simple_pipeline.json"); struct gitlab_pipeline pipeline = {0}; json_open_stream(&stream, f); ATF_REQUIRE(parse_gitlab_pipeline(ctx, &stream, &pipeline) == 0); ATF_CHECK(pipeline.id == 989897020); ATF_CHECK_STREQ(pipeline.status, "failed"); ATF_CHECK_EQ(pipeline.created_at, 1693665020); ATF_CHECK_EQ(pipeline.updated_at, 1693665100); ATF_CHECK_STREQ(pipeline.ref, "refs/merge-requests/219/head"); ATF_CHECK_STREQ(pipeline.sha, "742affb88a297a6b34201ad61c8b5b72ec6eb679"); ATF_CHECK_STREQ(pipeline.source, "merge_request_event"); json_close(&stream); gitlab_pipeline_free(&pipeline); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(gitlab_simple_repo); ATF_TC_BODY(gitlab_simple_repo, tc) { struct json_stream stream = {0}; struct gcli_ctx *ctx = test_context(); FILE *f = open_sample("gitlab_simple_repo.json"); struct gcli_repo repo = {0}; json_open_stream(&stream, f); ATF_REQUIRE(parse_gitlab_repo(ctx, &stream, &repo) == 0); ATF_CHECK(repo.id == 34707535); ATF_CHECK_STREQ(repo.full_name, "herrhotzenplotz/gcli"); ATF_CHECK_STREQ(repo.name, "gcli"); ATF_CHECK_STREQ(repo.owner, "herrhotzenplotz"); ATF_CHECK_EQ(repo.date, 1647968279); ATF_CHECK_STREQ(repo.visibility, "public"); ATF_CHECK(repo.is_fork == false); json_close(&stream); gcli_repo_free(&repo); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(gitlab_simple_snippet); ATF_TC_BODY(gitlab_simple_snippet, tc) { struct json_stream stream = {0}; struct gcli_ctx *ctx = test_context(); FILE *f = open_sample("gitlab_simple_snippet.json"); struct gcli_gitlab_snippet snippet = {0}; json_open_stream(&stream, f); ATF_REQUIRE(parse_gitlab_snippet(ctx, &stream, &snippet) == 0); ATF_CHECK(snippet.id == 2141655); ATF_CHECK_STREQ(snippet.title, "darcy-weisbach SPARC64"); ATF_CHECK_STREQ(snippet.filename, "darcy-weisbach SPARC64"); ATF_CHECK_STREQ(snippet.date, "2021-06-28T15:47:36.214Z"); ATF_CHECK_STREQ(snippet.author, "herrhotzenplotz"); ATF_CHECK_STREQ(snippet.visibility, "public"); ATF_CHECK_STREQ(snippet.raw_url, "https://gitlab.com/-/snippets/2141655/raw"); json_close(&stream); gcli_gitlab_snippet_free(&snippet); gcli_destroy(&ctx); } ATF_TC_WITHOUT_HEAD(gitlab_error_token_expired); ATF_TC_BODY(gitlab_error_token_expired, tc) { struct gcli_ctx *ctx = test_context(); struct gcli_fetch_buffer buffer = {0}; char const *errmsg = NULL; buffer.length = gcli_read_file(TESTSRCDIR"/samples/gitlab_token_expired.json", &buffer.data); ATF_REQUIRE(buffer.length); errmsg = gitlab_api_error_string(ctx, &buffer); ATF_CHECK_STREQ(errmsg, "Token has expired."); } ATF_TC_WITHOUT_HEAD(gitlab_error_unauthorised); ATF_TC_BODY(gitlab_error_unauthorised, tc) { struct gcli_ctx *ctx = test_context(); struct gcli_fetch_buffer buffer = {0}; char const *errmsg = NULL; buffer.length = gcli_read_file(TESTSRCDIR"/samples/gitlab_error_unauthorised.json", &buffer.data); ATF_REQUIRE(buffer.length); errmsg = gitlab_api_error_string(ctx, &buffer); ATF_CHECK_STREQ(errmsg, "401 Unauthorized"); } ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, gitlab_simple_fork); ATF_TP_ADD_TC(tp, gitlab_simple_issue); ATF_TP_ADD_TC(tp, gitlab_simple_label); ATF_TP_ADD_TC(tp, gitlab_simple_merge_request); ATF_TP_ADD_TC(tp, gitlab_simple_milestone); ATF_TP_ADD_TC(tp, gitlab_simple_pipeline); ATF_TP_ADD_TC(tp, gitlab_simple_release); ATF_TP_ADD_TC(tp, gitlab_simple_repo); ATF_TP_ADD_TC(tp, gitlab_simple_snippet); ATF_TP_ADD_TC(tp, gitlab_error_token_expired); ATF_TP_ADD_TC(tp, gitlab_error_unauthorised); return atf_no_error(); } gcli-2.9.1/tests/json-escape.c000066400000000000000000000021101507017207500161540ustar00rootroot00000000000000#include #include ATF_TC_WITHOUT_HEAD(newlines); ATF_TC_BODY(newlines, tc) { gcli_sv const input = SV("\n\r"); gcli_sv const escaped = gcli_json_escape(input); ATF_CHECK(gcli_sv_eq_to(escaped, "\\n\\r")); free(escaped.data); } ATF_TC_WITHOUT_HEAD(tabs); ATF_TC_BODY(tabs, tc) { gcli_sv const input = SV("\t\t\t"); gcli_sv const escaped = gcli_json_escape(input); ATF_CHECK(gcli_sv_eq_to(escaped, "\\t\\t\\t")); free(escaped.data); } ATF_TC_WITHOUT_HEAD(backslashes); ATF_TC_BODY(backslashes, tc) { gcli_sv const input = SV("\\"); gcli_sv const escaped = gcli_json_escape(input); ATF_CHECK(gcli_sv_eq_to(escaped, "\\\\")); free(escaped.data); } ATF_TC_WITHOUT_HEAD(torture); ATF_TC_BODY(torture, tc) { gcli_sv const input = SV("\n\r\n\n\n\t{}"); gcli_sv const escaped = gcli_json_escape(input); ATF_CHECK(gcli_sv_eq_to(escaped, "\\n\\r\\n\\n\\n\\t{}")); free(escaped.data); } ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, newlines); ATF_TP_ADD_TC(tp, tabs); ATF_TP_ADD_TC(tp, backslashes); ATF_TP_ADD_TC(tp, torture); return atf_no_error(); } gcli-2.9.1/tests/jsongen.c000066400000000000000000000144741507017207500154300ustar00rootroot00000000000000/* * Copyright 2023 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 ATF_TC_WITHOUT_HEAD(empty_object); ATF_TC_BODY(empty_object, tc) { struct gcli_jsongen gen = {0}; ATF_REQUIRE(gcli_jsongen_init(&gen) == 0); ATF_REQUIRE(gcli_jsongen_begin_object(&gen) == 0); ATF_REQUIRE(gcli_jsongen_end_object(&gen) == 0); char *str = gcli_jsongen_to_string(&gen); ATF_CHECK_STREQ(str, "{}"); free(str); gcli_jsongen_free(&gen); } ATF_TC_WITHOUT_HEAD(array_with_two_empty_objects); ATF_TC_BODY(array_with_two_empty_objects, tc) { struct gcli_jsongen gen = {0}; ATF_REQUIRE(gcli_jsongen_init(&gen) == 0); ATF_REQUIRE(gcli_jsongen_begin_array(&gen) == 0); for (int i = 0; i < 2; ++i) { ATF_REQUIRE(gcli_jsongen_begin_object(&gen) == 0); ATF_REQUIRE(gcli_jsongen_end_object(&gen) == 0); } ATF_REQUIRE(gcli_jsongen_end_array(&gen) == 0); char *str = gcli_jsongen_to_string(&gen); ATF_CHECK_STREQ(str, "[{}, {}]"); free(str); gcli_jsongen_free(&gen); } ATF_TC_WITHOUT_HEAD(empty_array); ATF_TC_BODY(empty_array, tc) { struct gcli_jsongen gen = {0}; ATF_REQUIRE(gcli_jsongen_init(&gen) == 0); ATF_REQUIRE(gcli_jsongen_begin_array(&gen) == 0); ATF_REQUIRE(gcli_jsongen_end_array(&gen) == 0); char *str = gcli_jsongen_to_string(&gen); ATF_CHECK_STREQ(str, "[]"); free(str); gcli_jsongen_free(&gen); } ATF_TC_WITHOUT_HEAD(object_with_number); ATF_TC_BODY(object_with_number, tc) { struct gcli_jsongen gen = {0}; ATF_REQUIRE(gcli_jsongen_init(&gen) == 0); ATF_REQUIRE(gcli_jsongen_begin_object(&gen) == 0); ATF_REQUIRE(gcli_jsongen_objmember(&gen, "number") == 0); ATF_REQUIRE(gcli_jsongen_number(&gen, 420) == 0); ATF_REQUIRE(gcli_jsongen_end_object(&gen) == 0); char *str = gcli_jsongen_to_string(&gen); ATF_CHECK_STREQ(str, "{\"number\": 420}"); free(str); gcli_jsongen_free(&gen); } ATF_TC_WITHOUT_HEAD(object_nested); ATF_TC_BODY(object_nested, tc) { struct gcli_jsongen gen = {0}; ATF_REQUIRE(gcli_jsongen_init(&gen) == 0); ATF_REQUIRE(gcli_jsongen_begin_object(&gen) == 0); ATF_REQUIRE(gcli_jsongen_objmember(&gen, "hiernenarray") == 0); ATF_REQUIRE(gcli_jsongen_begin_array(&gen) == 0); ATF_REQUIRE(gcli_jsongen_number(&gen, 69) == 0); ATF_REQUIRE(gcli_jsongen_number(&gen, 420) == 0); ATF_REQUIRE(gcli_jsongen_end_array(&gen) == 0); ATF_REQUIRE(gcli_jsongen_objmember(&gen, "empty_object") == 0); ATF_REQUIRE(gcli_jsongen_begin_object(&gen) == 0); ATF_REQUIRE(gcli_jsongen_end_object(&gen) == 0); ATF_REQUIRE(gcli_jsongen_end_object(&gen) == 0); char *str = gcli_jsongen_to_string(&gen); ATF_CHECK_STREQ(str, "{\"hiernenarray\": [69, 420], \"empty_object\": {}}"); free(str); gcli_jsongen_free(&gen); } ATF_TC_WITHOUT_HEAD(object_with_strings); ATF_TC_BODY(object_with_strings, tc) { struct gcli_jsongen gen = {0}; ATF_REQUIRE(gcli_jsongen_init(&gen) == 0); ATF_REQUIRE(gcli_jsongen_begin_object(&gen) == 0); ATF_REQUIRE(gcli_jsongen_objmember(&gen, "key") == 0); ATF_REQUIRE(gcli_jsongen_string(&gen, "value") == 0); ATF_REQUIRE(gcli_jsongen_end_object(&gen) == 0); char *str = gcli_jsongen_to_string(&gen); ATF_CHECK_STREQ(str, "{\"key\": \"value\"}"); free(str); gcli_jsongen_free(&gen); } ATF_TC_WITHOUT_HEAD(object_with_mixed_values); ATF_TC_BODY(object_with_mixed_values, tc) { struct gcli_jsongen gen = {0}; ATF_REQUIRE(gcli_jsongen_init(&gen) == 0); ATF_REQUIRE(gcli_jsongen_begin_object(&gen) == 0); ATF_REQUIRE(gcli_jsongen_objmember(&gen, "array") == 0); ATF_REQUIRE(gcli_jsongen_begin_array(&gen) == 0); ATF_REQUIRE(gcli_jsongen_number(&gen, 42) == 0); ATF_REQUIRE(gcli_jsongen_string(&gen, "a string literal") == 0); ATF_REQUIRE(gcli_jsongen_begin_object(&gen) == 0); ATF_REQUIRE(gcli_jsongen_end_object(&gen) == 0); ATF_REQUIRE(gcli_jsongen_end_array(&gen) == 0); ATF_REQUIRE(gcli_jsongen_end_object(&gen) == 0); char *str = gcli_jsongen_to_string(&gen); ATF_CHECK_STREQ(str, "{\"array\": [42, \"a string literal\", {}]}"); free(str); gcli_jsongen_free(&gen); } ATF_TC_WITHOUT_HEAD(object_with_two_keys_and_values_that_are_string); ATF_TC_BODY(object_with_two_keys_and_values_that_are_string, tc) { struct gcli_jsongen gen = {0}; ATF_REQUIRE(gcli_jsongen_init(&gen) == 0); ATF_REQUIRE(gcli_jsongen_begin_object(&gen) == 0); ATF_REQUIRE(gcli_jsongen_objmember(&gen, "key1") == 0); ATF_REQUIRE(gcli_jsongen_string(&gen, "value1") == 0); ATF_REQUIRE(gcli_jsongen_objmember(&gen, "key2") == 0); ATF_REQUIRE(gcli_jsongen_string(&gen, "value2") == 0); ATF_REQUIRE(gcli_jsongen_end_object(&gen) == 0); char *str = gcli_jsongen_to_string(&gen); ATF_CHECK_STREQ(str, "{\"key1\": \"value1\", \"key2\": \"value2\"}"); free(str); gcli_jsongen_free(&gen); } ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, empty_object); ATF_TP_ADD_TC(tp, array_with_two_empty_objects); ATF_TP_ADD_TC(tp, empty_array); ATF_TP_ADD_TC(tp, object_with_number); ATF_TP_ADD_TC(tp, object_nested); ATF_TP_ADD_TC(tp, object_with_strings); ATF_TP_ADD_TC(tp, object_with_mixed_values); ATF_TP_ADD_TC(tp, object_with_two_keys_and_values_that_are_string); return atf_no_error(); } gcli-2.9.1/tests/parse-url.c000066400000000000000000000067621507017207500157000ustar00rootroot00000000000000#include #include #define URL_TC(name, url, _scheme, _user, _host, _port, _path) \ ATF_TC_WITHOUT_HEAD(name); \ ATF_TC_BODY(name, tc) \ { \ char in[] = url; \ int rc = 0; \ struct gcli_url result = {0}; \ struct gcli_url const expected = { \ .scheme = _scheme, \ .user = _user, \ .host = _host, \ .port = _port, \ .path = _path, \ }; \ \ rc = gcli_parse_url(in, &result); \ ATF_REQUIRE(rc == 0); \ \ if (expected.scheme) \ ATF_CHECK_STREQ(result.scheme, expected.scheme); \ else \ ATF_CHECK(result.scheme == NULL); \ \ if (expected.user) \ ATF_CHECK_STREQ(result.user, expected.user); \ else \ ATF_CHECK(result.user == NULL); \ \ if (expected.host) \ ATF_CHECK_STREQ(result.host, expected.host); \ else \ ATF_CHECK(result.host == NULL); \ \ if (expected.port) \ ATF_CHECK_STREQ(result.port, expected.port); \ else \ ATF_CHECK(result.port == NULL); \ \ if (expected.path) \ ATF_CHECK_STREQ(result.path, expected.path); \ else \ ATF_CHECK(result.path == NULL); \ \ gcli_url_free(&result); \ } URL_TC(simple_http, "https://git.sr.ht/~herrhotzenplotz/gcli", "https", NULL, "git.sr.ht", NULL, "~herrhotzenplotz/gcli") URL_TC(simple_ssh, "git@github.com:herrhotzenplotz/gcli", NULL, "git", "github.com", NULL, "herrhotzenplotz/gcli") URL_TC(http_with_port, "https://git.foo.bar:4242/barf/bork/blerch", "https", NULL, "git.foo.bar", "4242", "barf/bork/blerch") URL_TC(simple_ssh_with_scheme, "ssh://git@github.com/herrhotzenplotz/gcli", "ssh", "git", "github.com", NULL, "herrhotzenplotz/gcli") URL_TC(ssh_with_port, "ssh://git@git.example.com:4242/herrhotzenplotz/gcli", "ssh", "git", "git.example.com", "4242", "herrhotzenplotz/gcli") ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, simple_http); ATF_TP_ADD_TC(tp, http_with_port); ATF_TP_ADD_TC(tp, simple_ssh); ATF_TP_ADD_TC(tp, simple_ssh_with_scheme); ATF_TP_ADD_TC(tp, ssh_with_port); return atf_no_error(); } gcli-2.9.1/tests/samples/000077500000000000000000000000001507017207500152535ustar00rootroot00000000000000gcli-2.9.1/tests/samples/01_diff_prelude.patch000066400000000000000000000066701507017207500212350ustar00rootroot00000000000000From 47b40f51cae6cec9a3132f888fd66c21ecb687fa Mon Sep 17 00:00:00 2001 From: Nico Sonack Date: Sun, 10 Oct 2021 12:23:11 +0200 Subject: [PATCH] Start submission implementation --- include/ghcli/pulls.h | 1 + src/ghcli.c | 55 +++++++++++++++++++++++++++++++++++++++++++ src/pulls.c | 9 +++++++ 3 files changed, 65 insertions(+) diff --git a/include/ghcli/pulls.h b/include/ghcli/pulls.h index 30a503cf..05d233eb 100644 --- a/include/ghcli/pulls.h +++ b/include/ghcli/pulls.h @@ -57,5 +57,6 @@ int ghcli_get_prs(const char *org, const char *reponame, bool all, ghcli_pull * void ghcli_print_pr_table(FILE *stream, ghcli_pull *pulls, int pulls_size); void ghcli_print_pr_diff(FILE *stream, const char *org, const char *reponame, int pr_number); void ghcli_pr_summary(FILE *stream, const char *org, const char *reponame, int pr_number); +void ghcli_pr_submit(const char *from, const char *to, int in_draft); #endif /* PULLS_H */ diff --git a/src/ghcli.c b/src/ghcli.c index 4ce96f5a..7c19cfdc 100644 --- a/src/ghcli.c +++ b/src/ghcli.c @@ -27,6 +27,7 @@ * OF THE POSSIBILITY OF SUCH DAMAGE. */ +#include #include #include #include @@ -49,6 +50,54 @@ shift(int *argc, char ***argv) return *((*argv)++); } +/** + * Create a pull request + */ +static int +subcommand_pull_create(int argc, char *argv[]) +{ + /* we'll use getopt_long here to parse the arguments */ + int is_draft = 0; + int ch; + char *from = NULL, *to = NULL; + + const struct option options[] = { + { .name = "from", .has_arg = required_argument, .flag = NULL, .val = 'f' }, + { .name = "to", .has_arg = required_argument, .flag = NULL, .val = 't' }, + { .name = "draft", .has_arg = no_argument, .flag = &is_draft, .val = 1 }, + {0}, + }; + + while ((ch = getopt_long(argc, argv, "f:t:d", options, NULL)) != -1) { + switch (ch) { + case 'f': + from = optarg; + break; + case 't': + to = optarg; + break; + case 'd': + is_draft = 1; + break; + default: + errx(1, "RTFM"); + } + } + + argc -= optind; + argv += optind; + + if (!from) + errx(1, "PR head is missing. Please specify --from"); + + if (!to) + errx(1, "PR base is missing. Please specify --to"); + + ghcli_pr_submit(from, to, is_draft); + + return EXIT_SUCCESS; +} + static int subcommand_pulls(int argc, char *argv[]) { @@ -61,6 +110,12 @@ subcommand_pulls(int argc, char *argv[]) int pulls_size = 0; bool all = false; + /* detect whether we wanna create a PR */ + if (argc > 1 && (strcmp(argv[1], "create") == 0)) { + shift(&argc, &argv); + return subcommand_pull_create(argc, argv); + } + /* Parse commandline options */ while ((ch = getopt(argc, argv, "o:r:p:a")) != -1) { switch (ch) { diff --git a/src/pulls.c b/src/pulls.c index c10a5012..dda0ecfe 100644 --- a/src/pulls.c +++ b/src/pulls.c @@ -443,3 +443,12 @@ ghcli_pr_summary(FILE *out, const char *org, const char *reponame, int pr_number fprintf(out, "\nCOMMITS\n"); ghcli_print_commits_table(out, commits, commits_size); } + +void +ghcli_pr_submit(const char *from, const char *to, int is_draft) +{ + (void) from; + (void) to; + (void) is_draft; + errx(1, "not yet implemented"); +} gcli-2.9.1/tests/samples/bugzilla_attachments.json000066400000000000000000000177751507017207500223730ustar00rootroot00000000000000{ "attachments": {}, "bugs": { "274920": [ { "bug_id": 274920, "creation_time": "2023-11-04T20:19:11Z", "creator": "nsonack@outlook.com", "size": 2290, "data": "RnJvbSA0ZWFiYjEzNTU4YTY5YmQwYWJlYmExMmQ0YzBmNjA5YmY3NTFjZTMyIE1vbiBTZXAgMTcgMDA6MDA6MDAgMjAwMQpGcm9tOiBOaWNvIFNvbmFjayA8bnNvbmFja0BoZXJyaG90emVucGxvdHouZGU+CkRhdGU6IEZyaSwgMjcgT2N0IDIwMjMgMTY6MTY6MjEgKzAwMDAKU3ViamVjdDogW1BBVENIXSBkZXZlbC9vcGVuNjI1NDE6IFVwZGF0ZSB0byB2ZXJzaW9uIDEuMy44CgpSZW1vdmVzIHRoZSBwYXRjaCBhcyB0aGUgdXBzdHJlYW0gYnVnIHJlcG9ydCBoYXMgYmVlbiByZXNvbHZlZC4KU2VlIGh0dHBzOi8vZ2l0aHViLmNvbS9vcGVuNjI1NDEvb3BlbjYyNTQxL2lzc3Vlcy82MDEwCgpTaWduZWQtb2ZmLWJ5OiBOaWNvIFNvbmFjayA8bnNvbmFja0BoZXJyaG90emVucGxvdHouZGU+Ci0tLQogZGV2ZWwvb3BlbjYyNTQxL01ha2VmaWxlICAgICAgICAgICAgICAgICAgIHwgIDIgKy0KIGRldmVsL29wZW42MjU0MS9kaXN0aW5mbyAgICAgICAgICAgICAgICAgICB8ICA2ICsrKy0tLQogZGV2ZWwvb3BlbjYyNTQxL2ZpbGVzL3BhdGNoLUNNYWtlTGlzdHMudHh0IHwgMTEgLS0tLS0tLS0tLS0KIDMgZmlsZXMgY2hhbmdlZCwgNCBpbnNlcnRpb25zKCspLCAxNSBkZWxldGlvbnMoLSkKIGRlbGV0ZSBtb2RlIDEwMDY0NCBkZXZlbC9vcGVuNjI1NDEvZmlsZXMvcGF0Y2gtQ01ha2VMaXN0cy50eHQKCmRpZmYgLS1naXQgYS9kZXZlbC9vcGVuNjI1NDEvTWFrZWZpbGUgYi9kZXZlbC9vcGVuNjI1NDEvTWFrZWZpbGUKaW5kZXggYTY5MDg0ZWY5NzQzLi5mOTkzMWJhNTE3ZjUgMTAwNjQ0Ci0tLSBhL2RldmVsL29wZW42MjU0MS9NYWtlZmlsZQorKysgYi9kZXZlbC9vcGVuNjI1NDEvTWFrZWZpbGUKQEAgLTEsNiArMSw2IEBACiBQT1JUTkFNRT0Jb3BlbjYyNTQxCiBESVNUVkVSU0lPTlBSRUZJWD0JdgotRElTVFZFUlNJT049CTEuMy43CitESVNUVkVSU0lPTj0JMS4zLjgKIENBVEVHT1JJRVM9CWRldmVsCiAKIE1BSU5UQUlORVI9CW5zb25hY2tAb3V0bG9vay5jb20KZGlmZiAtLWdpdCBhL2RldmVsL29wZW42MjU0MS9kaXN0aW5mbyBiL2RldmVsL29wZW42MjU0MS9kaXN0aW5mbwppbmRleCAyMTQzNzIyMWU2YjEuLjQ5YzQ4YzVhNjE5MCAxMDA2NDQKLS0tIGEvZGV2ZWwvb3BlbjYyNTQxL2Rpc3RpbmZvCisrKyBiL2RldmVsL29wZW42MjU0MS9kaXN0aW5mbwpAQCAtMSwzICsxLDMgQEAKLVRJTUVTVEFNUCA9IDE2OTQ0MzYwNjAKLVNIQTI1NiAob3BlbjYyNTQxLW9wZW42MjU0MS12MS4zLjdfR0gwLnRhci5neikgPSBkM2Y4NGYxZTI2MzJjMTVhMzg5MmRjNmM4OWYwY2Q2YjQxMzdlOTkwYjhhZWY4ZmUyNDVjZDhlNzVmYmI1Mzg4Ci1TSVpFIChvcGVuNjI1NDEtb3BlbjYyNTQxLXYxLjMuN19HSDAudGFyLmd6KSA9IDM4NzEwNTcKK1RJTUVTVEFNUCA9IDE2OTg0MjI4MTYKK1NIQTI1NiAob3BlbjYyNTQxLW9wZW42MjU0MS12MS4zLjhfR0gwLnRhci5neikgPSBiNjk0M2I1NjQ3ODdjNDk1M2I3N2NhOGQ3Zjk4N2M0Yjg5NmIzZjNlOTFmNDVkOWYxM2U5MDU2YjYxNDhiYzFkCitTSVpFIChvcGVuNjI1NDEtb3BlbjYyNTQxLXYxLjMuOF9HSDAudGFyLmd6KSA9IDM4NzQxODUKZGlmZiAtLWdpdCBhL2RldmVsL29wZW42MjU0MS9maWxlcy9wYXRjaC1DTWFrZUxpc3RzLnR4dCBiL2RldmVsL29wZW42MjU0MS9maWxlcy9wYXRjaC1DTWFrZUxpc3RzLnR4dApkZWxldGVkIGZpbGUgbW9kZSAxMDA2NDQKaW5kZXggMzBhNmVmZmM3ZjcxLi4wMDAwMDAwMDAwMDAKLS0tIGEvZGV2ZWwvb3BlbjYyNTQxL2ZpbGVzL3BhdGNoLUNNYWtlTGlzdHMudHh0CisrKyAvZGV2L251bGwKQEAgLTEsMTEgKzAsMCBAQAotLS0tIENNYWtlTGlzdHMudHh0Lm9yaWcJMjAyMy0wOS0xMSAxMjo0NzowOCBVVEMKLSsrKyBDTWFrZUxpc3RzLnR4dAotQEAgLTQzLDcgKzQzLDcgQEAgc2V0KENNQUtFX0FSQ0hJVkVfT1VUUFVUX0RJUkVDVE9SWSAke0NNQUtFX0JJTkFSWV9ESVJ9Ci0gIyBvdmVyd3JpdHRlbiB3aXRoIG1vcmUgZGV0YWlsZWQgaW5mb3JtYXRpb24gaWYgZ2l0IGlzIGF2YWlsYWJsZS4KLSBzZXQoT1BFTjYyNTQxX1ZFUl9NQUpPUiAxKQotIHNldChPUEVONjI1NDFfVkVSX01JTk9SIDMpCi0tc2V0KE9QRU42MjU0MV9WRVJfUEFUQ0ggNikKLStzZXQoT1BFTjYyNTQxX1ZFUl9QQVRDSCA3KQotIHNldChPUEVONjI1NDFfVkVSX0xBQkVMICItdW5kZWZpbmVkIikgIyBsaWtlICItcmMxIiBvciAiLWc0NTM4YWJjZCIgb3IgIi1nNDUzOGFiY2QtZGlydHkiCi0gc2V0KE9QRU42MjU0MV9WRVJfQ09NTUlUICJ1bmtub3duLWNvbW1pdCIpCi0gCi0tIAoyLjQyLjAKCg==", "file_name": "0001-devel-open62541-Update-to-version-1.3.8.patch", "content_type": "text/plain", "flags": [ { "status": "+", "setter": "nsonack@outlook.com", "id": 76972, "name": "maintainer-approval", "type_id": 1, "creation_date": "2023-11-04T20:19:11Z", "modification_date": "2023-11-04T20:19:11Z" } ], "is_private": 0, "id": 246131, "last_change_time": "2023-12-08T17:10:06Z", "is_patch": 1, "summary": "Patch for updating the port", "is_obsolete": 1 }, { "id": 246910, "is_private": 0, "is_obsolete": 0, "summary": "Patch v2 (now for version 1.3.9)", "last_change_time": "2023-12-08T17:10:06Z", "is_patch": 1, "creation_time": "2023-12-08T17:10:06Z", "bug_id": 274920, "flags": [ { "creation_date": "2023-12-08T17:10:06Z", "modification_date": "2023-12-08T17:10:06Z", "type_id": 1, "name": "maintainer-approval", "id": 77649, "setter": "nsonack@outlook.com", "status": "+" } ], "content_type": "text/plain", "data": "RnJvbSA5MTE2MmYyY2Y2NGI4NjlmOTEwYzBmMjRmMzIyZWU3Y2Q1ZDZmZDdlIE1vbiBTZXAgMTcgMDA6MDA6MDAgMjAwMQpGcm9tOiBOaWNvIFNvbmFjayA8bnNvbmFja0BoZXJyaG90emVucGxvdHouZGU+CkRhdGU6IEZyaSwgMjcgT2N0IDIwMjMgMTY6MTY6MjEgKzAwMDAKU3ViamVjdDogW1BBVENIXSBkZXZlbC9vcGVuNjI1NDE6IFVwZGF0ZSB0byB2ZXJzaW9uIDEuMy44CgpSZW1vdmVzIHRoZSBwYXRjaCBhcyB0aGUgdXBzdHJlYW0gYnVnIHJlcG9ydCBoYXMgYmVlbiByZXNvbHZlZC4KU2VlIGh0dHBzOi8vZ2l0aHViLmNvbS9vcGVuNjI1NDEvb3BlbjYyNTQxL2lzc3Vlcy82MDEwCgpTaWduZWQtb2ZmLWJ5OiBOaWNvIFNvbmFjayA8bnNvbmFja0BoZXJyaG90emVucGxvdHouZGU+Ci0tLQogZGV2ZWwvb3BlbjYyNTQxL01ha2VmaWxlICAgICAgICAgICAgICAgICAgIHwgIDYgKysrLS0tCiBkZXZlbC9vcGVuNjI1NDEvZGlzdGluZm8gICAgICAgICAgICAgICAgICAgfCAgNiArKystLS0KIGRldmVsL29wZW42MjU0MS9maWxlcy9wYXRjaC1DTWFrZUxpc3RzLnR4dCB8IDExIC0tLS0tLS0tLS0tCiAzIGZpbGVzIGNoYW5nZWQsIDYgaW5zZXJ0aW9ucygrKSwgMTcgZGVsZXRpb25zKC0pCiBkZWxldGUgbW9kZSAxMDA2NDQgZGV2ZWwvb3BlbjYyNTQxL2ZpbGVzL3BhdGNoLUNNYWtlTGlzdHMudHh0CgpkaWZmIC0tZ2l0IGEvZGV2ZWwvb3BlbjYyNTQxL01ha2VmaWxlIGIvZGV2ZWwvb3BlbjYyNTQxL01ha2VmaWxlCmluZGV4IGE2OTA4NGVmOTc0My4uMWE5Y2E1ZTJlZWI4IDEwMDY0NAotLS0gYS9kZXZlbC9vcGVuNjI1NDEvTWFrZWZpbGUKKysrIGIvZGV2ZWwvb3BlbjYyNTQxL01ha2VmaWxlCkBAIC0xLDYgKzEsNiBAQAogUE9SVE5BTUU9CW9wZW42MjU0MQogRElTVFZFUlNJT05QUkVGSVg9CXYKLURJU1RWRVJTSU9OPQkxLjMuNworRElTVFZFUlNJT049CTEuMy45CiBDQVRFR09SSUVTPQlkZXZlbAogCiBNQUlOVEFJTkVSPQluc29uYWNrQG91dGxvb2suY29tCkBAIC0xNiw5ICsxNiw5IEBAIFVTRV9MRENPTkZJRz0JeWVzCiAKIFNIRUJBTkdfR0xPQj0JKi5weQogCi1DTUFLRV9PTj0JQlVJTERfU0hBUkVEX0xJQlMKK0NNQUtFX09OPQlCVUlMRF9TSEFSRURfTElCUyBcCisJCUNNQUtFX0RJU0FCTEVfRklORF9QQUNLQUdFX0dpdAogQ01BS0VfT0ZGPQlVQV9GT1JDRV9XRVJST1IKLUJJTkFSWV9BTElBUz0JcHl0aG9uMz0ke1BZVEhPTl9DTUR9CiBQTElTVF9TVUI9CURJU1RWRVJTSU9OPSR7RElTVFZFUlNJT059CiAKIE9QVElPTlNfREVGSU5FPQkJT1NTTApkaWZmIC0tZ2l0IGEvZGV2ZWwvb3BlbjYyNTQxL2Rpc3RpbmZvIGIvZGV2ZWwvb3BlbjYyNTQxL2Rpc3RpbmZvCmluZGV4IDIxNDM3MjIxZTZiMS4uNDE4Njk4MTU4MTFmIDEwMDY0NAotLS0gYS9kZXZlbC9vcGVuNjI1NDEvZGlzdGluZm8KKysrIGIvZGV2ZWwvb3BlbjYyNTQxL2Rpc3RpbmZvCkBAIC0xLDMgKzEsMyBAQAotVElNRVNUQU1QID0gMTY5NDQzNjA2MAotU0hBMjU2IChvcGVuNjI1NDEtb3BlbjYyNTQxLXYxLjMuN19HSDAudGFyLmd6KSA9IGQzZjg0ZjFlMjYzMmMxNWEzODkyZGM2Yzg5ZjBjZDZiNDEzN2U5OTBiOGFlZjhmZTI0NWNkOGU3NWZiYjUzODgKLVNJWkUgKG9wZW42MjU0MS1vcGVuNjI1NDEtdjEuMy43X0dIMC50YXIuZ3opID0gMzg3MTA1NworVElNRVNUQU1QID0gMTcwMjA1NTE4MgorU0hBMjU2IChvcGVuNjI1NDEtb3BlbjYyNTQxLXYxLjMuOV9HSDAudGFyLmd6KSA9IDcxNzY0ZDRhMDYwY2ZhMDdlYWU3YWFhYmQxNzZkYTM4YjE1NWVmMDFjNjMxMDM1MTMzMzk2OTlmZDgwMjZlMmYKK1NJWkUgKG9wZW42MjU0MS1vcGVuNjI1NDEtdjEuMy45X0dIMC50YXIuZ3opID0gMzg3NDcwMQpkaWZmIC0tZ2l0IGEvZGV2ZWwvb3BlbjYyNTQxL2ZpbGVzL3BhdGNoLUNNYWtlTGlzdHMudHh0IGIvZGV2ZWwvb3BlbjYyNTQxL2ZpbGVzL3BhdGNoLUNNYWtlTGlzdHMudHh0CmRlbGV0ZWQgZmlsZSBtb2RlIDEwMDY0NAppbmRleCAzMGE2ZWZmYzdmNzEuLjAwMDAwMDAwMDAwMAotLS0gYS9kZXZlbC9vcGVuNjI1NDEvZmlsZXMvcGF0Y2gtQ01ha2VMaXN0cy50eHQKKysrIC9kZXYvbnVsbApAQCAtMSwxMSArMCwwIEBACi0tLS0gQ01ha2VMaXN0cy50eHQub3JpZwkyMDIzLTA5LTExIDEyOjQ3OjA4IFVUQwotKysrIENNYWtlTGlzdHMudHh0Ci1AQCAtNDMsNyArNDMsNyBAQCBzZXQoQ01BS0VfQVJDSElWRV9PVVRQVVRfRElSRUNUT1JZICR7Q01BS0VfQklOQVJZX0RJUn0KLSAjIG92ZXJ3cml0dGVuIHdpdGggbW9yZSBkZXRhaWxlZCBpbmZvcm1hdGlvbiBpZiBnaXQgaXMgYXZhaWxhYmxlLgotIHNldChPUEVONjI1NDFfVkVSX01BSk9SIDEpCi0gc2V0KE9QRU42MjU0MV9WRVJfTUlOT1IgMykKLS1zZXQoT1BFTjYyNTQxX1ZFUl9QQVRDSCA2KQotK3NldChPUEVONjI1NDFfVkVSX1BBVENIIDcpCi0gc2V0KE9QRU42MjU0MV9WRVJfTEFCRUwgIi11bmRlZmluZWQiKSAjIGxpa2UgIi1yYzEiIG9yICItZzQ1MzhhYmNkIiBvciAiLWc0NTM4YWJjZC1kaXJ0eSIKLSBzZXQoT1BFTjYyNTQxX1ZFUl9DT01NSVQgInVua25vd24tY29tbWl0IikKLSAKLS0gCjIuNDIuMAoK", "file_name": "0001-devel-open62541-Update-to-version-1.3.8.patch", "size": 2577, "creator": "nsonack@outlook.com" } ] } } gcli-2.9.1/tests/samples/bugzilla_comments.json000066400000000000000000000163301507017207500216670ustar00rootroot00000000000000{"comments":{},"bugs":{"275381":{"comments":[{"is_private":false,"time":"2023-11-27T17:14:39Z","text":"This is originally reported by khng@ on Telegram bsd dev group. Post it here to make it public.\n\nSteps to repeat:\n\nBoot with Ethernet interface disabled, then try to enable it.\n\n```\n> set hint.hn.0.disabled=\"1\"\n> boot\n...\n# devctl enable hn0\n```\n\n\nPart of core text dump:\n\nfreebsd dumped core - see /var/crash/vmcore.0\n\nMon Nov 20 04:17:24 UTC 2023\n\nFreeBSD freebsd 14.0-RELEASE FreeBSD 14.0-RELEASE #0 releng/14.0-n265380-f9716eee8ab4: Fri Nov 10 05:57:23 UTC 2023 root@releng1.nyi.freebsd.org:/usr/obj/usr/src/amd64.amd64/sys/GENERIC amd64\n\npanic: page fault\n\nGNU gdb (GDB) 13.2 [GDB v13.2 for FreeBSD]\nCopyright (C) 2023 Free Software Foundation, Inc.\nLicense GPLv3+: GNU GPL version 3 or later \nThis is free software: you are free to change and redistribute it.\nThere is NO WARRANTY, to the extent permitted by law.\nType \"show copying\" and \"show warranty\" for details.\nThis GDB was configured as \"x86_64-portbld-freebsd14.0\".\nType \"show configuration\" for configuration details.\nFor bug reporting instructions, please see:\n.\nFind the GDB manual and other documentation resources online at:\n .\n\nFor help, type \"help\".\nType \"apropos word\" to search for commands related to \"word\"...\nReading symbols from /boot/kernel/kernel...\nReading symbols from /usr/lib/debug//boot/kernel/kernel.debug...\n\nUnread portion of the kernel message buffer:\n\n\nFatal trap 12: page fault while in kernel mode\ncpuid = 1; apic id = 01\nfault virtual address\t= 0x28\nfault code\t\t= supervisor read data, page not present\ninstruction pointer\t= 0x20:0xffffffff80c5e0c8\nstack pointer\t = 0x28:0xfffffe0053f4b900\nframe pointer\t = 0x28:0xfffffe0053f4b940\ncode segment\t\t= base 0x0, limit 0xfffff, type 0x1b\n\t\t\t= DPL 0, pres 1, long 1, def32 0, gran 1\nprocessor eflags\t= interrupt enabled, resume, IOPL = 0\ncurrent process\t\t= 650 (devctl)\nrdi: fffff80006eb6800 rsi: fffff80001027500 rdx: 0000000000000001\nrcx: 0000000000000001 r8: 0000000000000000 r9: 8080808080808080\nrax: 0000000000000000 rbx: fffffe0054963c80 rbp: fffffe0053f4b940\nr10: ffffffff811e1f39 r11: 8b9091ff93939e00 r12: fffff80007fca000\nr13: fffff80007305c20 r14: ffffffff811e1f39 r15: 0000000000000000\ntrap number\t\t= 12\npanic: page fault\ncpuid = 1\ntime = 1700453806\nKDB: stack backtrace:\n#0 0xffffffff80b9002d at kdb_backtrace+0x5d\n#1 0xffffffff80b43132 at vpanic+0x132\n#2 0xffffffff80b42ff3 at panic+0x43\n#3 0xffffffff8100c85c at trap_fatal+0x40c\n#4 0xffffffff8100c8af at trap_pfault+0x4f\n#5 0xffffffff80fe3828 at calltrap+0x8\n#6 0xffffffff80c5ceb5 at if_attach_internal+0x55\n#7 0xffffffff80c6824c at ether_ifattach+0x2c\n#8 0xffffffff80f779c6 at hn_attach+0x21d6\n#9 0xffffffff80b7fa1e at device_attach+0x3be\n#10 0xffffffff80b84dcf at devctl2_ioctl+0x56f\n#11 0xffffffff809d10dc at devfs_ioctl+0xcc\n#12 0xffffffff80c3b9b4 at vn_ioctl+0xd4\n#13 0xffffffff809d177e at devfs_ioctl_f+0x1e\n#14 0xffffffff80bb1535 at kern_ioctl+0x255\n#15 0xffffffff80bb1273 at sys_ioctl+0x123\n#16 0xffffffff8100d119 at amd64_syscall+0x109\n#17 0xffffffff80fe413b at fast_syscall_common+0xf8\nUptime: 15s\nDumping 212 out of 470 MB:..8%..16%..23%..31%..46%..53%..61%..76%..83%..91%\n\n__curthread () at /usr/src/sys/amd64/include/pcpu_aux.h:57\n57\t/usr/src/sys/amd64/include/pcpu_aux.h: No such file or directory.\n(kgdb) #0 __curthread () at /usr/src/sys/amd64/include/pcpu_aux.h:57\n#1 doadump (textdump=)\n at /usr/src/sys/kern/kern_shutdown.c:405\n#2 0xffffffff80b42cc7 in kern_reboot (howto=260)\n at /usr/src/sys/kern/kern_shutdown.c:526\n#3 0xffffffff80b4319f in vpanic (fmt=0xffffffff81136b3b \"%s\", \n ap=ap@entry=0xfffffe0053f4b750) at /usr/src/sys/kern/kern_shutdown.c:970\n#4 0xffffffff80b42ff3 in panic (fmt=)\n at /usr/src/sys/kern/kern_shutdown.c:894\n#5 0xffffffff8100c85c in trap_fatal (frame=0xfffffe0053f4b840, eva=40)\n at /usr/src/sys/amd64/amd64/trap.c:952\n#6 0xffffffff8100c8af in trap_pfault (frame=0xfffffe0053f4b840, \n usermode=false, signo=, ucode=)\n at /usr/src/sys/amd64/amd64/trap.c:760\n#7 \n#8 0xffffffff80c5e0c8 in if_addgroup (ifp=ifp@entry=0xfffff80007fca000, \n groupname=0xffffffff811e1f39 \"all\") at /usr/src/sys/net/if.c:1477\n#9 0xffffffff80c5ceb5 in if_attach_internal (\n ifp=ifp@entry=0xfffff80007fca000, vmove=false)\n at /usr/src/sys/net/if.c:842\n#10 0xffffffff80c5ce59 in if_attach (ifp=0xfffff80006eb6800, \n ifp@entry=0xfffff80007fca000) at /usr/src/sys/net/if.c:772\n#11 0xffffffff80c6824c in ether_ifattach (ifp=0xfffff80006eb6800, \n ifp@entry=0xfffff80007fca000, lla=0xfffff80001027500 \"\", \n lla@entry=0xfffffe0053f4ba80 \"\") at /usr/src/sys/net/if_ethersubr.c:1001\n#12 0xffffffff80f779c6 in hn_attach (dev=0xfffff8000291ce00)\n at /usr/src/sys/dev/hyperv/netvsc/if_hn.c:2436\n#13 0xffffffff80b7fa1e in DEVICE_ATTACH (dev=0xfffff8000291ce00)\n at ./device_if.h:195\n#14 device_attach (dev=dev@entry=0xfffff8000291ce00)\n at /usr/src/sys/kern/subr_bus.c:2535\n#15 0xffffffff80b84dcf in devctl2_ioctl (cdev=, \n cmd=2157462531, data=, fflag=, \n td=0xfffffe0054963c80) at /usr/src/sys/kern/subr_bus.c:5433\n#16 0xffffffff809d10dc in devfs_ioctl (ap=0xfffffe0053f4bc40)\n at /usr/src/sys/fs/devfs/devfs_vnops.c:933\n#17 0xffffffff80c3b9b4 in vn_ioctl (fp=0xfffff8000704ce10, \n com=18446735277633467648, data=0xfffff8000779ee00, \n active_cred=0xfffff8000702cb00, td=0x0)\n at /usr/src/sys/kern/vfs_vnops.c:1701\n#18 0xffffffff809d177e in devfs_ioctl_f (fp=0xfffff80006eb6800, \n com=18446735277633467648, data=0x1, cred=0x1, td=0x0)\n at /usr/src/sys/fs/devfs/devfs_vnops.c:864\n#19 0xffffffff80bb1535 in fo_ioctl (fp=0xfffff8000704ce10, com=2157462531, \n data=0x1, active_cred=0x1, td=0xfffffe0054963c80)\n at /usr/src/sys/sys/file.h:366\n#20 kern_ioctl (td=td@entry=0xfffffe0054963c80, fd=, \n com=com@entry=2157462531, \n data=0x1 , \n data@entry=0xfffff8000779ee00 \"hn0\")\n at /usr/src/sys/kern/sys_generic.c:805\n#21 0xffffffff80bb1273 in sys_ioctl (td=0xfffffe0054963c80, \n uap=0xfffffe0054964080) at /usr/src/sys/kern/sys_generic.c:713\n#22 0xffffffff8100d119 in syscallenter (td=0xfffffe0054963c80)\n at /usr/src/sys/amd64/amd64/../../kern/subr_syscall.c:187\n#23 amd64_syscall (td=0xfffffe0054963c80, traced=0)\n at /usr/src/sys/amd64/amd64/trap.c:1197\n#24 \n#25 0x000032e7074bce0a in ?? ()\nBacktrace stopped: Cannot access memory at address 0x32e7069aff48\n(kgdb)","creation_time":"2023-11-27T17:14:39Z","bug_id":275381,"count":0,"id":1285941,"creator":"zlei@FreeBSD.org","attachment_id":null},{"text":"Other ethernet interface drivers are also affected, tested with re(4) and cxgbe(4).\n\nProposed fix: https://reviews.freebsd.org/D42678","time":"2023-11-27T17:20:15Z","bug_id":275381,"creation_time":"2023-11-27T17:20:15Z","is_private":false,"attachment_id":null,"creator":"zlei@FreeBSD.org","id":1285943,"count":1}]}}}gcli-2.9.1/tests/samples/bugzilla_simple_bug.json000066400000000000000000000025761507017207500221770ustar00rootroot00000000000000{ "bugs": [ { "platform": "Any", "target_milestone": "---", "op_sys": "Any", "summary": "[aha] [scsi] Toshiba MK156FB scsi drive does not work with 2.0 kernel", "blocks": [], "last_change_time": "2016-08-11T10:54:59Z", "priority": "Normal", "resolution": "FIXED", "is_cc_accessible": true, "deadline": null, "classification": "Unclassified", "is_creator_accessible": true, "dupe_of": null, "id": 1, "see_also": [], "status": "Closed", "groups": [], "is_open": false, "whiteboard": "", "assigned_to": "core@FreeBSD.org", "assigned_to_detail": { "name": "core@FreeBSD.org", "real_name": "FreeBSD Core Team", "id": 3, "email": "core@FreeBSD.org" }, "keywords": [], "alias": [], "cc_detail": [], "url": "", "cc": [], "version": "Unspecified", "is_confirmed": true, "component": "kern", "creator_detail": { "name": "root@hclb.demon.co.uk", "real_name": "Dave Evans", "email": "root@hclb.demon.co.uk", "id": 22 }, "depends_on": [], "creation_time": "1994-09-14T09:10:01Z", "severity": "Affects Only Me", "flags": [], "qa_contact": "", "creator": "root@hclb.demon.co.uk", "product": "Base System" } ] } gcli-2.9.1/tests/samples/gitea_simple_notification.json000066400000000000000000000064741507017207500233710ustar00rootroot00000000000000{ "id": 511579, "repository": { "id": 45938, "owner": { "id": 52534, "login": "schilytools", "login_name": "", "full_name": "", "email": "", "avatar_url": "https://codeberg.org/avatars/f29c1b0621d441e37b83d4bf59bf2563", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2022-05-24T12:20:59Z", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "http://schilytools.sourceforge.net/", "description": "This project maintains the schilytools, a collection of tools written or formerly managed by Jörg Schilling.", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 0, "username": "schilytools" }, "name": "schilytools", "full_name": "schilytools/schilytools", "description": "A collection of tools written or formerly managed by Jörg Schilling.", "empty": false, "private": false, "fork": false, "template": false, "parent": null, "mirror": false, "size": 16025, "language": "", "languages_url": "https://codeberg.org/api/v1/repos/schilytools/schilytools/languages", "html_url": "https://codeberg.org/schilytools/schilytools", "url": "https://codeberg.org/api/v1/repos/schilytools/schilytools", "link": "", "ssh_url": "git@codeberg.org:schilytools/schilytools.git", "clone_url": "https://codeberg.org/schilytools/schilytools.git", "original_url": "", "website": "", "stars_count": 20, "forks_count": 10, "watchers_count": 8, "open_issues_count": 21, "open_pr_counter": 1, "release_counter": 7, "default_branch": "master", "archived": false, "created_at": "2022-05-24T14:20:56Z", "updated_at": "2023-11-24T21:01:16Z", "archived_at": "1970-01-01T00:00:00Z", "has_issues": true, "internal_tracker": { "enable_time_tracker": true, "allow_only_contributors_to_track_time": true, "enable_issue_dependencies": true }, "has_wiki": true, "has_pull_requests": true, "has_projects": true, "has_releases": true, "has_packages": false, "has_actions": false, "ignore_whitespace_conflicts": false, "allow_merge_commits": true, "allow_rebase": true, "allow_rebase_explicit": true, "allow_squash_merge": true, "allow_rebase_update": true, "default_delete_branch_after_merge": false, "default_merge_style": "rebase", "default_allow_maintainer_edit": false, "avatar_url": "", "internal": false, "mirror_interval": "", "mirror_updated": "0001-01-01T00:00:00Z", "repo_transfer": null }, "subject": { "title": "Remove register from C++ sources", "url": "https://codeberg.org/api/v1/repos/schilytools/schilytools/issues/13", "latest_comment_url": "https://codeberg.org/api/v1/repos/schilytools/schilytools/issues/comments/1349554", "html_url": "https://codeberg.org/schilytools/schilytools/issues/13", "latest_comment_html_url": "https://codeberg.org/schilytools/schilytools/issues/13#issuecomment-1349554", "type": "Issue", "state": "closed" }, "unread": true, "pinned": false, "updated_at": "2023-11-24T21:01:50Z", "url": "https://codeberg.org/api/v1/notifications/threads/511579" } gcli-2.9.1/tests/samples/github_simple_check.json000066400000000000000000000070561507017207500221460ustar00rootroot00000000000000{ "id": 16437184455, "name": "test Windows x86", "node_id": "CR_kwDODwaEZM8AAAAD07uHxw", "head_sha": "03a8af3dab144ea38910c1efdcce03fb708f3179", "external_id": "85d15886-9467-5a76-a719-6058eec4b143", "url": "https://api.github.com/repos/quick-lint/quick-lint-js/check-runs/16437184455", "html_url": "https://github.com/quick-lint/quick-lint-js/actions/runs/6056687077/job/16437184455", "details_url": "https://github.com/quick-lint/quick-lint-js/actions/runs/6056687077/job/16437184455", "status": "completed", "conclusion": "success", "started_at": "2023-09-02T06:27:37Z", "completed_at": "2023-09-02T06:29:11Z", "output": { "title": null, "summary": null, "text": null, "annotations_count": 0, "annotations_url": "https://api.github.com/repos/quick-lint/quick-lint-js/check-runs/16437184455/annotations" }, "check_suite": { "id": 15750909232 }, "app": { "id": 15368, "slug": "github-actions", "node_id": "MDM6QXBwMTUzNjg=", "owner": { "login": "github", "id": 9919, "node_id": "MDEyOk9yZ2FuaXphdGlvbjk5MTk=", "avatar_url": "https://avatars.githubusercontent.com/u/9919?v=4", "gravatar_id": "", "url": "https://api.github.com/users/github", "html_url": "https://github.com/github", "followers_url": "https://api.github.com/users/github/followers", "following_url": "https://api.github.com/users/github/following{/other_user}", "gists_url": "https://api.github.com/users/github/gists{/gist_id}", "starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/github/subscriptions", "organizations_url": "https://api.github.com/users/github/orgs", "repos_url": "https://api.github.com/users/github/repos", "events_url": "https://api.github.com/users/github/events{/privacy}", "received_events_url": "https://api.github.com/users/github/received_events", "type": "Organization", "site_admin": false }, "name": "GitHub Actions", "description": "Automate your workflow from idea to production", "external_url": "https://help.github.com/en/actions", "html_url": "https://github.com/apps/github-actions", "created_at": "2018-07-30T09:30:17Z", "updated_at": "2019-12-10T19:04:12Z", "permissions": { "actions": "write", "administration": "read", "checks": "write", "contents": "write", "deployments": "write", "discussions": "write", "issues": "write", "merge_queues": "write", "metadata": "read", "packages": "write", "pages": "write", "pull_requests": "write", "repository_hooks": "write", "repository_projects": "write", "security_events": "write", "statuses": "write", "vulnerability_alerts": "read" }, "events": [ "branch_protection_rule", "check_run", "check_suite", "create", "delete", "deployment", "deployment_status", "discussion", "discussion_comment", "fork", "gollum", "issues", "issue_comment", "label", "merge_group", "milestone", "page_build", "project", "project_card", "project_column", "public", "pull_request", "pull_request_review", "pull_request_review_comment", "push", "registry_package", "release", "repository", "repository_dispatch", "status", "watch", "workflow_dispatch", "workflow_run" ] }, "pull_requests": [] } gcli-2.9.1/tests/samples/github_simple_comment.json000066400000000000000000000042551507017207500225310ustar00rootroot00000000000000{ "url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/comments/1424392601", "html_url": "https://github.com/herrhotzenplotz/gcli/issues/116#issuecomment-1424392601", "issue_url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/116", "id": 1424392601, "node_id": "IC_kwDOGLyhHc5U5oGZ", "user": { "login": "herrhotzenplotz", "id": 34663024, "node_id": "MDQ6VXNlcjM0NjYzMDI0", "avatar_url": "https://avatars.githubusercontent.com/u/34663024?u=13531d0ed7a7eb54da4599c39ac3068c7d0d71ab&v=4", "gravatar_id": "", "url": "https://api.github.com/users/herrhotzenplotz", "html_url": "https://github.com/herrhotzenplotz", "followers_url": "https://api.github.com/users/herrhotzenplotz/followers", "following_url": "https://api.github.com/users/herrhotzenplotz/following{/other_user}", "gists_url": "https://api.github.com/users/herrhotzenplotz/gists{/gist_id}", "starred_url": "https://api.github.com/users/herrhotzenplotz/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/herrhotzenplotz/subscriptions", "organizations_url": "https://api.github.com/users/herrhotzenplotz/orgs", "repos_url": "https://api.github.com/users/herrhotzenplotz/repos", "events_url": "https://api.github.com/users/herrhotzenplotz/events{/privacy}", "received_events_url": "https://api.github.com/users/herrhotzenplotz/received_events", "type": "User", "site_admin": false }, "created_at": "2023-02-09T15:37:54Z", "updated_at": "2023-02-09T15:37:54Z", "author_association": "OWNER", "body": "Hey,\n\nthe current trunk on Github might be a little outdated. I pushed the staging branch for version 1.0.0 from Gitlab to Github (cleanup-1.0). Could you try again with that branch and see if it still faults at the same place? If it does, please provide a full backtrace and if possible check with valgrind.\n", "reactions": { "url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/comments/1424392601/reactions", "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 }, "performed_via_github_app": null } gcli-2.9.1/tests/samples/github_simple_fork.json000066400000000000000000000133071507017207500220260ustar00rootroot00000000000000{ "id": 639263592, "node_id": "R_kgDOJhpjaA", "name": "quick-lint-js", "full_name": "gjnoonan/quick-lint-js", "private": false, "owner": { "login": "gjnoonan", "id": 702, "node_id": "MDQ6VXNlcjcwMg==", "avatar_url": "https://avatars.githubusercontent.com/u/702?v=4", "gravatar_id": "", "url": "https://api.github.com/users/gjnoonan", "html_url": "https://github.com/gjnoonan", "followers_url": "https://api.github.com/users/gjnoonan/followers", "following_url": "https://api.github.com/users/gjnoonan/following{/other_user}", "gists_url": "https://api.github.com/users/gjnoonan/gists{/gist_id}", "starred_url": "https://api.github.com/users/gjnoonan/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/gjnoonan/subscriptions", "organizations_url": "https://api.github.com/users/gjnoonan/orgs", "repos_url": "https://api.github.com/users/gjnoonan/repos", "events_url": "https://api.github.com/users/gjnoonan/events{/privacy}", "received_events_url": "https://api.github.com/users/gjnoonan/received_events", "type": "User", "site_admin": false }, "html_url": "https://github.com/gjnoonan/quick-lint-js", "description": "quick-lint-js finds bugs in JavaScript programs", "fork": true, "url": "https://api.github.com/repos/gjnoonan/quick-lint-js", "forks_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/forks", "keys_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/keys{/key_id}", "collaborators_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/collaborators{/collaborator}", "teams_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/teams", "hooks_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/hooks", "issue_events_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/issues/events{/number}", "events_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/events", "assignees_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/assignees{/user}", "branches_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/branches{/branch}", "tags_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/tags", "blobs_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/git/blobs{/sha}", "git_tags_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/git/tags{/sha}", "git_refs_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/git/refs{/sha}", "trees_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/git/trees{/sha}", "statuses_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/statuses/{sha}", "languages_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/languages", "stargazers_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/stargazers", "contributors_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/contributors", "subscribers_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/subscribers", "subscription_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/subscription", "commits_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/commits{/sha}", "git_commits_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/git/commits{/sha}", "comments_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/comments{/number}", "issue_comment_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/issues/comments{/number}", "contents_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/contents/{+path}", "compare_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/compare/{base}...{head}", "merges_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/merges", "archive_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/{archive_format}{/ref}", "downloads_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/downloads", "issues_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/issues{/number}", "pulls_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/pulls{/number}", "milestones_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/milestones{/number}", "notifications_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/notifications{?since,all,participating}", "labels_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/labels{/name}", "releases_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/releases{/id}", "deployments_url": "https://api.github.com/repos/gjnoonan/quick-lint-js/deployments", "created_at": "2023-05-11T05:37:41Z", "updated_at": "2023-05-22T08:57:35Z", "pushed_at": "2023-05-29T19:44:11Z", "git_url": "git://github.com/gjnoonan/quick-lint-js.git", "ssh_url": "git@github.com:gjnoonan/quick-lint-js.git", "clone_url": "https://github.com/gjnoonan/quick-lint-js.git", "svn_url": "https://github.com/gjnoonan/quick-lint-js", "homepage": "https://quick-lint-js.com", "size": 24816, "stargazers_count": 0, "watchers_count": 0, "language": "C++", "has_issues": false, "has_projects": true, "has_downloads": true, "has_wiki": false, "has_pages": false, "has_discussions": false, "forks_count": 0, "mirror_url": null, "archived": false, "disabled": false, "open_issues_count": 0, "license": { "key": "gpl-3.0", "name": "GNU General Public License v3.0", "spdx_id": "GPL-3.0", "url": "https://api.github.com/licenses/gpl-3.0", "node_id": "MDc6TGljZW5zZTk=" }, "allow_forking": true, "is_template": false, "web_commit_signoff_required": false, "topics": [], "visibility": "public", "forks": 0, "open_issues": 0, "watchers": 0, "default_branch": "master", "permissions": { "admin": false, "maintain": false, "push": false, "triage": false, "pull": true } } gcli-2.9.1/tests/samples/github_simple_issue.json000066400000000000000000000072761507017207500222250ustar00rootroot00000000000000{ "url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/115", "repository_url": "https://api.github.com/repos/herrhotzenplotz/gcli", "labels_url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/115/labels{/name}", "comments_url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/115/comments", "events_url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/115/events", "html_url": "https://github.com/herrhotzenplotz/gcli/issues/115", "id": 1176990242, "node_id": "I_kwDOGLyhHc5GJ3Ii", "number": 115, "title": "consider removing FILE *out from printing functions", "user": { "login": "herrhotzenplotz", "id": 34663024, "node_id": "MDQ6VXNlcjM0NjYzMDI0", "avatar_url": "https://avatars.githubusercontent.com/u/34663024?v=4", "gravatar_id": "", "url": "https://api.github.com/users/herrhotzenplotz", "html_url": "https://github.com/herrhotzenplotz", "followers_url": "https://api.github.com/users/herrhotzenplotz/followers", "following_url": "https://api.github.com/users/herrhotzenplotz/following{/other_user}", "gists_url": "https://api.github.com/users/herrhotzenplotz/gists{/gist_id}", "starred_url": "https://api.github.com/users/herrhotzenplotz/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/herrhotzenplotz/subscriptions", "organizations_url": "https://api.github.com/users/herrhotzenplotz/orgs", "repos_url": "https://api.github.com/users/herrhotzenplotz/repos", "events_url": "https://api.github.com/users/herrhotzenplotz/events{/privacy}", "received_events_url": "https://api.github.com/users/herrhotzenplotz/received_events", "type": "User", "site_admin": false }, "labels": [ ], "state": "closed", "locked": false, "assignee": null, "assignees": [ ], "milestone": null, "comments": 0, "created_at": "2022-03-22T16:06:10Z", "updated_at": "2022-04-13T18:33:40Z", "closed_at": "2022-04-13T18:33:40Z", "author_association": "OWNER", "active_lock_reason": null, "body": "We use these functions with ghcli only anyways. In the GUI stuff we use the datastructures returned by the api directly. And If we output, it is stdout everywhere.\n", "closed_by": { "login": "herrhotzenplotz", "id": 34663024, "node_id": "MDQ6VXNlcjM0NjYzMDI0", "avatar_url": "https://avatars.githubusercontent.com/u/34663024?v=4", "gravatar_id": "", "url": "https://api.github.com/users/herrhotzenplotz", "html_url": "https://github.com/herrhotzenplotz", "followers_url": "https://api.github.com/users/herrhotzenplotz/followers", "following_url": "https://api.github.com/users/herrhotzenplotz/following{/other_user}", "gists_url": "https://api.github.com/users/herrhotzenplotz/gists{/gist_id}", "starred_url": "https://api.github.com/users/herrhotzenplotz/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/herrhotzenplotz/subscriptions", "organizations_url": "https://api.github.com/users/herrhotzenplotz/orgs", "repos_url": "https://api.github.com/users/herrhotzenplotz/repos", "events_url": "https://api.github.com/users/herrhotzenplotz/events{/privacy}", "received_events_url": "https://api.github.com/users/herrhotzenplotz/received_events", "type": "User", "site_admin": false }, "reactions": { "url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/115/reactions", "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 }, "timeline_url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/115/timeline", "performed_via_github_app": null, "state_reason": "completed" } gcli-2.9.1/tests/samples/github_simple_label.json000066400000000000000000000003511507017207500221370ustar00rootroot00000000000000{ "id": 3431203676, "node_id": "LA_kwDOGLyhHc7MhANc", "url": "https://api.github.com/repos/herrhotzenplotz/gcli/labels/bug", "name": "bug", "color": "d73a4a", "default": true, "description": "Something isn't working" } gcli-2.9.1/tests/samples/github_simple_milestone.json000066400000000000000000000031631507017207500230630ustar00rootroot00000000000000{ "url": "https://api.github.com/repos/herrhotzenplotz/gcli/milestones/1", "html_url": "https://github.com/herrhotzenplotz/gcli/milestone/1", "labels_url": "https://api.github.com/repos/herrhotzenplotz/gcli/milestones/1/labels", "id": 7485436, "node_id": "MI_kwDOGLyhHc4Acjf8", "number": 1, "title": "Gitlab support", "description": "", "creator": { "login": "herrhotzenplotz", "id": 34663024, "node_id": "MDQ6VXNlcjM0NjYzMDI0", "avatar_url": "https://avatars.githubusercontent.com/u/34663024?v=4", "gravatar_id": "", "url": "https://api.github.com/users/herrhotzenplotz", "html_url": "https://github.com/herrhotzenplotz", "followers_url": "https://api.github.com/users/herrhotzenplotz/followers", "following_url": "https://api.github.com/users/herrhotzenplotz/following{/other_user}", "gists_url": "https://api.github.com/users/herrhotzenplotz/gists{/gist_id}", "starred_url": "https://api.github.com/users/herrhotzenplotz/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/herrhotzenplotz/subscriptions", "organizations_url": "https://api.github.com/users/herrhotzenplotz/orgs", "repos_url": "https://api.github.com/users/herrhotzenplotz/repos", "events_url": "https://api.github.com/users/herrhotzenplotz/events{/privacy}", "received_events_url": "https://api.github.com/users/herrhotzenplotz/received_events", "type": "User", "site_admin": false }, "open_issues": 0, "closed_issues": 8, "state": "open", "created_at": "2021-12-14T07:02:05Z", "updated_at": "2021-12-19T14:49:43Z", "due_on": null, "closed_at": null } gcli-2.9.1/tests/samples/github_simple_pull.json000066400000000000000000000476751507017207500220600ustar00rootroot00000000000000{ "url": "https://api.github.com/repos/herrhotzenplotz/gcli/pulls/113", "id": 886044243, "node_id": "PR_kwDOGLyhHc40z_ZT", "html_url": "https://github.com/herrhotzenplotz/gcli/pull/113", "diff_url": "https://github.com/herrhotzenplotz/gcli/pull/113.diff", "patch_url": "https://github.com/herrhotzenplotz/gcli/pull/113.patch", "issue_url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/113", "number": 113, "state": "closed", "locked": false, "title": "mark notifications as read/done", "user": { "login": "herrhotzenplotz", "id": 34663024, "node_id": "MDQ6VXNlcjM0NjYzMDI0", "avatar_url": "https://avatars.githubusercontent.com/u/34663024?v=4", "gravatar_id": "", "url": "https://api.github.com/users/herrhotzenplotz", "html_url": "https://github.com/herrhotzenplotz", "followers_url": "https://api.github.com/users/herrhotzenplotz/followers", "following_url": "https://api.github.com/users/herrhotzenplotz/following{/other_user}", "gists_url": "https://api.github.com/users/herrhotzenplotz/gists{/gist_id}", "starred_url": "https://api.github.com/users/herrhotzenplotz/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/herrhotzenplotz/subscriptions", "organizations_url": "https://api.github.com/users/herrhotzenplotz/orgs", "repos_url": "https://api.github.com/users/herrhotzenplotz/repos", "events_url": "https://api.github.com/users/herrhotzenplotz/events{/privacy}", "received_events_url": "https://api.github.com/users/herrhotzenplotz/received_events", "type": "User", "site_admin": false }, "body": "Fixes #99\n", "created_at": "2022-03-22T13:20:57Z", "updated_at": "2022-03-22T14:27:14Z", "closed_at": "2022-03-22T14:25:52Z", "merged_at": "2022-03-22T14:25:52Z", "merge_commit_sha": "81251c35de91dcc69612dd0d4e25e7fffaa08474", "assignee": null, "assignees": [ ], "requested_reviewers": [ ], "requested_teams": [ ], "labels": [ ], "milestone": null, "draft": false, "commits_url": "https://api.github.com/repos/herrhotzenplotz/gcli/pulls/113/commits", "review_comments_url": "https://api.github.com/repos/herrhotzenplotz/gcli/pulls/113/comments", "review_comment_url": "https://api.github.com/repos/herrhotzenplotz/gcli/pulls/comments{/number}", "comments_url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/113/comments", "statuses_url": "https://api.github.com/repos/herrhotzenplotz/gcli/statuses/a00f475af1e31d56c7a5839508a21e2b76a31e49", "head": { "label": "herrhotzenplotz:99", "ref": "99", "sha": "a00f475af1e31d56c7a5839508a21e2b76a31e49", "user": { "login": "herrhotzenplotz", "id": 34663024, "node_id": "MDQ6VXNlcjM0NjYzMDI0", "avatar_url": "https://avatars.githubusercontent.com/u/34663024?v=4", "gravatar_id": "", "url": "https://api.github.com/users/herrhotzenplotz", "html_url": "https://github.com/herrhotzenplotz", "followers_url": "https://api.github.com/users/herrhotzenplotz/followers", "following_url": "https://api.github.com/users/herrhotzenplotz/following{/other_user}", "gists_url": "https://api.github.com/users/herrhotzenplotz/gists{/gist_id}", "starred_url": "https://api.github.com/users/herrhotzenplotz/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/herrhotzenplotz/subscriptions", "organizations_url": "https://api.github.com/users/herrhotzenplotz/orgs", "repos_url": "https://api.github.com/users/herrhotzenplotz/repos", "events_url": "https://api.github.com/users/herrhotzenplotz/events{/privacy}", "received_events_url": "https://api.github.com/users/herrhotzenplotz/received_events", "type": "User", "site_admin": false }, "repo": { "id": 415015197, "node_id": "R_kgDOGLyhHQ", "name": "gcli", "full_name": "herrhotzenplotz/gcli", "private": false, "owner": { "login": "herrhotzenplotz", "id": 34663024, "node_id": "MDQ6VXNlcjM0NjYzMDI0", "avatar_url": "https://avatars.githubusercontent.com/u/34663024?v=4", "gravatar_id": "", "url": "https://api.github.com/users/herrhotzenplotz", "html_url": "https://github.com/herrhotzenplotz", "followers_url": "https://api.github.com/users/herrhotzenplotz/followers", "following_url": "https://api.github.com/users/herrhotzenplotz/following{/other_user}", "gists_url": "https://api.github.com/users/herrhotzenplotz/gists{/gist_id}", "starred_url": "https://api.github.com/users/herrhotzenplotz/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/herrhotzenplotz/subscriptions", "organizations_url": "https://api.github.com/users/herrhotzenplotz/orgs", "repos_url": "https://api.github.com/users/herrhotzenplotz/repos", "events_url": "https://api.github.com/users/herrhotzenplotz/events{/privacy}", "received_events_url": "https://api.github.com/users/herrhotzenplotz/received_events", "type": "User", "site_admin": false }, "html_url": "https://github.com/herrhotzenplotz/gcli", "description": "Somewhat portable and secure CLI utility to interact with both GitHub and GitLab. Development is happening at https://gitlab.com/herrhotzenplotz/gcli but you can also submit issues/PRs here.", "fork": false, "url": "https://api.github.com/repos/herrhotzenplotz/gcli", "forks_url": "https://api.github.com/repos/herrhotzenplotz/gcli/forks", "keys_url": "https://api.github.com/repos/herrhotzenplotz/gcli/keys{/key_id}", "collaborators_url": "https://api.github.com/repos/herrhotzenplotz/gcli/collaborators{/collaborator}", "teams_url": "https://api.github.com/repos/herrhotzenplotz/gcli/teams", "hooks_url": "https://api.github.com/repos/herrhotzenplotz/gcli/hooks", "issue_events_url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/events{/number}", "events_url": "https://api.github.com/repos/herrhotzenplotz/gcli/events", "assignees_url": "https://api.github.com/repos/herrhotzenplotz/gcli/assignees{/user}", "branches_url": "https://api.github.com/repos/herrhotzenplotz/gcli/branches{/branch}", "tags_url": "https://api.github.com/repos/herrhotzenplotz/gcli/tags", "blobs_url": "https://api.github.com/repos/herrhotzenplotz/gcli/git/blobs{/sha}", "git_tags_url": "https://api.github.com/repos/herrhotzenplotz/gcli/git/tags{/sha}", "git_refs_url": "https://api.github.com/repos/herrhotzenplotz/gcli/git/refs{/sha}", "trees_url": "https://api.github.com/repos/herrhotzenplotz/gcli/git/trees{/sha}", "statuses_url": "https://api.github.com/repos/herrhotzenplotz/gcli/statuses/{sha}", "languages_url": "https://api.github.com/repos/herrhotzenplotz/gcli/languages", "stargazers_url": "https://api.github.com/repos/herrhotzenplotz/gcli/stargazers", "contributors_url": "https://api.github.com/repos/herrhotzenplotz/gcli/contributors", "subscribers_url": "https://api.github.com/repos/herrhotzenplotz/gcli/subscribers", "subscription_url": "https://api.github.com/repos/herrhotzenplotz/gcli/subscription", "commits_url": "https://api.github.com/repos/herrhotzenplotz/gcli/commits{/sha}", "git_commits_url": "https://api.github.com/repos/herrhotzenplotz/gcli/git/commits{/sha}", "comments_url": "https://api.github.com/repos/herrhotzenplotz/gcli/comments{/number}", "issue_comment_url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/comments{/number}", "contents_url": "https://api.github.com/repos/herrhotzenplotz/gcli/contents/{+path}", "compare_url": "https://api.github.com/repos/herrhotzenplotz/gcli/compare/{base}...{head}", "merges_url": "https://api.github.com/repos/herrhotzenplotz/gcli/merges", "archive_url": "https://api.github.com/repos/herrhotzenplotz/gcli/{archive_format}{/ref}", "downloads_url": "https://api.github.com/repos/herrhotzenplotz/gcli/downloads", "issues_url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues{/number}", "pulls_url": "https://api.github.com/repos/herrhotzenplotz/gcli/pulls{/number}", "milestones_url": "https://api.github.com/repos/herrhotzenplotz/gcli/milestones{/number}", "notifications_url": "https://api.github.com/repos/herrhotzenplotz/gcli/notifications{?since,all,participating}", "labels_url": "https://api.github.com/repos/herrhotzenplotz/gcli/labels{/name}", "releases_url": "https://api.github.com/repos/herrhotzenplotz/gcli/releases{/id}", "deployments_url": "https://api.github.com/repos/herrhotzenplotz/gcli/deployments", "created_at": "2021-10-08T14:20:15Z", "updated_at": "2023-08-15T20:58:16Z", "pushed_at": "2023-08-11T07:58:26Z", "git_url": "git://github.com/herrhotzenplotz/gcli.git", "ssh_url": "git@github.com:herrhotzenplotz/gcli.git", "clone_url": "https://github.com/herrhotzenplotz/gcli.git", "svn_url": "https://github.com/herrhotzenplotz/gcli", "homepage": "https://herrhotzenplotz.de/gcli/", "size": 2169, "stargazers_count": 19, "watchers_count": 19, "language": "C", "has_issues": true, "has_projects": false, "has_downloads": true, "has_wiki": false, "has_pages": false, "has_discussions": false, "forks_count": 0, "mirror_url": null, "archived": false, "disabled": false, "open_issues_count": 0, "license": { "key": "bsd-2-clause", "name": "BSD 2-Clause \"Simplified\" License", "spdx_id": "BSD-2-Clause", "url": "https://api.github.com/licenses/bsd-2-clause", "node_id": "MDc6TGljZW5zZTQ=" }, "allow_forking": true, "is_template": false, "web_commit_signoff_required": false, "topics": [ "c", "cli", "freebsd", "github-api", "gitlab", "gitlab-api", "libcurl", "linux", "unix" ], "visibility": "public", "forks": 0, "open_issues": 0, "watchers": 19, "default_branch": "trunk" } }, "base": { "label": "herrhotzenplotz:trunk", "ref": "trunk", "sha": "5970c6ec82cefe2f4815edf42b6e982595077b1f", "user": { "login": "herrhotzenplotz", "id": 34663024, "node_id": "MDQ6VXNlcjM0NjYzMDI0", "avatar_url": "https://avatars.githubusercontent.com/u/34663024?v=4", "gravatar_id": "", "url": "https://api.github.com/users/herrhotzenplotz", "html_url": "https://github.com/herrhotzenplotz", "followers_url": "https://api.github.com/users/herrhotzenplotz/followers", "following_url": "https://api.github.com/users/herrhotzenplotz/following{/other_user}", "gists_url": "https://api.github.com/users/herrhotzenplotz/gists{/gist_id}", "starred_url": "https://api.github.com/users/herrhotzenplotz/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/herrhotzenplotz/subscriptions", "organizations_url": "https://api.github.com/users/herrhotzenplotz/orgs", "repos_url": "https://api.github.com/users/herrhotzenplotz/repos", "events_url": "https://api.github.com/users/herrhotzenplotz/events{/privacy}", "received_events_url": "https://api.github.com/users/herrhotzenplotz/received_events", "type": "User", "site_admin": false }, "repo": { "id": 415015197, "node_id": "R_kgDOGLyhHQ", "name": "gcli", "full_name": "herrhotzenplotz/gcli", "private": false, "owner": { "login": "herrhotzenplotz", "id": 34663024, "node_id": "MDQ6VXNlcjM0NjYzMDI0", "avatar_url": "https://avatars.githubusercontent.com/u/34663024?v=4", "gravatar_id": "", "url": "https://api.github.com/users/herrhotzenplotz", "html_url": "https://github.com/herrhotzenplotz", "followers_url": "https://api.github.com/users/herrhotzenplotz/followers", "following_url": "https://api.github.com/users/herrhotzenplotz/following{/other_user}", "gists_url": "https://api.github.com/users/herrhotzenplotz/gists{/gist_id}", "starred_url": "https://api.github.com/users/herrhotzenplotz/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/herrhotzenplotz/subscriptions", "organizations_url": "https://api.github.com/users/herrhotzenplotz/orgs", "repos_url": "https://api.github.com/users/herrhotzenplotz/repos", "events_url": "https://api.github.com/users/herrhotzenplotz/events{/privacy}", "received_events_url": "https://api.github.com/users/herrhotzenplotz/received_events", "type": "User", "site_admin": false }, "html_url": "https://github.com/herrhotzenplotz/gcli", "description": "Somewhat portable and secure CLI utility to interact with both GitHub and GitLab. Development is happening at https://gitlab.com/herrhotzenplotz/gcli but you can also submit issues/PRs here.", "fork": false, "url": "https://api.github.com/repos/herrhotzenplotz/gcli", "forks_url": "https://api.github.com/repos/herrhotzenplotz/gcli/forks", "keys_url": "https://api.github.com/repos/herrhotzenplotz/gcli/keys{/key_id}", "collaborators_url": "https://api.github.com/repos/herrhotzenplotz/gcli/collaborators{/collaborator}", "teams_url": "https://api.github.com/repos/herrhotzenplotz/gcli/teams", "hooks_url": "https://api.github.com/repos/herrhotzenplotz/gcli/hooks", "issue_events_url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/events{/number}", "events_url": "https://api.github.com/repos/herrhotzenplotz/gcli/events", "assignees_url": "https://api.github.com/repos/herrhotzenplotz/gcli/assignees{/user}", "branches_url": "https://api.github.com/repos/herrhotzenplotz/gcli/branches{/branch}", "tags_url": "https://api.github.com/repos/herrhotzenplotz/gcli/tags", "blobs_url": "https://api.github.com/repos/herrhotzenplotz/gcli/git/blobs{/sha}", "git_tags_url": "https://api.github.com/repos/herrhotzenplotz/gcli/git/tags{/sha}", "git_refs_url": "https://api.github.com/repos/herrhotzenplotz/gcli/git/refs{/sha}", "trees_url": "https://api.github.com/repos/herrhotzenplotz/gcli/git/trees{/sha}", "statuses_url": "https://api.github.com/repos/herrhotzenplotz/gcli/statuses/{sha}", "languages_url": "https://api.github.com/repos/herrhotzenplotz/gcli/languages", "stargazers_url": "https://api.github.com/repos/herrhotzenplotz/gcli/stargazers", "contributors_url": "https://api.github.com/repos/herrhotzenplotz/gcli/contributors", "subscribers_url": "https://api.github.com/repos/herrhotzenplotz/gcli/subscribers", "subscription_url": "https://api.github.com/repos/herrhotzenplotz/gcli/subscription", "commits_url": "https://api.github.com/repos/herrhotzenplotz/gcli/commits{/sha}", "git_commits_url": "https://api.github.com/repos/herrhotzenplotz/gcli/git/commits{/sha}", "comments_url": "https://api.github.com/repos/herrhotzenplotz/gcli/comments{/number}", "issue_comment_url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/comments{/number}", "contents_url": "https://api.github.com/repos/herrhotzenplotz/gcli/contents/{+path}", "compare_url": "https://api.github.com/repos/herrhotzenplotz/gcli/compare/{base}...{head}", "merges_url": "https://api.github.com/repos/herrhotzenplotz/gcli/merges", "archive_url": "https://api.github.com/repos/herrhotzenplotz/gcli/{archive_format}{/ref}", "downloads_url": "https://api.github.com/repos/herrhotzenplotz/gcli/downloads", "issues_url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues{/number}", "pulls_url": "https://api.github.com/repos/herrhotzenplotz/gcli/pulls{/number}", "milestones_url": "https://api.github.com/repos/herrhotzenplotz/gcli/milestones{/number}", "notifications_url": "https://api.github.com/repos/herrhotzenplotz/gcli/notifications{?since,all,participating}", "labels_url": "https://api.github.com/repos/herrhotzenplotz/gcli/labels{/name}", "releases_url": "https://api.github.com/repos/herrhotzenplotz/gcli/releases{/id}", "deployments_url": "https://api.github.com/repos/herrhotzenplotz/gcli/deployments", "created_at": "2021-10-08T14:20:15Z", "updated_at": "2023-08-15T20:58:16Z", "pushed_at": "2023-08-11T07:58:26Z", "git_url": "git://github.com/herrhotzenplotz/gcli.git", "ssh_url": "git@github.com:herrhotzenplotz/gcli.git", "clone_url": "https://github.com/herrhotzenplotz/gcli.git", "svn_url": "https://github.com/herrhotzenplotz/gcli", "homepage": "https://herrhotzenplotz.de/gcli/", "size": 2169, "stargazers_count": 19, "watchers_count": 19, "language": "C", "has_issues": true, "has_projects": false, "has_downloads": true, "has_wiki": false, "has_pages": false, "has_discussions": false, "forks_count": 0, "mirror_url": null, "archived": false, "disabled": false, "open_issues_count": 0, "license": { "key": "bsd-2-clause", "name": "BSD 2-Clause \"Simplified\" License", "spdx_id": "BSD-2-Clause", "url": "https://api.github.com/licenses/bsd-2-clause", "node_id": "MDc6TGljZW5zZTQ=" }, "allow_forking": true, "is_template": false, "web_commit_signoff_required": false, "topics": [ "c", "cli", "freebsd", "github-api", "gitlab", "gitlab-api", "libcurl", "linux", "unix" ], "visibility": "public", "forks": 0, "open_issues": 0, "watchers": 19, "default_branch": "trunk" } }, "_links": { "self": { "href": "https://api.github.com/repos/herrhotzenplotz/gcli/pulls/113" }, "html": { "href": "https://github.com/herrhotzenplotz/gcli/pull/113" }, "issue": { "href": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/113" }, "comments": { "href": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/113/comments" }, "review_comments": { "href": "https://api.github.com/repos/herrhotzenplotz/gcli/pulls/113/comments" }, "review_comment": { "href": "https://api.github.com/repos/herrhotzenplotz/gcli/pulls/comments{/number}" }, "commits": { "href": "https://api.github.com/repos/herrhotzenplotz/gcli/pulls/113/commits" }, "statuses": { "href": "https://api.github.com/repos/herrhotzenplotz/gcli/statuses/a00f475af1e31d56c7a5839508a21e2b76a31e49" } }, "author_association": "OWNER", "auto_merge": null, "active_lock_reason": null, "merged": true, "mergeable": null, "rebaseable": null, "mergeable_state": "unknown", "merged_by": { "login": "herrhotzenplotz", "id": 34663024, "node_id": "MDQ6VXNlcjM0NjYzMDI0", "avatar_url": "https://avatars.githubusercontent.com/u/34663024?v=4", "gravatar_id": "", "url": "https://api.github.com/users/herrhotzenplotz", "html_url": "https://github.com/herrhotzenplotz", "followers_url": "https://api.github.com/users/herrhotzenplotz/followers", "following_url": "https://api.github.com/users/herrhotzenplotz/following{/other_user}", "gists_url": "https://api.github.com/users/herrhotzenplotz/gists{/gist_id}", "starred_url": "https://api.github.com/users/herrhotzenplotz/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/herrhotzenplotz/subscriptions", "organizations_url": "https://api.github.com/users/herrhotzenplotz/orgs", "repos_url": "https://api.github.com/users/herrhotzenplotz/repos", "events_url": "https://api.github.com/users/herrhotzenplotz/events{/privacy}", "received_events_url": "https://api.github.com/users/herrhotzenplotz/received_events", "type": "User", "site_admin": false }, "comments": 0, "review_comments": 0, "maintainer_can_modify": false, "commits": 6, "additions": 177, "deletions": 82, "changed_files": 13 } gcli-2.9.1/tests/samples/github_simple_release.json000066400000000000000000000052701507017207500225050ustar00rootroot00000000000000{ "url": "https://api.github.com/repos/herrhotzenplotz/gcli/releases/116031718", "assets_url": "https://api.github.com/repos/herrhotzenplotz/gcli/releases/116031718/assets", "upload_url": "https://uploads.github.com/repos/herrhotzenplotz/gcli/releases/116031718/assets{?name,label}", "html_url": "https://github.com/herrhotzenplotz/gcli/releases/tag/1.2.0", "id": 116031718, "author": { "login": "herrhotzenplotz", "id": 34663024, "node_id": "MDQ6VXNlcjM0NjYzMDI0", "avatar_url": "https://avatars.githubusercontent.com/u/34663024?v=4", "gravatar_id": "", "url": "https://api.github.com/users/herrhotzenplotz", "html_url": "https://github.com/herrhotzenplotz", "followers_url": "https://api.github.com/users/herrhotzenplotz/followers", "following_url": "https://api.github.com/users/herrhotzenplotz/following{/other_user}", "gists_url": "https://api.github.com/users/herrhotzenplotz/gists{/gist_id}", "starred_url": "https://api.github.com/users/herrhotzenplotz/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/herrhotzenplotz/subscriptions", "organizations_url": "https://api.github.com/users/herrhotzenplotz/orgs", "repos_url": "https://api.github.com/users/herrhotzenplotz/repos", "events_url": "https://api.github.com/users/herrhotzenplotz/events{/privacy}", "received_events_url": "https://api.github.com/users/herrhotzenplotz/received_events", "type": "User", "site_admin": false }, "node_id": "RE_kwDOGLyhHc4G6oDm", "tag_name": "1.2.0", "target_commitish": "release", "name": "1.2.0", "draft": false, "prerelease": false, "created_at": "2023-08-11T07:42:37Z", "published_at": "2023-08-11T07:58:26Z", "assets": [ ], "tarball_url": "https://api.github.com/repos/herrhotzenplotz/gcli/tarball/1.2.0", "zipball_url": "https://api.github.com/repos/herrhotzenplotz/gcli/zipball/1.2.0", "body": "# Version 1.2.0\n\nThis is version 1.2.0 of gcli.\n\n## Notes\n\nPlease test and report bugs.\n\nYou can download autotoolized tarballs at: https://herrhotzenplotz.de/gcli/releases/gcli-1.2.0/\n\n## Bug Fixes\n\n- Fix compile error when providing --with-libcurl without any arguments\n- Fix memory leaks in string processing functions\n- Fix missing nul termination in read-file function\n- Fix segmentation fault when clearing the milestone of a PR on Gitea\n- Fix missing documentation for milestone action in issues and pulls\n- Set the 'merged' flag properly when showing Gitlab merge requests\n\n## New features\n\n- Add a config subcommand for managing ssh keys (see gcli-config(1))\n- Show number of comments/notes in list of issues and PRs\n- Add support for milestone management in pull requests\n" } gcli-2.9.1/tests/samples/github_simple_repo.json000066400000000000000000000153031507017207500220300ustar00rootroot00000000000000{ "id": 415015197, "node_id": "R_kgDOGLyhHQ", "name": "gcli", "full_name": "herrhotzenplotz/gcli", "private": false, "owner": { "login": "herrhotzenplotz", "id": 34663024, "node_id": "MDQ6VXNlcjM0NjYzMDI0", "avatar_url": "https://avatars.githubusercontent.com/u/34663024?v=4", "gravatar_id": "", "url": "https://api.github.com/users/herrhotzenplotz", "html_url": "https://github.com/herrhotzenplotz", "followers_url": "https://api.github.com/users/herrhotzenplotz/followers", "following_url": "https://api.github.com/users/herrhotzenplotz/following{/other_user}", "gists_url": "https://api.github.com/users/herrhotzenplotz/gists{/gist_id}", "starred_url": "https://api.github.com/users/herrhotzenplotz/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/herrhotzenplotz/subscriptions", "organizations_url": "https://api.github.com/users/herrhotzenplotz/orgs", "repos_url": "https://api.github.com/users/herrhotzenplotz/repos", "events_url": "https://api.github.com/users/herrhotzenplotz/events{/privacy}", "received_events_url": "https://api.github.com/users/herrhotzenplotz/received_events", "type": "User", "site_admin": false }, "html_url": "https://github.com/herrhotzenplotz/gcli", "description": "Somewhat portable and secure CLI utility to interact with both GitHub and GitLab. Development is happening at https://gitlab.com/herrhotzenplotz/gcli but you can also submit issues/PRs here.", "fork": false, "url": "https://api.github.com/repos/herrhotzenplotz/gcli", "forks_url": "https://api.github.com/repos/herrhotzenplotz/gcli/forks", "keys_url": "https://api.github.com/repos/herrhotzenplotz/gcli/keys{/key_id}", "collaborators_url": "https://api.github.com/repos/herrhotzenplotz/gcli/collaborators{/collaborator}", "teams_url": "https://api.github.com/repos/herrhotzenplotz/gcli/teams", "hooks_url": "https://api.github.com/repos/herrhotzenplotz/gcli/hooks", "issue_events_url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/events{/number}", "events_url": "https://api.github.com/repos/herrhotzenplotz/gcli/events", "assignees_url": "https://api.github.com/repos/herrhotzenplotz/gcli/assignees{/user}", "branches_url": "https://api.github.com/repos/herrhotzenplotz/gcli/branches{/branch}", "tags_url": "https://api.github.com/repos/herrhotzenplotz/gcli/tags", "blobs_url": "https://api.github.com/repos/herrhotzenplotz/gcli/git/blobs{/sha}", "git_tags_url": "https://api.github.com/repos/herrhotzenplotz/gcli/git/tags{/sha}", "git_refs_url": "https://api.github.com/repos/herrhotzenplotz/gcli/git/refs{/sha}", "trees_url": "https://api.github.com/repos/herrhotzenplotz/gcli/git/trees{/sha}", "statuses_url": "https://api.github.com/repos/herrhotzenplotz/gcli/statuses/{sha}", "languages_url": "https://api.github.com/repos/herrhotzenplotz/gcli/languages", "stargazers_url": "https://api.github.com/repos/herrhotzenplotz/gcli/stargazers", "contributors_url": "https://api.github.com/repos/herrhotzenplotz/gcli/contributors", "subscribers_url": "https://api.github.com/repos/herrhotzenplotz/gcli/subscribers", "subscription_url": "https://api.github.com/repos/herrhotzenplotz/gcli/subscription", "commits_url": "https://api.github.com/repos/herrhotzenplotz/gcli/commits{/sha}", "git_commits_url": "https://api.github.com/repos/herrhotzenplotz/gcli/git/commits{/sha}", "comments_url": "https://api.github.com/repos/herrhotzenplotz/gcli/comments{/number}", "issue_comment_url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues/comments{/number}", "contents_url": "https://api.github.com/repos/herrhotzenplotz/gcli/contents/{+path}", "compare_url": "https://api.github.com/repos/herrhotzenplotz/gcli/compare/{base}...{head}", "merges_url": "https://api.github.com/repos/herrhotzenplotz/gcli/merges", "archive_url": "https://api.github.com/repos/herrhotzenplotz/gcli/{archive_format}{/ref}", "downloads_url": "https://api.github.com/repos/herrhotzenplotz/gcli/downloads", "issues_url": "https://api.github.com/repos/herrhotzenplotz/gcli/issues{/number}", "pulls_url": "https://api.github.com/repos/herrhotzenplotz/gcli/pulls{/number}", "milestones_url": "https://api.github.com/repos/herrhotzenplotz/gcli/milestones{/number}", "notifications_url": "https://api.github.com/repos/herrhotzenplotz/gcli/notifications{?since,all,participating}", "labels_url": "https://api.github.com/repos/herrhotzenplotz/gcli/labels{/name}", "releases_url": "https://api.github.com/repos/herrhotzenplotz/gcli/releases{/id}", "deployments_url": "https://api.github.com/repos/herrhotzenplotz/gcli/deployments", "created_at": "2021-10-08T14:20:15Z", "updated_at": "2023-08-15T20:58:16Z", "pushed_at": "2023-09-01T22:01:34Z", "git_url": "git://github.com/herrhotzenplotz/gcli.git", "ssh_url": "git@github.com:herrhotzenplotz/gcli.git", "clone_url": "https://github.com/herrhotzenplotz/gcli.git", "svn_url": "https://github.com/herrhotzenplotz/gcli", "homepage": "https://herrhotzenplotz.de/gcli/", "size": 2531, "stargazers_count": 19, "watchers_count": 19, "language": "C", "has_issues": true, "has_projects": false, "has_downloads": true, "has_wiki": false, "has_pages": false, "has_discussions": false, "forks_count": 0, "mirror_url": null, "archived": false, "disabled": false, "open_issues_count": 0, "license": { "key": "bsd-2-clause", "name": "BSD 2-Clause \"Simplified\" License", "spdx_id": "BSD-2-Clause", "url": "https://api.github.com/licenses/bsd-2-clause", "node_id": "MDc6TGljZW5zZTQ=" }, "allow_forking": true, "is_template": false, "web_commit_signoff_required": false, "topics": [ "c", "cli", "freebsd", "github-api", "gitlab", "gitlab-api", "libcurl", "linux", "unix" ], "visibility": "public", "forks": 0, "open_issues": 0, "watchers": 19, "default_branch": "trunk", "permissions": { "admin": true, "maintain": true, "push": true, "triage": true, "pull": true }, "temp_clone_token": "", "allow_squash_merge": true, "allow_merge_commit": true, "allow_rebase_merge": true, "allow_auto_merge": false, "delete_branch_on_merge": false, "allow_update_branch": false, "use_squash_pr_title_as_default": false, "squash_merge_commit_message": "COMMIT_MESSAGES", "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", "merge_commit_message": "PR_TITLE", "merge_commit_title": "MERGE_MESSAGE", "security_and_analysis": { "secret_scanning": { "status": "disabled" }, "secret_scanning_push_protection": { "status": "disabled" }, "dependabot_security_updates": { "status": "disabled" } }, "network_count": 0, "subscribers_count": 4 } gcli-2.9.1/tests/samples/gitlab_error_unauthorised.json000066400000000000000000000000421507017207500234070ustar00rootroot00000000000000{ "message": "401 Unauthorized" } gcli-2.9.1/tests/samples/gitlab_simple_fork.json000066400000000000000000000131041507017207500220010ustar00rootroot00000000000000{ "id": 39885442, "description": "Somewhat portable and secure CLI utility to interact with various Git forges.", "name": "gcli", "name_with_namespace": "Gavin-John Noonan / gcli", "path": "gcli", "path_with_namespace": "gjnoonan/gcli", "created_at": "2022-10-02T13:54:20.517Z", "default_branch": "trunk", "tag_list": [], "topics": [], "ssh_url_to_repo": "git@gitlab.com:gjnoonan/gcli.git", "http_url_to_repo": "https://gitlab.com/gjnoonan/gcli.git", "web_url": "https://gitlab.com/gjnoonan/gcli", "readme_url": "https://gitlab.com/gjnoonan/gcli/-/blob/trunk/README.md", "forks_count": 0, "avatar_url": null, "star_count": 0, "last_activity_at": "2023-09-02T14:55:09.091Z", "namespace": { "id": 242706, "name": "Gavin-John Noonan", "path": "gjnoonan", "kind": "user", "full_path": "gjnoonan", "parent_id": null, "avatar_url": "https://secure.gravatar.com/avatar/89a6c7fbad7932af9cb579e240c9b4f6?s=80&d=identicon", "web_url": "https://gitlab.com/gjnoonan" }, "container_registry_image_prefix": "registry.gitlab.com/gjnoonan/gcli", "_links": { "self": "https://gitlab.com/api/v4/projects/39885442", "issues": "https://gitlab.com/api/v4/projects/39885442/issues", "merge_requests": "https://gitlab.com/api/v4/projects/39885442/merge_requests", "repo_branches": "https://gitlab.com/api/v4/projects/39885442/repository/branches", "labels": "https://gitlab.com/api/v4/projects/39885442/labels", "events": "https://gitlab.com/api/v4/projects/39885442/events", "members": "https://gitlab.com/api/v4/projects/39885442/members", "cluster_agents": "https://gitlab.com/api/v4/projects/39885442/cluster_agents" }, "packages_enabled": true, "empty_repo": false, "archived": false, "visibility": "public", "owner": { "id": 205880, "username": "gjnoonan", "name": "Gavin-John Noonan", "state": "active", "avatar_url": "https://secure.gravatar.com/avatar/89a6c7fbad7932af9cb579e240c9b4f6?s=80&d=identicon", "web_url": "https://gitlab.com/gjnoonan" }, "resolve_outdated_diff_discussions": false, "container_expiration_policy": { "cadence": "1d", "enabled": false, "keep_n": 10, "older_than": "90d", "name_regex": ".*", "name_regex_keep": null, "next_run_at": "2022-10-03T13:54:20.551Z" }, "issues_enabled": true, "merge_requests_enabled": true, "wiki_enabled": true, "jobs_enabled": true, "snippets_enabled": true, "container_registry_enabled": true, "service_desk_enabled": true, "can_create_merge_request_in": true, "issues_access_level": "enabled", "repository_access_level": "enabled", "merge_requests_access_level": "enabled", "forking_access_level": "enabled", "wiki_access_level": "enabled", "builds_access_level": "enabled", "snippets_access_level": "enabled", "pages_access_level": "enabled", "analytics_access_level": "enabled", "container_registry_access_level": "enabled", "security_and_compliance_access_level": "private", "releases_access_level": "enabled", "environments_access_level": "enabled", "feature_flags_access_level": "enabled", "infrastructure_access_level": "enabled", "monitor_access_level": "enabled", "emails_disabled": false, "emails_enabled": true, "shared_runners_enabled": true, "lfs_enabled": true, "creator_id": 205880, "forked_from_project": { "id": 34707535, "description": "Somewhat portable and secure CLI utility to interact with various Git forges.", "name": "gcli", "name_with_namespace": "Nico Sonack / gcli", "path": "gcli", "path_with_namespace": "herrhotzenplotz/gcli", "created_at": "2022-03-22T16:57:59.891Z", "default_branch": "trunk", "tag_list": [], "topics": [], "ssh_url_to_repo": "git@gitlab.com:herrhotzenplotz/gcli.git", "http_url_to_repo": "https://gitlab.com/herrhotzenplotz/gcli.git", "web_url": "https://gitlab.com/herrhotzenplotz/gcli", "readme_url": "https://gitlab.com/herrhotzenplotz/gcli/-/blob/trunk/README.md", "forks_count": 2, "avatar_url": null, "star_count": 2, "last_activity_at": "2023-09-02T16:44:49.863Z", "namespace": { "id": 7926179, "name": "Nico Sonack", "path": "herrhotzenplotz", "kind": "user", "full_path": "herrhotzenplotz", "parent_id": null, "avatar_url": "/uploads/-/system/user/avatar/5980462/avatar.png", "web_url": "https://gitlab.com/herrhotzenplotz" } }, "mr_default_target_self": false, "import_status": "finished", "open_issues_count": 0, "description_html": "

Somewhat portable and secure CLI utility to interact with various Git forges.

", "updated_at": "2023-09-02T14:55:09.091Z", "ci_config_path": "", "public_jobs": true, "shared_with_groups": [], "only_allow_merge_if_pipeline_succeeds": false, "allow_merge_on_skipped_pipeline": null, "request_access_enabled": true, "only_allow_merge_if_all_discussions_are_resolved": false, "remove_source_branch_after_merge": true, "printing_merge_request_link_enabled": true, "merge_method": "merge", "squash_option": "default_off", "enforce_auth_checks_on_uploads": true, "suggestion_commit_message": null, "merge_commit_template": null, "squash_commit_template": null, "issue_branch_template": null, "autoclose_referenced_issues": true, "external_authorization_classification_label": "", "requirements_enabled": false, "requirements_access_level": "enabled", "security_and_compliance_enabled": false, "compliance_frameworks": [], "permissions": { "project_access": null, "group_access": null } } gcli-2.9.1/tests/samples/gitlab_simple_issue.json000066400000000000000000000034461507017207500222000ustar00rootroot00000000000000{"id":132128913,"iid":193,"project_id":34707535,"title":"Make notifications API use a list struct containing both the ptr and size","description":"That would make some of the code much cleaner","state":"closed","created_at":"2023-08-13T18:43:05.766Z","updated_at":"2023-08-23T23:51:46.360Z","closed_at":"2023-08-23T23:51:46.350Z","closed_by":{"id":5980462,"username":"herrhotzenplotz","name":"Nico Sonack","state":"active","avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/5980462/avatar.png","web_url":"https://gitlab.com/herrhotzenplotz"},"labels":["good-first-issue"],"milestone":null,"assignees":[],"author":{"id":5980462,"username":"herrhotzenplotz","name":"Nico Sonack","state":"active","avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/5980462/avatar.png","web_url":"https://gitlab.com/herrhotzenplotz"},"type":"ISSUE","assignee":null,"user_notes_count":2,"merge_requests_count":0,"upvotes":0,"downvotes":0,"due_date":null,"confidential":false,"discussion_locked":null,"issue_type":"issue","web_url":"https://gitlab.com/herrhotzenplotz/gcli/-/issues/193","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"task_completion_status":{"count":0,"completed_count":0},"blocking_issues_count":0,"has_tasks":true,"task_status":"0 of 0 checklist items completed","_links":{"self":"https://gitlab.com/api/v4/projects/34707535/issues/193","notes":"https://gitlab.com/api/v4/projects/34707535/issues/193/notes","award_emoji":"https://gitlab.com/api/v4/projects/34707535/issues/193/award_emoji","project":"https://gitlab.com/api/v4/projects/34707535","closed_as_duplicate_of":null},"references":{"short":"#193","relative":"#193","full":"herrhotzenplotz/gcli#193"},"severity":"UNKNOWN","subscribed":true,"moved_to_id":null,"service_desk_reply_to":null}gcli-2.9.1/tests/samples/gitlab_simple_label.json000066400000000000000000000003551507017207500221230ustar00rootroot00000000000000{"id":24376073,"name":"bug","description":"Something isn't working as expected","description_html":"Something isn't working as expected","text_color":"#FFFFFF","color":"#d73a4a","subscribed":false,"priority":null,"is_project_label":true}gcli-2.9.1/tests/samples/gitlab_simple_merge_request.json000066400000000000000000000076711507017207500237230ustar00rootroot00000000000000{"id":246912053,"iid":216,"project_id":34707535,"title":"Fix test suite","description":"This finally fixes the broken test suite","state":"merged","created_at":"2023-08-31T23:37:50.848Z","updated_at":"2023-09-01T17:20:03.185Z","merged_by":{"id":5980462,"username":"herrhotzenplotz","name":"Nico Sonack","state":"active","avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/5980462/avatar.png","web_url":"https://gitlab.com/herrhotzenplotz"},"merge_user":{"id":5980462,"username":"herrhotzenplotz","name":"Nico Sonack","state":"active","avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/5980462/avatar.png","web_url":"https://gitlab.com/herrhotzenplotz"},"merged_at":"2023-09-01T17:20:02.486Z","closed_by":null,"closed_at":null,"target_branch":"trunk","source_branch":"fix-test-suite","user_notes_count":0,"upvotes":0,"downvotes":0,"author":{"id":5980462,"username":"herrhotzenplotz","name":"Nico Sonack","state":"active","avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/5980462/avatar.png","web_url":"https://gitlab.com/herrhotzenplotz"},"assignees":[],"assignee":null,"reviewers":[],"source_project_id":34707535,"target_project_id":34707535,"labels":[],"draft":false,"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"can_be_merged","detailed_merge_status":"not_open","sha":"3eab596a6806434e4a34bb19de12307ab1217af3","merge_commit_sha":"767e01417ac36f0090ee6d5da737cfb5135e6fc6","squash_commit_sha":null,"discussion_locked":null,"should_remove_source_branch":false,"force_remove_source_branch":null,"prepared_at":"2023-08-31T23:37:58.547Z","reference":"!216","references":{"short":"!216","relative":"!216","full":"herrhotzenplotz/gcli!216"},"web_url":"https://gitlab.com/herrhotzenplotz/gcli/-/merge_requests/216","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"squash_on_merge":false,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"approvals_before_merge":null,"subscribed":true,"changes_count":"98","latest_build_started_at":"2023-09-01T17:02:48.982Z","latest_build_finished_at":"2023-09-01T17:15:35.537Z","first_deployed_to_production_at":null,"pipeline":{"id":989409992,"iid":116,"project_id":34707535,"sha":"3eab596a6806434e4a34bb19de12307ab1217af3","ref":"refs/merge-requests/216/head","status":"success","source":"merge_request_event","created_at":"2023-09-01T17:02:48.091Z","updated_at":"2023-09-01T17:15:35.546Z","web_url":"https://gitlab.com/herrhotzenplotz/gcli/-/pipelines/989409992"},"head_pipeline":{"id":989409992,"iid":116,"project_id":34707535,"sha":"3eab596a6806434e4a34bb19de12307ab1217af3","ref":"refs/merge-requests/216/head","status":"success","source":"merge_request_event","created_at":"2023-09-01T17:02:48.091Z","updated_at":"2023-09-01T17:15:35.546Z","web_url":"https://gitlab.com/herrhotzenplotz/gcli/-/pipelines/989409992","before_sha":"0000000000000000000000000000000000000000","tag":false,"yaml_errors":null,"user":{"id":5980462,"username":"herrhotzenplotz","name":"Nico Sonack","state":"active","avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/5980462/avatar.png","web_url":"https://gitlab.com/herrhotzenplotz"},"started_at":"2023-09-01T17:02:48.982Z","finished_at":"2023-09-01T17:15:35.537Z","committed_at":null,"duration":450,"queued_duration":null,"coverage":null,"detailed_status":{"icon":"status_success","text":"passed","label":"passed","group":"success","tooltip":"passed","has_details":true,"details_path":"/herrhotzenplotz/gcli/-/pipelines/989409992","illustration":null,"favicon":"/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"}},"diff_refs":{"base_sha":"4295254675687c212df05a78f561566aea716780","head_sha":"3eab596a6806434e4a34bb19de12307ab1217af3","start_sha":"4295254675687c212df05a78f561566aea716780"},"merge_error":null,"first_contribution":false,"user":{"can_merge":true}}gcli-2.9.1/tests/samples/gitlab_simple_milestone.json000066400000000000000000000005201507017207500230350ustar00rootroot00000000000000{"id":2975318,"iid":2,"project_id":34707535,"title":"Version 2","description":"Things that need to be done for version 2","state":"active","created_at":"2023-02-05T19:08:20.379Z","updated_at":"2023-02-05T19:08:20.379Z","due_date":null,"start_date":null,"expired":false,"web_url":"https://gitlab.com/herrhotzenplotz/gcli/-/milestones/2"}gcli-2.9.1/tests/samples/gitlab_simple_pipeline.json000066400000000000000000000021671507017207500226540ustar00rootroot00000000000000{"id":989897020,"iid":121,"project_id":34707535,"sha":"742affb88a297a6b34201ad61c8b5b72ec6eb679","ref":"refs/merge-requests/219/head","status":"failed","source":"merge_request_event","created_at":"2023-09-02T14:30:20.925Z","updated_at":"2023-09-02T14:31:40.328Z","web_url":"https://gitlab.com/herrhotzenplotz/gcli/-/pipelines/989897020","before_sha":"0000000000000000000000000000000000000000","tag":false,"yaml_errors":null,"user":{"id":5980462,"username":"herrhotzenplotz","name":"Nico Sonack","state":"active","avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/5980462/avatar.png","web_url":"https://gitlab.com/herrhotzenplotz"},"started_at":"2023-09-02T14:30:21.867Z","finished_at":"2023-09-02T14:31:40.317Z","committed_at":null,"duration":73,"queued_duration":null,"coverage":null,"detailed_status":{"icon":"status_failed","text":"failed","label":"failed","group":"failed","tooltip":"failed","has_details":true,"details_path":"/herrhotzenplotz/gcli/-/pipelines/989897020","illustration":null,"favicon":"/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"},"name":null}gcli-2.9.1/tests/samples/gitlab_simple_release.json000066400000000000000000000066021507017207500224650ustar00rootroot00000000000000{"name":"1.2.0","tag_name":"1.2.0","description":"# Version 1.2.0\n\nThis is version 1.2.0 of gcli.\n\n## Notes\n\nPlease test and report bugs.\n\nYou can download autotoolized tarballs at: https://herrhotzenplotz.de/gcli/releases/gcli-1.2.0/\n\n## Bug Fixes\n\n- Fix compile error when providing --with-libcurl without any arguments\n- Fix memory leaks in string processing functions\n- Fix missing nul termination in read-file function\n- Fix segmentation fault when clearing the milestone of a PR on Gitea\n- Fix missing documentation for milestone action in issues and pulls\n- Set the 'merged' flag properly when showing Gitlab merge requests\n\n## New features\n\n- Add a config subcommand for managing ssh keys (see gcli-config(1))\n- Show number of comments/notes in list of issues and PRs\n- Add support for milestone management in pull requests\n","created_at":"2023-08-11T07:56:06.371Z","released_at":"2023-08-11T07:56:06.371Z","upcoming_release":false,"author":{"id":5980462,"username":"herrhotzenplotz","name":"Nico Sonack","state":"active","avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/5980462/avatar.png","web_url":"https://gitlab.com/herrhotzenplotz"},"commit":{"id":"a6e295d088b4215ad52cb25d269e54b204acc471","short_id":"a6e295d0","created_at":"2023-08-11T09:42:37.000+02:00","parent_ids":["2503abe114b4701ccd42cf187349e5dc7d382a2b","f1b942e965a5d462a350faf0ff37fda86c25a931"],"title":"Merge branch 'trunk' into release","message":"Merge branch 'trunk' into release\n","author_name":"Nico Sonack","author_email":"nsonack@herrhotzenplotz.de","authored_date":"2023-08-11T09:42:37.000+02:00","committer_name":"Nico Sonack","committer_email":"nsonack@herrhotzenplotz.de","committed_date":"2023-08-11T09:42:37.000+02:00","trailers":{},"web_url":"https://gitlab.com/herrhotzenplotz/gcli/-/commit/a6e295d088b4215ad52cb25d269e54b204acc471"},"commit_path":"/herrhotzenplotz/gcli/-/commit/a6e295d088b4215ad52cb25d269e54b204acc471","tag_path":"/herrhotzenplotz/gcli/-/tags/1.2.0","assets":{"count":4,"sources":[{"format":"zip","url":"https://gitlab.com/herrhotzenplotz/gcli/-/archive/1.2.0/gcli-1.2.0.zip"},{"format":"tar.gz","url":"https://gitlab.com/herrhotzenplotz/gcli/-/archive/1.2.0/gcli-1.2.0.tar.gz"},{"format":"tar.bz2","url":"https://gitlab.com/herrhotzenplotz/gcli/-/archive/1.2.0/gcli-1.2.0.tar.bz2"},{"format":"tar","url":"https://gitlab.com/herrhotzenplotz/gcli/-/archive/1.2.0/gcli-1.2.0.tar"}],"links":[]},"evidences":[{"sha":"c4daff0bf736e2a5b4abe83f2811d01fe2d03ba287ed","filepath":"https://gitlab.com/herrhotzenplotz/gcli/-/releases/1.2.0/evidences/5392288.json","collected_at":"2023-08-11T07:56:06.694Z"}],"_links":{"closed_issues_url":"https://gitlab.com/herrhotzenplotz/gcli/-/issues?release_tag=1.2.0\u0026scope=all\u0026state=closed","closed_merge_requests_url":"https://gitlab.com/herrhotzenplotz/gcli/-/merge_requests?release_tag=1.2.0\u0026scope=all\u0026state=closed","edit_url":"https://gitlab.com/herrhotzenplotz/gcli/-/releases/1.2.0/edit","merged_merge_requests_url":"https://gitlab.com/herrhotzenplotz/gcli/-/merge_requests?release_tag=1.2.0\u0026scope=all\u0026state=merged","opened_issues_url":"https://gitlab.com/herrhotzenplotz/gcli/-/issues?release_tag=1.2.0\u0026scope=all\u0026state=opened","opened_merge_requests_url":"https://gitlab.com/herrhotzenplotz/gcli/-/merge_requests?release_tag=1.2.0\u0026scope=all\u0026state=opened","self":"https://gitlab.com/herrhotzenplotz/gcli/-/releases/1.2.0"}}gcli-2.9.1/tests/samples/gitlab_simple_repo.json000066400000000000000000000113521507017207500220100ustar00rootroot00000000000000{"id":34707535,"description":"Somewhat portable and secure CLI utility to interact with various Git forges.","name":"gcli","name_with_namespace":"Nico Sonack / gcli","path":"gcli","path_with_namespace":"herrhotzenplotz/gcli","created_at":"2022-03-22T16:57:59.891Z","default_branch":"trunk","tag_list":[],"topics":[],"ssh_url_to_repo":"git@gitlab.com:herrhotzenplotz/gcli.git","http_url_to_repo":"https://gitlab.com/herrhotzenplotz/gcli.git","web_url":"https://gitlab.com/herrhotzenplotz/gcli","readme_url":"https://gitlab.com/herrhotzenplotz/gcli/-/blob/trunk/README.md","forks_count":2,"avatar_url":null,"star_count":2,"last_activity_at":"2023-09-03T22:07:05.424Z","namespace":{"id":7926179,"name":"Nico Sonack","path":"herrhotzenplotz","kind":"user","full_path":"herrhotzenplotz","parent_id":null,"avatar_url":"/uploads/-/system/user/avatar/5980462/avatar.png","web_url":"https://gitlab.com/herrhotzenplotz"},"container_registry_image_prefix":"registry.gitlab.com/herrhotzenplotz/gcli","_links":{"self":"https://gitlab.com/api/v4/projects/34707535","issues":"https://gitlab.com/api/v4/projects/34707535/issues","merge_requests":"https://gitlab.com/api/v4/projects/34707535/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/34707535/repository/branches","labels":"https://gitlab.com/api/v4/projects/34707535/labels","events":"https://gitlab.com/api/v4/projects/34707535/events","members":"https://gitlab.com/api/v4/projects/34707535/members","cluster_agents":"https://gitlab.com/api/v4/projects/34707535/cluster_agents"},"packages_enabled":true,"empty_repo":false,"archived":false,"visibility":"public","owner":{"id":5980462,"username":"herrhotzenplotz","name":"Nico Sonack","state":"active","avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/5980462/avatar.png","web_url":"https://gitlab.com/herrhotzenplotz"},"resolve_outdated_diff_discussions":false,"container_expiration_policy":{"cadence":"1d","enabled":false,"keep_n":10,"older_than":"90d","name_regex":".*","name_regex_keep":null,"next_run_at":"2022-03-23T16:57:59.919Z"},"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"container_registry_enabled":true,"service_desk_enabled":true,"service_desk_address":"contact-project+herrhotzenplotz-gcli-34707535-issue-@incoming.gitlab.com","can_create_merge_request_in":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","forking_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","pages_access_level":"enabled","analytics_access_level":"enabled","container_registry_access_level":"enabled","security_and_compliance_access_level":"private","releases_access_level":"enabled","environments_access_level":"enabled","feature_flags_access_level":"enabled","infrastructure_access_level":"enabled","monitor_access_level":"enabled","emails_disabled":false,"emails_enabled":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":5980462,"import_url":"https://github.com/herrhotzenplotz/ghcli.git","import_type":"github","import_status":"finished","import_error":null,"open_issues_count":20,"description_html":"\u003cp data-sourcepos=\"1:1-1:77\" dir=\"auto\"\u003eSomewhat portable and secure CLI utility to interact with various Git forges.\u003c/p\u003e","updated_at":"2023-09-03T22:07:05.424Z","ci_default_git_depth":20,"ci_forward_deployment_enabled":true,"ci_forward_deployment_rollback_allowed":true,"ci_job_token_scope_enabled":false,"ci_separated_caches":true,"ci_allow_fork_pipelines_to_run_in_parent_project":true,"build_git_strategy":"fetch","keep_latest_artifact":true,"restrict_user_defined_variables":false,"runners_token":"GR1348941v4uDTusyFqmfzCjiTcGb","runner_token_expiration_interval":null,"group_runners_enabled":true,"auto_cancel_pending_pipelines":"enabled","build_timeout":3600,"auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","ci_config_path":"","public_jobs":true,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"allow_merge_on_skipped_pipeline":false,"request_access_enabled":true,"only_allow_merge_if_all_discussions_are_resolved":true,"remove_source_branch_after_merge":true,"printing_merge_request_link_enabled":true,"merge_method":"merge","squash_option":"default_off","enforce_auth_checks_on_uploads":true,"suggestion_commit_message":"","merge_commit_template":null,"squash_commit_template":null,"issue_branch_template":null,"autoclose_referenced_issues":true,"external_authorization_classification_label":"","requirements_enabled":false,"requirements_access_level":"enabled","security_and_compliance_enabled":true,"compliance_frameworks":[],"permissions":{"project_access":{"access_level":50,"notification_level":3},"group_access":null}}gcli-2.9.1/tests/samples/gitlab_simple_snippet.json000066400000000000000000000014331507017207500225240ustar00rootroot00000000000000{"id":2141655,"title":"darcy-weisbach SPARC64","description":"Paste","visibility":"public","author":{"id":5980462,"username":"herrhotzenplotz","name":"Nico Sonack","state":"active","avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/5980462/avatar.png","web_url":"https://gitlab.com/herrhotzenplotz"},"created_at":"2021-06-28T15:47:36.214Z","updated_at":"2021-06-28T15:47:36.214Z","project_id":null,"web_url":"https://gitlab.com/-/snippets/2141655","raw_url":"https://gitlab.com/-/snippets/2141655/raw","ssh_url_to_repo":"git@gitlab.com:snippets/2141655.git","http_url_to_repo":"https://gitlab.com/snippets/2141655.git","file_name":"darcy-weisbach SPARC64","files":[{"path":"darcy-weisbach SPARC64","raw_url":"https://gitlab.com/-/snippets/2141655/raw/main/darcy-weisbach%20SPARC64"}]}gcli-2.9.1/tests/samples/gitlab_token_expired.json000066400000000000000000000001101507017207500223200ustar00rootroot00000000000000{ "error": "token_expired", "error_description": "Token has expired." } gcli-2.9.1/tests/samples/multiline_change_with_comment.diff000066400000000000000000000006641507017207500241770ustar00rootroot00000000000000From 361f83923b9924a3e8796b0ddf03f768e26a1236 Mon Sep 17 00:00:00 2001 diff --git a/README.md b/README.md index 5ab731f..2b99524 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,8 @@ Das hier ist nur ein kurzer Test. Ich füge zum Test hier mal eine neue Zeile ein. > This is the comment. > It also spans across multiple lines. { - - - +This is just a change. +Across multiple lines. } This line belongs to a different commit. gcli-2.9.1/tests/samples/patch_series_fail_get_comments.patch000066400000000000000000000025311507017207500245050ustar00rootroot00000000000000From 361f83923b9924a3e8796b0ddf03f768e26a1236 Mon Sep 17 00:00:00 2001 From: Nico Sonack Date: 2023-09-16T20:28:33.000Z Subject: Update README.md diff --git a/README.md b/README.md index e4bc08b3..361f8392 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ # README Das hier ist nur ein kurzer Test. Deine Mutter { + + +Ich füge zum Test hier mal eine neue Zeile ein. } -- 2.42.2 From d9cbace712a92fdd0bac4f08b6d42e75069af363 Mon Sep 17 00:00:00 2001 From: Nico Sonack Date: 2023-09-20T18:09:58.000Z Subject: Second commit diff --git a/README.md b/README.md index 361f8392..d9cbace7 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,11 @@ Das hier ist nur ein kurzer Test. Ich füge zum Test hier mal eine neue Zeile ein. + + + + + + + Naja... { +This line belongs to a different commit. } -- 2.42.2 From ee1a0406025c6efcb72081481f83b45ff24a863a Mon Sep 17 00:00:00 2001 From: Nico Sonack Date: 2023-10-15T21:49:54.000Z Subject: delete some shit diff --git a/README.md b/README.md index d9cbace7..ee1a0406 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,8 @@ Das hier ist nur ein kurzer Test. Ich füge zum Test hier mal eine neue Zeile ein. - - - +This is just a change. +Across multiple lines. This line belongs to a different commit. -- 2.42.2 gcli-2.9.1/tests/samples/simple_patch_series.patch000066400000000000000000000022721507017207500223210ustar00rootroot00000000000000GCLI: base_sha f00b4rc01dc0fee This is just a global comment. It should not end up in the patch prelude but in the patch series prelude. From 361f83923b9924a3e8796b0ddf03f768e26a1236 Mon Sep 17 00:00:00 2001 From: Nico Sonack Date: Sat, 16 Sep 2023 22:28:33 +0200 Subject: [PATCH 1/2] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5ab731f..2b99524 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ # README Das hier ist nur ein kurzer Test. > Why so much whitespace? { + + } +Ich füge zum Test hier mal eine neue Zeile ein. From d9cbace712a92fdd0bac4f08b6d42e75069af363 Mon Sep 17 00:00:00 2001 From: Nico Sonack Date: Wed, 20 Sep 2023 20:09:58 +0200 Subject: [PATCH 2/2] Second commit This is the body of the commit. --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 2b99524..90a258f 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,11 @@ Das hier ist nur ein kurzer Test. Ich füge zum Test hier mal eine neue Zeile ein. > Why all this whitespace? { + + + + + + + } +This line belongs to a different commit. gcli-2.9.1/tests/samples/simple_patch_with_comments.patch000066400000000000000000000017651507017207500237150ustar00rootroot00000000000000From 47b40f51cae6cec9a3132f888fd66c21ecb687fa Mon Sep 17 00:00:00 2001 From: Nico Sonack Date: Sun, 10 Oct 2021 12:23:11 +0200 Subject: [PATCH] Start submission implementation --- include/ghcli/pulls.h | 1 + src/ghcli.c | 55 +++++++++++++++++++++++++++++++++++++++++++ src/pulls.c | 9 +++++++ 3 files changed, 65 insertions(+) diff --git a/include/ghcli/pulls.h b/include/ghcli/pulls.h index 30a503cf..05d233eb 100644 --- a/include/ghcli/pulls.h +++ b/include/ghcli/pulls.h @@ -57,5 +57,6 @@ int ghcli_get_prs(const char *org, const char *reponame, bool all, ghcli_pull * void ghcli_print_pr_table(FILE *stream, ghcli_pull *pulls, int pulls_size); void ghcli_print_pr_diff(FILE *stream, const char *org, const char *reponame, int pr_number); void ghcli_pr_summary(FILE *stream, const char *org, const char *reponame, int pr_number); This is a comment on line 60. +void ghcli_pr_submit(const char *from, const char *to, int in_draft); #endif /* PULLS_H */ gcli-2.9.1/tests/samples/stuff_with_no_newline_in_diff.diff000066400000000000000000000003071507017207500241620ustar00rootroot00000000000000diff --git a/test.txt b/test.txt index 493021b..68a4528 100644 --- a/test.txt +++ b/test.txt @@ -1 +1 @@ > This is a comment { -this is a test file +this is a test file \ No newline at end of file } gcli-2.9.1/tests/samples/version_1_object_format.patch000066400000000000000000000007071507017207500231030ustar00rootroot00000000000000From a4545b5e32af1be6ba8f41a80dc885ce6c34d36aa5958dfba05b79ffeef8a084 Mon Sep 17 00:00:00 2001 From: Nico Sonack Date: Mon, 16 Oct 2023 17:36:56 +0200 Subject: [PATCH] Help 2 Signed-off-by: Nico Sonack --- bar | 1 + 1 file changed, 1 insertion(+) create mode 100644 bar diff --git a/bar b/bar new file mode 100644 index 0000000..1f0626c --- /dev/null +++ b/bar @@ -0,0 +1 @@ +lol -- 2.42.0 gcli-2.9.1/tests/update-samples.sh000077500000000000000000000040741507017207500170770ustar00rootroot00000000000000#!/bin/sh -x die() { printf -- "error: $@\n" >&2 exit 1 } which gcli > /dev/null 2>&1 || die "gcli is not in PATH" which jq > /dev/null 2>&1 || die "jq is not in PATH" gcli -t github api /repos/herrhotzenplotz/gcli/issues/115 > samples/github_simple_issue.json gcli -t github api /repos/herrhotzenplotz/gcli/labels/bug > samples/github_simple_label.json gcli -t github api /repos/herrhotzenplotz/gcli/pulls/113 > samples/github_simple_pull.json gcli -t github api /repos/herrhotzenplotz/gcli/milestones/1 > samples/github_simple_milestone.json gcli -t github api /repos/herrhotzenplotz/gcli/releases/116031718 > samples/github_simple_release.json gcli -t github api /repos/herrhotzenplotz/gcli > samples/github_simple_repo.json gcli -t github api /repos/quick-lint/quick-lint-js/forks | jq '.[] | select(.id == 639263592)' > samples/github_simple_fork.json gcli -t github api /repos/herrhotzenplotz/gcli/issues/comments/1424392601 > samples/github_simple_comment.json gcli -t github api /repos/quick-lint/quick-lint-js/commits/03a8af3dab144ea38910c1efdcce03fb708f3179/check-runs | jq '.check_runs | .[] | select(.id == 16437184455)' > samples/github_simple_check.json gcli -t gitlab api /projects/herrhotzenplotz%2Fgcli/issues/193 > samples/gitlab_simple_issue.json gcli -t gitlab api /projects/herrhotzenplotz%2Fgcli/labels/24376073 > samples/gitlab_simple_label.json gcli -t gitlab api /projects/herrhotzenplotz%2Fgcli/merge_requests/216 > samples/gitlab_simple_merge_request.json gcli -t gitlab api /projects/herrhotzenplotz%2Fgcli/releases/1.2.0 > samples/gitlab_simple_release.json gcli -t gitlab api /projects/herrhotzenplotz%2Fgcli/forks | jq '.[] | select(.id == 39885442)' > samples/gitlab_simple_fork.json gcli -t gitlab api /projects/herrhotzenplotz%2Fgcli/milestones/2975318 > samples/gitlab_simple_milestone.json gcli -t gitlab api /projects/herrhotzenplotz%2Fgcli/pipelines/989897020 > samples/gitlab_simple_pipeline.json gcli -t gitlab api /projects/herrhotzenplotz%2Fgcli > samples/gitlab_simple_repo.json gcli -t gitlab api /snippets/2141655 > samples/gitlab_simple_snippet.json gcli-2.9.1/tests/url-encode.c000066400000000000000000000024631507017207500160150ustar00rootroot00000000000000#include #include ATF_TC_WITHOUT_HEAD(simple_characters); ATF_TC_BODY(simple_characters, tc) { ATF_CHECK_STREQ(gcli_urlencode("%"), "%25"); ATF_CHECK_STREQ(gcli_urlencode(" "), "%20"); ATF_CHECK_STREQ(gcli_urlencode("-"), "-"); ATF_CHECK_STREQ(gcli_urlencode("_"), "_"); } ATF_TC_WITHOUT_HEAD(umlaute); ATF_TC_BODY(umlaute, tc) { ATF_CHECK_STREQ(gcli_urlencode("Ä"), "%C3%84"); ATF_CHECK_STREQ(gcli_urlencode("ä"), "%C3%A4"); ATF_CHECK_STREQ(gcli_urlencode("Ö"), "%C3%96"); ATF_CHECK_STREQ(gcli_urlencode("ö"), "%C3%B6"); ATF_CHECK_STREQ(gcli_urlencode("Ü"), "%C3%9C"); ATF_CHECK_STREQ(gcli_urlencode("ü"), "%C3%BC"); ATF_CHECK_STREQ(gcli_urlencode("ẞ"), "%E1%BA%9E"); ATF_CHECK_STREQ(gcli_urlencode("ß"), "%C3%9F"); } ATF_TC_WITHOUT_HEAD(torture); ATF_TC_BODY(torture, tc) { char text[] = "some-random url// with %%%%%content" "Rindfleischettikettierungsüberwachungsaufgabenübertragungsgesetz"; char *escaped = gcli_urlencode(text); char *expected = "some-random%20url%2F%2F%20with%20%25%25%25%25%25content" "Rindfleischettikettierungs%C3%BCberwachungsaufgaben%C3%BCbertragungsgesetz"; ATF_CHECK_STREQ(escaped, expected); } ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, simple_characters); ATF_TP_ADD_TC(tp, umlaute); ATF_TP_ADD_TC(tp, torture); return atf_no_error(); } gcli-2.9.1/thirdparty/000077500000000000000000000000001507017207500146375ustar00rootroot00000000000000gcli-2.9.1/thirdparty/pdjson/000077500000000000000000000000001507017207500161345ustar00rootroot00000000000000gcli-2.9.1/thirdparty/pdjson/UNLICENSE000066400000000000000000000022731507017207500174100ustar00rootroot00000000000000This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to gcli-2.9.1/thirdparty/pdjson/pdjson.c000066400000000000000000000633401507017207500176030ustar00rootroot00000000000000#ifndef _POSIX_C_SOURCE # define _POSIX_C_SOURCE 200112L #elif _POSIX_C_SOURCE < 200112L # error incompatible _POSIX_C_SOURCE level #endif #include #include #include #ifndef PDJSON_H # include "pdjson.h" #endif #define JSON_FLAG_ERROR (1u << 0) #define JSON_FLAG_STREAMING (1u << 1) #if defined(_MSC_VER) && (_MSC_VER < 1900) #define json_error(json, format, ...) \ if (!(json->flags & JSON_FLAG_ERROR)) { \ json->flags |= JSON_FLAG_ERROR; \ _snprintf_s(json->errmsg, sizeof(json->errmsg), \ _TRUNCATE, \ format, \ __VA_ARGS__); \ } \ #else #define json_error(json, format, ...) \ if (!(json->flags & JSON_FLAG_ERROR)) { \ json->flags |= JSON_FLAG_ERROR; \ snprintf(json->errmsg, sizeof(json->errmsg), \ format, \ __VA_ARGS__); \ } \ #endif /* _MSC_VER */ /* See also PDJSON_STACK_MAX below. */ #ifndef PDJSON_STACK_INC # define PDJSON_STACK_INC 4 #endif struct json_stack { enum json_type type; long count; }; static enum json_type push(json_stream *json, enum json_type type) { json->stack_top++; #ifdef PDJSON_STACK_MAX if (json->stack_top > PDJSON_STACK_MAX) { json_error(json, "%s", "maximum depth of nesting reached"); return JSON_ERROR; } #endif if (json->stack_top >= json->stack_size) { struct json_stack *stack; size_t size = (json->stack_size + PDJSON_STACK_INC) * sizeof(*json->stack); stack = (struct json_stack *)json->alloc.realloc(json->stack, size); if (stack == NULL) { json_error(json, "%s", "out of memory"); return JSON_ERROR; } json->stack_size += PDJSON_STACK_INC; json->stack = stack; } json->stack[json->stack_top].type = type; json->stack[json->stack_top].count = 0; return type; } /* Note: c is assumed not to be EOF. */ static enum json_type pop(json_stream *json, int c, enum json_type expected) { if (json->stack == NULL || json->stack[json->stack_top].type != expected) { json_error(json, "unexpected byte '%c'", c); return JSON_ERROR; } json->stack_top--; return expected == JSON_ARRAY ? JSON_ARRAY_END : JSON_OBJECT_END; } static int buffer_peek(struct json_source *source) { if (source->position < source->source.buffer.length) return source->source.buffer.buffer[source->position]; else return EOF; } static int buffer_get(struct json_source *source) { int c = source->peek(source); if (c != EOF) source->position++; return c; } static int stream_get(struct json_source *source) { int c = fgetc(source->source.stream.stream); if (c != EOF) source->position++; return c; } static int stream_peek(struct json_source *source) { int c = fgetc(source->source.stream.stream); ungetc(c, source->source.stream.stream); return c; } static void init(json_stream *json) { json->lineno = 1; json->flags = JSON_FLAG_STREAMING; json->errmsg[0] = '\0'; json->ntokens = 0; json->next = (enum json_type)0; json->stack = NULL; json->stack_top = -1; json->stack_size = 0; json->data.string = NULL; json->data.string_size = 0; json->data.string_fill = 0; json->source.position = 0; json->alloc.malloc = malloc; json->alloc.realloc = realloc; json->alloc.free = free; } static enum json_type is_match(json_stream *json, const char *pattern, enum json_type type) { int c; for (const char *p = pattern; *p; p++) { if (*p != (c = json->source.get(&json->source))) { if (c != EOF) { json_error(json, "expected '%c' instead of byte '%c'", *p, c); } else { json_error(json, "expected '%c' instead of end of text", *p); } return JSON_ERROR; } } return type; } static int pushchar(json_stream *json, int c) { if (json->data.string_fill == json->data.string_size) { size_t size = json->data.string_size * 2; char *buffer = (char *)json->alloc.realloc(json->data.string, size); if (buffer == NULL) { json_error(json, "%s", "out of memory"); return -1; } else { json->data.string_size = size; json->data.string = buffer; } } json->data.string[json->data.string_fill++] = c; return 0; } static int init_string(json_stream *json) { json->data.string_fill = 0; if (json->data.string == NULL) { json->data.string_size = 1024; json->data.string = (char *)json->alloc.malloc(json->data.string_size); if (json->data.string == NULL) { json_error(json, "%s", "out of memory"); return -1; } } json->data.string[0] = '\0'; return 0; } static int encode_utf8(json_stream *json, unsigned long c) { if (c < 0x80UL) { return pushchar(json, c); } else if (c < 0x0800UL) { return !((pushchar(json, (c >> 6 & 0x1F) | 0xC0) == 0) && (pushchar(json, (c >> 0 & 0x3F) | 0x80) == 0)); } else if (c < 0x010000UL) { if (c >= 0xd800 && c <= 0xdfff) { json_error(json, "invalid codepoint %06lx", c); return -1; } return !((pushchar(json, (c >> 12 & 0x0F) | 0xE0) == 0) && (pushchar(json, (c >> 6 & 0x3F) | 0x80) == 0) && (pushchar(json, (c >> 0 & 0x3F) | 0x80) == 0)); } else if (c < 0x110000UL) { return !((pushchar(json, (c >> 18 & 0x07) | 0xF0) == 0) && (pushchar(json, (c >> 12 & 0x3F) | 0x80) == 0) && (pushchar(json, (c >> 6 & 0x3F) | 0x80) == 0) && (pushchar(json, (c >> 0 & 0x3F) | 0x80) == 0)); } else { json_error(json, "unable to encode %06lx as UTF-8", c); return -1; } } static int hexchar(int c) { switch (c) { case '0': return 0; case '1': return 1; case '2': return 2; case '3': return 3; case '4': return 4; case '5': return 5; case '6': return 6; case '7': return 7; case '8': return 8; case '9': return 9; case 'a': case 'A': return 10; case 'b': case 'B': return 11; case 'c': case 'C': return 12; case 'd': case 'D': return 13; case 'e': case 'E': return 14; case 'f': case 'F': return 15; default: return -1; } } static long read_unicode_cp(json_stream *json) { long cp = 0; int shift = 12; for (size_t i = 0; i < 4; i++) { int c = json->source.get(&json->source); int hc; if (c == EOF) { json_error(json, "%s", "unterminated string literal in Unicode"); return -1; } else if ((hc = hexchar(c)) == -1) { json_error(json, "invalid escape Unicode byte '%c'", c); return -1; } cp += hc * (1 << shift); shift -= 4; } return cp; } static int read_unicode(json_stream *json) { long cp, h, l; if ((cp = read_unicode_cp(json)) == -1) { return -1; } if (cp >= 0xd800 && cp <= 0xdbff) { /* This is the high portion of a surrogate pair; we need to read the * lower portion to get the codepoint */ h = cp; int c = json->source.get(&json->source); if (c == EOF) { json_error(json, "%s", "unterminated string literal in Unicode"); return -1; } else if (c != '\\') { json_error(json, "invalid continuation for surrogate pair '%c', " "expected '\\'", c); return -1; } c = json->source.get(&json->source); if (c == EOF) { json_error(json, "%s", "unterminated string literal in Unicode"); return -1; } else if (c != 'u') { json_error(json, "invalid continuation for surrogate pair '%c', " "expected 'u'", c); return -1; } if ((l = read_unicode_cp(json)) == -1) { return -1; } if (l < 0xdc00 || l > 0xdfff) { json_error(json, "surrogate pair continuation \\u%04lx out " "of range (dc00-dfff)", l); return -1; } cp = ((h - 0xd800) * 0x400) + ((l - 0xdc00) + 0x10000); } else if (cp >= 0xdc00 && cp <= 0xdfff) { json_error(json, "dangling surrogate \\u%04lx", cp); return -1; } return encode_utf8(json, cp); } static int read_escaped(json_stream *json) { int c = json->source.get(&json->source); if (c == EOF) { json_error(json, "%s", "unterminated string literal in escape"); return -1; } else if (c == 'u') { if (read_unicode(json) != 0) return -1; } else { switch (c) { case '\\': case 'b': case 'f': case 'n': case 'r': case 't': case '/': case '"': { const char *codes = "\\bfnrt/\""; const char *p = strchr(codes, c); if (pushchar(json, "\\\b\f\n\r\t/\""[p - codes]) != 0) return -1; } break; default: json_error(json, "invalid escaped byte '%c'", c); return -1; } } return 0; } static int char_needs_escaping(int c) { if ((c >= 0) && (c < 0x20 || c == 0x22 || c == 0x5c)) { return 1; } return 0; } static int utf8_seq_length(char byte) { unsigned char u = (unsigned char) byte; if (u < 0x80) return 1; if (0x80 <= u && u <= 0xBF) { // second, third or fourth byte of a multi-byte // sequence, i.e. a "continuation byte" return 0; } else if (u == 0xC0 || u == 0xC1) { // overlong encoding of an ASCII byte return 0; } else if (0xC2 <= u && u <= 0xDF) { // 2-byte sequence return 2; } else if (0xE0 <= u && u <= 0xEF) { // 3-byte sequence return 3; } else if (0xF0 <= u && u <= 0xF4) { // 4-byte sequence return 4; } else { // u >= 0xF5 // Restricted (start of 4-, 5- or 6-byte sequence) or invalid UTF-8 return 0; } } static int is_legal_utf8(const unsigned char *bytes, int length) { if (0 == bytes || 0 == length) return 0; unsigned char a; const unsigned char* srcptr = bytes + length; switch (length) { default: return 0; // Everything else falls through when true. case 4: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return 0; /* FALLTHRU */ case 3: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return 0; /* FALLTHRU */ case 2: a = (*--srcptr); switch (*bytes) { case 0xE0: if (a < 0xA0 || a > 0xBF) return 0; break; case 0xED: if (a < 0x80 || a > 0x9F) return 0; break; case 0xF0: if (a < 0x90 || a > 0xBF) return 0; break; case 0xF4: if (a < 0x80 || a > 0x8F) return 0; break; default: if (a < 0x80 || a > 0xBF) return 0; break; } /* FALLTHRU */ case 1: if (*bytes >= 0x80 && *bytes < 0xC2) return 0; } return *bytes <= 0xF4; } static int read_utf8(json_stream* json, int next_char) { int count = utf8_seq_length(next_char); if (!count) { json_error(json, "%s", "invalid UTF-8 character"); return -1; } char buffer[4]; buffer[0] = next_char; int i; for (i = 1; i < count; ++i) { buffer[i] = json->source.get(&json->source); } if (!is_legal_utf8((unsigned char*) buffer, count)) { json_error(json, "%s", "invalid UTF-8 text"); return -1; } for (i = 0; i < count; ++i) { if (pushchar(json, buffer[i]) != 0) return -1; } return 0; } static enum json_type read_string(json_stream *json) { if (init_string(json) != 0) return JSON_ERROR; while (1) { int c = json->source.get(&json->source); if (c == EOF) { json_error(json, "%s", "unterminated string literal"); return JSON_ERROR; } else if (c == '"') { if (pushchar(json, '\0') == 0) return JSON_STRING; else return JSON_ERROR; } else if (c == '\\') { if (read_escaped(json) != 0) return JSON_ERROR; } else if ((unsigned) c >= 0x80) { if (read_utf8(json, c) != 0) return JSON_ERROR; } else { if (char_needs_escaping(c)) { json_error(json, "%s", "unescaped control character in string"); return JSON_ERROR; } if (pushchar(json, c) != 0) return JSON_ERROR; } } return JSON_ERROR; } static int is_digit(int c) { return c >= 48 /*0*/ && c <= 57 /*9*/; } static int read_digits(json_stream *json) { int c; unsigned nread = 0; while (is_digit(c = json->source.peek(&json->source))) { if (pushchar(json, json->source.get(&json->source)) != 0) return -1; nread++; } if (nread == 0) { if (c != EOF) { json_error(json, "expected digit instead of byte '%c'", c); } else { json_error(json, "%s", "expected digit instead of end of text"); } return -1; } return 0; } static enum json_type read_number(json_stream *json, int c) { if (pushchar(json, c) != 0) return JSON_ERROR; if (c == '-') { c = json->source.get(&json->source); if (is_digit(c)) { return read_number(json, c); } else { if (c != EOF) { json_error(json, "unexpected byte '%c' in number", c); } else { json_error(json, "%s", "unexpected end of text in number"); } return JSON_ERROR; } } else if (strchr("123456789", c) != NULL) { c = json->source.peek(&json->source); if (is_digit(c)) { if (read_digits(json) != 0) return JSON_ERROR; } } /* Up to decimal or exponent has been read. */ c = json->source.peek(&json->source); if (strchr(".eE", c) == NULL) { if (pushchar(json, '\0') != 0) return JSON_ERROR; else return JSON_NUMBER; } if (c == '.') { json->source.get(&json->source); // consume . if (pushchar(json, c) != 0) return JSON_ERROR; if (read_digits(json) != 0) return JSON_ERROR; } /* Check for exponent. */ c = json->source.peek(&json->source); if (c == 'e' || c == 'E') { json->source.get(&json->source); // consume e/E if (pushchar(json, c) != 0) return JSON_ERROR; c = json->source.peek(&json->source); if (c == '+' || c == '-') { json->source.get(&json->source); // consume if (pushchar(json, c) != 0) return JSON_ERROR; if (read_digits(json) != 0) return JSON_ERROR; } else if (is_digit(c)) { if (read_digits(json) != 0) return JSON_ERROR; } else { if (c != EOF) { json_error(json, "unexpected byte '%c' in number", c); } else { json_error(json, "%s", "unexpected end of text in number"); } return JSON_ERROR; } } if (pushchar(json, '\0') != 0) return JSON_ERROR; else return JSON_NUMBER; } bool json_isspace(int c) { switch (c) { case 0x09: case 0x0a: case 0x0d: case 0x20: return true; } return false; } /* Returns the next non-whitespace character in the stream. */ static int next(json_stream *json) { int c; while (json_isspace(c = json->source.get(&json->source))) if (c == '\n') json->lineno++; return c; } static enum json_type read_value(json_stream *json, int c) { json->ntokens++; switch (c) { case EOF: json_error(json, "%s", "unexpected end of text"); return JSON_ERROR; case '{': return push(json, JSON_OBJECT); case '[': return push(json, JSON_ARRAY); case '"': return read_string(json); case 'n': return is_match(json, "ull", JSON_NULL); case 'f': return is_match(json, "alse", JSON_FALSE); case 't': return is_match(json, "rue", JSON_TRUE); case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '-': if (init_string(json) != 0) return JSON_ERROR; return read_number(json, c); default: json_error(json, "unexpected byte '%c' in value", c); return JSON_ERROR; } } enum json_type json_peek(json_stream *json) { enum json_type next; if (json->next) next = json->next; else next = json->next = json_next(json); return next; } enum json_type json_next(json_stream *json) { if (json->flags & JSON_FLAG_ERROR) return JSON_ERROR; if (json->next != 0) { enum json_type next = json->next; json->next = (enum json_type)0; return next; } if (json->ntokens > 0 && json->stack_top == (size_t)-1) { /* In the streaming mode leave any trailing whitespaces in the stream. * This allows the user to validate any desired separation between * values (such as newlines) using json_source_get/peek() with any * remaining whitespaces ignored as leading when we parse the next * value. */ if (!(json->flags & JSON_FLAG_STREAMING)) { int c; do { c = json->source.peek(&json->source); if (json_isspace(c)) { c = json->source.get(&json->source); } } while (json_isspace(c)); if (c != EOF) { json_error(json, "expected end of text instead of byte '%c'", c); return JSON_ERROR; } } return JSON_DONE; } int c = next(json); if (json->stack_top == (size_t)-1) { if (c == EOF && (json->flags & JSON_FLAG_STREAMING)) return JSON_DONE; return read_value(json, c); } if (json->stack[json->stack_top].type == JSON_ARRAY) { if (json->stack[json->stack_top].count == 0) { if (c == ']') { return pop(json, c, JSON_ARRAY); } json->stack[json->stack_top].count++; return read_value(json, c); } else if (c == ',') { json->stack[json->stack_top].count++; return read_value(json, next(json)); } else if (c == ']') { return pop(json, c, JSON_ARRAY); } else { if (c != EOF) { json_error(json, "unexpected byte '%c'", c); } else { json_error(json, "%s", "unexpected end of text"); } return JSON_ERROR; } } else if (json->stack[json->stack_top].type == JSON_OBJECT) { if (json->stack[json->stack_top].count == 0) { if (c == '}') { return pop(json, c, JSON_OBJECT); } /* No member name/value pairs yet. */ enum json_type value = read_value(json, c); if (value != JSON_STRING) { if (value != JSON_ERROR) json_error(json, "%s", "expected member name or '}'"); return JSON_ERROR; } else { json->stack[json->stack_top].count++; return value; } } else if ((json->stack[json->stack_top].count % 2) == 0) { /* Expecting comma followed by member name. */ if (c != ',' && c != '}') { json_error(json, "%s", "expected ',' or '}' after member value"); return JSON_ERROR; } else if (c == '}') { return pop(json, c, JSON_OBJECT); } else { enum json_type value = read_value(json, next(json)); if (value != JSON_STRING) { if (value != JSON_ERROR) json_error(json, "%s", "expected member name"); return JSON_ERROR; } else { json->stack[json->stack_top].count++; return value; } } } else if ((json->stack[json->stack_top].count % 2) == 1) { /* Expecting colon followed by value. */ if (c != ':') { json_error(json, "%s", "expected ':' after member name"); return JSON_ERROR; } else { json->stack[json->stack_top].count++; return read_value(json, next(json)); } } } json_error(json, "%s", "invalid parser state"); return JSON_ERROR; } void json_reset(json_stream *json) { json->stack_top = -1; json->ntokens = 0; json->flags &= ~JSON_FLAG_ERROR; json->errmsg[0] = '\0'; } enum json_type json_skip(json_stream *json) { enum json_type type = json_next(json); size_t cnt_arr = 0; size_t cnt_obj = 0; for (enum json_type skip = type; ; skip = json_next(json)) { if (skip == JSON_ERROR || skip == JSON_DONE) return skip; if (skip == JSON_ARRAY) { ++cnt_arr; } else if (skip == JSON_ARRAY_END && cnt_arr > 0) { --cnt_arr; } else if (skip == JSON_OBJECT) { ++cnt_obj; } else if (skip == JSON_OBJECT_END && cnt_obj > 0) { --cnt_obj; } if (!cnt_arr && !cnt_obj) break; } return type; } enum json_type json_skip_until(json_stream *json, enum json_type type) { while (1) { enum json_type skip = json_skip(json); if (skip == JSON_ERROR || skip == JSON_DONE) return skip; if (skip == type) break; } return type; } const char *json_get_string(json_stream *json, size_t *length) { if (length != NULL) *length = json->data.string_fill; if (json->data.string == NULL) return ""; else return json->data.string; } double json_get_number(json_stream *json) { char *p = json->data.string; return p == NULL ? 0 : strtod(p, NULL); } const char *json_get_error(json_stream *json) { return json->flags & JSON_FLAG_ERROR ? json->errmsg : NULL; } size_t json_get_lineno(json_stream *json) { return json->lineno; } size_t json_get_position(json_stream *json) { return json->source.position; } size_t json_get_depth(json_stream *json) { return json->stack_top + 1; } /* Return the current parsing context, that is, JSON_OBJECT if we are inside an object, JSON_ARRAY if we are inside an array, and JSON_DONE if we are not yet/anymore in either. Additionally, for the first two cases, also return the number of parsing events that have already been observed at this level with json_next/peek(). In particular, inside an object, an odd number would indicate that the just observed JSON_STRING event is a member name. */ enum json_type json_get_context(json_stream *json, size_t *count) { if (json->stack_top == (size_t)-1) return JSON_DONE; if (count != NULL) *count = json->stack[json->stack_top].count; return json->stack[json->stack_top].type; } int json_source_get(json_stream *json) { int c = json->source.get(&json->source); if (c == '\n') json->lineno++; return c; } int json_source_peek(json_stream *json) { return json->source.peek(&json->source); } void json_open_buffer(json_stream *json, const void *buffer, size_t size) { init(json); json->source.get = buffer_get; json->source.peek = buffer_peek; json->source.source.buffer.buffer = (const char *)buffer; json->source.source.buffer.length = size; } void json_open_string(json_stream *json, const char *string) { json_open_buffer(json, string, strlen(string)); } void json_open_stream(json_stream *json, FILE * stream) { init(json); json->source.get = stream_get; json->source.peek = stream_peek; json->source.source.stream.stream = stream; } static int user_get(struct json_source *json) { int c = json->source.user.get(json->source.user.ptr); if (c != EOF) json->position++; return c; } static int user_peek(struct json_source *json) { return json->source.user.peek(json->source.user.ptr); } void json_open_user(json_stream *json, json_user_io get, json_user_io peek, void *user) { init(json); json->source.get = user_get; json->source.peek = user_peek; json->source.source.user.ptr = user; json->source.source.user.get = get; json->source.source.user.peek = peek; } void json_set_allocator(json_stream *json, json_allocator *a) { json->alloc = *a; } void json_set_streaming(json_stream *json, bool streaming) { if (streaming) json->flags |= JSON_FLAG_STREAMING; else json->flags &= ~JSON_FLAG_STREAMING; } void json_close(json_stream *json) { json->alloc.free(json->stack); json->alloc.free(json->data.string); } gcli-2.9.1/thirdparty/pdjson/pdjson.h000066400000000000000000000062541507017207500176110ustar00rootroot00000000000000#ifndef PDJSON_H #define PDJSON_H #ifndef PDJSON_SYMEXPORT # define PDJSON_SYMEXPORT #endif #ifdef __cplusplus extern "C" { #else #if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) #include #else #ifndef bool #define bool int #define true 1 #define false 0 #endif /* bool */ #endif /* __STDC_VERSION__ */ #endif /* __cplusplus */ #include enum json_type { JSON_ERROR = 1, JSON_DONE, JSON_OBJECT, JSON_OBJECT_END, JSON_ARRAY, JSON_ARRAY_END, JSON_STRING, JSON_NUMBER, JSON_TRUE, JSON_FALSE, JSON_NULL }; struct json_allocator { void *(*malloc)(size_t); void *(*realloc)(void *, size_t); void (*free)(void *); }; typedef int (*json_user_io)(void *user); typedef struct json_stream json_stream; typedef struct json_allocator json_allocator; PDJSON_SYMEXPORT void json_open_buffer(json_stream *json, const void *buffer, size_t size); PDJSON_SYMEXPORT void json_open_string(json_stream *json, const char *string); PDJSON_SYMEXPORT void json_open_stream(json_stream *json, FILE *stream); PDJSON_SYMEXPORT void json_open_user(json_stream *json, json_user_io get, json_user_io peek, void *user); PDJSON_SYMEXPORT void json_close(json_stream *json); PDJSON_SYMEXPORT void json_set_allocator(json_stream *json, json_allocator *a); PDJSON_SYMEXPORT void json_set_streaming(json_stream *json, bool mode); PDJSON_SYMEXPORT enum json_type json_next(json_stream *json); PDJSON_SYMEXPORT enum json_type json_peek(json_stream *json); PDJSON_SYMEXPORT void json_reset(json_stream *json); PDJSON_SYMEXPORT const char *json_get_string(json_stream *json, size_t *length); PDJSON_SYMEXPORT double json_get_number(json_stream *json); PDJSON_SYMEXPORT enum json_type json_skip(json_stream *json); PDJSON_SYMEXPORT enum json_type json_skip_until(json_stream *json, enum json_type type); PDJSON_SYMEXPORT size_t json_get_lineno(json_stream *json); PDJSON_SYMEXPORT size_t json_get_position(json_stream *json); PDJSON_SYMEXPORT size_t json_get_depth(json_stream *json); PDJSON_SYMEXPORT enum json_type json_get_context(json_stream *json, size_t *count); PDJSON_SYMEXPORT const char *json_get_error(json_stream *json); PDJSON_SYMEXPORT int json_source_get(json_stream *json); PDJSON_SYMEXPORT int json_source_peek(json_stream *json); PDJSON_SYMEXPORT bool json_isspace(int c); /* internal */ struct json_source { int (*get)(struct json_source *); int (*peek)(struct json_source *); size_t position; union { struct { FILE *stream; } stream; struct { const char *buffer; size_t length; } buffer; struct { void *ptr; json_user_io get; json_user_io peek; } user; } source; }; struct json_stream { size_t lineno; struct json_stack *stack; size_t stack_top; size_t stack_size; enum json_type next; unsigned flags; struct { char *string; size_t string_fill; size_t string_size; } data; size_t ntokens; struct json_source source; struct json_allocator alloc; char errmsg[128]; }; #ifdef __cplusplus } /* extern "C" */ #endif /* __cplusplus */ #endif gcli-2.9.1/tools/000077500000000000000000000000001507017207500136055ustar00rootroot00000000000000gcli-2.9.1/tools/gentarball.sh000077500000000000000000000014141507017207500162570ustar00rootroot00000000000000#!/bin/sh # command -v git > /dev/null 2>&1 || (echo "error: you need git to run this script" && exit 1) findversion() { eval $(grep PACKAGE_VERSION $(dirname $0)/../configure | sed 1q) echo $PACKAGE_VERSION } VERSION=$(findversion) DIR=$(git rev-parse --show-toplevel)/dist/gcli-${VERSION} mkdir -p $DIR echo "Making BZIP tarball" git archive --format=tar --prefix=gcli-$VERSION/ @ \ | bzip2 -v > $DIR/gcli-$VERSION.tar.bz2 echo "Making XZ tarball" git archive --format=tar --prefix=gcli-$VERSION/ @ \ | xz -v > $DIR/gcli-$VERSION.tar.xz echo "Making GZIP tarball" git archive --format=tar --prefix=gcli-$VERSION/ @ \ | gzip -v > $DIR/gcli-$VERSION.tar.gz ( cd $DIR echo "Calculating SHA256SUMS" sha256sum *.tar* > SHA256SUMS ) echo "Release Tarballs are at $DIR"