pax_global_header00006660000000000000000000000064146371065060014522gustar00rootroot0000000000000052 comment=517d2c0e2ec3e85465d56e39f55d87007fc33619 hut-0.6.0/000077500000000000000000000000001463710650600123255ustar00rootroot00000000000000hut-0.6.0/.build.yml000066400000000000000000000005211463710650600142230ustar00rootroot00000000000000image: alpine/edge packages: - go - scdoc - bmake sources: - https://git.xenrox.net/~xenrox/hut tasks: - build: | cd hut make - test: | cd hut go test ./... - gofmt: | cd hut test -z $(gofmt -l . | grep -v '^srht/.*/gql.go$') - build-bmake: | cd hut make clean bmake hut-0.6.0/.gitignore000066400000000000000000000000551463710650600143150ustar00rootroot00000000000000/hut /doc/hut.1 /hut.bash /hut.fish /hut.zsh hut-0.6.0/.hut.scfg000066400000000000000000000001571463710650600140520ustar00rootroot00000000000000tracker https://todo.sr.ht/~xenrox/hut development-mailing-list ~xenrox/hut-dev@lists.sr.ht patch-prefix false hut-0.6.0/.prettierignore000066400000000000000000000000201463710650600153600ustar00rootroot00000000000000schema.graphqls hut-0.6.0/.prettierrc000066400000000000000000000001671463710650600145150ustar00rootroot00000000000000{ "tabWidth": 4, "overrides": [ { "files": ["*.yaml", "*.yml"], "options": { "tabWidth": 2 } } ] } hut-0.6.0/LICENSE000066400000000000000000001033331463710650600133350ustar00rootroot00000000000000 GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . hut-0.6.0/Makefile000066400000000000000000000025771463710650600140000ustar00rootroot00000000000000.POSIX: .SUFFIXES: GO = go RM = rm INSTALL = install SCDOC = scdoc GOFLAGS = PREFIX = /usr/local BINDIR = bin MANDIR = share/man BASHCOMPDIR = $(PREFIX)/share/bash-completion/completions ZSHCOMPDIR = $(PREFIX)/share/zsh/site-functions FISHCOMPDIR = $(PREFIX)/share/fish/vendor_completions.d all: hut completions doc/hut.1 hut: $(GO) build $(GOFLAGS) completions: hut.bash hut.zsh hut.fish hut.bash: hut ./hut completion bash >hut.bash hut.zsh: hut ./hut completion zsh >hut.zsh hut.fish: hut ./hut completion fish >hut.fish doc/hut.1: doc/hut.1.scd $(SCDOC) doc/hut.1 clean: $(RM) -f hut doc/hut.1 hut.bash hut.zsh hut.fish install: $(INSTALL) -d \ $(DESTDIR)$(PREFIX)/$(BINDIR)/ \ $(DESTDIR)$(PREFIX)/$(MANDIR)/man1/ \ $(DESTDIR)$(BASHCOMPDIR) \ $(DESTDIR)$(ZSHCOMPDIR) \ $(DESTDIR)$(FISHCOMPDIR) $(INSTALL) -pm 0755 hut $(DESTDIR)$(PREFIX)/$(BINDIR)/ $(INSTALL) -pm 0644 doc/hut.1 $(DESTDIR)$(PREFIX)/$(MANDIR)/man1/ $(INSTALL) -pm 0644 hut.bash $(DESTDIR)$(BASHCOMPDIR)/hut $(INSTALL) -pm 0644 hut.zsh $(DESTDIR)$(ZSHCOMPDIR)/_hut $(INSTALL) -pm 0644 hut.fish $(DESTDIR)$(FISHCOMPDIR)/hut.fish uninstall: $(RM) -f \ $(DESTDIR)$(PREFIX)/$(BINDIR)/hut \ $(DESTDIR)$(PREFIX)/$(MANDIR)/man1/hut.1 \ $(DESTDIR)$(BASHCOMPDIR)/hut \ $(DESTDIR)$(ZSHCOMPDIR)/_hut \ $(DESTDIR)$(FISHCOMPDIR)/hut.fish .PHONY: all hut clean install uninstall completions hut-0.6.0/README.md000066400000000000000000000015331463710650600136060ustar00rootroot00000000000000# [hut] [![builds.sr.ht status](https://builds.xenrox.net/~xenrox/hut/commits/master.svg)](https://builds.xenrox.net/~xenrox/hut/commits/master?) A CLI tool for [sr.ht]. ## Usage Run `hut init` to get started. Read the man page to learn about all commands. ## Building Dependencies: - Go - scdoc (optional, for man pages) For end users, a `Makefile` is provided: make sudo make install ## Contributing Send patches to the [mailing list], report bugs on the [issue tracker]. Join the IRC channel: [#hut on Libera Chat]. ## License AGPLv3 only, see [LICENSE]. Copyright (C) 2021 Simon Ser [hut]: https://sr.ht/~xenrox/hut/ [sr.ht]: https://sr.ht/~sircmpwn/sourcehut/ [mailing list]: https://lists.sr.ht/~xenrox/hut-dev [issue tracker]: https://todo.sr.ht/~xenrox/hut [#hut on Libera Chat]: ircs://irc.libera.chat/#hut [LICENSE]: LICENSE hut-0.6.0/builds.go000066400000000000000000000632601463710650600141450ustar00rootroot00000000000000package main import ( "context" "errors" "fmt" "io" "log" "net/http" "os" "os/exec" "path/filepath" "strconv" "strings" "time" "github.com/dustin/go-humanize" "github.com/juju/ansiterm/tabwriter" "github.com/spf13/cobra" "git.sr.ht/~xenrox/hut/srht/buildssrht" "git.sr.ht/~xenrox/hut/termfmt" ) func newBuildsCommand() *cobra.Command { cmd := &cobra.Command{ Use: "builds", Short: "Use the builds API", } cmd.AddCommand(newBuildsSubmitCommand()) cmd.AddCommand(newBuildsResubmitCommand()) cmd.AddCommand(newBuildsCancelCommand()) cmd.AddCommand(newBuildsShowCommand()) cmd.AddCommand(newBuildsListCommand()) cmd.AddCommand(newBuildsSecretCommand()) cmd.AddCommand(newBuildsSSHCommand()) cmd.AddCommand(newBuildsArtifactsCommand()) cmd.AddCommand(newBuildsUserWebhookCommand()) return cmd } const buildsSubmitPrefill = ` # Please write a build manifest above. The build manifest reference is # available at: # https://man.sr.ht/builds.sr.ht/manifest.md ` func newBuildsSubmitCommand() *cobra.Command { var follow, edit bool var note, tagString, visibility string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("builds", cmd) buildsVisibility, err := buildssrht.ParseVisibility(visibility) if err != nil { log.Fatal(err) } filenames := args if len(args) == 0 { if _, err := os.Stat(".build.yml"); err == nil { filenames = append(filenames, ".build.yml") } if matches, err := filepath.Glob(".builds/*.yml"); err == nil { filenames = append(filenames, matches...) } } if len(filenames) == 0 && !edit { log.Fatal("no build manifest found") } if len(filenames) > 1 && follow { log.Fatal("--follow cannot be used when submitting multiple jobs") } tags := strings.Split(tagString, "/") var manifests []string for _, name := range filenames { var b []byte var err error if name == "-" { b, err = io.ReadAll(os.Stdin) } else { b, err = os.ReadFile(name) } if err != nil { log.Fatalf("failed to read manifest from %q: %v", name, err) } manifests = append(manifests, string(b)) } if edit { if len(manifests) == 0 { manifests = append(manifests, buildsSubmitPrefill) } for i, manifest := range manifests { var err error manifests[i], err = getInputWithEditor("hut*.yml", manifest) if err != nil { log.Fatal(err) } } } for _, manifest := range manifests { job, err := buildssrht.Submit(c.Client, ctx, manifest, tags, ¬e, &buildsVisibility) if err != nil { log.Fatal(err) } if termfmt.IsTerminal() { log.Printf("Started build %v/%v/job/%v", c.BaseURL, job.Owner.CanonicalName, job.Id) } else { fmt.Printf("%v/%v/job/%v\n", c.BaseURL, job.Owner.CanonicalName, job.Id) } if follow { id := job.Id job, err := followJob(ctx, c, job.Id) if err != nil { log.Fatal(err) } if job.Status != buildssrht.JobStatusSuccess { offerSSHConnection(ctx, c, id) } } } } cmd := &cobra.Command{ Use: "submit [manifest...]", Short: "Submit a build manifest", ValidArgsFunction: cobra.FixedCompletions([]string{"yml", "yaml"}, cobra.ShellCompDirectiveFilterFileExt), Run: run, } cmd.Flags().BoolVarP(&follow, "follow", "f", false, "follow build logs") cmd.Flags().BoolVarP(&edit, "edit", "e", false, "edit manifest") cmd.Flags().StringVarP(¬e, "note", "n", "", "short job description") cmd.RegisterFlagCompletionFunc("note", cobra.NoFileCompletions) cmd.Flags().StringVarP(&tagString, "tags", "t", "", "job tags (slash separated)") cmd.RegisterFlagCompletionFunc("tags", cobra.NoFileCompletions) cmd.Flags().StringVarP(&visibility, "visibility", "v", "unlisted", "builds visibility") cmd.RegisterFlagCompletionFunc("visibility", completeVisibility) return cmd } func newBuildsResubmitCommand() *cobra.Command { var follow, edit bool var note, visibility string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() id, instance, err := parseBuildID(args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("builds", cmd, instance) oldJob, err := buildssrht.Manifest(c.Client, ctx, id) if err != nil { log.Fatalf("failed to get build manifest: %v", err) } else if oldJob == nil { log.Fatal("failed to get build manifest: invalid job ID") } var buildsVisibility buildssrht.Visibility if visibility != "" { var err error buildsVisibility, err = buildssrht.ParseVisibility(visibility) if err != nil { log.Fatal(err) } } else { buildsVisibility = oldJob.Visibility } if edit { content, err := getInputWithEditor("hut*.yml", oldJob.Manifest) if err != nil { log.Fatal(err) } oldJob.Manifest = content } if note == "" { note = fmt.Sprintf("Resubmission of build [#%d](/%s/job/%d)", id, oldJob.Owner.CanonicalName, id) if edit { note += " (edited)" } } job, err := buildssrht.Submit(c.Client, ctx, oldJob.Manifest, nil, ¬e, &buildsVisibility) if err != nil { log.Fatal(err) } if termfmt.IsTerminal() { log.Printf("Started build %v/%v/job/%v", c.BaseURL, job.Owner.CanonicalName, job.Id) } else { fmt.Printf("%v/%v/job/%v\n", c.BaseURL, job.Owner.CanonicalName, job.Id) } if follow { id := job.Id job, err := followJob(ctx, c, job.Id) if err != nil { log.Fatal(err) } if job.Status != buildssrht.JobStatusSuccess { offerSSHConnection(ctx, c, id) } } } cmd := &cobra.Command{ Use: "resubmit ", Short: "Resubmit a build", Args: cobra.ExactArgs(1), ValidArgsFunction: completeAnyJobs, Run: run, } cmd.Flags().BoolVarP(&follow, "follow", "f", false, "follow build logs") cmd.Flags().BoolVarP(&edit, "edit", "e", false, "edit manifest") cmd.Flags().StringVarP(¬e, "note", "n", "", "short job description") cmd.RegisterFlagCompletionFunc("note", cobra.NoFileCompletions) cmd.Flags().StringVarP(&visibility, "visibility", "v", "", "builds visibility") cmd.RegisterFlagCompletionFunc("visibility", completeVisibility) return cmd } func newBuildsCancelCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() for _, arg := range args { id, instance, err := parseBuildID(arg) if err != nil { log.Fatal(err) } c := createClientWithInstance("builds", cmd, instance) job, err := buildssrht.Cancel(c.Client, ctx, id) if err != nil { log.Fatalf("failed to cancel job %d: %v", id, err) } log.Printf("%d is cancelled\n", job.Id) } } cmd := &cobra.Command{ Use: "cancel ", Short: "Cancel jobs", Args: cobra.MinimumNArgs(1), ValidArgsFunction: completeRunningJobs, Run: run, } return cmd } func newBuildsShowCommand() *cobra.Command { var follow bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var ( id int32 c *Client ) if len(args) == 0 { // get last build c = createClient("builds", cmd) jobs, err := buildssrht.JobIDs(c.Client, ctx) if err != nil { log.Fatal(err) } if len(jobs.Results) == 0 { log.Fatal("cannot show last job: no jobs found") } id = jobs.Results[0].Id } else { var ( instance string err error ) id, instance, err = parseBuildID(args[0]) if err != nil { log.Fatal(err) } c = createClientWithInstance("builds", cmd, instance) } var ( err error job *buildssrht.Job ) if follow { job, err = followJobShow(ctx, c, id) if err != nil { log.Fatal(err) } } else { job, err = buildssrht.Show(c.Client, ctx, id) if err != nil { log.Fatal(err) } else if job == nil { log.Fatal("invalid job ID") } } printJob(os.Stdout, job) failedTask := -1 for i, task := range job.Tasks { if task.Status == buildssrht.TaskStatusFailed { failedTask = i break } } if job.Status == buildssrht.JobStatusFailed { if failedTask == -1 { fmt.Printf("\nSetup log:\n") if err := fetchJobLogs(ctx, c, new(buildLog), job); err != nil { log.Fatalf("failed to fetch job logs: %v", err) } } else { name := job.Tasks[failedTask].Name fmt.Printf("\n%s log:\n", name) if err := fetchTaskLogs(ctx, c, new(buildLog), job.Tasks[failedTask]); err != nil { log.Fatalf("failed to fetch task logs: %v", err) } } } } cmd := &cobra.Command{ Use: "show [ID]", Short: "Show job status", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeAnyJobs, Run: run, } cmd.Flags().BoolVarP(&follow, "follow", "f", false, "follow job status") return cmd } func newBuildsListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("builds", cmd) var cursor *buildssrht.Cursor var username string if len(args) > 0 { username = strings.TrimLeft(args[0], ownerPrefixes) } err := pagerify(func(p pager) error { var jobs *buildssrht.JobCursor if len(username) > 0 { user, err := buildssrht.JobsByUser(c.Client, ctx, username, cursor) if err != nil { return err } else if user == nil { return errors.New("no such user") } jobs = user.Jobs } else { var err error jobs, err = buildssrht.Jobs(c.Client, ctx, cursor) if err != nil { return err } } for _, job := range jobs.Results { printJob(p, &job) } cursor = jobs.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list [owner]", Short: "List jobs", Args: cobra.MaximumNArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } return cmd } func newBuildsSSHCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() id, instance, err := parseBuildID(args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("builds", cmd, instance) job, ver, err := buildssrht.GetSSHInfo(c.Client, ctx, id) if err != nil { log.Fatal(err) } else if job == nil { log.Fatalf("no such job with ID %d", id) } err = sshConnection(job, ver.Settings.SshUser) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "ssh ", Short: "SSH into job", Args: cobra.ExactArgs(1), ValidArgsFunction: completeRunningJobs, Run: run, } return cmd } func newBuildsArtifactsCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() id, instance, err := parseBuildID(args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("builds", cmd, instance) job, err := buildssrht.Artifacts(c.Client, ctx, id) if err != nil { log.Fatal(err) } else if job == nil { log.Fatalf("no such job with ID %d", id) } if len(job.Artifacts) == 0 { log.Println("No artifacts for this job.") return } tw := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0) defer tw.Flush() for _, artifact := range job.Artifacts { name := artifact.Path[strings.LastIndex(artifact.Path, "/")+1:] s := fmt.Sprintf("%s\t%s\t", termfmt.Bold.String(name), humanize.Bytes(uint64(artifact.Size))) if artifact.Url == nil { s += termfmt.Dim.Sprint("(pruned after 90 days)") } else { s += *artifact.Url } fmt.Fprintln(tw, s) } } cmd := &cobra.Command{ Use: "artifacts ", Short: "List artifacts", Args: cobra.ExactArgs(1), ValidArgsFunction: completeAnyJobs, Run: run, } return cmd } func newBuildsUserWebhookCommand() *cobra.Command { cmd := &cobra.Command{ Use: "user-webhook", Short: "Manage user webhooks", } cmd.AddCommand(newBuildsUserWebhookCreateCommand()) cmd.AddCommand(newBuildsUserWebhookListCommand()) cmd.AddCommand(newBuildsUserWebhookDeleteCommand()) return cmd } func newBuildsUserWebhookCreateCommand() *cobra.Command { var events []string var stdin bool var url string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("builds", cmd) var config buildssrht.UserWebhookInput config.Url = url whEvents, err := buildssrht.ParseUserEvents(events) if err != nil { log.Fatal(err) } config.Events = whEvents config.Query = readWebhookQuery(stdin) webhook, err := buildssrht.CreateUserWebhook(c.Client, ctx, config) if err != nil { log.Fatal(err) } log.Printf("Created user webhook with ID %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "create", Short: "Create a user webhook", Args: cobra.ExactArgs(0), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().StringSliceVarP(&events, "events", "e", nil, "webhook events") cmd.RegisterFlagCompletionFunc("events", completeBuildsUserWebhookEvents) cmd.MarkFlagRequired("events") cmd.Flags().BoolVar(&stdin, "stdin", !isStdinTerminal, "read webhook query from stdin") cmd.Flags().StringVarP(&url, "url", "u", "", "payload url") cmd.RegisterFlagCompletionFunc("url", cobra.NoFileCompletions) cmd.MarkFlagRequired("url") return cmd } func newBuildsUserWebhookListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("builds", cmd) var cursor *buildssrht.Cursor err := pagerify(func(p pager) error { webhooks, err := buildssrht.UserWebhooks(c.Client, ctx, cursor) if err != nil { return err } for _, webhook := range webhooks.Results { fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url) } cursor = webhooks.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list", Short: "List user webhooks", Args: cobra.ExactArgs(0), Run: run, } return cmd } func newBuildsUserWebhookDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("builds", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } webhook, err := buildssrht.DeleteUserWebhook(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Deleted webhook %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete a user webhook", Args: cobra.ExactArgs(1), ValidArgsFunction: completeBuildsUserWebhookID, Run: run, } return cmd } func printJob(w io.Writer, job *buildssrht.Job) { fmt.Fprint(w, termfmt.DarkYellow.Sprintf("#%d", job.Id)) if tagString := formatJobTags(job); tagString != "" { fmt.Fprintf(w, " - %s", termfmt.Bold.String(tagString)) } fmt.Fprintf(w, ": %s\n", job.Status.TermString()) for _, task := range job.Tasks { fmt.Fprintf(w, "%s %s ", task.Status.TermIcon(), task.Name) } fmt.Fprintln(w) if job.Group != nil && len(job.Group.Jobs) > 1 { fmt.Fprintf(w, "Group: ") for _, j := range job.Group.Jobs { if j.Id == job.Id { continue } fmt.Fprintf(w, "%s %s ", j.Status.TermIcon(), termfmt.DarkYellow.Sprintf("#%d", j.Id)) } } if job.Note != nil && *job.Note != "" { fmt.Fprintln(w, "\n"+indent(strings.TrimSpace(*job.Note), " ")) } fmt.Fprintln(w) } func newBuildsSecretCommand() *cobra.Command { cmd := &cobra.Command{ Use: "secret", Short: "Manage secrets", } cmd.AddCommand(newBuildsSecretListCommand()) cmd.AddCommand(newBuildsSecretShareCommand()) return cmd } func newBuildsSecretListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("builds", cmd) var cursor *buildssrht.Cursor err := pagerify(func(p pager) error { secrets, err := buildssrht.Secrets(c.Client, ctx, cursor) if err != nil { return err } for i, secret := range secrets.Results { if i != 0 { fmt.Fprintln(p) } printSecret(p, &secret) } cursor = secrets.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list", Short: "List secrets", Args: cobra.ExactArgs(0), Run: run, } return cmd } func printSecret(w io.Writer, secret *buildssrht.Secret) { var s string created := termfmt.Dim.String(humanize.Time(secret.Created.Time)) s += fmt.Sprintf("%s\t%s\n", termfmt.DarkYellow.Sprint(secret.Uuid), created) if secret.FromUser != nil { s += termfmt.Dim.Sprintf("Shared by %s\n", secret.FromUser.CanonicalName) } if secret.Name != nil && *secret.Name != "" { s += fmt.Sprintf("%s\n", *secret.Name) } switch v := secret.Value.(type) { case *buildssrht.SecretFile: s += fmt.Sprintf("File: %s %o\n", v.Path, v.Mode) case *buildssrht.SSHKey: s += "SSH Key\n" case *buildssrht.PGPKey: s += "PGP Key\n" } fmt.Fprint(w, s) } func newBuildsSecretShareCommand() *cobra.Command { var userName string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("builds", cmd) userName = strings.TrimLeft(userName, ownerPrefixes) secret, err := buildssrht.ShareSecret(c.Client, ctx, args[0], userName) if err != nil { log.Fatal(err) } log.Printf("Shared secret %q with user %q\n", secret.Uuid, userName) } cmd := &cobra.Command{ Use: "share ", Short: "Share a secret", Args: cobra.ExactArgs(1), ValidArgsFunction: completeSecret, Run: run, } cmd.Flags().StringVarP(&userName, "user", "u", "", "username") cmd.MarkFlagRequired("user") cmd.RegisterFlagCompletionFunc("user", completeCoMaintainers) return cmd } type buildLog struct { offset int64 done bool } func followJob(ctx context.Context, c *Client, id int32) (*buildssrht.Job, error) { ticker := time.NewTicker(time.Second) defer ticker.Stop() logs := make(map[string]*buildLog) for { // TODO: rig up timeout to context job, err := buildssrht.Monitor(c.Client, ctx, id) if err != nil { return nil, fmt.Errorf("failed to monitor job: %v", err) } if len(logs) == 0 { logs[""] = new(buildLog) for _, task := range job.Tasks { logs[task.Name] = new(buildLog) } } if err := fetchJobLogs(ctx, c, logs[""], job); err != nil { return nil, fmt.Errorf("failed to fetch job logs: %v", err) } for _, task := range job.Tasks { if err := fetchTaskLogs(ctx, c, logs[task.Name], task); err != nil { return nil, fmt.Errorf("failed to fetch task %q logs: %v", task.Name, err) } } if jobStatusDone(job.Status) { fmt.Println(job.Status.TermString()) return job, nil } select { case <-ctx.Done(): return nil, ctx.Err() case <-ticker.C: // Continue looping } } } func fetchJobLogs(ctx context.Context, c *Client, l *buildLog, job *buildssrht.Job) error { switch job.Status { case buildssrht.JobStatusPending, buildssrht.JobStatusQueued: return nil } if err := fetchBuildLogs(ctx, c, l, job.Log.FullURL); err != nil { return err } l.done = jobStatusDone(job.Status) return nil } func fetchTaskLogs(ctx context.Context, c *Client, l *buildLog, task buildssrht.Task) error { switch task.Status { case buildssrht.TaskStatusPending, buildssrht.TaskStatusSkipped: return nil } if err := fetchBuildLogs(ctx, c, l, task.Log.FullURL); err != nil { return err } switch task.Status { case buildssrht.TaskStatusPending, buildssrht.TaskStatusRunning: return nil } l.done = true return nil } func fetchBuildLogs(ctx context.Context, c *Client, l *buildLog, url string) error { if l.done { return nil } req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return fmt.Errorf("failed to create HTTP request: %v", err) } req.Header.Set("Range", fmt.Sprintf("bytes=%v-", l.offset)) resp, err := c.HTTP.Do(req) if err != nil { return fmt.Errorf("HTTP request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusPartialContent { return fmt.Errorf("invalid HTTP status: want Partial Content, got: %v %v", resp.StatusCode, resp.Status) } var rangeStart, rangeEnd int64 var rangeSize string _, err = fmt.Sscanf(resp.Header.Get("Content-Range"), "bytes %d-%d/%s", &rangeStart, &rangeEnd, &rangeSize) if err != nil { return fmt.Errorf("failed to parse Content-Range header: %v", err) } // Skip the first byte, because rangeEnd is inclusive if rangeStart > 0 { io.ReadFull(resp.Body, []byte{0}) } if _, err := io.Copy(os.Stdout, resp.Body); err != nil { return fmt.Errorf("failed to copy response body: %v", err) } l.offset = rangeEnd return nil } func jobStatusDone(status buildssrht.JobStatus) bool { switch status { case buildssrht.JobStatusPending, buildssrht.JobStatusQueued, buildssrht.JobStatusRunning: return false default: return true } } func parseBuildID(s string) (id int32, instance string, err error) { s, _, instance = parseResourceName(s) s = strings.TrimPrefix(s, "job/") id64, err := strconv.ParseInt(s, 10, 32) if err != nil { return 0, "", fmt.Errorf("invalid build ID: %v", err) } return int32(id64), instance, nil } func indent(s, prefix string) string { return prefix + strings.ReplaceAll(s, "\n", "\n"+prefix) } func formatJobTags(job *buildssrht.Job) string { var s string for i, tag := range job.Tags { if tag == "" { break } if i > 0 { s += "/" } s += tag } return s } func sshConnection(job *buildssrht.Job, user string) error { if job.Runner == nil { return errors.New("job has no runner assigned yet") } cmd := exec.Command("ssh", "-t", fmt.Sprintf("%s@%s", user, *job.Runner), "connect", fmt.Sprint(job.Id)) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } func followJobShow(ctx context.Context, c *Client, id int32) (*buildssrht.Job, error) { ticker := time.NewTicker(time.Second) defer ticker.Stop() for { job, err := buildssrht.Show(c.Client, ctx, id) if err != nil { return nil, fmt.Errorf("failed to monitor job: %v", err) } else if job == nil { return nil, errors.New("invalid job ID") } var taskString string for _, task := range job.Tasks { taskString += fmt.Sprintf("%s %s ", task.Status.TermIcon(), task.Name) } fmt.Printf("%v%v: %s with %s", termfmt.ReplaceLine(), termfmt.DarkYellow.Sprintf("#%d", job.Id), job.Status.TermString(), taskString) if jobStatusDone(job.Status) { fmt.Print(termfmt.ReplaceLine()) return job, nil } select { case <-ctx.Done(): return nil, ctx.Err() case <-ticker.C: // Continue looping } } } func completeRunningJobs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completeJobs(cmd, true) } func completeAnyJobs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completeJobs(cmd, false) } func completeJobs(cmd *cobra.Command, onlyRunning bool) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("builds", cmd) var jobList []string jobs, err := buildssrht.Jobs(c.Client, ctx, nil) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, job := range jobs.Results { // TODO: filter with API if onlyRunning && jobStatusDone(job.Status) { continue } if cmd.Name() == "cancel" && hasCmdArg(cmd, strconv.Itoa(int(job.Id))) { continue } str := fmt.Sprintf("%d\t", job.Id) if tagString := formatJobTags(&job); tagString != "" { str += tagString } if len(job.Tags) > 0 && job.Note != nil { str += " - " } if job.Note != nil { str += *job.Note } jobList = append(jobList, str) } return jobList, cobra.ShellCompDirectiveNoFileComp } func completeBuildsUserWebhookEvents(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var eventList []string events := [1]string{"job_created"} set := strings.ToLower(cmd.Flag("events").Value.String()) for _, event := range events { if !strings.Contains(set, event) { eventList = append(eventList, event) } } return eventList, cobra.ShellCompDirectiveNoFileComp } func completeBuildsUserWebhookID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("builds", cmd) var webhookList []string webhooks, err := buildssrht.UserWebhooks(c.Client, ctx, nil) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, webhook := range webhooks.Results { s := fmt.Sprintf("%d\t%s", webhook.Id, webhook.Url) webhookList = append(webhookList, s) } return webhookList, cobra.ShellCompDirectiveNoFileComp } func offerSSHConnection(ctx context.Context, c *Client, id int32) { if !isStdinTerminal || !isStdoutTerminal { os.Exit(1) } termfmt.Bell() if !getConfirmation(fmt.Sprintf("\n%s Do you want to log in with SSH?", termfmt.Red.String("Build failed."))) { return } job, ver, err := buildssrht.GetSSHInfo(c.Client, ctx, id) if err != nil { log.Fatal(err) } else if job == nil { log.Fatalf("no such job with ID %d", id) } err = sshConnection(job, ver.Settings.SshUser) if err != nil { log.Fatal(err) } } func completeSecret(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("builds", cmd) var secretList []string secrets, err := buildssrht.CompleteSecrets(c.Client, ctx) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, secret := range secrets.Results { s := secret.Uuid if secret.Name != nil && *secret.Name != "" { s = fmt.Sprintf("%s\t%s", s, *secret.Name) } secretList = append(secretList, s) } return secretList, cobra.ShellCompDirectiveNoFileComp } hut-0.6.0/client.go000066400000000000000000000053651463710650600141430ustar00rootroot00000000000000package main import ( "fmt" "log" "net" "net/http" "os/exec" "strings" "time" "git.sr.ht/~emersion/gqlclient" "github.com/spf13/cobra" ) type Client struct { *gqlclient.Client BaseURL string HTTP *http.Client } func createClient(service string, cmd *cobra.Command) *Client { return createClientWithInstance(service, cmd, "") } func createClientWithInstance(service string, cmd *cobra.Command, instanceName string) *Client { cfg := loadConfig(cmd) if len(cfg.Instances) == 0 { log.Fatalf("no sr.ht instance configured") } if instanceFlag, err := cmd.Flags().GetString("instance"); err != nil { log.Fatal(err) } else if instanceFlag != "" { if instanceName != "" && !instancesEqual(instanceName, instanceFlag) { log.Fatalf("conflicting instances: %v and --instance=%v", instanceName, instanceFlag) } instanceName = instanceFlag } var inst *InstanceConfig if instanceName != "" { for _, instance := range cfg.Instances { if instance.match(instanceName) { inst = instance break } } if inst == nil { log.Fatalf("no instance for %s found", instanceName) } } else { inst = cfg.Instances[0] } var token string if len(inst.AccessTokenCmd) > 0 { cmd := exec.Command(inst.AccessTokenCmd[0], inst.AccessTokenCmd[1:]...) output, err := cmd.Output() if err != nil { log.Fatalf("could not execute access-token-cmd: %v", err) } fields := strings.Fields(string(output)) if len(fields) == 0 { log.Fatalf("access-token-cmd did not return a token") } token = fields[0] } else { token = inst.AccessToken } var baseURL string if serviceCfg := inst.Services()[service]; serviceCfg != nil { baseURL = serviceCfg.Origin } if baseURL == "" && strings.Contains(inst.Name, ".") && net.ParseIP(inst.Name) == nil { baseURL = fmt.Sprintf("https://%s.%s", service, inst.Name) } if baseURL == "" { log.Fatalf("failed to get origin for service %q in instance %q", service, inst.Name) } debug, err := cmd.Flags().GetBool("debug") if err != nil { log.Fatal(err) } return createClientWithToken(baseURL, token, debug) } func createClientWithToken(baseURL, token string, debug bool) *Client { gqlEndpoint := baseURL + "/query" httpClient := &http.Client{ Transport: &httpTransport{accessToken: token, logRequest: debug}, Timeout: 30 * time.Second, } return &Client{ Client: gqlclient.New(gqlEndpoint, httpClient), BaseURL: baseURL, HTTP: httpClient, } } type httpTransport struct { accessToken string logRequest bool } func (tr *httpTransport) RoundTrip(req *http.Request) (*http.Response, error) { req.Header.Set("User-Agent", "hut") req.Header.Set("Authorization", "Bearer "+tr.accessToken) if tr.logRequest { log.Println(req.Body) } return http.DefaultTransport.RoundTrip(req) } hut-0.6.0/config.go000066400000000000000000000141171463710650600141250ustar00rootroot00000000000000package main import ( "bufio" "context" "errors" "fmt" "log" "os" "path/filepath" "strings" "git.sr.ht/~emersion/go-scfg" "github.com/spf13/cobra" "git.sr.ht/~xenrox/hut/srht/metasrht" "git.sr.ht/~xenrox/hut/termfmt" ) type Config struct { Instances []*InstanceConfig `scfg:"instance"` } type InstanceConfig struct { Name string `scfg:",param"` AccessToken string `scfg:"access-token"` AccessTokenCmd []string `scfg:"access-token-cmd"` Builds *ServiceConfig `scfg:"builds"` Git *ServiceConfig `scfg:"git"` Hg *ServiceConfig `scfg:"hg"` Lists *ServiceConfig `scfg:"lists"` Meta *ServiceConfig `scfg:"meta"` Pages *ServiceConfig `scfg:"pages"` Paste *ServiceConfig `scfg:"paste"` Todo *ServiceConfig `scfg:"todo"` } func (instance *InstanceConfig) match(name string) bool { if instancesEqual(name, instance.Name) { return true } for _, service := range instance.Services() { if service.Origin != "" && stripProtocol(service.Origin) == name { return true } } return false } func (instance *InstanceConfig) Services() map[string]*ServiceConfig { all := map[string]*ServiceConfig{ "builds": instance.Builds, "git": instance.Git, "hg": instance.Hg, "lists": instance.Lists, "meta": instance.Meta, "pages": instance.Pages, "paste": instance.Paste, "todo": instance.Todo, } m := make(map[string]*ServiceConfig) for name, service := range all { if service != nil { m[name] = service } } return m } type ServiceConfig struct { Origin string `scfg:"origin"` } func instancesEqual(a, b string) bool { return a == b || strings.HasSuffix(a, "."+b) || strings.HasSuffix(b, "."+a) } func loadConfigFile(filename string) (*Config, error) { f, err := os.Open(filename) if err != nil { return nil, err } defer f.Close() cfg := new(Config) if err := scfg.NewDecoder(f).Decode(cfg); err != nil { return nil, err } instanceNames := make(map[string]struct{}) for _, instance := range cfg.Instances { if _, ok := instanceNames[instance.Name]; ok { return nil, fmt.Errorf("duplicate instance name %q", instance.Name) } instanceNames[instance.Name] = struct{}{} if instance.AccessTokenCmd != nil && len(instance.AccessTokenCmd) == 0 { return nil, fmt.Errorf("instance %q: missing command name in access-token-cmd directive", instance.Name) } if instance.AccessToken == "" && len(instance.AccessTokenCmd) == 0 { return nil, fmt.Errorf("instance %q: missing access-token or access-token-cmd", instance.Name) } if instance.AccessToken != "" && len(instance.AccessTokenCmd) > 0 { return nil, fmt.Errorf("instance %q: access-token and access-token-cmd can't be both specified", instance.Name) } } return cfg, nil } func loadConfig(cmd *cobra.Command) *Config { type configContextKey struct{} if v := cmd.Context().Value(configContextKey{}); v != nil { return v.(*Config) } customConfigFile := true configFile, err := cmd.Flags().GetString("config") if err != nil { log.Fatal(err) } else if configFile == "" { configFile = defaultConfigFilename() customConfigFile = false } cfg, err := loadConfigFile(configFile) if err != nil { // This error message doesn't make sense if a config was // provided with "--config". In that case, the normal log // message is always desired. if !customConfigFile && errors.Is(err, os.ErrNotExist) { os.Stderr.WriteString("Looks like hut's config file hasn't been set up yet.\nRun `hut init` to configure it.\n") os.Exit(1) } log.Fatalf("failed to load config file: %v", err) } ctx := cmd.Context() ctx = context.WithValue(ctx, configContextKey{}, cfg) cmd.SetContext(ctx) return cfg } func defaultConfigFilename() string { configDir, err := os.UserConfigDir() if err != nil { log.Fatalf("failed to get user config dir: %v", err) } return filepath.Join(configDir, "hut", "config") } func newInitCommand() *cobra.Command { cmd := &cobra.Command{ Use: "init", Short: "Initialize hut", Args: cobra.ExactArgs(0), } cmd.Run = func(cmd *cobra.Command, args []string) { ctx := cmd.Context() filename, err := cmd.Flags().GetString("config") if err != nil { log.Fatal(err) } else if filename == "" { filename = defaultConfigFilename() } // Perform an early sanity check to avoid asking the user to login if // the config file already exists if _, err := os.Stat(filename); err == nil { log.Fatalf("config file %q already exists (delete it if you want to overwrite it)", filename) } else if err != nil && !errors.Is(err, os.ErrNotExist) { log.Fatal(err) } instance, err := cmd.Flags().GetString("instance") if err != nil { log.Fatal(err) } else if instance == "" { instance = "sr.ht" } baseURL := "https://meta." + instance fmt.Printf("Generate a new OAuth2 access token at:\n") fmt.Printf("%s/oauth2/personal-token\n", baseURL) fmt.Printf("Then copy-paste it here: ") scanner := bufio.NewScanner(os.Stdin) scanner.Scan() token := strings.TrimSpace(scanner.Text()) if err := scanner.Err(); err != nil { log.Fatalf("failed to read token from stdin: %v", err) } else if token == "" { log.Fatal("no token provided") } config := fmt.Sprintf("instance %q {\n access-token %q\n}\n", instance, token) c := createClientWithToken(baseURL, token, false) user, err := metasrht.FetchMe(c.Client, ctx) if err != nil { log.Fatalf("failed to check OAuth2 token: %v", err) } if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil { log.Fatalf("failed to create config file parent directory: %v", err) } f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) if os.IsExist(err) { log.Fatalf("config file %q already exists (delete it if you want to overwrite it)", filename) } else if err != nil { log.Fatalf("failed to create config file: %v", err) } defer f.Close() if _, err := f.WriteString(config); err != nil { log.Fatalf("failed to write config file: %v", err) } if err := f.Close(); err != nil { log.Fatalf("failed to close config file: %v", err) } log.Printf("hut initialized for user %v\n", termfmt.Bold.String(user.CanonicalName)) } return cmd } hut-0.6.0/contrib/000077500000000000000000000000001463710650600137655ustar00rootroot00000000000000hut-0.6.0/contrib/update_schemas.sh000077500000000000000000000010561463710650600173130ustar00rootroot00000000000000#!/bin/sh -eu set -- builds git lists meta pages paste todo for service in "$@"; do url="https://git.sr.ht/~sircmpwn/$service.sr.ht" dir="$service"srht api_dir="/api" if [ "$service" = pages ]; then api_dir="" fi tag=$(git -c 'versionsort.suffix=-' ls-remote --refs --sort='version:refname' --tags "$url" | tail --lines=1 | cut --delimiter='/' --fields=3) echo "$url/blob/$tag$api_dir/graph/schema.graphqls" curl -f -o "srht/$dir/schema.graphqls" "$url/blob/$tag$api_dir/graph/schema.graphqls" done hut-0.6.0/doc/000077500000000000000000000000001463710650600130725ustar00rootroot00000000000000hut-0.6.0/doc/hut.1.scd000066400000000000000000000503601463710650600145300ustar00rootroot00000000000000hut(1) # NAME hut - A CLI tool for sr.ht # SYNOPSIS *hut* [commands...] [options...] # DESCRIPTION hut is a CLI companion utility to interact with sr.ht. Resources (such as build jobs, todo tickets, lists patchsets, git repositories, and so on) can be specified in multiple forms: name, owner and name, or full URL. For instance, the repository _hut_ owned by _~emersion_ on _git.sr.ht_ can be referred to via: - "hut" - "~emersion/hut" - "https://git.sr.ht/~emersion/hut" Additionally, mailing lists can be referred to by their email address. _hut_ commands that read input, like *hut graphql* or *hut builds user-webhook create* read input depending on whether their stdin is on a terminal or not: - If stdin is not on a terminal, for example, because stdin is redirected from a file or from a pipe, _hut_ reads input from stdin. - Otherwise, if option *--stdin* is specified to the command, _hut_ reads input from stdin. - Otherwise, _hut_ assumes to run in a terminal and starts the command provided by environment variable _$EDITOR_ to read input. # OPTIONS *-h*, *--help* Show help message and quit. Can be used after a command to get more information it. *--config* Explicitly select a configuration file that should be used over the default configuration. *--debug* Prints the command's underlying GraphQL request to _stderr_. *--instance* Select which sr.ht instance from the config file should be used. By default the first one will be selected. # COMMANDS *help* Help about any command. *graphql* Write a GraphQL query and execute it. The JSON response is written to stdout. _service_ is the sr.ht service to execute the query on (for instance "meta" or "builds"). A tool like *jq*(1) can be used to prettify the output and process the data. Example: ``` hut graphql meta <= Set a file variable. *--stdin* Read query from _stdin_. *-v*, *--var* = Set a raw variable. Example: ``` hut graphql meta -v username=emersion <<'EOF' query($username: String!) { userByName(username: $username) { bio } } EOF ``` *init* Initialize hut's configuration file. *export* [resource|service...] Export account data. By default, all data of the current user will be exported. Alternatively, an explicit list of instance services (e.g. "todo.sr.ht") or resources (e.g. "todo.sr.ht/~user/tracker") can be specified. *import* Import account data. ## builds *artifacts* List artifacts. *cancel* Cancel jobs. *list* [owner] List jobs. *resubmit* Resubmit a build. Options are: *-e*, *--edit* Edit manifest with _$EDITOR_. *-f*, *--follow* Follow build logs. *-n*, *--note* Provide a short job description. *-v*, *--visibility* Visibility to use (public, unlisted, private). Defaults to the same visibility used by the original build job. *secret list* List secrets. *secret share* Share a secret. Options are: *-u*, *--user* User with whom to share the secret (required). *show* [ID] [options...] Show job status. If no ID is specified, the latest build will be printed. Options are: *-f*, *--follow* Follow job status. *ssh* Connect with SSH to a job. *submit* [manifest...] [options...] Submit a build manifest. If no build manifest is specified, build manifests are discovered at _.build.yml_ and _.builds/\*.yml_. Options are: *-e*, *--edit* Edit manifest with _$EDITOR_. *-f*, *--follow* Follow build logs. *-n*, *--note* Provide a short job description. *-t*, *--tags* Slash separated tags (e.g. "hut/test"). *-v*, *--visibility* Visibility to use (public, unlisted, private). Defaults to unlisted. *user-webhook create* [options...] Create a user webhook. Options are: *-e*, *--events* List of events that should trigger the webhook (JOB_CREATED). Required. *--stdin* Read query from _stdin_. *-u*, *--url* The payload URL which receives the _POST_ request. Required. *user-webhook delete* Delete a user webhook. *user-webhook list* List user webhooks. ## git Options are: *-r*, *--repo* Name of repository. *acl delete* Delete an ACL entry. *acl list* [repo] List ACL entries of a repo. Defaults to current repo. *acl update* [options...] Update or add an ACL entry for user. Options are: *-m*, *--mode* Access mode to set (RW, RO). *artifact delete* Delete an artifact. *artifact list* [options...] List artifacts. *artifact upload* [options...] Upload artifacts. Options are: *--rev* Revision tag. Defaults to the last Git tag. *clone* This will clone the repository to _CWD_ and try to configure it for _git send-email_ if possible. *create* [options...] Create a repository. If *--clone* is not used, the remote URL will be printed to _stdout_. Options are: *-c*, *--clone* Clone repository to _CWD_. *-d*, *--description* Description of the repository. *--import-url* Import the repository from the given URL. *-v*, *--visibility* Visibility to use (public, unlisted, private). Defaults to public. *delete* [repo] [options...] Delete a repository. By default the current repo will be deleted. Options are: *-y*, *--yes* Confirm deletion without prompt. *list* [owner] List repositories. *show* [repo] Display information about a repository. *update* [repo] [options...] Update a repository. By default the current repo will be updated. Options are: *-b*, *--default-branch* Set the default branch. *-d*, *--description* Set one-line repository description. *--readme* Update the custom README settings. You can read the HTML from a file or pass "-" as the filename to read from _stdin_. To clear the custom README use an empty string "". *-v*, *--visibility* Visibility to use (public, unlisted, private). *user-webhook create* [options...] Create a user webhook. Options are: *-e*, *--events* List of events that should trigger the webhook (REPO_CREATED, REPO_UPDATE, REPO_DELETED). Required. *--stdin* Read query from _stdin_. *-u*, *--url* The payload URL which receives the _POST_ request. Required. *user-webhook delete* Delete a user webhook. *user-webhook list* List user webhooks. ## hg Options are: *-r*, *--repo* Name of repository. *acl delete* Delete an ACL entry. *acl list* [repo] List ACL entries of a repo. Defaults to current repo. *acl update* [options...] Update or add an ACL entry for user. Options are: *-m*, *--mode* Access mode to set (RW, RO). *create* [options...] Create a repository. If *--clone* is not used, the remote URL will be printed to _stdout_. Options are: *-c*, *--clone* Clone repository to _CWD_. *-d*, *--description* Description of the repository. *-v*, *--visibility* Visibility to use (public, unlisted, private). Defaults to public. *delete* [repo] [options...] Delete a repository. By default the current repo will be deleted. Options are: *-y*, *--yes* Confirm deletion without prompt. *list* [owner] List repositories. *update* [repo] [options...] Update a repository. By default the current repo will be updated. Options are: *-d*, *--description* Set one-line repository description. *--non-publishing* Controls whether this repository is a non-publishing repository. *--readme* Update the custom README settings. You can read the HTML from a file or pass "-" as the filename to read from _stdin_. To clear the custom README use an empty string "". *-v*, *--visibility* Visibility to use (public, unlisted, private). *user-webhook create* [options...] Create a user webhook. Options are: *-e*, *--events* List of events that should trigger the webhook (REPO_CREATED, REPO_UPDATE, REPO_DELETED). Required. *--stdin* Read query from _stdin_. *-u*, *--url* The payload URL which receives the _POST_ request. Required. *user-webhook delete* Delete a user webhook. *user-webhook list* List user webhooks. ## lists Options are: *-l*, *--mailing-list* Select a mailing list. By default, the mailing list configured for the current Git repository will be selected. *acl delete* Delete an ACL entry. *acl list* [list] List ACL entries of a mailing list. *archive* [list] [options...] Download a mailing list archive as an mbox file to _stdout_. Options are: *-d*, *--days* Number of last days for which the archive should be downloaded. By default the entire archive will be selected. *create* [options...] Create a mailing list. Options are: *--stdin* Read description from _stdin_. *-v*, *--visibility* Visibility to use (public, unlisted, private). Defaults to public. *delete* [list] [options...] Delete a mailing list. Options are: *-y*, *--yes* Confirm deletion without prompt. *list* [owner] List mailing lists. *patchset apply* Apply a patchset. *patchset list* [list] [options...] List patchsets in list. Options are: *-u*, *--user* List patchsets by user instead of by list. *patchset show* Show a patchset. *patchset update* Update a patchset. Options are: *-s*, *--status* Patchset status to set (required). *subscribe* [list] Subscribe to a mailing list. *subscriptions* List mailing list subscriptions. *unsubscribe* [list] Unsubscribe from a mailing list. *update* [list] [options...] Update a mailing list. Options are: *--description* Edit description. *-v*, *--visibility* Visibility to use (public, unlisted, private). *user-webhook create* [options...] Create a user webhook. Options are: *-e*, *--events* List of events that should trigger the webhook (LIST_CREATED, LIST_UPDATED, LIST_DELETED, EMAIL_RECEIVED, PATCHSET_RECEIVED). Required. *--stdin* Read query from _stdin_. *-u*, *--url* The payload URL which receives the _POST_ request. Required. *user-webhook delete* Delete a user webhook. *user-webhook list* List user webhooks. *webhook create* [list] [options...] Create a mailing list webhook. Options are: *-e*, *--events* List of events that should trigger the webhook (LIST_UPDATED, LIST_DELETED, EMAIL_RECEIVED, PATCHSET_RECEIVED). Required. *--stdin* Read query from _stdin_. *-u*, *--url* The payload URL which receives the _POST_ request. Required. *webhook delete* Delete a tracker webhook. *webhook list* [list] List mailing list webhooks. ## meta *audit-log* Display your audit log. *oauth tokens* List personal access tokens. *pgp-key create* [path] Upload a PGP public key and associate it with your account. The public key must be in the armored format. If _path_ is not specified, the default public key from the local GPG keyring is used. *pgp-key delete* Delete a PGP key from your account. *pgp-key list* [username] [options...] List PGP public keys. Options are: *-r*, *--raw* Only print raw public key *show* [username] Show a user's profile. If _username_ is not specified, your profile is displayed. *ssh-key create* [path] Upload an SSH public key and associate it with your account. If _path_ is not specified, the default SSH public key is used. *ssh-key delete* Delete an SSH public key from your account. *ssh-key list* [username] [options...] List SSH public keys. Options are: *-r*, *--raw* Only print raw public key *update* [options...] Update account. Options are: *--bio* Edit biography. *--email* Set Email address. *--location* Set location. *--url* Set URL. *user-webhook create* [options...] Create a user webhook. Options are: *-e*, *--events* List of events that should trigger the webhook (PROFILE_UPDATE, PGP_KEY_ADDED, PGP_KEY_REMOVED, SSH_KEY_ADDED, SSH_KEY_REMOVED). Required. *--stdin* Read query from _stdin_. *-u*, *--url* The payload URL which receives the _POST_ request. Required. *user-webhook delete* Delete a user webhook. *user-webhook list* List user webhooks. ## pages *list* List registered sites. *publish* [file] [options...] Publish a website. The input _file_ can be either a gzip tarball or a directory. If _file_ is not specified, standard input is used. Options are: *-d*, *--domain* Fully qualified domain name. *-p*, *--protocol* Protocol to use (either HTTPS or GEMINI; defaults to HTTPS) *--site-config* Path to site configuration file (for e.g. cache-control). *-s*, *--subdirectory* If specified, only this subdirectory is updated, the rest of the site is left untouched. *unpublish* [options...] Unpublish a website. Options are: *-d*, *--domain* Fully qualified domain name. *-p*, *--protocol* Protocol to use (either HTTPS or GEMINI; defaults to HTTPS) *user-webhook create* [options...] Create a user webhook. Options are: *-e*, *--events* List of events that should trigger the webhook (SITE_PUBLISHED, SITE_UNPUBLISHED). Required. *--stdin* Read query from _stdin_. *-u*, *--url* The payload URL which receives the _POST_ request. Required. *user-webhook delete* Delete a user webhook. *user-webhook list* List user webhooks. ## paste *create* Create a new paste. Options are: *-v*, *--visibility* Visibility to use (public, unlisted, private). Defaults to unlisted. *-n*, *--name* Name of the created paste. Only valid when reading from stdin. *delete* Delete pastes. *list* List pastes. *show* Display a paste. *update* [options...] Update a paste's visibility. Options are: *-v*, *--visibility* Visibility to use (public, unlisted, private) *user-webhook create* [options...] Create a user webhook. Options are: *-e*, *--events* List of events that should trigger the webhook (PASTE_CREATED, PASTE_UPDATED, PASTE_DELETED). Required. *--stdin* Read query from _stdin_. *-u*, *--url* The payload URL which receives the _POST_ request. Required. *user-webhook delete* Delete a user webhook. *user-webhook list* List user webhooks. ## todo Options are: *-t*, *--tracker* Name of tracker. *acl delete* Delete an ACL entry. *acl list* [tracker] List ACL entries of a tracker. *create* [options...] Create a tracker. Options are: *--stdin* Read description from _stdin_. *-v*, *--visibility* Visibility to use (public, unlisted, private). Defaults to public. *delete* [tracker] [options...] Delete a tracker. Options are: *-y*, *--yes* Confirm deletion without prompt. *label create* [options...] Create a label. Options are: *-b*, *--background* Background color in hex format (required). *-f*, *--foreground* Foreground color in hex format. If omitted either black or white will be selected for an optimized contrast. *label delete* Delete a label. *label list* List labels. *label update* [options...] Update a label. Options are: *-b*, *--background* Background color in hex format. *-f*, *--foreground* Foreground color in hex format. *-n*, *--name* New label name. *list* [owner] List trackers. *subscribe* [tracker] Subscribe to a tracker. *ticket assign* [options...] Assign a user to a ticket. Options are: *-u*, *--user* Username of the new assignee (required). *ticket comment* [options...] Comment on a ticket. Options are: *-r*, *--resolution* Resolution for resolved tickets. If status is omitted, it will be set to _RESOLVED_. *-s*, *--status* New ticket status. If set to _RESOLVED_, resolution will default to _CLOSED_. *--stdin* Read comment from _stdin_. *ticket create* [options...] Create a new ticket. Options are: *--stdin* Read ticket from _stdin_. *ticket delete* [options...] Delete a ticket. Options are: *-y*, *--yes* Confirm deletion without prompt. *ticket edit* Edit a ticket. *ticket label* [options...] Add a label to a ticket. Options are: *-l*, *--label* Name of the label (required). *ticket list* [options...] List tickets. Options are: *-s*, *--status* Filter by ticket status. *ticket show* Display a ticket. *ticket subscribe* Subscribe to a ticket. *ticket unassign* [options...] Unassign a user from a ticket. Options are: *-u*, *--user* Username of the assignee (required). *ticket unlabel* [options...] Remove a label from a ticket. Options are: *-l*, *--label* Name of the label (required). *ticket unsubscribe* Unsubscribe from a ticket. *ticket update-status* [options...] Update status of a ticket. Options are: *-r*, *--resolution* Resolution for resolved tickets (required if status _RESOLVED_ is used). If status is omitted, it will be set to _RESOLVED_. *-s*, *--status* New ticket status. *ticket webhook create* [options...] Create a ticket webhook. Options are: *-e*, *--events* List of events that should trigger the webhook (EVENT_CREATED, TICKET_UPDATE, TICKET_DELETED). Required. *--stdin* Read query from _stdin_. *-u*, *--url* The payload URL which receives the _POST_ request. Required. *ticket webhook delete* Delete a ticket webhook. *ticket webhook list* List ticket webhooks. *unsubscribe* [tracker] Unsubscribe from a tracker. *update* [tracker] [options...] Update a tracker. Options are: *--description* Edit description. *-v*, *--visibility* Visibility to use (public, unlisted, private). *user-webhook create* [options...] Create a user webhook. Options are: *-e*, *--events* List of events that should trigger the webhook (TRACKER_CREATED, TRACKER_UPDATE, TRACKER_DELETED, TICKET_CREATED). Required. *--stdin* Read query from _stdin_. *-u*, *--url* The payload URL which receives the _POST_ request. Required. *user-webhook delete* Delete a user webhook. *user-webhook list* List user webhooks. *webhook create* [tracker] [options...] Create a tracker webhook. Options are: *-e*, *--events* List of events that should trigger the webhook (TRACKER_UPDATE, TRACKER_DELETED, LABEL_CREATED, LABEL_UPDATE, LABEL_DELETED, TICKET_CREATED, TICKET_UPDATE, TICKET_DELETED, EVENT_CREATED). Required. *--stdin* Read query from _stdin_. *-u*, *--url* The payload URL which receives the _POST_ request. Required. *webhook delete* Delete a tracker webhook. *webhook list* [tracker] List tracker webhooks. # CONFIGURATION Generate a new OAuth2 access token on _meta.sr.ht_. On startup hut will look for a file at *$XDG_CONFIG_HOME/hut/config*. If unset, _$XDG_CONFIG_HOME_ defaults to *~/.config/*. ``` instance "sr.ht" { access-token "" # As an alternative you can specify a command whose first line of output # will be parsed as the token access-token-cmd pass token meta { # You can set the origin for each service. As fallback hut will # construct the origin from the instance name and the service. origin "https://meta.sr.ht" } } ``` # Project configuration file The project configuration file is a top-level file called _.hut.scfg_ in a repository, where the assosciated tracker and development mailing list can be specified. These resources will be used whenever a command is called which needs a tracker/mailing list and none is explicitly set. Furthermore it is possible to configure that patches should contain the repository name in their prefix. The _hut git clone_ command will configure a freshly cloned repository to make contributing easier. ``` tracker https://todo.sr.ht/~xenrox/hut development-mailing-list ~xenrox/hut-dev@lists.sr.ht patch-prefix false ``` # AUTHORS Originally written by Simon Ser . Currently maintained by Thorben Günther , who is assisted by other open-source contributors. For more information about hut development, see . hut-0.6.0/export.go000066400000000000000000000110571463710650600142010ustar00rootroot00000000000000package main import ( "context" "encoding/json" "fmt" "log" "os" "path" "strings" "time" "github.com/spf13/cobra" "git.sr.ht/~xenrox/hut/export" ) type ExportInfo struct { Instance string `json:"instance"` Service string `json:"service"` Date time.Time `json:"date"` } type exporter struct { export.Exporter Name string BaseURL string } func newExportCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { var exporters []exporter mc := createClient("meta", cmd) meta := export.NewMetaExporter(mc.Client) exporters = append(exporters, exporter{meta, "meta.sr.ht", mc.BaseURL}) gc := createClient("git", cmd) git := export.NewGitExporter(gc.Client, gc.BaseURL) exporters = append(exporters, exporter{git, "git.sr.ht", gc.BaseURL}) hc := createClient("hg", cmd) hg := export.NewHgExporter(hc.Client, hc.BaseURL) exporters = append(exporters, exporter{hg, "hg.sr.ht", hc.BaseURL}) bc := createClient("builds", cmd) builds := export.NewBuildsExporter(bc.Client, bc.HTTP) exporters = append(exporters, exporter{builds, "builds.sr.ht", bc.BaseURL}) pc := createClient("paste", cmd) paste := export.NewPasteExporter(pc.Client, pc.HTTP) exporters = append(exporters, exporter{paste, "paste.sr.ht", pc.BaseURL}) lc := createClient("lists", cmd) lists := export.NewListsExporter(lc.Client, lc.HTTP) exporters = append(exporters, exporter{lists, "lists.sr.ht", lc.BaseURL}) tc := createClient("todo", cmd) todo := export.NewTodoExporter(tc.Client, tc.HTTP) exporters = append(exporters, exporter{todo, "todo.sr.ht", tc.BaseURL}) if _, ok := os.LookupEnv("SSH_AUTH_SOCK"); !ok { log.Println("Warning! SSH_AUTH_SOCK is not set in your environment.") log.Println("Using an SSH agent is advised to avoid unlocking your SSH keys repeatedly during the export.") } ctx := cmd.Context() log.Println("Exporting account data...") out := args[0] resources := args[1:] // Export all services by default if len(resources) == 0 { for _, ex := range exporters { resources = append(resources, ex.BaseURL) } } for _, resource := range resources { log.Println(resource) var name, owner, instance string if res := stripProtocol(resource); !strings.Contains(res, "/") { instance = res } else { name, owner, instance = parseResourceName(resource) owner = strings.TrimLeft(owner, ownerPrefixes) } var ex *exporter for _, e := range exporters { if stripProtocol(e.BaseURL) == instance { ex = &e break } } if ex == nil { log.Fatalf("Unknown resource instance: %s", resource) } var err error if name == "" && owner == "" { err = exportService(ctx, out, ex) } else if name != "" && owner != "" { err = exportResource(ctx, out, ex, owner, name) } else { err = fmt.Errorf("unknown resource") } if err != nil { log.Printf("Failed to export %q: %v", resource, err) } } log.Println("Export complete.") } return &cobra.Command{ Use: "export [resource|service...]", Short: "Exports your account data", Args: cobra.MinimumNArgs(1), ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) <= 1 { return nil, cobra.ShellCompDirectiveFilterDirs } // TODO: completion on export resources return nil, cobra.ShellCompDirectiveNoFileComp }, Run: run, } } func exportService(ctx context.Context, out string, ex *exporter) error { base := path.Join(out, ex.Name) if err := os.MkdirAll(base, 0o755); err != nil { return fmt.Errorf("failed to create export directory: %v", err) } stamp := path.Join(base, "service.json") if _, err := os.Stat(stamp); err == nil { log.Printf("Skipping %s (already exported)", ex.Name) return nil } if err := ex.Export(ctx, base); err != nil { return err } info := ExportInfo{ Instance: ex.BaseURL, Service: ex.Name, Date: time.Now().UTC(), } if err := writeExportStamp(stamp, &info); err != nil { return fmt.Errorf("failed writing stamp: %v", err) } return nil } func writeExportStamp(path string, info *ExportInfo) error { file, err := os.Create(path) if err != nil { return err } defer file.Close() return json.NewEncoder(file).Encode(info) } func exportResource(ctx context.Context, out string, ex *exporter, owner, name string) error { base := path.Join(out, ex.Name, name) if err := os.MkdirAll(base, 0o755); err != nil { return fmt.Errorf("failed to create export directory: %v", err) } return ex.ExportResource(ctx, base, owner, name) } hut-0.6.0/export/000077500000000000000000000000001463710650600136465ustar00rootroot00000000000000hut-0.6.0/export/builds.go000066400000000000000000000077731463710650600154750ustar00rootroot00000000000000package export import ( "context" "errors" "fmt" "io" "log" "net/http" "os" "path" "strconv" "strings" "time" "git.sr.ht/~emersion/gqlclient" "git.sr.ht/~xenrox/hut/srht/buildssrht" ) type BuildsExporter struct { client *gqlclient.Client http *http.Client } func NewBuildsExporter(client *gqlclient.Client, http *http.Client) *BuildsExporter { newHttp := *http newHttp.Timeout = 10 * time.Minute // XXX: Sane default? return &BuildsExporter{ client: client, http: &newHttp, } } type JobInfo struct { Info Id int32 `json:"id"` Status string `json:"status"` Note *string `json:"note,omitempty"` Tags []string `json:"tags"` Visibility buildssrht.Visibility `json:"visibility"` } func (ex *BuildsExporter) Export(ctx context.Context, dir string) error { var cursor *buildssrht.Cursor var ret error for { jobs, err := buildssrht.ExportJobs(ex.client, ctx, cursor) if err != nil { return err } for _, job := range jobs.Results { if job.Status != "SUCCESS" && job.Status != "FAILED" { continue } base := path.Join(dir, strconv.Itoa(int(job.Id))) if err := os.MkdirAll(base, 0o755); err != nil { return err } if err := ex.exportJob(ctx, &job, base); err != nil { var pe partialError if errors.As(err, &pe) { ret = err continue } return err } } cursor = jobs.Cursor if cursor == nil { break } } return ret } func (ex *BuildsExporter) ExportResource(ctx context.Context, dir, owner, resource string) error { resource = strings.TrimPrefix(resource, "job/") id, err := strconv.ParseInt(resource, 10, 64) if err != nil { return fmt.Errorf("failed to parse builds resource %v: %v", resource, err) } job, err := buildssrht.ExportJob(ex.client, ctx, int32(id)) if err != nil { return err } return ex.exportJob(ctx, job, dir) } func (ex *BuildsExporter) exportJob(ctx context.Context, job *buildssrht.Job, base string) error { infoPath := path.Join(base, infoFilename) if _, err := os.Stat(infoPath); err == nil { log.Printf("\tSkipping #%d (already exists)", job.Id) return nil } log.Printf("\tJob #%d", job.Id) req, err := http.NewRequestWithContext(ctx, http.MethodGet, job.Log.FullURL, nil) if err != nil { return err } resp, err := ex.http.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return partialError{fmt.Errorf("#%d: server returned non-200 status %d", job.Id, resp.StatusCode)} } file, err := os.Create(path.Join(base, "_build.log")) if err != nil { return err } defer file.Close() if _, err := io.Copy(file, resp.Body); err != nil { return err } var ret error for _, task := range job.Tasks { if err := ex.exportTask(ctx, ex.http, job, &task, base); err != nil { ret = err } } jobInfo := JobInfo{ Info: Info{ Service: "builds.sr.ht", Name: strconv.Itoa(int(job.Id)), }, Id: job.Id, Note: job.Note, Tags: job.Tags, Visibility: job.Visibility, } if err := writeJSON(infoPath, &jobInfo); err != nil { return err } return ret } func (ex *BuildsExporter) exportTask(ctx context.Context, client *http.Client, job *buildssrht.Job, task *buildssrht.Task, base string) error { if task.Status != "SUCCESS" && task.Status != "FAILED" { return nil } req, err := http.NewRequestWithContext(ctx, http.MethodGet, task.Log.FullURL, nil) if err != nil { return err } resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return partialError{fmt.Errorf("#%d: server returned non-200 status %d", job.Id, resp.StatusCode)} } file, err := os.Create(path.Join(base, fmt.Sprintf("%s.log", task.Name))) if err != nil { return err } defer file.Close() if _, err := io.Copy(file, resp.Body); err != nil { return err } return nil } func (ex *BuildsExporter) ImportResource(ctx context.Context, dir string) error { panic("not implemented") } hut-0.6.0/export/git.go000066400000000000000000000100171463710650600147570ustar00rootroot00000000000000package export import ( "context" "fmt" "log" "net/url" "os" "os/exec" "path" "strings" "git.sr.ht/~emersion/gqlclient" "git.sr.ht/~xenrox/hut/srht/gitsrht" ) const gitRepositoryDir = "repository.git" type GitExporter struct { client *gqlclient.Client baseURL string baseCloneURL string } func NewGitExporter(client *gqlclient.Client, baseURL string) *GitExporter { return &GitExporter{ client: client, baseURL: baseURL, } } // A subset of gitsrht.Repository which only contains the fields we want to // export (i.e. the ones filled in by the GraphQL query) type GitRepoInfo struct { Info Description *string `json:"description"` Visibility gitsrht.Visibility `json:"visibility"` Readme *string `json:"readme"` Head *string `json:"head"` } func (ex *GitExporter) Export(ctx context.Context, dir string) error { var cursor *gitsrht.Cursor for { repos, err := gitsrht.ExportRepositories(ex.client, ctx, cursor) if err != nil { return err } for _, repo := range repos.Results { base := path.Join(dir, repo.Name) if err := ex.exportRepository(ctx, &repo, base); err != nil { return err } } cursor = repos.Cursor if cursor == nil { break } } return nil } func (ex *GitExporter) ExportResource(ctx context.Context, dir, owner, resource string) error { user, err := gitsrht.ExportRepository(ex.client, ctx, owner, resource) if err != nil { return err } return ex.exportRepository(ctx, user.Repository, dir) } func (ex *GitExporter) exportRepository(ctx context.Context, repo *gitsrht.Repository, base string) error { // Cache base clone URL in exporter. if ex.baseCloneURL == "" { settings, err := gitsrht.SshSettings(ex.client, ctx) if err != nil { return err } sshUser := settings.Settings.SshUser baseURL, err := url.Parse(ex.baseURL) if err != nil { panic(err) } ex.baseCloneURL = fmt.Sprintf("%s@%s", sshUser, baseURL.Host) } // TODO: Should we fetch & store ACLs? infoPath := path.Join(base, infoFilename) clonePath := path.Join(base, gitRepositoryDir) cloneURL := fmt.Sprintf("%s:%s/%s", ex.baseCloneURL, repo.Owner.CanonicalName, repo.Name) if _, err := os.Stat(clonePath); err == nil { log.Printf("\tSkipping %s (already exists)", repo.Name) return nil } if err := os.MkdirAll(base, 0o755); err != nil { return err } log.Printf("\tCloning %s", repo.Name) cmd := exec.CommandContext(ctx, "git", "clone", "--mirror", cloneURL, clonePath) if err := cmd.Run(); err != nil { return err } var head *string if repo.HEAD != nil { h := strings.TrimPrefix(repo.HEAD.Name, "refs/heads/") head = &h } repoInfo := GitRepoInfo{ Info: Info{ Service: "git.sr.ht", Name: repo.Name, }, Description: repo.Description, Visibility: repo.Visibility, Readme: repo.Readme, Head: head, } return writeJSON(infoPath, &repoInfo) } func (ex *GitExporter) ImportResource(ctx context.Context, dir string) error { settings, err := gitsrht.SshSettings(ex.client, ctx) if err != nil { return fmt.Errorf("failed to get Git SSH settings: %v", err) } sshUser := settings.Settings.SshUser baseURL, err := url.Parse(ex.baseURL) if err != nil { panic(err) } var info GitRepoInfo if err := readJSON(path.Join(dir, infoFilename), &info); err != nil { return err } g, err := gitsrht.CreateRepository(ex.client, ctx, info.Name, info.Visibility, info.Description, nil) if err != nil { return fmt.Errorf("failed to create Git repository: %v", err) } clonePath := path.Join(dir, gitRepositoryDir) cloneURL := fmt.Sprintf("%s@%s:%s/%s", sshUser, baseURL.Host, g.Owner.CanonicalName, info.Name) cmd := exec.Command("git", "-C", clonePath, "push", "--mirror", cloneURL) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to push Git repository: %v", err) } if _, err := gitsrht.UpdateRepository(ex.client, ctx, g.Id, gitsrht.RepoInput{ Readme: info.Readme, HEAD: info.Head, }); err != nil { return fmt.Errorf("failed to update Git repository: %v", err) } return nil } hut-0.6.0/export/hg.go000066400000000000000000000070441463710650600146000ustar00rootroot00000000000000package export import ( "context" "fmt" "log" "net/url" "os" "os/exec" "path" "git.sr.ht/~emersion/gqlclient" "git.sr.ht/~xenrox/hut/srht/hgsrht" ) const hgRepositoryDir = "repository" type HgExporter struct { client *gqlclient.Client baseURL string } func NewHgExporter(client *gqlclient.Client, baseURL string) *HgExporter { return &HgExporter{client, baseURL} } // A subset of hgsrht.Repository which only contains the fields we want to // export (i.e. the ones filled in by the GraphQL query) type HgRepoInfo struct { Info Description *string `json:"description"` Visibility hgsrht.Visibility `json:"visibility"` Readme *string `json:"readme"` NonPublishing bool `json:"nonPublishing"` } func (ex *HgExporter) Export(ctx context.Context, dir string) error { var cursor *hgsrht.Cursor for { repos, err := hgsrht.ExportRepositories(ex.client, ctx, cursor) if err != nil { return err } for _, repo := range repos.Results { base := path.Join(dir, repo.Name) if err := ex.exportRepository(ctx, repo, base); err != nil { return err } } cursor = repos.Cursor if cursor == nil { break } } return nil } func (ex *HgExporter) ExportResource(ctx context.Context, dir, owner, resource string) error { user, err := hgsrht.ExportRepository(ex.client, ctx, owner, resource) if err != nil { return err } return ex.exportRepository(ctx, user.Repository, dir) } func (ex *HgExporter) exportRepository(ctx context.Context, repo *hgsrht.Repository, base string) error { // TODO: Should we fetch & store ACLs? baseURL, err := url.Parse(ex.baseURL) if err != nil { panic(err) } infoPath := path.Join(base, infoFilename) clonePath := path.Join(base, hgRepositoryDir) cloneURL := fmt.Sprintf("ssh://hg@%s/%s/%s", baseURL.Host, repo.Owner.CanonicalName, repo.Name) if _, err := os.Stat(clonePath); err == nil { log.Printf("\tSkipping %s (already exists)", repo.Name) return nil } if err := os.MkdirAll(base, 0o755); err != nil { return err } log.Printf("\tCloning %s", repo.Name) cmd := exec.CommandContext(ctx, "hg", "clone", "-U", cloneURL, clonePath) if err := cmd.Run(); err != nil { return err } repoInfo := HgRepoInfo{ Info: Info{ Service: "hg.sr.ht", Name: repo.Name, }, Description: repo.Description, Visibility: repo.Visibility, Readme: repo.Readme, NonPublishing: repo.NonPublishing, } return writeJSON(infoPath, &repoInfo) } func (ex *HgExporter) ImportResource(ctx context.Context, dir string) error { baseURL, err := url.Parse(ex.baseURL) if err != nil { panic(err) } var info HgRepoInfo if err := readJSON(path.Join(dir, infoFilename), &info); err != nil { return err } description := "" if info.Description != nil { description = *info.Description } h, err := hgsrht.CreateRepository(ex.client, ctx, info.Name, info.Visibility, description) if err != nil { return fmt.Errorf("failed to create Mercurial repository: %v", err) } clonePath := path.Join(dir, hgRepositoryDir) cloneURL := fmt.Sprintf("ssh://hg@%s/%s/%s", baseURL.Host, h.Owner.CanonicalName, info.Name) cmd := exec.Command("hg", "push", "--cwd", clonePath, cloneURL) cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("failed to push Mercurial repository: %v", err) } if _, err := hgsrht.UpdateRepository(ex.client, ctx, h.Id, hgsrht.RepoInput{ Readme: info.Readme, NonPublishing: &info.NonPublishing, }); err != nil { return fmt.Errorf("failed to update Mercurial repository: %v", err) } return nil } hut-0.6.0/export/iface.go000066400000000000000000000030211463710650600152400ustar00rootroot00000000000000package export import ( "context" "encoding/json" "io/fs" "os" "path/filepath" ) const infoFilename = "info.json" type Info struct { Service string `json:"service"` Name string `json:"name"` } type Exporter interface { Export(ctx context.Context, dir string) error ExportResource(ctx context.Context, dir, owner, name string) error ImportResource(ctx context.Context, dir string) error } type partialError struct { error } func (err partialError) Unwrap() error { return err.error } func writeJSON(filename string, v interface{}) error { f, err := os.Create(filename) if err != nil { return err } defer f.Close() if err := json.NewEncoder(f).Encode(v); err != nil { return err } return f.Close() } func readJSON(filename string, v interface{}) error { f, err := os.Open(filename) if err != nil { return err } defer f.Close() return json.NewDecoder(f).Decode(v) } type DirResource struct { Info Path string } func FindDirResources(dir string) ([]DirResource, error) { var l []DirResource err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { return nil } f, err := os.Open(filepath.Join(path, infoFilename)) if os.IsNotExist(err) { return nil } else if err != nil { return err } defer f.Close() var info Info if err := json.NewDecoder(f).Decode(&info); err != nil { return err } l = append(l, DirResource{ Info: info, Path: path, }) return filepath.SkipDir }) return l, err } hut-0.6.0/export/lists.go000066400000000000000000000105131463710650600153330ustar00rootroot00000000000000package export import ( "context" "errors" "fmt" "io" "log" "net/http" "os" "path" "time" "git.sr.ht/~emersion/gqlclient" "git.sr.ht/~xenrox/hut/srht/listssrht" ) const archiveFilename = "archive.mbox" type ListsExporter struct { client *gqlclient.Client http *http.Client } func NewListsExporter(client *gqlclient.Client, http *http.Client) *ListsExporter { newHttp := *http // XXX: Is this a sane default? Maybe large lists or slow // connections could require more. Would be nice to ensure a // constant flow of data rather than ensuring the entire request is // complete within a deadline. Would also be nice to use range // headers to be able to resume this on failure or interruption. newHttp.Timeout = 10 * time.Minute return &ListsExporter{ client: client, http: &newHttp, } } // A subset of listssrht.MailingList which only contains the fields we want to // export (i.e. the ones filled in by the GraphQL query) type MailingListInfo struct { Info Description *string `json:"description"` Visibility listssrht.Visibility `json:"visibility"` PermitMime []string `json:"permitMime"` RejectMime []string `json:"rejectMime"` } func (ex *ListsExporter) Export(ctx context.Context, dir string) error { var cursor *listssrht.Cursor var ret error for { user, err := listssrht.ExportMailingLists(ex.client, ctx, cursor) if err != nil { return err } for _, list := range user.Lists.Results { base := path.Join(dir, list.Name) if err := os.MkdirAll(base, 0o755); err != nil { return err } if err := ex.exportList(ctx, &list, base); err != nil { var pe partialError if errors.As(err, &pe) { ret = err continue } return err } } cursor = user.Lists.Cursor if cursor == nil { break } } return ret } func (ex *ListsExporter) ExportResource(ctx context.Context, dir, owner, resource string) error { user, err := listssrht.ExportMailingList(ex.client, ctx, owner, resource) if err != nil { return err } return ex.exportList(ctx, user.List, dir) } func (ex *ListsExporter) exportList(ctx context.Context, list *listssrht.MailingList, base string) error { infoPath := path.Join(base, infoFilename) if _, err := os.Stat(infoPath); err == nil { log.Printf("\tSkipping %s (already exists)", list.Name) return nil } log.Printf("\t%s", list.Name) req, err := http.NewRequestWithContext(ctx, http.MethodGet, string(list.Archive), nil) if err != nil { return err } resp, err := ex.http.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return partialError{fmt.Errorf("%s: server returned non-200 status %d", list.Name, resp.StatusCode)} } archive, err := os.Create(path.Join(base, archiveFilename)) if err != nil { return err } defer archive.Close() if _, err := io.Copy(archive, resp.Body); err != nil { return err } listInfo := MailingListInfo{ Info: Info{ Service: "lists.sr.ht", Name: list.Name, }, Description: list.Description, Visibility: list.Visibility, PermitMime: list.PermitMime, RejectMime: list.RejectMime, } if err := writeJSON(infoPath, &listInfo); err != nil { return err } return nil } func (ex *ListsExporter) ImportResource(ctx context.Context, dir string) error { var info MailingListInfo if err := readJSON(path.Join(dir, infoFilename), &info); err != nil { return err } return ex.importList(ctx, &info, dir) } func (ex *ListsExporter) importList(ctx context.Context, list *MailingListInfo, base string) error { l, err := listssrht.CreateMailingList(ex.client, ctx, list.Name, list.Description, list.Visibility) if err != nil { return fmt.Errorf("failed to create mailing list: %v", err) } if _, err := listssrht.UpdateMailingList(ex.client, ctx, l.Id, listssrht.MailingListInput{ PermitMime: list.PermitMime, RejectMime: list.RejectMime, }); err != nil { return fmt.Errorf("failed to update mailing list: %v", err) } archive, err := os.Open(path.Join(base, archiveFilename)) if err != nil { return err } defer archive.Close() if _, err := listssrht.ImportMailingListSpool(ex.client, ctx, l.Id, gqlclient.Upload{ Filename: archiveFilename, MIMEType: "application/mbox", Body: archive, }); err != nil { return fmt.Errorf("failed to import mailing list emails: %v", err) } return nil } hut-0.6.0/export/meta.go000066400000000000000000000060121463710650600151220ustar00rootroot00000000000000package export import ( "bufio" "context" "fmt" "log" "os" "path" "strings" "git.sr.ht/~emersion/gqlclient" "git.sr.ht/~xenrox/hut/srht/metasrht" ) const ( sshKeysFilename = "ssh.keys" pgpKeysFilename = "keys.pgp" ) type MetaExporter struct { client *gqlclient.Client } func NewMetaExporter(client *gqlclient.Client) *MetaExporter { return &MetaExporter{client} } func (ex *MetaExporter) Export(ctx context.Context, dir string) error { me, err := metasrht.FetchMe(ex.client, ctx) if err != nil { return err } if err := writeJSON(path.Join(dir, "profile.json"), me); err != nil { return err } var cursor *metasrht.Cursor sshFile, err := os.Create(path.Join(dir, sshKeysFilename)) if err != nil { return err } defer sshFile.Close() for { user, err := metasrht.ListRawSSHKeys(ex.client, ctx, cursor) if err != nil { return err } for _, key := range user.SshKeys.Results { if _, err := fmt.Fprintln(sshFile, key.Key); err != nil { return err } } cursor = user.SshKeys.Cursor if cursor == nil { break } } pgpFile, err := os.Create(path.Join(dir, pgpKeysFilename)) if err != nil { return err } defer pgpFile.Close() for { user, err := metasrht.ListRawPGPKeys(ex.client, ctx, cursor) if err != nil { return err } for _, key := range user.PgpKeys.Results { if _, err := fmt.Fprintln(pgpFile, key.Key); err != nil { return err } } cursor = user.PgpKeys.Cursor if cursor == nil { break } } if err := writeJSON(path.Join(dir, infoFilename), &Info{ Service: "meta.sr.ht", Name: me.CanonicalName, }); err != nil { return err } return nil } func (ex *MetaExporter) ExportResource(ctx context.Context, dir, owner, resource string) error { return fmt.Errorf("exporting individual meta resources is not supported") } func (ex *MetaExporter) ImportResource(ctx context.Context, dir string) error { sshFile, err := os.Open(path.Join(dir, sshKeysFilename)) if err != nil { return err } defer sshFile.Close() sshScanner := bufio.NewScanner(sshFile) for sshScanner.Scan() { if sshScanner.Text() == "" { continue } if _, err := metasrht.CreateSSHKey(ex.client, ctx, sshScanner.Text()); err != nil { log.Printf("Error importing SSH key: %v", err) continue } } if sshScanner.Err() != nil { return err } pgpFile, err := os.Open(path.Join(dir, pgpKeysFilename)) if err != nil { return err } defer pgpFile.Close() var key strings.Builder pgpScanner := bufio.NewScanner(pgpFile) for pgpScanner.Scan() { if strings.HasPrefix(pgpScanner.Text(), "-----BEGIN") { key.Reset() } key.WriteString(pgpScanner.Text()) key.WriteByte('\n') if strings.HasPrefix(pgpScanner.Text(), "-----END") { if _, err := metasrht.CreatePGPKey(ex.client, ctx, key.String()); err != nil { log.Printf("Error importing PGP key: %v", err) continue } key.Reset() } } if pgpScanner.Err() != nil { return err } if strings.TrimSpace(key.String()) != "" { log.Printf("Error importing PGP key: malformed file") } return nil } hut-0.6.0/export/paste.go000066400000000000000000000102571463710650600153160ustar00rootroot00000000000000package export import ( "context" "errors" "fmt" "io" "log" "net/http" "os" "path" "time" "git.sr.ht/~emersion/gqlclient" "git.sr.ht/~xenrox/hut/srht/pastesrht" ) const pasteFilesDir = "files" type PasteExporter struct { client *gqlclient.Client http *http.Client } func NewPasteExporter(client *gqlclient.Client, http *http.Client) *PasteExporter { // XXX: Is this a sane default? newHttp := *http newHttp.Timeout = 10 * time.Minute return &PasteExporter{ client: client, http: &newHttp, } } type PasteInfo struct { Info Visibility pastesrht.Visibility `json:"visibility"` } func (ex *PasteExporter) Export(ctx context.Context, dir string) error { var cursor *pastesrht.Cursor var ret error for { pastes, err := pastesrht.PasteContents(ex.client, ctx, cursor) if err != nil { return err } for _, paste := range pastes.Results { base := path.Join(dir, paste.Id) if err := ex.exportPaste(ctx, &paste, base); err != nil { var pe partialError if errors.As(err, &pe) { ret = err continue } return err } } cursor = pastes.Cursor if cursor == nil { break } } return ret } func (ex *PasteExporter) ExportResource(ctx context.Context, dir, owner, resource string) error { paste, err := pastesrht.PasteContentsByID(ex.client, ctx, resource) if err != nil { return err } return ex.exportPaste(ctx, paste, dir) } func (ex *PasteExporter) exportPaste(ctx context.Context, paste *pastesrht.Paste, base string) error { infoPath := path.Join(base, infoFilename) if _, err := os.Stat(infoPath); err == nil { log.Printf("\tSkipping %s (already exists)", paste.Id) return nil } log.Printf("\t%s", paste.Id) files := path.Join(base, pasteFilesDir) if err := os.MkdirAll(files, 0o755); err != nil { return err } var ret error for _, file := range paste.Files { if err := ex.exportFile(ctx, paste, files, &file); err != nil { ret = err } } pasteInfo := PasteInfo{ Info: Info{ Service: "paste.sr.ht", Name: paste.Id, }, Visibility: paste.Visibility, } if err := writeJSON(infoPath, &pasteInfo); err != nil { return err } return ret } func (ex *PasteExporter) exportFile(ctx context.Context, paste *pastesrht.Paste, base string, file *pastesrht.File) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, string(file.Contents), nil) if err != nil { return err } resp, err := ex.http.Do(req) if err != nil { return err } defer resp.Body.Close() name := paste.Id if file.Filename != nil && *file.Filename != "" { name = *file.Filename } if resp.StatusCode != http.StatusOK { return partialError{fmt.Errorf("%s/%s: server returned non-200 status %d", paste.Id, name, resp.StatusCode)} } f, err := os.Create(path.Join(base, name)) if err != nil { return err } defer f.Close() _, err = io.Copy(f, resp.Body) if err != nil { return err } return nil } func (ex *PasteExporter) ImportResource(ctx context.Context, dir string) error { var info PasteInfo if err := readJSON(path.Join(dir, infoFilename), &info); err != nil { return err } return ex.importPaste(ctx, &info, dir) } func (ex *PasteExporter) importPaste(ctx context.Context, paste *PasteInfo, base string) error { filesPath := path.Join(base, pasteFilesDir) items, err := os.ReadDir(filesPath) if err != nil { return err } var files []gqlclient.Upload for _, item := range items { if item.IsDir() { continue } f, err := os.Open(path.Join(filesPath, item.Name())) if err != nil { return err } defer f.Close() var name string if item.Name() != paste.Name { name = item.Name() } files = append(files, gqlclient.Upload{ Filename: name, // MIMEType is not used by the API, except for checking that it is a "text". // Parsing the MIME type from the extension would cause issues: ".json" is parsed as "application/json", // which gets rejected because it is not a "text/". // Since the API does not use the type besides that, always send a dummy text value. MIMEType: "text/plain", Body: f, }) } if _, err := pastesrht.CreatePaste(ex.client, ctx, files, paste.Visibility); err != nil { return fmt.Errorf("failed to create paste: %v", err) } return nil } hut-0.6.0/export/todo.go000066400000000000000000000065521463710650600151520ustar00rootroot00000000000000package export import ( "context" "errors" "fmt" "io" "log" "net/http" "os" "path" "time" "git.sr.ht/~emersion/gqlclient" "git.sr.ht/~xenrox/hut/srht/todosrht" ) const trackerFilename = "tracker.json.gz" type TodoExporter struct { client *gqlclient.Client http *http.Client } func NewTodoExporter(client *gqlclient.Client, http *http.Client) *TodoExporter { newHttp := *http // XXX: Is this a sane default? newHttp.Timeout = 10 * time.Minute return &TodoExporter{ client: client, http: &newHttp, } } type TrackerInfo struct { Info Description *string `json:"description"` Visibility todosrht.Visibility `json:"visibility"` } func (ex *TodoExporter) Export(ctx context.Context, dir string) error { var cursor *todosrht.Cursor var ret error for { trackers, err := todosrht.ExportTrackers(ex.client, ctx, cursor) if err != nil { return err } for _, tracker := range trackers.Results { base := path.Join(dir, tracker.Name) if err := ex.exportTracker(ctx, &tracker, base); err != nil { var pe partialError if errors.As(err, &pe) { ret = err continue } return err } } cursor = trackers.Cursor if cursor == nil { break } } return ret } func (ex *TodoExporter) ExportResource(ctx context.Context, dir, owner, resource string) error { user, err := todosrht.ExportTracker(ex.client, ctx, owner, resource) if err != nil { return err } return ex.exportTracker(ctx, user.Tracker, dir) } func (ex *TodoExporter) exportTracker(ctx context.Context, tracker *todosrht.Tracker, base string) error { infoPath := path.Join(base, infoFilename) if _, err := os.Stat(infoPath); err == nil { log.Printf("\tSkipping %s (already exists)", tracker.Name) return nil } dataPath := path.Join(base, trackerFilename) log.Printf("\t%s", tracker.Name) if err := os.MkdirAll(base, 0o755); err != nil { return err } req, err := http.NewRequestWithContext(ctx, http.MethodGet, string(tracker.Export), nil) if err != nil { return err } resp, err := ex.http.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return partialError{fmt.Errorf("%s: server returned non-200 status %d", tracker.Name, resp.StatusCode)} } f, err := os.Create(dataPath) if err != nil { return err } defer f.Close() if _, err := io.Copy(f, resp.Body); err != nil { return err } trackerInfo := TrackerInfo{ Info: Info{ Service: "todo.sr.ht", Name: tracker.Name, }, Description: tracker.Description, Visibility: tracker.Visibility, } if err := writeJSON(infoPath, &trackerInfo); err != nil { return err } return nil } func (ex *TodoExporter) ImportResource(ctx context.Context, dir string) error { var info TrackerInfo if err := readJSON(path.Join(dir, infoFilename), &info); err != nil { return err } return ex.importTracker(ctx, &info, dir) } func (ex *TodoExporter) importTracker(ctx context.Context, tracker *TrackerInfo, base string) error { f, err := os.Open(path.Join(base, trackerFilename)) if err != nil { return err } defer f.Close() _, err = todosrht.ImportTracker(ex.client, ctx, tracker.Name, tracker.Description, tracker.Visibility, gqlclient.Upload{ Filename: trackerFilename, MIMEType: "application/gzip", Body: f, }) if err != nil { return fmt.Errorf("failed to import issue tracker: %v", err) } return nil } hut-0.6.0/git.go000066400000000000000000000730351463710650600134470ustar00rootroot00000000000000package main import ( "context" "errors" "fmt" "io" "log" "net/url" "os" "os/exec" "path/filepath" "strings" "git.sr.ht/~emersion/gqlclient" "github.com/dustin/go-humanize" "github.com/spf13/cobra" "git.sr.ht/~xenrox/hut/srht/gitsrht" "git.sr.ht/~xenrox/hut/termfmt" ) func newGitCommand() *cobra.Command { cmd := &cobra.Command{ Use: "git", Short: "Use the git API", } cmd.AddCommand(newGitArtifactCommand()) cmd.AddCommand(newGitCreateCommand()) cmd.AddCommand(newGitListCommand()) cmd.AddCommand(newGitDeleteCommand()) cmd.AddCommand(newGitCloneCommand()) cmd.AddCommand(newGitACLCommand()) cmd.AddCommand(newGitShowCommand()) cmd.AddCommand(newGitUserWebhookCommand()) cmd.AddCommand(newGitUpdateCommand()) cmd.PersistentFlags().StringP("repo", "r", "", "name of repository") cmd.RegisterFlagCompletionFunc("repo", completeGitRepo) return cmd } func newGitCreateCommand() *cobra.Command { var visibility, desc, importURL string var clone bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("git", cmd) gitVisibility, err := gitsrht.ParseVisibility(visibility) if err != nil { log.Fatal(err) } var importURLPtr *string if importURL != "" { importURLPtr = &importURL } var description *string if desc != "" { description = &desc } repo, err := gitsrht.CreateRepository(c.Client, ctx, args[0], gitVisibility, description, importURLPtr) if err != nil { log.Fatal(err) } log.Printf("Created repository %s\n", repo.Name) ver, err := gitsrht.SshSettings(c.Client, ctx) if err != nil { log.Fatalf("failed to retrieve settings: %v", err) } u, err := url.Parse(c.BaseURL) if err != nil { log.Fatalf("failed to parse base URL: %v", err) } cloneURL := fmt.Sprintf("%s@%s:%s/%s", ver.Settings.SshUser, u.Hostname(), repo.Owner.CanonicalName, repo.Name) if clone { cloneCmd := exec.Command("git", "clone", cloneURL) cloneCmd.Stdin = os.Stdin cloneCmd.Stdout = os.Stdout cloneCmd.Stderr = os.Stderr err = cloneCmd.Run() if err != nil { log.Fatal(err) } } else { fmt.Printf("%s\n", cloneURL) } } cmd := &cobra.Command{ Use: "create ", Short: "Create a repository", Args: cobra.ExactArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().StringVarP(&visibility, "visibility", "v", "public", "repo visibility") cmd.RegisterFlagCompletionFunc("visibility", completeVisibility) cmd.Flags().StringVarP(&desc, "description", "d", "", "repo description") cmd.RegisterFlagCompletionFunc("description", cobra.NoFileCompletions) cmd.Flags().BoolVarP(&clone, "clone", "c", false, "autoclone repo") cmd.Flags().StringVar(&importURL, "import-url", "", "import repo from given URL") cmd.RegisterFlagCompletionFunc("import-url", cobra.NoFileCompletions) return cmd } func newGitListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var cursor *gitsrht.Cursor var owner, instance string if len(args) > 0 { owner, instance = parseOwnerName(args[0]) } err := pagerify(func(p pager) error { var repos *gitsrht.RepositoryCursor if len(owner) > 0 { c := createClientWithInstance("git", cmd, instance) username := strings.TrimLeft(owner, ownerPrefixes) user, err := gitsrht.RepositoriesByUser(c.Client, ctx, username, cursor) if err != nil { return err } else if user == nil { return errors.New("no such user") } repos = user.Repositories } else { c := createClient("git", cmd) var err error repos, err = gitsrht.Repositories(c.Client, ctx, cursor) if err != nil { return err } } for _, repo := range repos.Results { printGitRepo(p, &repo) } cursor = repos.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list [user]", Short: "List repos", Args: cobra.MaximumNArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } return cmd } func printGitRepo(w io.Writer, repo *gitsrht.Repository) { fmt.Fprintf(w, "%s (%s)\n", termfmt.Bold.String(repo.Name), repo.Visibility.TermString()) if repo.Description != nil && *repo.Description != "" { fmt.Fprintf(w, " %s\n", *repo.Description) } fmt.Fprintln(w) } func newGitDeleteCommand() *cobra.Command { var autoConfirm bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseResourceName(args[0]) } else { var err error name, owner, instance, err = getGitRepoName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("git", cmd, instance) id := getGitRepoID(c, ctx, name, owner) if !autoConfirm && !getConfirmation(fmt.Sprintf("Do you really want to delete the repo %s", name)) { log.Println("Aborted") return } repo, err := gitsrht.DeleteRepository(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Deleted repository %s\n", repo.Name) } cmd := &cobra.Command{ Use: "delete [repo]", Short: "Delete a repository", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeGitRepo, Run: run, } cmd.Flags().BoolVarP(&autoConfirm, "yes", "y", false, "auto confirm") return cmd } func newGitCloneCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { log.Println("Cloning repository") cloneCmd := exec.Command("git", "clone", args[0]) cloneCmd.Stdin = os.Stdin cloneCmd.Stdout = os.Stdout cloneCmd.Stderr = os.Stderr err := cloneCmd.Run() if err != nil { log.Fatalf("failed to clone repo: %v", err) } s := strings.Split(args[0], "/") dir, err := os.Getwd() if err != nil { log.Fatalf("failed to get current directory: %v", err) } err = os.Chdir(filepath.Join(dir, s[len(s)-1])) if err != nil { log.Fatalf("failed to change current working directory: %v", err) } cfg, err := loadProjectConfig() if err != nil { log.Fatalf("failed to load project config: %v", err) } if cfg != nil { if cfg.DevList != "" { log.Printf("Configuring repository for %q\n", "git send-email") sendemailCmd := exec.Command("git", "config", "sendemail.to", cfg.DevList) sendemailCmd.Stdin = os.Stdin sendemailCmd.Stdout = os.Stdout sendemailCmd.Stderr = os.Stderr err = sendemailCmd.Run() if err != nil { log.Fatalf("failed to set %q: %v", "git config sendemail.to", err) } } if cfg.PatchPrefix { prefixCmd := exec.Command("git", "config", "format.subjectPrefix", fmt.Sprintf("PATCH %s", s[len(s)-1])) prefixCmd.Stdin = os.Stdin prefixCmd.Stdout = os.Stdout prefixCmd.Stderr = os.Stderr err = prefixCmd.Run() if err != nil { log.Fatalf("failed to set %q: %v", "git config format.subjectPrefix", err) } } } } cmd := &cobra.Command{ Use: "clone ", Short: "Clone a repository", Args: cobra.ExactArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } return cmd } func newGitArtifactCommand() *cobra.Command { cmd := &cobra.Command{ Use: "artifact", Short: "Manage artifacts", } cmd.AddCommand(newGitArtifactUploadCommand()) cmd.AddCommand(newGitArtifactListCommand()) cmd.AddCommand(newGitArtifactDeleteCommand()) return cmd } func newGitArtifactUploadCommand() *cobra.Command { var rev string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() repoName, owner, instance, err := getGitRepoName(ctx, cmd) if err != nil { log.Fatal(err) } c := createClientWithInstance("git", cmd, instance) c.HTTP.Timeout = fileTransferTimeout repoID := getGitRepoID(c, ctx, repoName, owner) if rev == "" { var err error rev, err = guessRev() if err != nil { log.Fatal(err) } } for _, filename := range args { f, err := os.Open(filename) if err != nil { log.Fatalf("failed to open input file: %v", err) } defer f.Close() file := gqlclient.Upload{Filename: filepath.Base(filename), Body: f} artifact, err := gitsrht.UploadArtifact(c.Client, ctx, repoID, rev, file) if err != nil { log.Fatal(err) } log.Printf("Uploaded %s\n", artifact.Filename) } } cmd := &cobra.Command{ Use: "upload ", Short: "Upload artifacts", Args: cobra.MinimumNArgs(1), Run: run, } cmd.Flags().StringVar(&rev, "rev", "", "revision tag") cmd.RegisterFlagCompletionFunc("rev", completeRev) return cmd } func newGitArtifactListCommand() *cobra.Command { // TODO: Filter by rev run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() repoName, owner, instance, err := getGitRepoName(ctx, cmd) if err != nil { log.Fatal(err) } c := createClientWithInstance("git", cmd, instance) var ( username string user *gitsrht.User ) if owner != "" { username = strings.TrimLeft(owner, ownerPrefixes) user, err = gitsrht.ListArtifactsByUser(c.Client, ctx, username, repoName) } else { user, err = gitsrht.ListArtifacts(c.Client, ctx, repoName) } if err != nil { log.Fatal(err) } else if user == nil { log.Fatalf("no such user %q", username) } else if user.Repository == nil { log.Fatalf("no such repository %q", repoName) } for _, ref := range user.Repository.References.Results { if len(ref.Artifacts.Results) == 0 { continue } name := ref.Name[strings.LastIndex(ref.Name, "/")+1:] fmt.Printf("Tag %s:\n", termfmt.Bold.String(name)) for _, artifact := range ref.Artifacts.Results { fmt.Printf(" %s %s\n", termfmt.DarkYellow.Sprintf("#%d", artifact.Id), artifact.Filename) } } } cmd := &cobra.Command{ Use: "list", Short: "List artifacts", Args: cobra.ExactArgs(0), Run: run, } return cmd } func newGitArtifactDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("git", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } artifact, err := gitsrht.DeleteArtifact(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Deleted artifact %s\n", artifact.Filename) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete an artifact", Args: cobra.ExactArgs(1), ValidArgsFunction: completeArtifacts, Run: run, } return cmd } func newGitACLCommand() *cobra.Command { cmd := &cobra.Command{ Use: "acl", Short: "Manage access-control lists", } cmd.AddCommand(newGitACLListCommand()) cmd.AddCommand(newGitACLUpdateCommand()) cmd.AddCommand(newGitACLDeleteCommand()) return cmd } func newGitACLListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseResourceName(args[0]) } else { var err error name, owner, instance, err = getGitRepoName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("git", cmd, instance) var ( cursor *gitsrht.Cursor user *gitsrht.User username string err error ) if owner != "" { username = strings.TrimLeft(owner, ownerPrefixes) } err = pagerify(func(p pager) error { if username != "" { user, err = gitsrht.AclByUser(c.Client, ctx, username, name, cursor) } else { user, err = gitsrht.AclByRepoName(c.Client, ctx, name, cursor) } if err != nil { return err } else if user == nil { return errors.New("no such user") } else if user.Repository == nil { return fmt.Errorf("no such repository %q", name) } for _, acl := range user.Repository.Acls.Results { printGitACLEntry(p, &acl) } cursor = user.Repository.Acls.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list [repo]", Short: "List ACL entries", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeGitRepo, Run: run, } return cmd } func printGitACLEntry(w io.Writer, acl *gitsrht.ACL) { var mode string if acl.Mode != nil { mode = string(*acl.Mode) } created := termfmt.Dim.String(humanize.Time(acl.Created.Time)) fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", termfmt.DarkYellow.Sprintf("#%d", acl.Id), acl.Entity.CanonicalName, mode, created) } func newGitACLUpdateCommand() *cobra.Command { var mode string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() accessMode, err := gitsrht.ParseAccessMode(mode) if err != nil { log.Fatal(err) } if strings.IndexAny(args[0], ownerPrefixes) != 0 { log.Fatal("user must be in canonical form") } name, owner, instance, err := getGitRepoName(ctx, cmd) if err != nil { log.Fatal(err) } c := createClientWithInstance("git", cmd, instance) id := getGitRepoID(c, ctx, name, owner) acl, err := gitsrht.UpdateACL(c.Client, ctx, id, accessMode, args[0]) if err != nil { log.Fatal(err) } log.Printf("Updated access rights for %s\n", acl.Entity.CanonicalName) } cmd := &cobra.Command{ Use: "update ", Short: "Update/add ACL entries", Args: cobra.ExactArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().StringVarP(&mode, "mode", "m", "", "access mode") cmd.RegisterFlagCompletionFunc("mode", completeRepoAccessMode) cmd.MarkFlagRequired("mode") return cmd } func newGitACLDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("git", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } acl, err := gitsrht.DeleteACL(c.Client, ctx, id) if err != nil { log.Fatal(err) } else if acl == nil { log.Fatalf("failed to delete ACL entry with ID %d", id) } log.Printf("Deleted ACL entry for %s in repository %s\n", acl.Entity.CanonicalName, acl.Repository.Name) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete an ACL entry", Args: cobra.ExactArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } return cmd } func newGitShowCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseResourceName(args[0]) } else { var err error name, owner, instance, err = getGitRepoName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("git", cmd, instance) var ( user *gitsrht.User username string err error ) if owner == "" { user, err = gitsrht.RepositoryByName(c.Client, ctx, name) } else { username = strings.TrimLeft(owner, ownerPrefixes) user, err = gitsrht.RepositoryByUser(c.Client, ctx, username, name) } if err != nil { log.Fatal(err) } else if user == nil { log.Fatalf("no such user %q", username) } else if user.Repository == nil { log.Fatalf("no such repository %q", name) } repo := user.Repository // prints basic information fmt.Printf("%s (%s)\n", termfmt.Bold.String(repo.Name), repo.Visibility.TermString()) if repo.Description != nil && *repo.Description != "" { fmt.Printf(" %s\n", *repo.Description) } // prints latest tag tags := repo.References.Tags() if len(tags) > 0 { fmt.Println() fmt.Printf(" Latest tag: %s\n", tags[len(tags)-1]) } // prints branches branches := repo.References.Heads() if len(branches) > 0 { fmt.Println() fmt.Printf(" Branches:\n") for i := 0; i < len(branches); i++ { fmt.Printf(" %s\n", branches[i]) } } // prints the three most recent commits if len(repo.Log.Results) >= 3 { fmt.Println() fmt.Printf(" Recent log:\n") for _, commit := range repo.Log.Results[:3] { fmt.Printf(" %s %s <%s> (%s)\n", termfmt.Yellow.Sprintf("%s", commit.ShortId), commit.Author.Name, commit.Author.Email, humanize.Time(commit.Author.Time.Time)) commitLines := strings.Split(commit.Message, "\n") fmt.Printf(" %s\n", commitLines[0]) } } } cmd := &cobra.Command{ Use: "show [repo]", Short: "Shows a repository", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeGitRepo, Run: run, } return cmd } func newGitUserWebhookCommand() *cobra.Command { cmd := &cobra.Command{ Use: "user-webhook", Short: "Manage user webhooks", } cmd.AddCommand(newGitUserWebhookCreateCommand()) cmd.AddCommand(newGitUserWebhookListCommand()) cmd.AddCommand(newGitUserWebhookDeleteCommand()) return cmd } func newGitUserWebhookCreateCommand() *cobra.Command { var events []string var stdin bool var url string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("git", cmd) var config gitsrht.UserWebhookInput config.Url = url whEvents, err := gitsrht.ParseEvents(events) if err != nil { log.Fatal(err) } config.Events = whEvents config.Query = readWebhookQuery(stdin) webhook, err := gitsrht.CreateUserWebhook(c.Client, ctx, config) if err != nil { log.Fatal(err) } log.Printf("Created user webhook with ID %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "create", Short: "Create a user webhook", Args: cobra.ExactArgs(0), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().StringSliceVarP(&events, "events", "e", nil, "webhook events") cmd.RegisterFlagCompletionFunc("events", completeGitUserWebhookEvents) cmd.MarkFlagRequired("events") cmd.Flags().BoolVar(&stdin, "stdin", !isStdinTerminal, "read webhook query from stdin") cmd.Flags().StringVarP(&url, "url", "u", "", "payload url") cmd.RegisterFlagCompletionFunc("url", cobra.NoFileCompletions) cmd.MarkFlagRequired("url") return cmd } func newGitUserWebhookListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("git", cmd) var cursor *gitsrht.Cursor err := pagerify(func(p pager) error { webhooks, err := gitsrht.UserWebhooks(c.Client, ctx, cursor) if err != nil { return err } for _, webhook := range webhooks.Results { fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url) } cursor = webhooks.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list", Short: "List user webhooks", Args: cobra.ExactArgs(0), Run: run, } return cmd } func newGitUserWebhookDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("git", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } webhook, err := gitsrht.DeleteUserWebhook(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Deleted webhook %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete a user webhook", Args: cobra.ExactArgs(1), ValidArgsFunction: completeGitUserWebhookID, Run: run, } return cmd } func newGitUpdateCommand() *cobra.Command { var visibility, branch, readme, description string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseResourceName(args[0]) } else { var err error name, owner, instance, err = getGitRepoName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("git", cmd, instance) id := getGitRepoID(c, ctx, name, owner) var input gitsrht.RepoInput if visibility != "" { repoVisibility, err := gitsrht.ParseVisibility(visibility) if err != nil { log.Fatal(err) } input.Visibility = &repoVisibility } if cmd.Flags().Changed("description") { if description == "" { _, err := gitsrht.ClearDescription(c.Client, ctx, id) if err != nil { log.Fatalf("failed to clear description: %v", err) } } else { input.Description = &description } } if branch != "" { input.HEAD = &branch } if readme == "" && cmd.Flags().Changed("readme") { _, err := gitsrht.ClearCustomReadme(c.Client, ctx, id) if err != nil { log.Fatalf("failed to unset custom README: %v", err) } } else if readme != "" { var ( b []byte err error ) if readme == "-" { b, err = io.ReadAll(os.Stdin) } else { b, err = os.ReadFile(readme) } if err != nil { log.Fatalf("failed to read custom README: %v", err) } s := string(b) input.Readme = &s } repo, err := gitsrht.UpdateRepository(c.Client, ctx, id, input) if err != nil { log.Fatal(err) } else if repo == nil { log.Fatalf("failed to update repository %q", name) } log.Printf("Successfully updated repository %q\n", repo.Name) } cmd := &cobra.Command{ Use: "update [repo]", Short: "Update a repository", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeGitRepo, Run: run, } cmd.Flags().StringVarP(&visibility, "visibility", "v", "", "repository visibility") cmd.RegisterFlagCompletionFunc("visibility", completeVisibility) cmd.Flags().StringVarP(&branch, "default-branch", "b", "", "default branch") cmd.RegisterFlagCompletionFunc("default-branch", completeBranches) cmd.Flags().StringVar(&readme, "readme", "", "update the custom README") cmd.Flags().StringVarP(&description, "description", "d", "", "repository description") cmd.RegisterFlagCompletionFunc("description", cobra.NoFileCompletions) return cmd } func getGitRepoName(ctx context.Context, cmd *cobra.Command) (repoName, owner, instance string, err error) { repoName, err = cmd.Flags().GetString("repo") if err != nil { return "", "", "", err } else if repoName != "" { repoName, owner, instance = parseResourceName(repoName) return repoName, owner, instance, nil } return guessGitRepoName(ctx, cmd) } func guessGitRepoName(ctx context.Context, cmd *cobra.Command) (repoName, owner, instance string, err error) { remoteURLs, err := gitRemoteURLs(ctx) if err != nil { return "", "", "", err } cfg := loadConfig(cmd) for _, remoteURL := range remoteURLs { if remoteURL.Host == "" { continue } match := false for _, instance := range cfg.Instances { if instance.match(remoteURL.Host) { match = true break } } if !match { continue } parts := strings.Split(strings.Trim(remoteURL.Path, "/"), "/") if len(parts) != 2 { return "", "", "", fmt.Errorf("failed to parse Git URL %q: expected 2 path components", remoteURL) } owner, repoName = parts[0], parts[1] // TODO: ignore port in host return repoName, owner, remoteURL.Host, nil } return "", "", "", fmt.Errorf("no sr.ht Git repository found in current directory") } func getGitRepoID(c *Client, ctx context.Context, name, owner string) int32 { var ( user *gitsrht.User username string err error ) if owner == "" { user, err = gitsrht.RepositoryIDByName(c.Client, ctx, name) } else { username = strings.TrimLeft(owner, ownerPrefixes) user, err = gitsrht.RepositoryIDByUser(c.Client, ctx, username, name) } if err != nil { log.Fatalf("failed to get repository ID: %v", err) } else if user == nil { log.Fatalf("no such user %q", username) } else if user.Repository == nil { log.Fatalf("no such repository %q", name) } return user.Repository.Id } func gitRemoteURLs(ctx context.Context) ([]*url.URL, error) { // TODO: iterate over all remotes out, err := exec.CommandContext(ctx, "git", "remote", "get-url", "--all", "origin").Output() if err != nil { return nil, fmt.Errorf("failed to get remote URL: %v", err) } var urls []*url.URL l := strings.Split(strings.TrimSpace(string(out)), "\n") for _, raw := range l { var u *url.URL switch { case strings.Contains(raw, "://"): u, err = url.Parse(raw) if err != nil { return nil, err } case strings.HasPrefix(raw, "/"): u = &url.URL{Scheme: "file", Path: raw} default: i := strings.Index(raw, ":") if i < 0 { return nil, fmt.Errorf("invalid scp-like Git URL %q: missing colon", raw) } host, path := raw[:i], raw[i+1:] // Strip optional user if i := strings.Index(host, "@"); i >= 0 { host = host[i+1:] } u = &url.URL{Scheme: "ssh", Host: host, Path: path} } urls = append(urls, u) } return urls, nil } func guessRev() (string, error) { out, err := exec.Command("git", "describe", "--abbrev=0").Output() if err != nil { return "", fmt.Errorf("failed to autodetect revision tag: %v", err) } return strings.TrimSpace(string(out)), nil } func completeGitRepo(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("git", cmd) var repoList []string repos, err := gitsrht.CompleteRepositories(c.Client, ctx) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, repo := range repos.Results { repoList = append(repoList, repo.Name) } return repoList, cobra.ShellCompDirectiveNoFileComp } func completeRev(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { repo, err := cmd.Flags().GetString("repo") if err == nil && repo != "" { ctx := cmd.Context() c := createClient("git", cmd) user, err := gitsrht.RevsByRepoName(c.Client, ctx, repo) if err != nil || user.Repository == nil { return nil, cobra.ShellCompDirectiveNoFileComp } return user.Repository.References.Tags(), cobra.ShellCompDirectiveNoFileComp } output, err := exec.Command("git", "tag").Output() if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } revs := strings.Split(string(output), "\n") return revs, cobra.ShellCompDirectiveNoFileComp } func completeArtifacts(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() repoName, owner, instance, err := getGitRepoName(ctx, cmd) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } c := createClientWithInstance("git", cmd, instance) var user *gitsrht.User var artifactList []string if owner != "" { username := strings.TrimLeft(owner, ownerPrefixes) user, err = gitsrht.ListArtifactsByUser(c.Client, ctx, username, repoName) } else { user, err = gitsrht.ListArtifacts(c.Client, ctx, repoName) } if err != nil || user == nil || user.Repository == nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, ref := range user.Repository.References.Results { for _, artifact := range ref.Artifacts.Results { name := ref.Name[strings.LastIndex(ref.Name, "/")+1:] s := fmt.Sprintf("%d\t%s (%s)", artifact.Id, artifact.Filename, name) artifactList = append(artifactList, s) } } return artifactList, cobra.ShellCompDirectiveNoFileComp } func completeGitUserWebhookEvents(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var eventList []string events := [3]string{"repo_created", "repo_update", "repo_deleted"} set := strings.ToLower(cmd.Flag("events").Value.String()) for _, event := range events { if !strings.Contains(set, event) { eventList = append(eventList, event) } } return eventList, cobra.ShellCompDirectiveNoFileComp } func completeGitUserWebhookID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("git", cmd) var webhookList []string webhooks, err := gitsrht.UserWebhooks(c.Client, ctx, nil) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, webhook := range webhooks.Results { s := fmt.Sprintf("%d\t%s", webhook.Id, webhook.Url) webhookList = append(webhookList, s) } return webhookList, cobra.ShellCompDirectiveNoFileComp } func completeBranches(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() repoName, owner, instace, err := getGitRepoName(ctx, cmd) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } c := createClientWithInstance("git", cmd, instace) var user *gitsrht.User if owner != "" { username := strings.TrimLeft(owner, ownerPrefixes) user, err = gitsrht.RevsByUser(c.Client, ctx, username, repoName) } else { user, err = gitsrht.RevsByRepoName(c.Client, ctx, repoName) } if err != nil || user == nil || user.Repository == nil { return nil, cobra.ShellCompDirectiveNoFileComp } return user.Repository.References.Heads(), cobra.ShellCompDirectiveNoFileComp } func completeCoMaintainers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() // Since completeCoMaintainers is intended to be called from other services // than git, we cannot use getRepoName which requires the "repo" flag to be set. repoName, _, instace, err := guessGitRepoName(ctx, cmd) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } c := createClientWithInstance("git", cmd, instace) var userList []string user, err := gitsrht.CompleteCoMaintainers(c.Client, ctx, repoName) if err != nil || user.Repositories == nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, acl := range user.Repository.Acls.Results { userList = append(userList, acl.Entity.CanonicalName) } return userList, cobra.ShellCompDirectiveNoFileComp } hut-0.6.0/go.mod000066400000000000000000000013771463710650600134430ustar00rootroot00000000000000module git.sr.ht/~xenrox/hut go 1.17 require ( git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082 git.sr.ht/~emersion/gqlclient v0.0.0-20230820050442-8873fe0204b9 github.com/dustin/go-humanize v1.0.1 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/juju/ansiterm v1.0.0 github.com/spf13/cobra v1.8.0 golang.org/x/term v0.20.0 ) require ( github.com/agnivade/levenshtein v1.1.1 // indirect github.com/dave/jennifer v1.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.7.0 // indirect github.com/vektah/gqlparser/v2 v2.5.8 // indirect golang.org/x/sys v0.20.0 // indirect ) hut-0.6.0/go.sum000066400000000000000000000143431463710650600134650ustar00rootroot00000000000000git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082 h1:9Udx5fm4vRtmgDIBjy2ef5QioHbzpw5oHabbhpAUyEw= git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082/go.mod h1:ybgvEJTIx5XbaspSviB3KNa6OdPmAZqDoSud7z8fFlw= git.sr.ht/~emersion/gqlclient v0.0.0-20230820050442-8873fe0204b9 h1:QNwHP6WknvS7X6MEFxCpefQb1QJMqgIIt+vn/PVoMMg= git.sr.ht/~emersion/gqlclient v0.0.0-20230820050442-8873fe0204b9/go.mod h1:kvl/JK0Z3VRmtbBxdOJR4ydyXVouUIcFIXgv4H6rVAY= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/dave/jennifer v1.7.0 h1:uRbSBH9UTS64yXbh4FrMHfgfY762RD+C7bUPKODpSJE= github.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/juju/ansiterm v1.0.0 h1:gmMvnZRq7JZJx6jkfSq9/+2LMrVEwGwt7UR6G+lmDEg= github.com/juju/ansiterm v1.0.0/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/vektah/gqlparser/v2 v2.5.8 h1:pm6WOnGdzFOCfcQo9L3+xzW51mKrlwTEg4Wr7AH1JW4= github.com/vektah/gqlparser/v2 v2.5.8/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= hut-0.6.0/graphql.go000066400000000000000000000054261463710650600143210ustar00rootroot00000000000000package main import ( "encoding/json" "fmt" "io" "log" "mime" "os" "path/filepath" "strings" "git.sr.ht/~emersion/gqlclient" "github.com/spf13/cobra" ) const graphqlPrefill = ` # Please write the GraphQL query you want to execute above. The GraphQL schema # for %v.sr.ht is available at: # %v` func newGraphqlCommand() *cobra.Command { var stringVars, fileVars []string var stdin bool run := func(cmd *cobra.Command, args []string) { service := args[0] ctx := cmd.Context() c := createClient(service, cmd) var query string if stdin { b, err := io.ReadAll(os.Stdin) if err != nil { log.Fatalf("failed to read GraphQL query: %v", err) } query = string(b) } else { prefill := fmt.Sprintf(graphqlPrefill, service, graphqlSchemaURL(service)) var err error query, err = getInputWithEditor("hut_query*.graphql", prefill) if err != nil { log.Fatalf("failed to read GraphQL query: %v", err) } query = dropComment(query, prefill) } if strings.TrimSpace(query) == "" { fmt.Fprintln(os.Stderr, "Aborting due to empty query") os.Exit(1) } op := gqlclient.NewOperation(query) for _, kv := range stringVars { op.Var(splitKeyValue(kv)) } for _, kv := range fileVars { k, filename := splitKeyValue(kv) f, err := os.Open(filename) if err != nil { log.Fatalf("in variable definition %q: %v", kv, err) } defer f.Close() op.Var(k, gqlclient.Upload{ Filename: filepath.Base(filename), MIMEType: mime.TypeByExtension(filename), Body: f, }) } var data json.RawMessage if err := c.Execute(ctx, op, &data); err != nil { log.Fatal(err) } enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") if err := enc.Encode(data); err != nil { log.Fatalf("failed to write JSON response: %v", err) } } cmd := &cobra.Command{ Use: "graphql ", Short: "Execute a GraphQL query", Args: cobra.ExactArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().StringSliceVarP(&stringVars, "var", "v", nil, "set string variable") cmd.Flags().StringSliceVar(&fileVars, "file", nil, "set file variable") cmd.Flags().BoolVar(&stdin, "stdin", !isStdinTerminal, "read query from stdin") // TODO: JSON variable return cmd } func splitKeyValue(kv string) (string, string) { parts := strings.SplitN(kv, "=", 2) if len(parts) != 2 { log.Fatalf("in variable definition %q: missing equal sign", kv) } return parts[0], parts[1] } func graphqlSchemaURL(service string) string { var filename string switch service { case "pages": filename = "graph/schema.graphqls" default: filename = "api/graph/schema.graphqls" } return fmt.Sprintf("https://git.sr.ht/~sircmpwn/%v.sr.ht/tree/master/item/%v", service, filename) } hut-0.6.0/hg.go000066400000000000000000000432341463710650600132600ustar00rootroot00000000000000package main import ( "context" "errors" "fmt" "io" "log" "net/url" "os" "os/exec" "strconv" "strings" "git.sr.ht/~xenrox/hut/srht/hgsrht" "git.sr.ht/~xenrox/hut/termfmt" "github.com/dustin/go-humanize" "github.com/spf13/cobra" ) func newHgCommand() *cobra.Command { cmd := &cobra.Command{ Use: "hg", Short: "Use the hg API", } cmd.AddCommand(newHgListCommand()) cmd.AddCommand(newHgCreateCommand()) cmd.AddCommand(newHgDeleteCommand()) cmd.AddCommand(newHgUpdateCommand()) cmd.AddCommand(newHgACLCommand()) cmd.AddCommand(newHgUserWebhookCommand()) cmd.PersistentFlags().StringP("repo", "r", "", "name of repository") cmd.RegisterFlagCompletionFunc("repo", completeHgRepo) return cmd } func newHgListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("hg", cmd) var cursor *hgsrht.Cursor var username string if len(args) > 0 { username = strings.TrimLeft(args[0], ownerPrefixes) } err := pagerify(func(p pager) error { var repos *hgsrht.RepositoryCursor if len(username) > 0 { user, err := hgsrht.RepositoriesByUser(c.Client, ctx, username, cursor) if err != nil { return err } else if user == nil { return errors.New("no such user") } repos = user.Repositories } else { var err error repos, err = hgsrht.Repositories(c.Client, ctx, cursor) if err != nil { return err } } for _, repo := range repos.Results { printHgRepo(p, repo) } cursor = repos.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list [user]", Short: "List repos", Args: cobra.MaximumNArgs(1), Run: run, } return cmd } func printHgRepo(w io.Writer, repo *hgsrht.Repository) { fmt.Fprintf(w, "%s (%s)\n", termfmt.Bold.String(repo.Name), repo.Visibility.TermString()) if repo.Description != nil && *repo.Description != "" { fmt.Fprintf(w, " %s\n", *repo.Description) } fmt.Fprintln(w) } func newHgCreateCommand() *cobra.Command { var visibility, desc string var clone bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("hg", cmd) hgVisibility, err := hgsrht.ParseVisibility(visibility) if err != nil { log.Fatal(err) } repo, err := hgsrht.CreateRepository(c.Client, ctx, args[0], hgVisibility, desc) if err != nil { log.Fatal(err) } else if repo == nil { log.Fatal("failed to create repository") } log.Printf("Created repository %s\n", repo.Name) ver, err := hgsrht.SshSettings(c.Client, ctx) if err != nil { log.Fatalf("failed to retrieve settings: %v", err) } u, err := url.Parse(c.BaseURL) if err != nil { log.Fatalf("failed to parse base URL: %v", err) } cloneURL := fmt.Sprintf("ssh://%s@%s/%s/%s", ver.Settings.SshUser, u.Hostname(), repo.Owner.CanonicalName, repo.Name) if clone { cloneCmd := exec.Command("hg", "clone", cloneURL) cloneCmd.Stdin = os.Stdin cloneCmd.Stdout = os.Stdout cloneCmd.Stderr = os.Stderr err = cloneCmd.Run() if err != nil { log.Fatal(err) } } else { fmt.Printf("%s\n", cloneURL) } } cmd := &cobra.Command{ Use: "create ", Short: "Create a repository", Args: cobra.ExactArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().StringVarP(&visibility, "visibility", "v", "public", "repo visibility") cmd.RegisterFlagCompletionFunc("visibility", completeVisibility) cmd.Flags().StringVarP(&desc, "description", "d", "", "repo description") cmd.RegisterFlagCompletionFunc("description", cobra.NoFileCompletions) cmd.Flags().BoolVarP(&clone, "clone", "c", false, "autoclone repo") return cmd } func newHgDeleteCommand() *cobra.Command { var autoConfirm bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseResourceName(args[0]) } else { var err error name, owner, instance, err = getHgRepoName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("hg", cmd, instance) id := getHgRepoID(c, ctx, name, owner) if !autoConfirm && !getConfirmation(fmt.Sprintf("Do you really want to delete the repo %s", name)) { log.Println("Aborted") return } repo, err := hgsrht.DeleteRepository(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Deleted repository %s\n", repo.Name) } cmd := &cobra.Command{ Use: "delete [repo]", Short: "Delete a repository", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeHgRepo, Run: run, } cmd.Flags().BoolVarP(&autoConfirm, "yes", "y", false, "auto confirm") return cmd } func newHgUpdateCommand() *cobra.Command { var description, nonPublishing, readme, visibility string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseResourceName(args[0]) } else { var err error name, owner, instance, err = getHgRepoName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("hg", cmd, instance) id := getHgRepoID(c, ctx, name, owner) var input hgsrht.RepoInput if cmd.Flags().Changed("description") { if description == "" { _, err := hgsrht.ClearDescription(c.Client, ctx, id) if err != nil { log.Fatalf("failed to clear description: %v", err) } } else { input.Description = &description } } if nonPublishing != "" { b, err := strconv.ParseBool(nonPublishing) if err != nil { log.Fatalf("failure with %q: %v", "non-publishing", err) } input.NonPublishing = &b } if readme == "" && cmd.Flags().Changed("readme") { _, err := hgsrht.ClearCustomReadme(c.Client, ctx, id) if err != nil { log.Fatalf("failed to unset custom README: %v", err) } } else if readme != "" { var ( b []byte err error ) if readme == "-" { b, err = io.ReadAll(os.Stdin) } else { b, err = os.ReadFile(readme) } if err != nil { log.Fatalf("failed to read custom README: %v", err) } s := string(b) input.Readme = &s } if visibility != "" { repoVisibility, err := hgsrht.ParseVisibility(visibility) if err != nil { log.Fatal(err) } input.Visibility = &repoVisibility } repo, err := hgsrht.UpdateRepository(c.Client, ctx, id, input) if err != nil { log.Fatal(err) } else if repo == nil { log.Fatalf("failed to update repository %q", name) } log.Printf("Successfully updated repository %q\n", repo.Name) } cmd := &cobra.Command{ Use: "update [repo]", Short: "Update a repository", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeHgRepo, Run: run, } cmd.Flags().StringVarP(&description, "description", "d", "", "repository description") cmd.RegisterFlagCompletionFunc("description", cobra.NoFileCompletions) cmd.Flags().StringVar(&nonPublishing, "non-publishing", "", "non-publishing repository") cmd.RegisterFlagCompletionFunc("non-publishing", completeBoolean) cmd.Flags().StringVar(&readme, "readme", "", "update the custom README") cmd.Flags().StringVarP(&visibility, "visibility", "v", "", "repository visibility") cmd.RegisterFlagCompletionFunc("visibility", completeVisibility) return cmd } func newHgACLCommand() *cobra.Command { cmd := &cobra.Command{ Use: "acl", Short: "Manage access-control lists", } cmd.AddCommand(newHgACLListCommand()) cmd.AddCommand(newHgACLUpdateCommand()) cmd.AddCommand(newHgACLDeleteCommand()) return cmd } func newHgACLListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseResourceName(args[0]) } else { var err error name, owner, instance, err = getHgRepoName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("hg", cmd, instance) var ( cursor *hgsrht.Cursor user *hgsrht.User username string err error ) if owner != "" { username = strings.TrimLeft(owner, ownerPrefixes) } err = pagerify(func(p pager) error { if username != "" { user, err = hgsrht.AclByUser(c.Client, ctx, username, name, cursor) } else { user, err = hgsrht.AclByRepoName(c.Client, ctx, name, cursor) } if err != nil { return err } else if user == nil { return errors.New("no such user") } else if user.Repository == nil { return fmt.Errorf("no such repository %q", name) } for _, acl := range user.Repository.AccessControlList.Results { printHgACLEntry(p, acl) } cursor = user.Repository.AccessControlList.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list [repo]", Short: "List ACL entries", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeHgRepo, Run: run, } return cmd } func printHgACLEntry(w io.Writer, acl *hgsrht.ACL) { var mode string if acl.Mode != nil { mode = string(*acl.Mode) } created := termfmt.Dim.String(humanize.Time(acl.Created.Time)) fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", termfmt.DarkYellow.Sprintf("#%d", acl.Id), acl.Entity.CanonicalName, mode, created) } func newHgACLUpdateCommand() *cobra.Command { var mode string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() accessMode, err := hgsrht.ParseAccessMode(mode) if err != nil { log.Fatal(err) } if strings.IndexAny(args[0], ownerPrefixes) != 0 { log.Fatal("user must be in canonical form") } name, owner, instance, err := getHgRepoName(ctx, cmd) if err != nil { log.Fatal(err) } c := createClientWithInstance("hg", cmd, instance) id := getHgRepoID(c, ctx, name, owner) acl, err := hgsrht.UpdateACL(c.Client, ctx, id, accessMode, args[0]) if err != nil { log.Fatal(err) } log.Printf("Updated access rights for %s\n", acl.Entity.CanonicalName) } cmd := &cobra.Command{ Use: "update ", Short: "Update/add ACL entries", Args: cobra.ExactArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().StringVarP(&mode, "mode", "m", "", "access mode") cmd.RegisterFlagCompletionFunc("mode", completeRepoAccessMode) cmd.MarkFlagRequired("mode") return cmd } func newHgACLDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("hg", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } acl, err := hgsrht.DeleteACL(c.Client, ctx, id) if err != nil { log.Fatal(err) } else if acl == nil { log.Fatalf("failed to delete ACL entry with ID %d", id) } log.Printf("Deleted ACL entry for %s in repository %s\n", acl.Entity.CanonicalName, acl.Repository.Name) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete an ACL entry", Args: cobra.ExactArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } return cmd } func newHgUserWebhookCommand() *cobra.Command { cmd := &cobra.Command{ Use: "user-webhook", Short: "Manage user webhooks", } cmd.AddCommand(newHgUserWebhookCreateCommand()) cmd.AddCommand(newHgUserWebhookListCommand()) cmd.AddCommand(newHgUserWebhookDeleteCommand()) return cmd } func newHgUserWebhookCreateCommand() *cobra.Command { var events []string var stdin bool var url string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("hg", cmd) var config hgsrht.UserWebhookInput config.Url = url whEvents, err := hgsrht.ParseUserEvents(events) if err != nil { log.Fatal(err) } config.Events = whEvents config.Query = readWebhookQuery(stdin) webhook, err := hgsrht.CreateUserWebhook(c.Client, ctx, config) if err != nil { log.Fatal(err) } log.Printf("Created user webhook with ID %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "create", Short: "Create a user webhook", Args: cobra.ExactArgs(0), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().StringSliceVarP(&events, "events", "e", nil, "webhook events") cmd.RegisterFlagCompletionFunc("events", completeHgUserWebhookEvents) cmd.MarkFlagRequired("events") cmd.Flags().BoolVar(&stdin, "stdin", !isStdinTerminal, "read webhook query from stdin") cmd.Flags().StringVarP(&url, "url", "u", "", "payload url") cmd.RegisterFlagCompletionFunc("url", cobra.NoFileCompletions) cmd.MarkFlagRequired("url") return cmd } func newHgUserWebhookListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("hg", cmd) var cursor *hgsrht.Cursor err := pagerify(func(p pager) error { webhooks, err := hgsrht.UserWebhooks(c.Client, ctx, cursor) if err != nil { return err } for _, webhook := range webhooks.Results { fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url) } cursor = webhooks.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list", Short: "List user webhooks", Args: cobra.ExactArgs(0), Run: run, } return cmd } func newHgUserWebhookDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("hg", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } webhook, err := hgsrht.DeleteUserWebhook(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Deleted webhook %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete a user webhook", Args: cobra.ExactArgs(1), ValidArgsFunction: completeHgUserWebhookID, Run: run, } return cmd } func getHgRepoName(ctx context.Context, cmd *cobra.Command) (repoName, owner, instance string, err error) { repoName, err = cmd.Flags().GetString("repo") if err != nil { return "", "", "", err } else if repoName != "" { repoName, owner, instance = parseResourceName(repoName) return repoName, owner, instance, nil } return guessHgRepoName(ctx) } func guessHgRepoName(ctx context.Context) (repoName, owner, instance string, err error) { remoteURL, err := hgRemoteUrl(ctx) if err != nil { return "", "", "", err } parts := strings.Split(strings.Trim(remoteURL.Path, "/"), "/") if len(parts) != 2 { return "", "", "", fmt.Errorf("failed to parse Hg URL %q: expected 2 path components", remoteURL) } owner, repoName = parts[0], parts[1] // TODO: ignore port in host return repoName, owner, remoteURL.Host, nil } func hgRemoteUrl(ctx context.Context) (*url.URL, error) { out, err := exec.CommandContext(ctx, "hg", "paths", "default").Output() if err != nil { return nil, fmt.Errorf("failed to get remote URL: %v", err) } raw := strings.TrimSpace(string(out)) switch { case strings.Contains(raw, "://"): return url.Parse(raw) case strings.HasPrefix(raw, "/"): return &url.URL{Scheme: "file", Path: raw}, nil default: i := strings.Index(raw, ":") if i < 0 { return nil, fmt.Errorf("invalid scp-like Hg URL %q: missing colon", raw) } host, path := raw[:i], raw[i+1:] // Strip optional user if i := strings.Index(host, "@"); i >= 0 { host = host[i+1:] } return &url.URL{Scheme: "ssh", Host: host, Path: path}, nil } } func getHgRepoID(c *Client, ctx context.Context, name, owner string) int32 { var ( user *hgsrht.User username string err error ) if owner == "" { user, err = hgsrht.RepositoryIDByName(c.Client, ctx, name) } else { username = strings.TrimLeft(owner, ownerPrefixes) user, err = hgsrht.RepositoryIDByUser(c.Client, ctx, username, name) } if err != nil { log.Fatalf("failed to get repository ID: %v", err) } else if user == nil { log.Fatalf("no such user %q", username) } else if user.Repository == nil { log.Fatalf("no such repository %q", name) } return user.Repository.Id } func completeHgUserWebhookEvents(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var eventList []string events := [3]string{"repo_created", "repo_update", "repo_deleted"} set := strings.ToLower(cmd.Flag("events").Value.String()) for _, event := range events { if !strings.Contains(set, event) { eventList = append(eventList, event) } } return eventList, cobra.ShellCompDirectiveNoFileComp } func completeHgUserWebhookID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("hg", cmd) var webhookList []string webhooks, err := hgsrht.UserWebhooks(c.Client, ctx, nil) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, webhook := range webhooks.Results { s := fmt.Sprintf("%d\t%s", webhook.Id, webhook.Url) webhookList = append(webhookList, s) } return webhookList, cobra.ShellCompDirectiveNoFileComp } func completeHgRepo(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("hg", cmd) var repoList []string repos, err := hgsrht.CompleteRepositories(c.Client, ctx) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, repo := range repos.Results { repoList = append(repoList, repo.Name) } return repoList, cobra.ShellCompDirectiveNoFileComp } hut-0.6.0/import.go000066400000000000000000000045101463710650600141660ustar00rootroot00000000000000package main import ( "log" "os" "github.com/spf13/cobra" "git.sr.ht/~xenrox/hut/export" ) func newImportCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { importers := make(map[string]export.Exporter) mc := createClient("meta", cmd) meta := export.NewMetaExporter(mc.Client) importers["meta.sr.ht"] = meta gc := createClient("git", cmd) git := export.NewGitExporter(gc.Client, gc.BaseURL) importers["git.sr.ht"] = git hc := createClient("hg", cmd) hg := export.NewHgExporter(hc.Client, hc.BaseURL) importers["hg.sr.ht"] = hg pc := createClient("paste", cmd) paste := export.NewPasteExporter(pc.Client, pc.HTTP) importers["paste.sr.ht"] = paste lc := createClient("lists", cmd) lists := export.NewListsExporter(lc.Client, lc.HTTP) importers["lists.sr.ht"] = lists tc := createClient("todo", cmd) todo := export.NewTodoExporter(tc.Client, tc.HTTP) importers["todo.sr.ht"] = todo if _, ok := os.LookupEnv("SSH_AUTH_SOCK"); !ok { log.Println("Warning! SSH_AUTH_SOCK is not set in your environment.") log.Println("Using an SSH agent is advised to avoid unlocking your SSH keys repeatedly during the import.") } var resources []export.DirResource for _, dir := range args { l, err := export.FindDirResources(dir) if err != nil { log.Fatalf("Failed to find resources to import in %q: %v", dir, err) } resources = append(resources, l...) } if len(resources) == 0 { log.Fatal("No data found") } ctx := cmd.Context() log.Println("Importing account data...") var lastService string for _, res := range resources { importer, ok := importers[res.Service] if !ok { continue // Some services are exported but never imported } if lastService != res.Service { log.Println(res.Service) lastService = res.Service } log.Printf("\t%s", res.Name) if err := importer.ImportResource(ctx, res.Path); err != nil { log.Printf("Error importing %q: %v", res.Path, err) } } log.Println("Import complete.") } return &cobra.Command{ Use: "import ", Short: "Imports your account data", Args: cobra.MinimumNArgs(1), ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveFilterDirs }, Run: run, } } hut-0.6.0/lists.go000066400000000000000000001103471463710650600140200ustar00rootroot00000000000000package main import ( "bytes" "context" "errors" "fmt" "io" "log" "net/http" "net/mail" "os" "os/exec" "strings" "github.com/dustin/go-humanize" "github.com/spf13/cobra" "git.sr.ht/~xenrox/hut/srht/listssrht" "git.sr.ht/~xenrox/hut/termfmt" ) func newListsCommand() *cobra.Command { cmd := &cobra.Command{ Use: "lists", Short: "Use the lists API", } cmd.AddCommand(newListsDeleteCommand()) cmd.AddCommand(newListsListCommand()) cmd.AddCommand(newListsUpdateCommand()) cmd.AddCommand(newListsSubscribeCommand()) cmd.AddCommand(newListsUnsubscribeCommand()) cmd.AddCommand(newListsCreateCommand()) cmd.AddCommand(newListsArchiveCommand()) cmd.AddCommand(newListsPatchsetCommand()) cmd.AddCommand(newListsACLCommand()) cmd.AddCommand(newListsUserWebhookCommand()) cmd.AddCommand(newListsWebhookCommand()) cmd.AddCommand(newListsSubscriptions()) cmd.PersistentFlags().StringP("mailing-list", "l", "", "mailing list name") cmd.RegisterFlagCompletionFunc("mailing-list", completeList) return cmd } func newListsDeleteCommand() *cobra.Command { var autoConfirm bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseMailingListName(args[0]) } else { var err error name, owner, instance, err = getMailingListName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("lists", cmd, instance) id := getMailingListID(c, ctx, name, owner) if !autoConfirm && !getConfirmation(fmt.Sprintf("Do you really want to delete the list %s", name)) { log.Println("Aborted") return } list, err := listssrht.DeleteMailingList(c.Client, ctx, id) if err != nil { log.Fatal(err) } else if list == nil { log.Fatalf("failed to delete list with ID %d", id) } log.Printf("Deleted mailing list %s\n", list.Name) } cmd := &cobra.Command{ Use: "delete [list]", Short: "Delete a mailing list", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeList, Run: run, } cmd.Flags().BoolVarP(&autoConfirm, "yes", "y", false, "auto confirm") return cmd } func newListsListCommand() *cobra.Command { cmd := &cobra.Command{ Use: "list [username]", Short: "List mailing lists", Args: cobra.MaximumNArgs(1), ValidArgsFunction: cobra.NoFileCompletions, } cmd.Run = func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("lists", cmd) var cursor *listssrht.Cursor var username string if len(args) > 0 { username = strings.TrimLeft(args[0], ownerPrefixes) } err := pagerify(func(p pager) error { var lists *listssrht.MailingListCursor if len(username) > 0 { user, err := listssrht.MailingListsByUser(c.Client, ctx, username, cursor) if err != nil { return err } else if user == nil { return errors.New("no such user") } lists = user.Lists } else { var err error user, err := listssrht.MailingLists(c.Client, ctx, cursor) if err != nil { return err } lists = user.Lists } for _, list := range lists.Results { printList(p, &list) } cursor = lists.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } return cmd } func printList(w io.Writer, list *listssrht.MailingList) { fmt.Fprintf(w, "%s (%s)\n", termfmt.Bold.String(list.Name), list.Visibility.TermString()) if list.Description != nil && *list.Description != "" { fmt.Fprintln(w, "\n"+indent(*list.Description, " ")+"\n") } } func newListsUpdateCommand() *cobra.Command { var visibility string var description bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseMailingListName(args[0]) } else { var err error name, owner, instance, err = getMailingListName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("lists", cmd, instance) id := getMailingListID(c, ctx, name, owner) // TODO: Support permitMime, rejectMime var input listssrht.MailingListInput if visibility != "" { listVisibility, err := listssrht.ParseVisibility(visibility) if err != nil { log.Fatal(err) } input.Visibility = &listVisibility } if description { if !isStdinTerminal { b, err := io.ReadAll(os.Stdin) if err != nil { log.Fatalf("failed to read description: %v", err) } description := string(b) input.Description = &description } else { var ( err error user *listssrht.User username string ) if owner != "" { username = strings.TrimLeft(owner, ownerPrefixes) user, err = listssrht.MailingListDescriptionByUser(c.Client, ctx, username, name) } else { user, err = listssrht.MailingListDescription(c.Client, ctx, name) } if err != nil { log.Fatalf("failed to fetch description: %v", err) } else if user == nil { log.Fatalf("no such user %q", username) } else if user.List == nil { log.Fatalf("no such mailing list %q", name) } var prefill string if user.List.Description != nil { prefill = *user.List.Description } text, err := getInputWithEditor("hut_description*.md", prefill) if err != nil { log.Fatalf("failed to read description: %v", err) } if strings.TrimSpace(text) == "" { _, err := listssrht.ClearDescription(c.Client, ctx, id) if err != nil { log.Fatalf("failed to clear description: %v", err) } } else { input.Description = &text } } } _, err := listssrht.UpdateMailingList(c.Client, ctx, id, input) if err != nil { log.Fatal(err) } log.Printf("Updated mailing list\n") } cmd := &cobra.Command{ Use: "update [list]", Short: "Update a mailing list", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeList, Run: run, } cmd.Flags().StringVarP(&visibility, "visibility", "v", "", "mailing list visibility") cmd.RegisterFlagCompletionFunc("visibility", completeVisibility) cmd.Flags().BoolVar(&description, "description", false, "edit description") return cmd } func newListsSubscribeCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseMailingListName(args[0]) } else { var err error name, owner, instance, err = getMailingListName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("lists", cmd, instance) id := getMailingListID(c, ctx, name, owner) subscription, err := listssrht.MailingListSubscribe(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Subscribed to %s/%s/%s\n", c.BaseURL, subscription.List.Owner.CanonicalName, subscription.List.Name) } cmd := &cobra.Command{ Use: "subscribe [list]", Short: "Subscribe to a mailing list", Args: cobra.MaximumNArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } return cmd } func newListsUnsubscribeCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseMailingListName(args[0]) } else { var err error name, owner, instance, err = getMailingListName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("lists", cmd, instance) id := getMailingListID(c, ctx, name, owner) subscription, err := listssrht.MailingListUnsubscribe(c.Client, ctx, id) if err != nil { log.Fatal(err) } else if subscription == nil { log.Fatalf("you were not subscribed to %s/%s/%s", c.BaseURL, owner, name) } log.Printf("Unsubscribed from %s/%s/%s\n", c.BaseURL, subscription.List.Owner.CanonicalName, subscription.List.Name) } cmd := &cobra.Command{ Use: "unsubscribe [list]", Short: "Unubscribe from a mailing list", Args: cobra.MaximumNArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } return cmd } const listsCreatePrefill = ` ` func newListsCreateCommand() *cobra.Command { var visibility string var stdin bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("lists", cmd) listVisibility, err := listssrht.ParseVisibility(visibility) if err != nil { log.Fatal(err) } var description *string if stdin { b, err := io.ReadAll(os.Stdin) if err != nil { log.Fatalf("failed to read mailing list description: %v", err) } desc := string(b) description = &desc } else { text, err := getInputWithEditor("hut_mailing-list*.md", listsCreatePrefill) if err != nil { log.Fatalf("failed to read mailing list description: %v", err) } text = dropComment(text, listsCreatePrefill) description = &text } list, err := listssrht.CreateMailingList(c.Client, ctx, args[0], description, listVisibility) if err != nil { log.Fatal(err) } else if list == nil { log.Fatal("failed to create mailing list") } log.Printf("Created mailing list %q\n", list.Name) } cmd := &cobra.Command{ Use: "create ", Short: "Create a mailing list", Args: cobra.ExactArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().BoolVar(&stdin, "stdin", !isStdinTerminal, "read mailing list from stdin") cmd.Flags().StringVarP(&visibility, "visibility", "v", "public", "mailing list visibility") cmd.RegisterFlagCompletionFunc("visibility", completeVisibility) return cmd } func newListsArchiveCommand() *cobra.Command { var days int run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseMailingListName(args[0]) } else { var err error name, owner, instance, err = getMailingListName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("lists", cmd, instance) var ( user *listssrht.User username string err error ) if owner == "" { user, err = listssrht.Archive(c.Client, ctx, name) } else { username = strings.TrimLeft(owner, ownerPrefixes) user, err = listssrht.ArchiveByUser(c.Client, ctx, username, name) } if err != nil { log.Fatal(err) } else if user == nil { log.Fatalf("no such user %q", username) } else if user.List == nil { if owner == "" { log.Fatalf("no such mailing list %s", name) } log.Fatalf("no such mailing list %s/%s/%s", c.BaseURL, owner, name) } url := string(user.List.Archive) if days != 0 { url = fmt.Sprintf("%s?since=%d", url, days) } c.HTTP.Timeout = fileTransferTimeout req, err := http.NewRequestWithContext(ctx, http.MethodGet, string(url), nil) if err != nil { log.Fatalf("Failed to create request to fetch archive: %v", err) } resp, err := c.HTTP.Do(req) if err != nil { log.Fatalf("Failed to fetch archive: %v", err) } defer resp.Body.Close() if _, err := io.Copy(os.Stdout, resp.Body); err != nil { log.Fatalf("Failed to copy to stdout: %v", err) } } cmd := &cobra.Command{ Use: "archive [list]", Short: "Download a mailing list archive", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeList, Run: run, } cmd.Flags().IntVarP(&days, "days", "d", 0, "number of last days to download") cmd.RegisterFlagCompletionFunc("days", cobra.NoFileCompletions) return cmd } func newListsPatchsetCommand() *cobra.Command { cmd := &cobra.Command{ Use: "patchset", Short: "Manage patchsets", } cmd.AddCommand(newListsPatchsetListCommand()) cmd.AddCommand(newListsPatchsetUpdateCommand()) cmd.AddCommand(newListsPatchsetApplyCommand()) cmd.AddCommand(newListsPatchsetShowCommand()) return cmd } func newListsPatchsetListCommand() *cobra.Command { var byUser bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseMailingListName(args[0]) } else { var err error name, owner, instance, err = getMailingListName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("lists", cmd, instance) var ( cursor *listssrht.Cursor patches *listssrht.PatchsetCursor user *listssrht.User username string err error ) if byUser { if len(args) > 0 { username = strings.TrimLeft(name, ownerPrefixes) } } else { if owner != "" { username = strings.TrimLeft(owner, ownerPrefixes) } } err = pagerify(func(p pager) error { if byUser { if username != "" { user, err = listssrht.PatchesByUser(c.Client, ctx, name, cursor) } else { user, err = listssrht.Patches(c.Client, ctx, cursor) } if err != nil { return err } else if user == nil { return fmt.Errorf("no such user %q", name) } patches = user.Patches } else { if username != "" { user, err = listssrht.ListPatchesByUser(c.Client, ctx, username, name, cursor) } else { user, err = listssrht.ListPatches(c.Client, ctx, name, cursor) } if err != nil { return err } else if user == nil { return fmt.Errorf("no such user %q", username) } else if user.List == nil { return fmt.Errorf("no such list %q", name) } patches = user.List.Patches } for _, patchset := range patches.Results { printPatchset(p, byUser, &patchset) } cursor = patches.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list [list]", Short: "List patchsets", Args: cobra.MaximumNArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().BoolVarP(&byUser, "user", "u", false, "list patches by user") return cmd } func printPatchset(w io.Writer, byUser bool, patchset *listssrht.Patchset) { s := fmt.Sprintf("%s\t%s\t", termfmt.DarkYellow.Sprintf("#%d", patchset.Id), patchset.Status.TermString()) if patchset.Prefix != nil && *patchset.Prefix != "" { s += fmt.Sprintf("[%s] ", *patchset.Prefix) } s += patchset.Subject if patchset.Version != 1 { s += fmt.Sprintf(" v%d", patchset.Version) } created := termfmt.Dim.String(humanize.Time(patchset.Created.Time)) if byUser { s += fmt.Sprintf("\t%s/%s\t%s", patchset.List.Owner.CanonicalName, patchset.List.Name, created) } else { s += fmt.Sprintf("\t%s\t%s", patchset.Submitter.CanonicalName, created) } fmt.Fprintln(w, s) } func newListsPatchsetUpdateCommand() *cobra.Command { var status string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() patchStatus, err := listssrht.ParsePatchsetStatus(status) if err != nil { log.Fatal(err) } id, instance, err := parsePatchID(ctx, cmd, args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("lists", cmd, instance) patch, err := listssrht.UpdatePatchset(c.Client, ctx, id, patchStatus) if err != nil { log.Fatal(err) } else if patch == nil { log.Fatalf("failed to update patchset with ID %d", id) } log.Printf("Updated patchset %q by %s\n", patch.Subject, patch.Submitter.CanonicalName) } cmd := &cobra.Command{ Use: "update ", Short: "Update a patchset", Args: cobra.ExactArgs(1), ValidArgsFunction: completePatchsetID, Run: run, } cmd.Flags().StringVarP(&status, "status", "s", "", "patchset status") cmd.RegisterFlagCompletionFunc("status", completePatchsetStatus) cmd.MarkFlagRequired("status") return cmd } func newListsPatchsetShowCommand() *cobra.Command { cmd := cobra.Command{ Use: "show ", Short: "Show a patchset", Args: cobra.ExactArgs(1), ValidArgsFunction: completePatchsetID, } cmd.Run = func(cmd *cobra.Command, args []string) { ctx := cmd.Context() id, instance, err := parsePatchID(ctx, cmd, args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("lists", cmd, instance) var cursor *listssrht.Cursor for { patchset, err := listssrht.PatchsetById(c.Client, ctx, id, cursor) if err != nil { log.Fatal(err) } else if patchset == nil { log.Fatalf("no such patchset %d", id) } for _, patch := range patchset.Patches.Results { formatPatch(os.Stdout, &patch) } cursor = patchset.Patches.Cursor if cursor == nil { break } } } return &cmd } func newListsPatchsetApplyCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() id, instance, err := parsePatchID(ctx, cmd, args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("lists", cmd, instance) var cursor *listssrht.Cursor var mbox bytes.Buffer for { patchset, err := listssrht.PatchsetById(c.Client, ctx, id, cursor) if err != nil { log.Fatal(err) } else if patchset == nil { log.Fatalf("no such patchset %d", id) } for _, patch := range patchset.Patches.Results { formatPatch(&mbox, &patch) } cursor = patchset.Patches.Cursor if cursor == nil { break } } applyCmd := exec.Command("git", "am", "-3") applyCmd.Stdin = &mbox applyCmd.Stdout = os.Stdout applyCmd.Stderr = os.Stderr if err := applyCmd.Run(); err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "apply ", Short: "Apply a patchset", Args: cobra.ExactArgs(1), ValidArgsFunction: completePatchsetID, Run: run, } return cmd } func formatPatch(w io.Writer, email *listssrht.Email) { fmt.Fprintf(w, "From nobody %s\n", email.Date.Format(dateLayout)) fmt.Fprintf(w, "From: %s\n", email.Header[0]) fmt.Fprintf(w, "Subject: %s\n", email.Subject) fmt.Fprintf(w, "Date: %s\n\n", email.Date.Format(dateLayout)) io.WriteString(w, strings.ReplaceAll(email.Body, "\r\n", "\n")) } func newListsACLCommand() *cobra.Command { cmd := &cobra.Command{ Use: "acl", Short: "Manage access-control lists", } cmd.AddCommand(newListsACLListCommand()) cmd.AddCommand(newListsACLDeleteCommand()) return cmd } func newListsACLListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseMailingListName(args[0]) } else { var err error name, owner, instance, err = getMailingListName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("lists", cmd, instance) var ( cursor *listssrht.Cursor user *listssrht.User username string err error ) if owner != "" { username = strings.TrimLeft(owner, ownerPrefixes) } err = pagerify(func(p pager) error { if username != "" { user, err = listssrht.AclByUser(c.Client, ctx, username, name, cursor) } else { user, err = listssrht.AclByListName(c.Client, ctx, name, cursor) } if err != nil { return err } else if user == nil { return fmt.Errorf("no such user %q", username) } else if user.List == nil { return fmt.Errorf("no such list %q", name) } if cursor == nil { // only print once fmt.Fprintln(p, termfmt.Bold.Sprint("Default permissions")) fmt.Fprintln(p, user.List.DefaultACL.TermString()) if len(user.List.Acl.Results) > 0 { fmt.Fprintln(p, termfmt.Bold.Sprint("\nUser permissions")) } } for _, acl := range user.List.Acl.Results { printListsACLEntry(p, &acl) } cursor = user.List.Acl.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list", Short: "List ACL entries", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeList, Run: run, } return cmd } func printListsACLEntry(w io.Writer, acl *listssrht.MailingListACL) { s := fmt.Sprintf("%s browse %s reply %s post %s moderate", listssrht.PermissionIcon(acl.Browse), listssrht.PermissionIcon(acl.Reply), listssrht.PermissionIcon(acl.Post), listssrht.PermissionIcon(acl.Moderate)) created := termfmt.Dim.String(humanize.Time(acl.Created.Time)) fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", termfmt.DarkYellow.Sprintf("#%d", acl.Id), acl.Entity.CanonicalName, s, created) } func newListsACLDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("lists", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } acl, err := listssrht.DeleteACL(c.Client, ctx, id) if err != nil { log.Fatal(err) } else if acl == nil { log.Fatalf("failed to delete ACL entry with ID %d", id) } log.Printf("Deleted ACL entry for %q in mailing list %q\n", acl.Entity.CanonicalName, acl.List.Name) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete an ACL entry", Args: cobra.ExactArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } return cmd } func newListsUserWebhookCommand() *cobra.Command { cmd := &cobra.Command{ Use: "user-webhook", Short: "Manage user webhooks", } cmd.AddCommand(newListsUserWebhookCreateCommand()) cmd.AddCommand(newListsUserWebhookListCommand()) cmd.AddCommand(newListsUserWebhookDeleteCommand()) return cmd } func newListsUserWebhookCreateCommand() *cobra.Command { var events []string var stdin bool var url string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("lists", cmd) var config listssrht.UserWebhookInput config.Url = url whEvents, err := listssrht.ParseUserEvents(events) if err != nil { log.Fatal(err) } config.Events = whEvents config.Query = readWebhookQuery(stdin) webhook, err := listssrht.CreateUserWebhook(c.Client, ctx, config) if err != nil { log.Fatal(err) } log.Printf("Created user webhook with ID %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "create", Short: "Create a user webhook", Args: cobra.ExactArgs(0), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().StringSliceVarP(&events, "events", "e", nil, "webhook events") cmd.RegisterFlagCompletionFunc("events", completeListsUserWebhookEvents) cmd.MarkFlagRequired("events") cmd.Flags().BoolVar(&stdin, "stdin", !isStdinTerminal, "read webhook query from stdin") cmd.Flags().StringVarP(&url, "url", "u", "", "payload url") cmd.RegisterFlagCompletionFunc("url", cobra.NoFileCompletions) cmd.MarkFlagRequired("url") return cmd } func newListsUserWebhookListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("lists", cmd) var cursor *listssrht.Cursor err := pagerify(func(p pager) error { webhooks, err := listssrht.UserWebhooks(c.Client, ctx, cursor) if err != nil { return err } for _, webhook := range webhooks.Results { fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url) } cursor = webhooks.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list", Short: "List user webhooks", Args: cobra.ExactArgs(0), Run: run, } return cmd } func newListsUserWebhookDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("lists", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } webhook, err := listssrht.DeleteUserWebhook(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Deleted webhook %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete a user webhook", Args: cobra.ExactArgs(1), ValidArgsFunction: completeListsUserWebhookID, Run: run, } return cmd } func newListsWebhookCommand() *cobra.Command { cmd := &cobra.Command{ Use: "webhook", Short: "Manage mailing list webhooks", } cmd.AddCommand(newListsWebhookCreateCommand()) cmd.AddCommand(newListsWebhookListCommand()) cmd.AddCommand(newListsWebhookDeleteCommand()) return cmd } func newListsWebhookCreateCommand() *cobra.Command { var events []string var stdin bool var url string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseMailingListName(args[0]) } else { var err error name, owner, instance, err = getMailingListName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("lists", cmd, instance) id := getMailingListID(c, ctx, name, owner) var config listssrht.MailingListWebhookInput config.Url = url whEvents, err := listssrht.ParseMailingListWebhookEvents(events) if err != nil { log.Fatal(err) } config.Events = whEvents config.Query = readWebhookQuery(stdin) webhook, err := listssrht.CreateMailingListWebhook(c.Client, ctx, id, config) if err != nil { log.Fatal(err) } log.Printf("Created mailing list webhook with ID %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "create [list]", Short: "Create a mailing list webhook", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeList, Run: run, } cmd.Flags().StringSliceVarP(&events, "events", "e", nil, "webhook events") cmd.RegisterFlagCompletionFunc("events", completeMailingListWebhookEvents) cmd.MarkFlagRequired("events") cmd.Flags().BoolVar(&stdin, "stdin", !isStdinTerminal, "read webhook query from stdin") cmd.Flags().StringVarP(&url, "url", "u", "", "payload url") cmd.RegisterFlagCompletionFunc("url", cobra.NoFileCompletions) cmd.MarkFlagRequired("url") return cmd } func newListsWebhookListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseMailingListName(args[0]) } else { var err error name, owner, instance, err = getMailingListName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("lists", cmd, instance) var ( cursor *listssrht.Cursor user *listssrht.User username string err error ) if owner != "" { username = strings.TrimLeft(owner, ownerPrefixes) } err = pagerify(func(p pager) error { if username != "" { user, err = listssrht.MailingListWebhooksByUser(c.Client, ctx, username, name, cursor) } else { user, err = listssrht.MailingListWebhooks(c.Client, ctx, name, cursor) } if err != nil { return err } else if user == nil { return fmt.Errorf("no such user %q", username) } else if user.List == nil { return fmt.Errorf("no such mailing list %q", name) } for _, webhook := range user.List.Webhooks.Results { fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url) } cursor = user.List.Webhooks.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list [list]", Short: "List mailing list webhooks", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeList, Run: run, } return cmd } func newListsWebhookDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("lists", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } webhook, err := listssrht.DeleteMailingListWebhook(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Deleted webhook %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete a mailing list webhook", Args: cobra.ExactArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } return cmd } func newListsSubscriptions() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("lists", cmd) var cursor *listssrht.Cursor err := pagerify(func(p pager) error { subscriptions, err := listssrht.Subscriptions(c.Client, ctx, cursor) if err != nil { return err } for _, sub := range subscriptions.Results { printMailingListSubscription(p, &sub) } cursor = subscriptions.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "subscriptions", Short: "List mailing list subscriptions", Args: cobra.ExactArgs(0), Run: run, } return cmd } func printMailingListSubscription(w io.Writer, sub *listssrht.ActivitySubscription) { mlSub, ok := sub.Value.(*listssrht.MailingListSubscription) if !ok { return } created := termfmt.Dim.String(humanize.Time(sub.Created.Time)) fmt.Fprintf(w, "%s/%s %s\n", mlSub.List.Owner.CanonicalName, mlSub.List.Name, created) } // parseMailingListName parses a mailing list name, following either the // generic resource name syntax, or "/@". func parseMailingListName(s string) (name, owner, instance string) { slash := strings.Index(s, "/") at := strings.Index(s, "@") if strings.Count(s, "/") != 1 || strings.Count(s, "@") != 1 || at < slash { return parseResourceName(s) } owner = s[:slash] name = s[slash+1 : at] instance = s[at+1:] return name, owner, instance } func getMailingListID(c *Client, ctx context.Context, name, owner string) int32 { var ( user *listssrht.User username string err error ) if owner == "" { user, err = listssrht.MailingListIDByName(c.Client, ctx, name) } else { username = strings.TrimLeft(owner, ownerPrefixes) user, err = listssrht.MailingListIDByUser(c.Client, ctx, username, name) } if err != nil { log.Fatal(err) } else if user == nil { log.Fatalf("no such user %q", username) } else if user.List == nil { if owner == "" { log.Fatalf("no such mailing list %s", name) } log.Fatalf("no such mailing list %s/%s/%s", c.BaseURL, owner, name) } return user.List.Id } func getMailingListName(ctx context.Context, cmd *cobra.Command) (name, owner, instance string, err error) { s, err := cmd.Flags().GetString("mailing-list") if err != nil { return "", "", "", err } else if s != "" { name, owner, instance = parseMailingListName(s) return name, owner, instance, nil } cfg, err := loadProjectConfig() if err != nil { return "", "", "", err } if cfg != nil && cfg.DevList != "" { name, owner, instance = parseMailingListName(cfg.DevList) return name, owner, instance, nil } name, owner, instance, err = guessMailingListName(ctx) if err != nil { return "", "", "", err } return name, owner, instance, nil } func guessMailingListName(ctx context.Context) (name, owner, instance string, err error) { addr, err := getGitSendEmailTo(ctx) if err != nil { return "", "", "", err } else if addr == nil { return "", "", "", errors.New("no mailing list specified and no mailing list configured for current Git repository") } name, owner, instance = parseMailingListName(addr.Address) return name, owner, instance, nil } func getGitSendEmailTo(ctx context.Context) (*mail.Address, error) { out, err := exec.CommandContext(ctx, "git", "config", "--default=", "sendemail.to").Output() if err != nil { return nil, fmt.Errorf("failed to get git sendemail.to config: %v", err) } out = bytes.TrimSpace(out) if len(out) == 0 { return nil, nil } addr, err := mail.ParseAddress(string(out)) if err != nil { return nil, fmt.Errorf("failed to parse git sendemail.to: %v", err) } return addr, nil } func parsePatchID(ctx context.Context, cmd *cobra.Command, s string) (id int32, instance string, err error) { if strings.Contains(s, "/") { s, _, instance = parseResourceName(s) split := strings.Split(s, "/") s = split[len(split)-1] id, err = parseInt32(s) if err != nil { return 0, "", fmt.Errorf("invalid patchset ID: %v", err) } } else { id, err = parseInt32(s) if err != nil { return 0, "", err } _, _, instance, err = getMailingListName(ctx, cmd) if err != nil { return 0, "", err } } return id, instance, nil } var completePatchsetStatus = cobra.FixedCompletions([]string{ "unknown", "proposed", "needs_revision", "superseded", "approved", "rejected", "applied", }, cobra.ShellCompDirectiveNoFileComp) func completePatchsetID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() var patchList []string name, owner, instance, err := getMailingListName(ctx, cmd) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } c := createClientWithInstance("lists", cmd, instance) var user *listssrht.User if owner != "" { username := strings.TrimLeft(owner, ownerPrefixes) user, err = listssrht.CompletePatchsetIdByUser(c.Client, ctx, username, name) } else { user, err = listssrht.CompletePatchsetId(c.Client, ctx, name) } if err != nil || user == nil || user.List == nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, patchset := range user.List.Patches.Results { // TODO: filter with API if cmd.Name() == "apply" && !patchsetApplicable(patchset.Status) { continue } if cmd.Name() == "update" && strings.EqualFold(cmd.Flag("status").Value.String(), string(patchset.Status)) { continue } s := fmt.Sprintf("%d\t", patchset.Id) if patchset.Prefix != nil && *patchset.Prefix != "" { s += fmt.Sprintf("[%s] ", *patchset.Prefix) } s += patchset.Subject if patchset.Version != 1 { s += fmt.Sprintf(" v%d", patchset.Version) } patchList = append(patchList, s) } return patchList, cobra.ShellCompDirectiveNoFileComp } func patchsetApplicable(status listssrht.PatchsetStatus) bool { switch status { case listssrht.PatchsetStatusApplied, listssrht.PatchsetStatusNeedsRevision, listssrht.PatchsetStatusSuperseded, listssrht.PatchsetStatusRejected: return false default: return true } } func completeListsUserWebhookEvents(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var eventList []string events := [5]string{"list_created", "list_updated", "list_deleted", "email_received", "patchset_received"} set := strings.ToLower(cmd.Flag("events").Value.String()) for _, event := range events { if !strings.Contains(set, event) { eventList = append(eventList, event) } } return eventList, cobra.ShellCompDirectiveNoFileComp } func completeListsUserWebhookID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("lists", cmd) var webhookList []string webhooks, err := listssrht.UserWebhooks(c.Client, ctx, nil) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, webhook := range webhooks.Results { s := fmt.Sprintf("%d\t%s", webhook.Id, webhook.Url) webhookList = append(webhookList, s) } return webhookList, cobra.ShellCompDirectiveNoFileComp } func completeList(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("lists", cmd) var listsList []string user, err := listssrht.CompleteLists(c.Client, ctx) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, list := range user.Lists.Results { listsList = append(listsList, list.Name) } return listsList, cobra.ShellCompDirectiveNoFileComp } func completeMailingListWebhookEvents(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var eventList []string events := [4]string{"list_updated", "list_deleted", "email_received", "patchset_received"} set := strings.ToLower(cmd.Flag("events").Value.String()) for _, event := range events { if !strings.Contains(set, event) { eventList = append(eventList, event) } } return eventList, cobra.ShellCompDirectiveNoFileComp } hut-0.6.0/main.go000066400000000000000000000131541463710650600136040ustar00rootroot00000000000000package main import ( "bufio" "context" "errors" "fmt" "io" "log" "os" "os/exec" "strconv" "strings" "time" "unicode" "git.sr.ht/~xenrox/hut/termfmt" "github.com/google/shlex" "github.com/spf13/cobra" "golang.org/x/term" ) // ownerPrefixes is the set of characters used to prefix sr.ht owners. "~" is // used to indicate users. const ownerPrefixes = "~" const dateLayout = "Mon, 02 Jan 2006 15:04:05 -0700" const fileTransferTimeout = 10 * time.Minute // use these in the main program to decide on how to process input or output. // Use the less explicit termfmt.IsTerminal() only when the decision is about // how to print something. var isStdinTerminal = term.IsTerminal(int(os.Stdin.Fd())) var isStdoutTerminal = term.IsTerminal(int(os.Stdout.Fd())) func main() { termfmt.InitIsTerminal(isStdoutTerminal) log.SetFlags(0) // disable date/time prefix ctx := context.Background() cmd := &cobra.Command{ Use: "hut", Short: "hut is a CLI tool for sr.ht", CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true}, } cmd.PersistentFlags().String("instance", "", "sr.ht instance to use") cmd.RegisterFlagCompletionFunc("instance", cobra.NoFileCompletions) cmd.PersistentFlags().String("config", "", "config file to use") cmd.PersistentFlags().Bool("debug", false, "display GraphQL request") cmd.AddCommand(newBuildsCommand()) cmd.AddCommand(newExportCommand()) cmd.AddCommand(newGitCommand()) cmd.AddCommand(newGraphqlCommand()) cmd.AddCommand(newHgCommand()) cmd.AddCommand(newImportCommand()) cmd.AddCommand(newInitCommand()) cmd.AddCommand(newListsCommand()) cmd.AddCommand(newMetaCommand()) cmd.AddCommand(newPagesCommand()) cmd.AddCommand(newPasteCommand()) cmd.AddCommand(newTodoCommand()) if err := cmd.ExecuteContext(ctx); err != nil { os.Exit(1) } } var completeVisibility = cobra.FixedCompletions([]string{"public", "unlisted", "private"}, cobra.ShellCompDirectiveNoFileComp) var completeBoolean = cobra.FixedCompletions([]string{"true", "false"}, cobra.ShellCompDirectiveNoFileComp) var completeRepoAccessMode = cobra.FixedCompletions([]string{"RO", "RW"}, cobra.ShellCompDirectiveNoFileComp) func getConfirmation(msg string) bool { reader := bufio.NewReader(os.Stdin) for { fmt.Printf("%s [y/n]: ", msg) input, err := reader.ReadString('\n') if err != nil { log.Fatal(err) } switch strings.ToLower(strings.TrimSpace(input)) { case "yes", "y": return true case "no", "n": return false default: fmt.Println(`Expected "yes" or "no"`) } } } func parseOwnerName(name string) (owner, instance string) { name = stripProtocol(name) parsed := strings.Split(name, "/") switch len(parsed) { case 1: owner = name case 2: instance = parsed[0] owner = parsed[1] if strings.IndexAny(owner, ownerPrefixes) != 0 { log.Fatalf("Invalid owner name %q: must start with %q", owner, ownerPrefixes) } default: log.Fatalf("Invalid owner name %q", name) } return owner, instance } func parseResourceName(name string) (resource, owner, instance string) { name = stripProtocol(name) parsed := strings.Split(name, "/") if len(parsed) == 1 { return strings.TrimLeft(parsed[0], "#"), owner, instance } if len(parsed) > 2 && strings.IndexAny(parsed[1], ownerPrefixes) == 0 { instance = parsed[0] owner = parsed[1] resource = strings.Join(parsed[2:], "/") } else if strings.IndexAny(parsed[0], ownerPrefixes) == 0 { owner = parsed[0] resource = strings.Join(parsed[1:], "/") } else { resource = strings.Join(parsed, "/") } return resource, owner, instance } func parseInt32(s string) (int32, error) { i, err := strconv.ParseInt(s, 10, 32) return int32(i), err } func getInputWithEditor(pattern, initialText string) (string, error) { editor := os.Getenv("EDITOR") if editor == "" { return "", errors.New("EDITOR not set") } commandSplit, err := shlex.Split(editor) if err != nil { return "", err } file, err := os.CreateTemp("", pattern) if err != nil { return "", err } defer os.Remove(file.Name()) if initialText != "" { _, err = file.WriteString(initialText) if err != nil { return "", err } } err = file.Close() if err != nil { return "", err } commandSplit = append(commandSplit, file.Name()) cmd := exec.Command(commandSplit[0], commandSplit[1:]...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Run() if err != nil { return "", err } content, err := os.ReadFile(file.Name()) if err != nil { return "", err } return string(content), nil } func dropComment(text, comment string) string { // Drop our prefilled comment, but without stripping leading // whitespace text = strings.TrimRightFunc(text, unicode.IsSpace) text = strings.TrimSuffix(text, comment) text = strings.TrimRightFunc(text, unicode.IsSpace) return text } func stripProtocol(s string) string { i := strings.Index(s, "://") if i != -1 { s = s[i+3:] } return s } func hasCmdArg(cmd *cobra.Command, arg string) bool { for _, v := range cmd.Flags().Args() { if v == arg { return true } } return false } func readWebhookQuery(stdin bool) string { var query string if stdin { b, err := io.ReadAll(os.Stdin) if err != nil { log.Fatalf("failed to read webhook query: %v", err) } query = string(b) } else { var err error query, err = getInputWithEditor("hut_query*.graphql", "") if err != nil { log.Fatalf("failed to read webhook query: %v", err) } } if query == "" { log.Println("Aborting due to empty query.") os.Exit(1) } return query } func sliceContains(s []string, v string) bool { for i := range s { if v == s[i] { return true } } return false } hut-0.6.0/meta.go000066400000000000000000000465111463710650600136110ustar00rootroot00000000000000package main import ( "errors" "fmt" "io" "log" "os" "os/exec" "path/filepath" "strings" "github.com/dustin/go-humanize" "github.com/juju/ansiterm/tabwriter" "github.com/spf13/cobra" "git.sr.ht/~xenrox/hut/srht/metasrht" "git.sr.ht/~xenrox/hut/termfmt" ) func newMetaCommand() *cobra.Command { cmd := &cobra.Command{ Use: "meta", Short: "Use the meta API", } cmd.AddCommand(newMetaShowCommand()) cmd.AddCommand(newMetaAuditLogCommand()) cmd.AddCommand(newMetaUpdateCommand()) cmd.AddCommand(newMetaSSHKeyCommand()) cmd.AddCommand(newMetaPGPKeyCommand()) cmd.AddCommand(newMetaUserWebhookCommand()) cmd.AddCommand(newMetaOAuthCommand()) return cmd } func newMetaShowCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var ( user *metasrht.User err error ) if len(args) > 0 { owner, instance := parseOwnerName(args[0]) c := createClientWithInstance("meta", cmd, instance) username := strings.TrimLeft(owner, ownerPrefixes) user, err = metasrht.FetchUser(c.Client, ctx, username) } else { c := createClient("meta", cmd) user, err = metasrht.FetchMe(c.Client, ctx) } if err != nil { log.Fatal(err) } else if user == nil { log.Fatal("no such user") } fmt.Printf("%v <%v>\n", termfmt.Bold.String(user.CanonicalName), user.Email) if user.Url != nil { fmt.Println(*user.Url) } if user.Location != nil { fmt.Println(*user.Location) } if user.Bio != nil { fmt.Printf("\n%v\n", *user.Bio) } } cmd := &cobra.Command{ Use: "show [user]", Short: "Show a user profile", Args: cobra.MaximumNArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } return cmd } func newMetaAuditLogCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("meta", cmd) var cursor *metasrht.Cursor err := pagerify(func(p pager) error { logs, err := metasrht.AuditLog(c.Client, ctx, cursor) if err != nil { return err } for _, log := range logs.Results { printAuditLog(p, &log) } cursor = logs.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "audit-log", Short: "Display your audit log", Args: cobra.ExactArgs(0), Run: run, } return cmd } func printAuditLog(w io.Writer, log *metasrht.AuditLogEntry) { s := log.IpAddress if log.Details != nil { s += fmt.Sprintf("\t%s\t", *log.Details) } else { s += fmt.Sprintf("\t%s\t", log.EventType) } s += termfmt.Dim.String(humanize.Time(log.Created.Time)) fmt.Fprintln(w, s) } func newMetaUpdateCommand() *cobra.Command { var email, location, url string var bio bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("meta", cmd) var input metasrht.UserInput if bio { if !isStdinTerminal { b, err := io.ReadAll(os.Stdin) if err != nil { log.Fatalf("failed to read bio: %v", err) } biography := string(b) input.Bio = &biography } else { me, err := metasrht.Bio(c.Client, ctx) if err != nil { log.Fatalf("failed to fetch bio: %v", err) } var prefill string if me.Bio != nil { prefill = *me.Bio } text, err := getInputWithEditor("hut_bio*.md", prefill) if err != nil { log.Fatalf("failed to read bio: %v", err) } if strings.TrimSpace(text) == "" { _, err := metasrht.ClearBio(c.Client, ctx) if err != nil { log.Fatalf("failed to clear bio: %v", err) } } else { input.Bio = &text } } } if cmd.Flags().Changed("email") { input.Email = &email } if cmd.Flags().Changed("location") { if location == "" { _, err := metasrht.ClearUserLocation(c.Client, ctx) if err != nil { log.Fatalf("failed to clear location: %v", err) } } else { input.Location = &location } } if cmd.Flags().Changed("url") { if url == "" { _, err := metasrht.ClearUserURL(c.Client, ctx) if err != nil { log.Fatalf("failed to clear URL: %v", err) } } else { input.Url = &url } } _, err := metasrht.UpdateUser(c.Client, ctx, &input) if err != nil { log.Fatal(err) } log.Println("Successfully updated account") if cmd.Flags().Changed("email") { log.Printf("An email has been sent to %q to confirm the change\n", email) } } cmd := &cobra.Command{ Use: "update", Short: "Update account", Args: cobra.ExactArgs(0), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().BoolVar(&bio, "bio", false, "edit biography") cmd.Flags().StringVar(&email, "email", "", "email") cmd.RegisterFlagCompletionFunc("email", cobra.NoFileCompletions) cmd.Flags().StringVar(&location, "location", "", "location") cmd.RegisterFlagCompletionFunc("location", cobra.NoFileCompletions) cmd.Flags().StringVar(&url, "url", "", "URL") cmd.RegisterFlagCompletionFunc("url", cobra.NoFileCompletions) return cmd } func newMetaSSHKeyCommand() *cobra.Command { cmd := &cobra.Command{ Use: "ssh-key", Short: "Manage SSH keys", } cmd.AddCommand(newMetaSSHKeyCreateCommand()) cmd.AddCommand(newMetaSSHKeyDeleteCommand()) cmd.AddCommand(newMetaSSHKeyListCommand()) return cmd } func newMetaSSHKeyCreateCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("meta", cmd) var filename string if len(args) > 0 { filename = args[0] } else { var err error filename, err = guessSSHPubKeyFilename() if err != nil { log.Fatalf("failed to find default SSH public key: %v", err) } } b, err := os.ReadFile(filename) if err != nil { log.Fatal(err) } // Sanity check, mostly to avoid uploading private keys if !strings.HasPrefix(string(b), "ssh-") { log.Fatalf("%q doesn't look like an SSH public key file", filename) } key, err := metasrht.CreateSSHKey(c.Client, ctx, string(b)) if err != nil { log.Fatal(err) } log.Printf("Uploaded SSH public key %v", key.Fingerprint) if key.Comment != nil { log.Printf(" (%v)", *key.Comment) } log.Println() } cmd := &cobra.Command{ Use: "create [path]", Short: "Create a new SSH key", Args: cobra.MaximumNArgs(1), Run: run, } return cmd } func guessSSHPubKeyFilename() (string, error) { homeDir, err := os.UserHomeDir() if err != nil { return "", err } var match string for _, name := range []string{"id_ed25519", "id_rsa"} { filename := filepath.Join(homeDir, ".ssh", name+".pub") if _, err := os.Stat(filename); err == nil { if match != "" { return "", fmt.Errorf("multiple SSH public keys found") } match = filename } else if !errors.Is(err, os.ErrNotExist) { return "", err } } if match == "" { return "", fmt.Errorf("no SSH public key found") } return match, nil } func newMetaSSHKeyDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("meta", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } key, err := metasrht.DeleteSSHKey(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Deleted SSH key %s\n", key.Fingerprint) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete an SSH key", Args: cobra.ExactArgs(1), ValidArgsFunction: completeSSHKeys, Run: run, } return cmd } func newMetaSSHKeyListCommand() *cobra.Command { var raw bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("meta", cmd) var ( cursor *metasrht.Cursor user *metasrht.User username string err error ) if len(args) > 0 { username = strings.TrimLeft(args[0], ownerPrefixes) } err = pagerify(func(p pager) error { if username != "" { if raw { user, err = metasrht.ListRawSSHKeysByUser(c.Client, ctx, username, cursor) } else { user, err = metasrht.ListSSHKeysByUser(c.Client, ctx, username, cursor) } } else { if raw { user, err = metasrht.ListRawSSHKeys(c.Client, ctx, cursor) } else { user, err = metasrht.ListSSHKeys(c.Client, ctx, cursor) } } if err != nil { return err } else if user == nil { return fmt.Errorf("no such user %q", username) } if raw { for _, key := range user.SshKeys.Results { fmt.Fprintln(p, key.Key) } } else { for _, key := range user.SshKeys.Results { fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", key.Id), key.Fingerprint) if key.Comment != nil { fmt.Fprintf(p, " %s\n", *key.Comment) } fmt.Fprintln(p) } } cursor = user.SshKeys.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list [user]", Short: "List SSH keys", Args: cobra.MaximumNArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().BoolVarP(&raw, "raw", "r", false, "print raw public keys") return cmd } func newMetaPGPKeyCommand() *cobra.Command { cmd := &cobra.Command{ Use: "pgp-key", Short: "Manage PGP keys", } cmd.AddCommand(newMetaPGPKeyCreateCommand()) cmd.AddCommand(newMetaPGPKeyDeleteCommand()) cmd.AddCommand(newMetaPGPKeyListCommand()) return cmd } func newMetaPGPKeyCreateCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("meta", cmd) var ( keyBytes []byte err error ) if len(args) > 0 { keyBytes, err = os.ReadFile(args[0]) } else { keyBytes, err = exportDefaultPGPKey() } if err != nil { log.Fatal(err) } // Sanity check, mostly to avoid uploading private keys if !strings.HasPrefix(string(keyBytes), "-----BEGIN PGP PUBLIC KEY BLOCK-----") { log.Fatalf("input doesn't look like a PGP public key file") } key, err := metasrht.CreatePGPKey(c.Client, ctx, string(keyBytes)) if err != nil { log.Fatal(err) } log.Printf("Uploaded PGP public key %v\n", key.Fingerprint) } return &cobra.Command{ Use: "create [path]", Short: "Create a new PGP key", Args: cobra.MaximumNArgs(1), Run: run, } } func exportDefaultPGPKey() ([]byte, error) { out, err := exec.Command("gpg", "--list-secret-keys", "--with-colons").Output() if err != nil { return nil, fmt.Errorf("failed to list keys in GPG keyring: %v", err) } var keyID string for _, l := range strings.Split(string(out), "\n") { if !strings.HasPrefix(l, "sec:") { continue } if keyID != "" { return nil, fmt.Errorf("multiple keys found in GPG keyring") } fields := strings.Split(l, ":") if len(fields) <= 4 { continue } keyID = fields[4] } if keyID == "" { return nil, fmt.Errorf("no key found in GPG keyring") } out, err = exec.Command("gpg", "--export", "--armor", "--export-options=export-minimal", keyID).Output() if err != nil { return nil, fmt.Errorf("failed to export key from GPG kerying: %v", err) } return out, nil } func newMetaPGPKeyDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("meta", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } key, err := metasrht.DeletePGPKey(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Deleted PGP key %s\n", key.Fingerprint) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete a PGP key", Args: cobra.ExactArgs(1), ValidArgsFunction: completePGPKeys, Run: run, } return cmd } func newMetaPGPKeyListCommand() *cobra.Command { var raw bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("meta", cmd) var ( cursor *metasrht.Cursor user *metasrht.User username string err error ) if len(args) > 0 { username = strings.TrimLeft(args[0], ownerPrefixes) } err = pagerify(func(p pager) error { if username != "" { if raw { user, err = metasrht.ListRawPGPKeysByUser(c.Client, ctx, username, cursor) } else { user, err = metasrht.ListPGPKeysByUser(c.Client, ctx, username, cursor) } } else { if raw { user, err = metasrht.ListRawPGPKeys(c.Client, ctx, cursor) } else { user, err = metasrht.ListPGPKeys(c.Client, ctx, cursor) } } if err != nil { return err } else if user == nil { return fmt.Errorf("no such user %q", username) } if raw { for _, key := range user.PgpKeys.Results { fmt.Fprintln(p, key.Key) } } else { for _, key := range user.PgpKeys.Results { fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", key.Id), key.Fingerprint) fmt.Fprintln(p) } } cursor = user.PgpKeys.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list [user]", Short: "List PGP keys", Args: cobra.MaximumNArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().BoolVarP(&raw, "raw", "r", false, "print raw public keys") return cmd } func newMetaUserWebhookCommand() *cobra.Command { cmd := &cobra.Command{ Use: "user-webhook", Short: "Manage user webhooks", } cmd.AddCommand(newMetaUserWebhookCreateCommand()) cmd.AddCommand(newMetaUserWebhookListCommand()) cmd.AddCommand(newMetaUserWebhookDeleteCommand()) return cmd } func newMetaUserWebhookCreateCommand() *cobra.Command { var events []string var stdin bool var url string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("meta", cmd) var config metasrht.ProfileWebhookInput config.Url = url whEvents, err := metasrht.ParseUserEvents(events) if err != nil { log.Fatal(err) } config.Events = whEvents config.Query = readWebhookQuery(stdin) webhook, err := metasrht.CreateUserWebhook(c.Client, ctx, config) if err != nil { log.Fatal(err) } log.Printf("Created user webhook with ID %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "create", Short: "Create a user webhook", Args: cobra.ExactArgs(0), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().StringSliceVarP(&events, "events", "e", nil, "webhook events") cmd.RegisterFlagCompletionFunc("events", completeMetaUserWebhookEvents) cmd.MarkFlagRequired("events") cmd.Flags().BoolVar(&stdin, "stdin", !isStdinTerminal, "read webhook query from stdin") cmd.Flags().StringVarP(&url, "url", "u", "", "payload url") cmd.RegisterFlagCompletionFunc("url", cobra.NoFileCompletions) cmd.MarkFlagRequired("url") return cmd } func newMetaUserWebhookListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("meta", cmd) var cursor *metasrht.Cursor err := pagerify(func(p pager) error { webhooks, err := metasrht.UserWebhooks(c.Client, ctx, cursor) if err != nil { return err } for _, webhook := range webhooks.Results { fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url) } cursor = webhooks.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list", Short: "List user webhooks", Args: cobra.ExactArgs(0), Run: run, } return cmd } func newMetaUserWebhookDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("meta", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } webhook, err := metasrht.DeleteUserWebhook(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Deleted webhook %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete a user webhook", Args: cobra.ExactArgs(1), ValidArgsFunction: completeMetaUserWebhookID, Run: run, } return cmd } func newMetaOAuthCommand() *cobra.Command { cmd := &cobra.Command{ Use: "oauth", Short: "List OAuth credentials", } cmd.AddCommand(newMetaOAuthTokensCommand()) return cmd } func newMetaOAuthTokensCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("meta", cmd) tokens, err := metasrht.PersonalAccessTokens(c.Client, ctx) if err != nil { log.Fatal(err) } tw := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0) defer tw.Flush() fmt.Fprint(tw, termfmt.Bold.String("Comment\tIssued\tExpires\tGrant\n")) for _, token := range tokens { var s string if token.Comment != nil { s = fmt.Sprintf("%s\t", *token.Comment) } else { s = "\t" } issued := humanize.Time(token.Issued.Time) expires := humanize.Time(token.Expires.Time) s += fmt.Sprintf("%s\t%s\t", issued, expires) if token.Grants != nil { s += *token.Grants } fmt.Fprintln(tw, s) } } cmd := &cobra.Command{ Use: "tokens", Short: "List personal access tokens", Args: cobra.ExactArgs(0), Run: run, } return cmd } func completeSSHKeys(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("meta", cmd) var keyList []string user, err := metasrht.ListSSHKeys(c.Client, ctx, nil) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, key := range user.SshKeys.Results { str := fmt.Sprintf("%d\t%s", key.Id, key.Fingerprint) if key.Comment != nil { str += fmt.Sprintf(" %s", *key.Comment) } keyList = append(keyList, str) } return keyList, cobra.ShellCompDirectiveNoFileComp } func completePGPKeys(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("meta", cmd) var keyList []string user, err := metasrht.ListPGPKeys(c.Client, ctx, nil) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, key := range user.PgpKeys.Results { str := fmt.Sprintf("%d\t%s", key.Id, key.Fingerprint) keyList = append(keyList, str) } return keyList, cobra.ShellCompDirectiveNoFileComp } func completeMetaUserWebhookEvents(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var eventList []string events := [5]string{"profile_update", "pgp_key_added", "pgp_key_removed", "ssh_key_added", "ssh_key_removed"} set := strings.ToLower(cmd.Flag("events").Value.String()) for _, event := range events { if !strings.Contains(set, event) { eventList = append(eventList, event) } } return eventList, cobra.ShellCompDirectiveNoFileComp } func completeMetaUserWebhookID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("meta", cmd) var webhookList []string webhooks, err := metasrht.UserWebhooks(c.Client, ctx, nil) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, webhook := range webhooks.Results { s := fmt.Sprintf("%d\t%s", webhook.Id, webhook.Url) webhookList = append(webhookList, s) } return webhookList, cobra.ShellCompDirectiveNoFileComp } hut-0.6.0/pager.go000066400000000000000000000034411463710650600137540ustar00rootroot00000000000000package main import ( "errors" "io" "log" "os" "os/exec" "github.com/google/shlex" ) type pager interface { io.WriteCloser Running() bool } var pagerDone error = errors.New("paging is done") func newPager() pager { if !isStdoutTerminal { return &singleWritePager{os.Stdout, true} } name, ok := os.LookupEnv("PAGER") if !ok { name = "less" } commandSplit, err := shlex.Split(name) if err != nil { log.Fatalf("Failed to parse pager command: %v", err) } cmd := exec.Command(commandSplit[0], commandSplit[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Env = append(os.Environ(), "LESS=FRX") w, err := cmd.StdinPipe() if err != nil { log.Fatalf("Failed to create stdin pipe for pager: %v", err) } if err := cmd.Start(); err != nil { log.Fatalf("Failed to start pager %q: %v", cmd.Args[0], err) } done := make(chan struct{}) go func() { defer close(done) if err := cmd.Wait(); err != nil { log.Fatalf("Failed to run pager: %v", err) } }() return &cmdPager{w, done} } type pagerifyFn func(p pager) error func pagerify(fn pagerifyFn) error { pager := newPager() defer pager.Close() for pager.Running() { err := fn(pager) if err == pagerDone { return nil } else if err != nil { return err } } return nil } type singleWritePager struct { io.WriteCloser running bool } func (p *singleWritePager) Write(b []byte) (int, error) { p.running = false return p.WriteCloser.Write(b) } func (p *singleWritePager) Running() bool { return p.running } type cmdPager struct { io.WriteCloser done <-chan struct{} } func (p *cmdPager) Close() error { if err := p.WriteCloser.Close(); err != nil { return err } <-p.done return nil } func (p *cmdPager) Running() bool { select { case <-p.done: return false default: return true } } hut-0.6.0/pages.go000066400000000000000000000252421463710650600137600ustar00rootroot00000000000000package main import ( "archive/tar" "compress/gzip" "encoding/json" "fmt" "io" "io/fs" "log" "os" "path/filepath" "strings" "git.sr.ht/~emersion/gqlclient" "github.com/spf13/cobra" "git.sr.ht/~xenrox/hut/srht/pagessrht" "git.sr.ht/~xenrox/hut/termfmt" ) func newPagesCommand() *cobra.Command { cmd := &cobra.Command{ Use: "pages", Short: "Use the pages API", } cmd.AddCommand(newPagesPublishCommand()) cmd.AddCommand(newPagesUnpublishCommand()) cmd.AddCommand(newPagesListCommand()) cmd.AddCommand(newPagesUserWebhookCommand()) return cmd } func newPagesPublishCommand() *cobra.Command { var domain, protocol, subdirectory, siteConfigFile string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var filename string if len(args) > 0 { filename = args[0] } pagesProtocol, err := pagessrht.ParseProtocol(protocol) if err != nil { log.Fatal(err) } siteConfig := pagessrht.SiteConfig{} if siteConfigFile != "" { config, err := readSiteConfig(siteConfigFile) if err != nil { log.Fatalf("failed to read site-config: %v", err) } siteConfig = *config } c := createClient("pages", cmd) c.HTTP.Timeout = fileTransferTimeout var f *os.File if filename == "" { f = os.Stdin } else { f, err = os.Open(filename) if err != nil { log.Fatalf("failed to open input file: %v", err) } } defer f.Close() fi, err := f.Stat() if err != nil { log.Fatalf("failed to stat input file: %v", err) } var upload gqlclient.Upload if fi.IsDir() { pr, pw := io.Pipe() defer pr.Close() go func() { pw.CloseWithError(writeSiteArchive(pw, filename)) }() upload = gqlclient.Upload{Body: pr} } else { upload = gqlclient.Upload{Body: f, Filename: filepath.Base(filename)} } upload.MIMEType = "application/gzip" site, err := pagessrht.Publish(c.Client, ctx, domain, upload, pagesProtocol, subdirectory, siteConfig) if err != nil { log.Fatalf("failed to publish site: %v", err) } log.Printf("Published site at %s\n", site.Domain) } cmd := &cobra.Command{ Use: "publish [file]", Short: "Publish a website", Args: cobra.MaximumNArgs(1), Run: run, } cmd.Flags().StringVarP(&domain, "domain", "d", "", "domain name") cmd.MarkFlagRequired("domain") cmd.RegisterFlagCompletionFunc("domain", completeDomain) cmd.Flags().StringVarP(&protocol, "protocol", "p", "HTTPS", "protocol (HTTPS or GEMINI)") cmd.RegisterFlagCompletionFunc("protocol", completeProtocol) cmd.Flags().StringVarP(&subdirectory, "subdirectory", "s", "/", "subdirectory") cmd.Flags().StringVar(&siteConfigFile, "site-config", "", "path to site configuration file (for e.g. cache-control)") cmd.RegisterFlagCompletionFunc("site-config", cobra.FixedCompletions([]string{"json"}, cobra.ShellCompDirectiveFilterFileExt)) return cmd } func writeSiteArchive(w io.Writer, dir string) error { gzipWriter := gzip.NewWriter(w) defer gzipWriter.Close() tarWriter := tar.NewWriter(gzipWriter) defer tarWriter.Close() err := filepath.WalkDir(dir, func(path string, de fs.DirEntry, err error) error { if err != nil { return err } if de.IsDir() { return nil } if t := de.Type(); t != 0 { // Symlink, pipe, socket, device, etc return fmt.Errorf("unsupported file %q type (%v)", path, t) } fi, err := de.Info() if err != nil { return err } rel, err := filepath.Rel(dir, path) if err != nil { return err } f, err := os.Open(path) if err != nil { return err } defer f.Close() header := tar.Header{ Typeflag: tar.TypeReg, Name: filepath.ToSlash(rel), ModTime: fi.ModTime(), Mode: 0600, Size: fi.Size(), } if err := tarWriter.WriteHeader(&header); err != nil { return err } _, err = io.Copy(tarWriter, f) return err }) if err != nil { return fmt.Errorf("failed to walk directory: %v", err) } if err := tarWriter.Close(); err != nil { return fmt.Errorf("failed to close tar writer: %v", err) } if err := gzipWriter.Close(); err != nil { return fmt.Errorf("failed to close gzip writer: %v", err) } return nil } func readSiteConfig(filename string) (*pagessrht.SiteConfig, error) { f, err := os.Open(filename) if err != nil { return nil, err } defer f.Close() var config pagessrht.SiteConfig dec := json.NewDecoder(f) dec.DisallowUnknownFields() if err := dec.Decode(&config); err != nil { return nil, err } return &config, nil } func newPagesUnpublishCommand() *cobra.Command { var domain, protocol string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() pagesProtocol, err := pagessrht.ParseProtocol(protocol) if err != nil { log.Fatal(err) } c := createClient("pages", cmd) site, err := pagessrht.Unpublish(c.Client, ctx, domain, pagesProtocol) if err != nil { log.Fatalf("failed to unpublish site: %v", err) } if site == nil { log.Printf("This site does not exist\n") } else { log.Printf("Unpublished site at %s\n", site.Domain) } } cmd := &cobra.Command{ Use: "unpublish", Short: "Unpublish a website", Run: run, } cmd.Flags().StringVarP(&domain, "domain", "d", "", "domain name") cmd.MarkFlagRequired("domain") cmd.RegisterFlagCompletionFunc("domain", completeDomain) cmd.Flags().StringVarP(&protocol, "protocol", "p", "HTTPS", "protocol (HTTPS or GEMINI)") cmd.RegisterFlagCompletionFunc("protocol", completeProtocol) return cmd } func newPagesListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("pages", cmd) var cursor *pagessrht.Cursor err := pagerify(func(p pager) error { sites, err := pagessrht.Sites(c.Client, ctx, cursor) if err != nil { return fmt.Errorf("failed to list sites: %v", err) } for _, site := range sites.Results { fmt.Fprintf(p, "%s (%s)\n", termfmt.Bold.Sprintf(site.Domain), site.Protocol) } cursor = sites.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list", Short: "List registered sites", Run: run, } return cmd } func newPagesUserWebhookCommand() *cobra.Command { cmd := &cobra.Command{ Use: "user-webhook", Short: "Manage user webhooks", } cmd.AddCommand(newPagesUserWebhookCreateCommand()) cmd.AddCommand(newPagesUserWebhookListCommand()) cmd.AddCommand(newPagesUserWebhookDeleteCommand()) return cmd } func newPagesUserWebhookCreateCommand() *cobra.Command { var events []string var stdin bool var url string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("pages", cmd) var config pagessrht.UserWebhookInput config.Url = url whEvents, err := pagessrht.ParseEvents(events) if err != nil { log.Fatal(err) } config.Events = whEvents config.Query = readWebhookQuery(stdin) webhook, err := pagessrht.CreateUserWebhook(c.Client, ctx, config) if err != nil { log.Fatal(err) } log.Printf("Created user webhook with ID %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "create", Short: "Create a user webhook", Args: cobra.ExactArgs(0), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().StringSliceVarP(&events, "events", "e", nil, "webhook events") cmd.RegisterFlagCompletionFunc("events", completePagesUserWebhookEvents) cmd.MarkFlagRequired("events") cmd.Flags().BoolVar(&stdin, "stdin", !isStdinTerminal, "read webhook query from stdin") cmd.Flags().StringVarP(&url, "url", "u", "", "payload url") cmd.RegisterFlagCompletionFunc("url", cobra.NoFileCompletions) cmd.MarkFlagRequired("url") return cmd } func newPagesUserWebhookListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("pages", cmd) var cursor *pagessrht.Cursor err := pagerify(func(p pager) error { webhooks, err := pagessrht.UserWebhooks(c.Client, ctx, cursor) if err != nil { return err } for _, webhook := range webhooks.Results { fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url) } cursor = webhooks.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list", Short: "List user webhooks", Args: cobra.ExactArgs(0), Run: run, } return cmd } func newPagesUserWebhookDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("pages", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } webhook, err := pagessrht.DeleteUserWebhook(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Deleted webhook %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete a user webhook", Args: cobra.ExactArgs(1), ValidArgsFunction: completePagesUserWebhookID, Run: run, } return cmd } var completeProtocol = cobra.FixedCompletions([]string{"https", "gemini"}, cobra.ShellCompDirectiveNoFileComp) func completeDomain(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("pages", cmd) var domainList []string protocol, err := cmd.Flags().GetString("protocol") if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } sites, err := pagessrht.Sites(c.Client, ctx, nil) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, site := range sites.Results { if strings.EqualFold(protocol, string(site.Protocol)) { domainList = append(domainList, site.Domain) } } return domainList, cobra.ShellCompDirectiveNoFileComp } func completePagesUserWebhookEvents(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var eventList []string events := [2]string{"site_published", "site_unpublished"} set := strings.ToLower(cmd.Flag("events").Value.String()) for _, event := range events { if !strings.Contains(set, event) { eventList = append(eventList, event) } } return eventList, cobra.ShellCompDirectiveNoFileComp } func completePagesUserWebhookID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("pages", cmd) var webhookList []string webhooks, err := pagessrht.UserWebhooks(c.Client, ctx, nil) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, webhook := range webhooks.Results { s := fmt.Sprintf("%d\t%s", webhook.Id, webhook.Url) webhookList = append(webhookList, s) } return webhookList, cobra.ShellCompDirectiveNoFileComp } hut-0.6.0/parsing_test.go000066400000000000000000000027371463710650600153670ustar00rootroot00000000000000package main import "testing" func TestParseResourceName(t *testing.T) { tests := []struct { s string resource string owner string instance string }{ {"https://git.sr.ht/~emersion/hut", "hut", "~emersion", "git.sr.ht"}, {"sr.ht/~emersion/hut", "hut", "~emersion", "sr.ht"}, {"~emersion/hut", "hut", "~emersion", ""}, {"hut", "hut", "", ""}, } for _, test := range tests { resource, owner, instance := parseResourceName(test.s) if resource != test.resource { t.Errorf("parseResourceName(%q) resource: expected %q, got %q", test.s, test.resource, resource) } if owner != test.owner { t.Errorf("parseResourceName(%q) owner: expected %q, got %q", test.s, test.owner, owner) } if instance != test.instance { t.Errorf("parseResourceName(%q) instance: expected %q, got %q", test.s, test.instance, instance) } } } func TestParseBuildID(t *testing.T) { tests := []struct { s string id int32 instance string }{ {"https://builds.sr.ht/~emersion/job/1", 1, "builds.sr.ht"}, {"~emersion/job/1", 1, ""}, {"job/1", 1, ""}, {"1", 1, ""}, } for _, test := range tests { id, instance, err := parseBuildID(test.s) if err != nil { t.Errorf("parseBuildID(%q) error: %v", test.s, err) } if id != test.id { t.Errorf("parseBuildID(%q) id: expected %d, got %d", test.s, test.id, id) } if instance != test.instance { t.Errorf("parseBuildID(%q) instance: expected %q, got %q", test.s, test.instance, instance) } } } hut-0.6.0/paste.go000066400000000000000000000260241463710650600137740ustar00rootroot00000000000000package main import ( "context" "fmt" "io" "log" "mime" "net/http" "os" "path/filepath" "strings" "git.sr.ht/~emersion/gqlclient" "github.com/dustin/go-humanize" "github.com/spf13/cobra" "git.sr.ht/~xenrox/hut/srht/pastesrht" "git.sr.ht/~xenrox/hut/termfmt" ) func newPasteCommand() *cobra.Command { cmd := &cobra.Command{ Use: "paste", Short: "Use the paste API", } cmd.AddCommand(newPasteCreateCommand()) cmd.AddCommand(newPasteDeleteCommand()) cmd.AddCommand(newPasteListCommand()) cmd.AddCommand(newPasteShowCommand()) cmd.AddCommand(newPasteUpdateCommand()) cmd.AddCommand(newPasteUserWebhookCommand()) return cmd } func newPasteCreateCommand() *cobra.Command { var visibility string var name string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() pasteVisibility, err := pastesrht.ParseVisibility(visibility) if err != nil { log.Fatal(err) } c := createClient("paste", cmd) if name != "" && len(args) > 0 { log.Fatalln("--name is only supported when reading from stdin") } var files []gqlclient.Upload for _, filename := range args { f, err := os.Open(filename) if err != nil { log.Fatalf("failed to open input file: %v", err) } defer f.Close() t := mime.TypeByExtension(filename) if t == "" { t = "text/plain" } files = append(files, gqlclient.Upload{ Filename: filepath.Base(filename), MIMEType: t, Body: f, }) } if len(args) == 0 { files = append(files, gqlclient.Upload{ Filename: name, MIMEType: "text/plain", Body: os.Stdin, }) } paste, err := pastesrht.CreatePaste(c.Client, ctx, files, pasteVisibility) if err != nil { log.Fatal(err) } if termfmt.IsTerminal() { log.Printf("Created paste %v/%v/%v", c.BaseURL, paste.User.CanonicalName, paste.Id) } else { fmt.Printf("%v/%v/%v\n", c.BaseURL, paste.User.CanonicalName, paste.Id) } } cmd := &cobra.Command{ Use: "create [filenames...]", Short: "Create a new paste", Run: run, } cmd.Flags().StringVarP(&visibility, "visibility", "v", "unlisted", "paste visibility") cmd.RegisterFlagCompletionFunc("visibility", completeVisibility) cmd.Flags().StringVarP(&name, "name", "n", "", "paste name (when reading from stdin)") return cmd } func newPasteDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() for _, arg := range args { id, _, instance := parseResourceName(arg) c := createClientWithInstance("paste", cmd, instance) paste, err := pastesrht.Delete(c.Client, ctx, id) if err != nil { log.Fatalf("failed to delete paste %s: %v", id, err) } if paste == nil { log.Printf("Paste %s does not exist\n", id) } else { log.Printf("Deleted paste %s\n", paste.Id) } } } cmd := &cobra.Command{ Use: "delete ", Short: "Delete pastes", Args: cobra.MinimumNArgs(1), ValidArgsFunction: completePasteID, Run: run, } return cmd } func newPasteListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("paste", cmd) var cursor *pastesrht.Cursor err := pagerify(func(p pager) error { pastes, err := pastesrht.Pastes(c.Client, ctx, cursor) if err != nil { return err } for _, paste := range pastes.Results { printPaste(p, &paste) fmt.Fprintln(p) } cursor = pastes.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list", Short: "List pastes", Run: run, } return cmd } func printPaste(w io.Writer, paste *pastesrht.Paste) { fmt.Fprintf(w, "%s %s %s\n", termfmt.DarkYellow.Sprint(paste.Id), paste.Visibility.TermString(), humanize.Time(paste.Created.Time)) for _, file := range paste.Files { if file.Filename != nil && *file.Filename != "" { fmt.Fprintf(w, " %s\n", *file.Filename) } } } func newPasteShowCommand() *cobra.Command { cmd := &cobra.Command{ Use: "show ", Short: "Display a paste", Args: cobra.ExactArgs(1), ValidArgsFunction: completePasteID, } cmd.Run = func(cmd *cobra.Command, args []string) { ctx := cmd.Context() id, _, instance := parseResourceName(args[0]) c := createClientWithInstance("paste", cmd, instance) paste, err := pastesrht.ShowPaste(c.Client, ctx, id) if err != nil { log.Fatal(err) } else if paste == nil { log.Fatalf("Paste %q does not exist", id) } fmt.Printf("%s %s %s\n", termfmt.DarkYellow.Sprint(paste.Id), paste.Visibility.TermString(), humanize.Time(paste.Created.Time)) for _, file := range paste.Files { fmt.Print("\n■ ") if file.Filename != nil && *file.Filename != "" { fmt.Println(termfmt.Bold.String(*file.Filename)) } else { fmt.Println(termfmt.Dim.String("(untitled)")) } fmt.Println() fetchPasteFile(ctx, c.HTTP, &file) } } return cmd } func fetchPasteFile(ctx context.Context, c *http.Client, file *pastesrht.File) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, string(file.Contents), nil) if err != nil { log.Fatalf("Failed to create request to fetch file: %v", err) } resp, err := c.Do(req) if err != nil { log.Fatalf("Failed to fetch file: %v", err) } defer resp.Body.Close() if _, err := io.Copy(os.Stdout, resp.Body); err != nil { log.Fatalf("Failed to copy to stdout: %v", err) } } func newPasteUpdateCommand() *cobra.Command { var visibility string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("paste", cmd) pasteVisibility, err := pastesrht.ParseVisibility(visibility) if err != nil { log.Fatal(err) } paste, err := pastesrht.Update(c.Client, ctx, args[0], pasteVisibility) if err != nil { log.Fatal(err) } if paste == nil { log.Fatalf("Paste %s does not exist\n", args[0]) } log.Printf("Updated paste %s visibility to %s\n", paste.Id, pasteVisibility) } cmd := &cobra.Command{ Use: "update ", Short: "Update a paste's visibility", Args: cobra.ExactArgs(1), ValidArgsFunction: completePasteID, Run: run, } cmd.Flags().StringVarP(&visibility, "visibility", "v", "", "paste visibility") cmd.MarkFlagRequired("visibility") cmd.RegisterFlagCompletionFunc("visibility", completeVisibility) return cmd } func newPasteUserWebhookCommand() *cobra.Command { cmd := &cobra.Command{ Use: "user-webhook", Short: "Manage user webhooks", } cmd.AddCommand(newPasteUserWebhookCreateCommand()) cmd.AddCommand(newPasteUserWebhookListCommand()) cmd.AddCommand(newPasteUserWebhookDeleteCommand()) return cmd } func newPasteUserWebhookCreateCommand() *cobra.Command { var events []string var stdin bool var url string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("paste", cmd) var config pastesrht.UserWebhookInput config.Url = url whEvents, err := pastesrht.ParseEvents(events) if err != nil { log.Fatal(err) } config.Events = whEvents config.Query = readWebhookQuery(stdin) webhook, err := pastesrht.CreateUserWebhook(c.Client, ctx, config) if err != nil { log.Fatal(err) } log.Printf("Created user webhook with ID %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "create", Short: "Create a user webhook", Args: cobra.ExactArgs(0), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().StringSliceVarP(&events, "events", "e", nil, "webhook events") cmd.RegisterFlagCompletionFunc("events", completePasteUserWebhookEvents) cmd.MarkFlagRequired("events") cmd.Flags().BoolVar(&stdin, "stdin", !isStdinTerminal, "read webhook query from stdin") cmd.Flags().StringVarP(&url, "url", "u", "", "payload url") cmd.RegisterFlagCompletionFunc("url", cobra.NoFileCompletions) cmd.MarkFlagRequired("url") return cmd } func newPasteUserWebhookListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("paste", cmd) var cursor *pastesrht.Cursor err := pagerify(func(p pager) error { webhooks, err := pastesrht.UserWebhooks(c.Client, ctx, cursor) if err != nil { return err } for _, webhook := range webhooks.Results { fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url) } cursor = webhooks.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list", Short: "List user webhooks", Args: cobra.ExactArgs(0), Run: run, } return cmd } func newPasteUserWebhookDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("paste", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } webhook, err := pastesrht.DeleteUserWebhook(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Deleted webhook %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete a user webhook", Args: cobra.ExactArgs(1), ValidArgsFunction: completePasteUserWebhookID, Run: run, } return cmd } func completePasteID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("paste", cmd) var pasteList []string pastes, err := pastesrht.PasteCompletionList(c.Client, ctx) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, paste := range pastes.Results { if cmd.Name() == "delete" && hasCmdArg(cmd, paste.Id) { continue } str := paste.Id var files string for i, file := range paste.Files { if file.Filename != nil && *file.Filename != "" { if i != 0 { files += ", " } files += *file.Filename } } if files != "" { str += fmt.Sprintf("\t%s", files) } pasteList = append(pasteList, str) } return pasteList, cobra.ShellCompDirectiveNoFileComp } func completePasteUserWebhookEvents(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var eventList []string events := [3]string{"paste_created", "paste_updated", "paste_deleted"} set := strings.ToLower(cmd.Flag("events").Value.String()) for _, event := range events { if !strings.Contains(set, event) { eventList = append(eventList, event) } } return eventList, cobra.ShellCompDirectiveNoFileComp } func completePasteUserWebhookID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("paste", cmd) var webhookList []string webhooks, err := pastesrht.UserWebhooks(c.Client, ctx, nil) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, webhook := range webhooks.Results { s := fmt.Sprintf("%d\t%s", webhook.Id, webhook.Url) webhookList = append(webhookList, s) } return webhookList, cobra.ShellCompDirectiveNoFileComp } hut-0.6.0/project.go000066400000000000000000000020061463710650600143200ustar00rootroot00000000000000package main import ( "errors" "os" "path/filepath" "git.sr.ht/~emersion/go-scfg" ) type projectConfig struct { Tracker string `scfg:"tracker"` DevList string `scfg:"development-mailing-list"` PatchPrefix bool `scfg:"patch-prefix"` } func loadProjectConfig() (*projectConfig, error) { fileName, err := findProjectConfig() if err != nil { return nil, err } if fileName == "" { return nil, nil } f, err := os.Open(fileName) if err != nil { return nil, err } defer f.Close() cfg := new(projectConfig) if err := scfg.NewDecoder(f).Decode(cfg); err != nil { return nil, err } return cfg, nil } func findProjectConfig() (string, error) { cur, err := os.Getwd() if err != nil { return "", err } for { fileName := filepath.Join(cur, ".hut.scfg") _, err := os.Stat(fileName) if err == nil { return fileName, nil } else if !errors.Is(err, os.ErrNotExist) { return "", err } next := filepath.Dir(cur) if next == cur { break } cur = next } return "", nil } hut-0.6.0/srht/000077500000000000000000000000001463710650600133055ustar00rootroot00000000000000hut-0.6.0/srht/README.md000066400000000000000000000002571463710650600145700ustar00rootroot00000000000000Code in this package and its sub-packages is auto-generated with gqlclientgen. To add or change a GraphQL query, edit the `.graphql` file and then run: go generate ./... hut-0.6.0/srht/buildssrht/000077500000000000000000000000001463710650600154705ustar00rootroot00000000000000hut-0.6.0/srht/buildssrht/gql.go000066400000000000000000000564471463710650600166220ustar00rootroot00000000000000// Code generated by gqlclientgen - DO NOT EDIT. package buildssrht import ( "context" "encoding/json" "fmt" gqlclient "git.sr.ht/~emersion/gqlclient" ) type AccessKind string const ( AccessKindRo AccessKind = "RO" AccessKindRw AccessKind = "RW" ) type AccessScope string const ( AccessScopeProfile AccessScope = "PROFILE" AccessScopeJobs AccessScope = "JOBS" AccessScopeLogs AccessScope = "LOGS" AccessScopeSecrets AccessScope = "SECRETS" ) type Artifact struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` // Original path in the guest Path string `json:"path"` // Size in bytes Size int32 `json:"size"` // URL at which the artifact may be downloaded, or null if pruned Url *string `json:"url,omitempty"` } type Binary string type Cursor string type EmailTrigger struct { Condition TriggerCondition `json:"condition"` To string `json:"to"` Cc *string `json:"cc,omitempty"` InReplyTo *string `json:"inReplyTo,omitempty"` } func (*EmailTrigger) isTrigger() {} type EmailTriggerInput struct { To string `json:"to"` Cc *string `json:"cc,omitempty"` InReplyTo *string `json:"inReplyTo,omitempty"` } type Entity struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` // The canonical name of this entity. For users, this is their username // prefixed with '~'. Additional entity types will be supported in the future. CanonicalName string `json:"canonicalName"` // Underlying value of the GraphQL interface Value EntityValue `json:"-"` } func (base *Entity) UnmarshalJSON(b []byte) error { type Raw Entity var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "User": base.Value = new(User) case "": return nil default: return fmt.Errorf("gqlclient: interface Entity: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // EntityValue is one of: User type EntityValue interface { isEntity() } type File string type Job struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` Status JobStatus `json:"status"` Manifest string `json:"manifest"` Note *string `json:"note,omitempty"` Tags []string `json:"tags"` Visibility Visibility `json:"visibility"` // Name of the build image Image string `json:"image"` // Name of the build runner which picked up this job, or null if the job is // pending or queued. Runner *string `json:"runner,omitempty"` Owner *Entity `json:"owner"` Group *JobGroup `json:"group,omitempty"` Tasks []Task `json:"tasks"` Artifacts []Artifact `json:"artifacts"` // The job's top-level log file, not associated with any tasks Log *Log `json:"log,omitempty"` // List of secrets available to this job, or null if they were disabled Secrets []Secret `json:"secrets,omitempty"` } // A cursor for enumerating a list of jobs // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type JobCursor struct { Results []Job `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type JobEvent struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` Job *Job `json:"job"` } func (*JobEvent) isWebhookPayload() {} type JobGroup struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Note *string `json:"note,omitempty"` Owner *Entity `json:"owner"` Jobs []Job `json:"jobs"` Triggers []Trigger `json:"triggers"` } type JobStatus string const ( JobStatusPending JobStatus = "PENDING" JobStatusQueued JobStatus = "QUEUED" JobStatusRunning JobStatus = "RUNNING" JobStatusSuccess JobStatus = "SUCCESS" JobStatusFailed JobStatus = "FAILED" JobStatusTimeout JobStatus = "TIMEOUT" JobStatusCancelled JobStatus = "CANCELLED" ) type Log struct { // The most recently written 128 KiB of the build log. Last128KiB string `json:"last128KiB"` // The URL at which the full build log can be downloaded with an authenticated // GET request (text/plain). FullURL string `json:"fullURL"` } type OAuthClient struct { Uuid string `json:"uuid"` } type PGPKey struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Uuid string `json:"uuid"` Name *string `json:"name,omitempty"` FromUser *Entity `json:"fromUser,omitempty"` PrivateKey Binary `json:"privateKey"` } func (*PGPKey) isSecret() {} type SSHKey struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Uuid string `json:"uuid"` Name *string `json:"name,omitempty"` FromUser *Entity `json:"fromUser,omitempty"` PrivateKey Binary `json:"privateKey"` } func (*SSHKey) isSecret() {} type Secret struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Uuid string `json:"uuid"` Name *string `json:"name,omitempty"` // Set when this secret was copied from another user account FromUser *Entity `json:"fromUser,omitempty"` // Underlying value of the GraphQL interface Value SecretValue `json:"-"` } func (base *Secret) UnmarshalJSON(b []byte) error { type Raw Secret var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "SSHKey": base.Value = new(SSHKey) case "PGPKey": base.Value = new(PGPKey) case "SecretFile": base.Value = new(SecretFile) case "": return nil default: return fmt.Errorf("gqlclient: interface Secret: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // SecretValue is one of: SSHKey | PGPKey | SecretFile type SecretValue interface { isSecret() } // A cursor for enumerating a list of secrets // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type SecretCursor struct { Results []Secret `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type SecretFile struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Uuid string `json:"uuid"` Name *string `json:"name,omitempty"` FromUser *Entity `json:"fromUser,omitempty"` Path string `json:"path"` Mode int32 `json:"mode"` Data Binary `json:"data"` } func (*SecretFile) isSecret() {} type Settings struct { SshUser string `json:"sshUser"` BuildTimeout string `json:"buildTimeout"` } type Task struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` Name string `json:"name"` Status TaskStatus `json:"status"` Log *Log `json:"log,omitempty"` Job *Job `json:"job"` } type TaskStatus string const ( TaskStatusPending TaskStatus = "PENDING" TaskStatusRunning TaskStatus = "RUNNING" TaskStatusSuccess TaskStatus = "SUCCESS" TaskStatusFailed TaskStatus = "FAILED" TaskStatusSkipped TaskStatus = "SKIPPED" ) // Triggers run upon the completion of all of the jobs in a job group. Note that // these triggers are distinct from the ones defined by an individual job's // build manifest, but are similar in functionality. type Trigger struct { Condition TriggerCondition `json:"condition"` // Underlying value of the GraphQL interface Value TriggerValue `json:"-"` } func (base *Trigger) UnmarshalJSON(b []byte) error { type Raw Trigger var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "EmailTrigger": base.Value = new(EmailTrigger) case "WebhookTrigger": base.Value = new(WebhookTrigger) case "": return nil default: return fmt.Errorf("gqlclient: interface Trigger: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // TriggerValue is one of: EmailTrigger | WebhookTrigger type TriggerValue interface { isTrigger() } type TriggerCondition string const ( TriggerConditionSuccess TriggerCondition = "SUCCESS" TriggerConditionFailure TriggerCondition = "FAILURE" TriggerConditionAlways TriggerCondition = "ALWAYS" ) type TriggerInput struct { Type TriggerType `json:"type"` Condition TriggerCondition `json:"condition"` Email *EmailTriggerInput `json:"email,omitempty"` Webhook *WebhookTriggerInput `json:"webhook,omitempty"` } type TriggerType string const ( TriggerTypeEmail TriggerType = "EMAIL" TriggerTypeWebhook TriggerType = "WEBHOOK" ) type User struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` CanonicalName string `json:"canonicalName"` Username string `json:"username"` Email string `json:"email"` Url *string `json:"url,omitempty"` Location *string `json:"location,omitempty"` Bio *string `json:"bio,omitempty"` // Jobs submitted by this user. Jobs *JobCursor `json:"jobs"` } func (*User) isEntity() {} type UserWebhookInput struct { Url string `json:"url"` Events []WebhookEvent `json:"events"` Query string `json:"query"` } type UserWebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` Client *OAuthClient `json:"client,omitempty"` Deliveries *WebhookDeliveryCursor `json:"deliveries"` Sample string `json:"sample"` } func (*UserWebhookSubscription) isWebhookSubscription() {} type Version struct { Major int32 `json:"major"` Minor int32 `json:"minor"` Patch int32 `json:"patch"` // If this API version is scheduled for deprecation, this is the date on which // it will stop working; or null if this API version is not scheduled for // deprecation. DeprecationDate gqlclient.Time `json:"deprecationDate,omitempty"` Settings *Settings `json:"settings"` } type Visibility string const ( VisibilityPublic Visibility = "PUBLIC" VisibilityUnlisted Visibility = "UNLISTED" VisibilityPrivate Visibility = "PRIVATE" ) type WebhookDelivery struct { Uuid string `json:"uuid"` Date gqlclient.Time `json:"date"` Event WebhookEvent `json:"event"` Subscription *WebhookSubscription `json:"subscription"` RequestBody string `json:"requestBody"` // These details are provided only after a response is received from the // remote server. If a response is sent whose Content-Type is not text/*, or // cannot be decoded as UTF-8, the response body will be null. It will be // truncated after 64 KiB. ResponseBody *string `json:"responseBody,omitempty"` ResponseHeaders *string `json:"responseHeaders,omitempty"` ResponseStatus *int32 `json:"responseStatus,omitempty"` } // A cursor for enumerating a list of webhook deliveries // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type WebhookDeliveryCursor struct { Results []WebhookDelivery `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type WebhookEvent string const ( WebhookEventJobCreated WebhookEvent = "JOB_CREATED" ) type WebhookPayload struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` // Underlying value of the GraphQL interface Value WebhookPayloadValue `json:"-"` } func (base *WebhookPayload) UnmarshalJSON(b []byte) error { type Raw WebhookPayload var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "JobEvent": base.Value = new(JobEvent) case "": return nil default: return fmt.Errorf("gqlclient: interface WebhookPayload: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // WebhookPayloadValue is one of: JobEvent type WebhookPayloadValue interface { isWebhookPayload() } type WebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` // If this webhook was registered by an authorized OAuth 2.0 client, this // field is non-null. Client *OAuthClient `json:"client,omitempty"` // All deliveries which have been sent to this webhook. Deliveries *WebhookDeliveryCursor `json:"deliveries"` // Returns a sample payload for this subscription, for testing purposes Sample string `json:"sample"` // Underlying value of the GraphQL interface Value WebhookSubscriptionValue `json:"-"` } func (base *WebhookSubscription) UnmarshalJSON(b []byte) error { type Raw WebhookSubscription var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "UserWebhookSubscription": base.Value = new(UserWebhookSubscription) case "": return nil default: return fmt.Errorf("gqlclient: interface WebhookSubscription: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // WebhookSubscriptionValue is one of: UserWebhookSubscription type WebhookSubscriptionValue interface { isWebhookSubscription() } // A cursor for enumerating a list of webhook subscriptions // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type WebhookSubscriptionCursor struct { Results []WebhookSubscription `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type WebhookTrigger struct { Condition TriggerCondition `json:"condition"` Url string `json:"url"` } func (*WebhookTrigger) isTrigger() {} type WebhookTriggerInput struct { Url string `json:"url"` } func Submit(client *gqlclient.Client, ctx context.Context, manifest string, tags []string, note *string, visibility *Visibility) (submit *Job, err error) { op := gqlclient.NewOperation("mutation submit ($manifest: String!, $tags: [String!], $note: String, $visibility: Visibility) {\n\tsubmit(manifest: $manifest, tags: $tags, note: $note, visibility: $visibility) {\n\t\tid\n\t\towner {\n\t\t\tcanonicalName\n\t\t}\n\t}\n}\n") op.Var("manifest", manifest) op.Var("tags", tags) op.Var("note", note) op.Var("visibility", visibility) var respData struct { Submit *Job } err = client.Execute(ctx, op, &respData) return respData.Submit, err } func Cancel(client *gqlclient.Client, ctx context.Context, jobId int32) (cancel *Job, err error) { op := gqlclient.NewOperation("mutation cancel ($jobId: Int!) {\n\tcancel(jobId: $jobId) {\n\t\tid\n\t}\n}\n") op.Var("jobId", jobId) var respData struct { Cancel *Job } err = client.Execute(ctx, op, &respData) return respData.Cancel, err } func CreateUserWebhook(client *gqlclient.Client, ctx context.Context, config UserWebhookInput) (createUserWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation createUserWebhook ($config: UserWebhookInput!) {\n\tcreateUserWebhook(config: $config) {\n\t\tid\n\t}\n}\n") op.Var("config", config) var respData struct { CreateUserWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.CreateUserWebhook, err } func DeleteUserWebhook(client *gqlclient.Client, ctx context.Context, id int32) (deleteUserWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation deleteUserWebhook ($id: Int!) {\n\tdeleteUserWebhook(id: $id) {\n\t\tid\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteUserWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.DeleteUserWebhook, err } func ShareSecret(client *gqlclient.Client, ctx context.Context, uuid string, user string) (shareSecret *Secret, err error) { op := gqlclient.NewOperation("mutation shareSecret ($uuid: String!, $user: String!) {\n\tshareSecret(uuid: $uuid, user: $user) {\n\t\tuuid\n\t}\n}\n") op.Var("uuid", uuid) op.Var("user", user) var respData struct { ShareSecret *Secret } err = client.Execute(ctx, op, &respData) return respData.ShareSecret, err } func Monitor(client *gqlclient.Client, ctx context.Context, id int32) (job *Job, err error) { op := gqlclient.NewOperation("query monitor ($id: Int!) {\n\tjob(id: $id) {\n\t\tstatus\n\t\tlog {\n\t\t\tfullURL\n\t\t}\n\t\ttasks {\n\t\t\tname\n\t\t\tstatus\n\t\t\tlog {\n\t\t\t\tfullURL\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("id", id) var respData struct { Job *Job } err = client.Execute(ctx, op, &respData) return respData.Job, err } func Manifest(client *gqlclient.Client, ctx context.Context, id int32) (job *Job, err error) { op := gqlclient.NewOperation("query manifest ($id: Int!) {\n\tjob(id: $id) {\n\t\tmanifest\n\t\towner {\n\t\t\tcanonicalName\n\t\t}\n\t\tvisibility\n\t}\n}\n") op.Var("id", id) var respData struct { Job *Job } err = client.Execute(ctx, op, &respData) return respData.Job, err } func JobIDs(client *gqlclient.Client, ctx context.Context) (jobs *JobCursor, err error) { op := gqlclient.NewOperation("query jobIDs {\n\tjobs {\n\t\tresults {\n\t\t\tid\n\t\t}\n\t}\n}\n") var respData struct { Jobs *JobCursor } err = client.Execute(ctx, op, &respData) return respData.Jobs, err } func Jobs(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (jobs *JobCursor, err error) { op := gqlclient.NewOperation("query jobs ($cursor: Cursor) {\n\tjobs(cursor: $cursor) {\n\t\t... jobs\n\t}\n}\nfragment jobs on JobCursor {\n\tresults {\n\t\tid\n\t\tstatus\n\t\tnote\n\t\ttags\n\t\ttasks {\n\t\t\tname\n\t\t\tstatus\n\t\t}\n\t}\n\tcursor\n}\n") op.Var("cursor", cursor) var respData struct { Jobs *JobCursor } err = client.Execute(ctx, op, &respData) return respData.Jobs, err } func JobsByUser(client *gqlclient.Client, ctx context.Context, username string, cursor *Cursor) (userByName *User, err error) { op := gqlclient.NewOperation("query jobsByUser ($username: String!, $cursor: Cursor) {\n\tuserByName(username: $username) {\n\t\tjobs(cursor: $cursor) {\n\t\t\t... jobs\n\t\t}\n\t}\n}\nfragment jobs on JobCursor {\n\tresults {\n\t\tid\n\t\tstatus\n\t\tnote\n\t\ttags\n\t\ttasks {\n\t\t\tname\n\t\t\tstatus\n\t\t}\n\t}\n\tcursor\n}\n") op.Var("username", username) op.Var("cursor", cursor) var respData struct { UserByName *User } err = client.Execute(ctx, op, &respData) return respData.UserByName, err } func ExportJob(client *gqlclient.Client, ctx context.Context, id int32) (job *Job, err error) { op := gqlclient.NewOperation("query exportJob ($id: Int!) {\n\tjob(id: $id) {\n\t\t... jobExport\n\t}\n}\nfragment jobExport on Job {\n\tid\n\tstatus\n\tnote\n\ttags\n\tvisibility\n\tlog {\n\t\tfullURL\n\t}\n\ttasks {\n\t\tname\n\t\tstatus\n\t\tlog {\n\t\t\tfullURL\n\t\t}\n\t}\n}\n") op.Var("id", id) var respData struct { Job *Job } err = client.Execute(ctx, op, &respData) return respData.Job, err } func ExportJobs(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (jobs *JobCursor, err error) { op := gqlclient.NewOperation("query exportJobs ($cursor: Cursor) {\n\tjobs(cursor: $cursor) {\n\t\tresults {\n\t\t\t... jobExport\n\t\t}\n\t\tcursor\n\t}\n}\nfragment jobExport on Job {\n\tid\n\tstatus\n\tnote\n\ttags\n\tvisibility\n\tlog {\n\t\tfullURL\n\t}\n\ttasks {\n\t\tname\n\t\tstatus\n\t\tlog {\n\t\t\tfullURL\n\t\t}\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { Jobs *JobCursor } err = client.Execute(ctx, op, &respData) return respData.Jobs, err } func Show(client *gqlclient.Client, ctx context.Context, id int32) (job *Job, err error) { op := gqlclient.NewOperation("query show ($id: Int!) {\n\tjob(id: $id) {\n\t\tid\n\t\tstatus\n\t\tnote\n\t\ttags\n\t\tlog {\n\t\t\tfullURL\n\t\t}\n\t\ttasks {\n\t\t\tname\n\t\t\tstatus\n\t\t\tlog {\n\t\t\t\tfullURL\n\t\t\t}\n\t\t}\n\t\tgroup {\n\t\t\tjobs {\n\t\t\t\tid\n\t\t\t\tstatus\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("id", id) var respData struct { Job *Job } err = client.Execute(ctx, op, &respData) return respData.Job, err } func Secrets(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (secrets *SecretCursor, err error) { op := gqlclient.NewOperation("query secrets ($cursor: Cursor) {\n\tsecrets(cursor: $cursor) {\n\t\tresults {\n\t\t\tcreated\n\t\t\tuuid\n\t\t\tname\n\t\t\tfromUser {\n\t\t\t\tcanonicalName\n\t\t\t}\n\t\t\t__typename\n\t\t\t... on SecretFile {\n\t\t\t\tpath\n\t\t\t\tmode\n\t\t\t}\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { Secrets *SecretCursor } err = client.Execute(ctx, op, &respData) return respData.Secrets, err } func GetSSHInfo(client *gqlclient.Client, ctx context.Context, id int32) (job *Job, version *Version, err error) { op := gqlclient.NewOperation("query getSSHInfo ($id: Int!) {\n\tjob(id: $id) {\n\t\tid\n\t\trunner\n\t}\n\tversion {\n\t\tsettings {\n\t\t\tsshUser\n\t\t}\n\t}\n}\n") op.Var("id", id) var respData struct { Job *Job Version *Version } err = client.Execute(ctx, op, &respData) return respData.Job, respData.Version, err } func RunningJobs(client *gqlclient.Client, ctx context.Context) (jobs *JobCursor, err error) { op := gqlclient.NewOperation("query runningJobs {\n\tjobs {\n\t\tresults {\n\t\t\tid\n\t\t\tstatus\n\t\t\tnote\n\t\t\ttags\n\t\t}\n\t}\n}\n") var respData struct { Jobs *JobCursor } err = client.Execute(ctx, op, &respData) return respData.Jobs, err } func Artifacts(client *gqlclient.Client, ctx context.Context, id int32) (job *Job, err error) { op := gqlclient.NewOperation("query artifacts ($id: Int!) {\n\tjob(id: $id) {\n\t\tartifacts {\n\t\t\tpath\n\t\t\tsize\n\t\t\turl\n\t\t}\n\t}\n}\n") op.Var("id", id) var respData struct { Job *Job } err = client.Execute(ctx, op, &respData) return respData.Job, err } func UserWebhooks(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (userWebhooks *WebhookSubscriptionCursor, err error) { op := gqlclient.NewOperation("query userWebhooks ($cursor: Cursor) {\n\tuserWebhooks(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\turl\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { UserWebhooks *WebhookSubscriptionCursor } err = client.Execute(ctx, op, &respData) return respData.UserWebhooks, err } func CompleteSecrets(client *gqlclient.Client, ctx context.Context) (secrets *SecretCursor, err error) { op := gqlclient.NewOperation("query completeSecrets {\n\tsecrets {\n\t\tresults {\n\t\t\tuuid\n\t\t\tname\n\t\t}\n\t}\n}\n") var respData struct { Secrets *SecretCursor } err = client.Execute(ctx, op, &respData) return respData.Secrets, err } hut-0.6.0/srht/buildssrht/operations.graphql000066400000000000000000000065401463710650600212400ustar00rootroot00000000000000mutation submit( $manifest: String! $tags: [String!] $note: String $visibility: Visibility ) { submit( manifest: $manifest tags: $tags note: $note visibility: $visibility ) { id owner { canonicalName } } } mutation cancel($jobId: Int!) { cancel(jobId: $jobId) { id } } mutation createUserWebhook($config: UserWebhookInput!) { createUserWebhook(config: $config) { id } } mutation deleteUserWebhook($id: Int!) { deleteUserWebhook(id: $id) { id } } mutation shareSecret($uuid: String!, $user: String!) { shareSecret(uuid: $uuid, user: $user) { uuid } } query monitor($id: Int!) { job(id: $id) { status log { fullURL } tasks { name status log { fullURL } } } } query manifest($id: Int!) { job(id: $id) { manifest owner { canonicalName } visibility } } query jobIDs { jobs { results { id } } } query jobs($cursor: Cursor) { jobs(cursor: $cursor) { ...jobs } } query jobsByUser($username: String!, $cursor: Cursor) { userByName(username: $username) { jobs(cursor: $cursor) { ...jobs } } } fragment jobs on JobCursor { results { id status note tags tasks { name status } } cursor } query exportJob($id: Int!) { job(id: $id) { ...jobExport } } query exportJobs($cursor: Cursor) { jobs(cursor: $cursor) { results { ...jobExport } cursor } } fragment jobExport on Job { id status note tags visibility log { fullURL } tasks { name status log { fullURL } } } query show($id: Int!) { job(id: $id) { id status note tags log { fullURL } tasks { name status log { fullURL } } group { jobs { id status } } } } query secrets($cursor: Cursor) { secrets(cursor: $cursor) { results { created uuid name fromUser { canonicalName } __typename ... on SecretFile { path mode } } cursor } } query getSSHInfo($id: Int!) { job(id: $id) { id runner } version { settings { sshUser } } } query runningJobs { jobs { results { id status note tags } } } query artifacts($id: Int!) { job(id: $id) { artifacts { path size url } } } query userWebhooks($cursor: Cursor) { userWebhooks(cursor: $cursor) { results { id url } cursor } } query completeSecrets { secrets { results { uuid name } } } hut-0.6.0/srht/buildssrht/schema.graphqls000066400000000000000000000302751463710650600205020ustar00rootroot00000000000000# This schema definition is available in the public domain, or under the terms # of CC-0, at your choice. scalar Time # %Y-%m-%dT%H:%M:%SZ scalar Binary # base64'd string scalar Cursor scalar File "Used to provide a human-friendly description of an access scope" directive @scopehelp(details: String!) on ENUM_VALUE """ This is used to decorate fields which are only accessible with a personal access token, and are not available to clients using OAuth 2.0 access tokens. """ directive @private on FIELD_DEFINITION """ This is used to decorate fields which are for internal use, and are not available to normal API users. """ directive @internal on FIELD_DEFINITION enum AccessScope { PROFILE @scopehelp(details: "profile information") JOBS @scopehelp(details: "build jobs") LOGS @scopehelp(details: "build logs") SECRETS @scopehelp(details: "stored secrets") } enum AccessKind { RO @scopehelp(details: "read") RW @scopehelp(details: "read and write") } """ Decorates fields for which access requires a particular OAuth 2.0 scope with read or write access. """ directive @access(scope: AccessScope!, kind: AccessKind!) on FIELD_DEFINITION """ This used to decorate private resolvers which are only accessible to build workers, and are used to facilitate the build process. """ directive @worker on FIELD_DEFINITION # https://semver.org type Version { major: Int! minor: Int! patch: Int! """ If this API version is scheduled for deprecation, this is the date on which it will stop working; or null if this API version is not scheduled for deprecation. """ deprecationDate: Time # Config settings settings: Settings! } # Instance specific settings type Settings { sshUser: String! buildTimeout: String! } interface Entity { id: Int! created: Time! updated: Time! """ The canonical name of this entity. For users, this is their username prefixed with '~'. Additional entity types will be supported in the future. """ canonicalName: String! } type User implements Entity { id: Int! created: Time! updated: Time! canonicalName: String! username: String! email: String! url: String location: String bio: String "Jobs submitted by this user." jobs(cursor: Cursor): JobCursor! @access(scope: JOBS, kind: RO) } enum JobStatus { PENDING QUEUED RUNNING SUCCESS FAILED TIMEOUT CANCELLED } enum Visibility { PUBLIC UNLISTED PRIVATE } type Job { id: Int! created: Time! updated: Time! status: JobStatus! manifest: String! note: String tags: [String!]! visibility: Visibility! "Name of the build image" image: String! """ Name of the build runner which picked up this job, or null if the job is pending or queued. """ runner: String owner: Entity! @access(scope: PROFILE, kind: RO) group: JobGroup tasks: [Task!]! artifacts: [Artifact!]! "The job's top-level log file, not associated with any tasks" log: Log @access(scope: LOGS, kind: RO) "List of secrets available to this job, or null if they were disabled" secrets: [Secret!] @access(scope: SECRETS, kind: RO) } type Log { "The most recently written 128 KiB of the build log." last128KiB: String! """ The URL at which the full build log can be downloaded with an authenticated GET request (text/plain). """ fullURL: String! } type Artifact { id: Int! created: Time! "Original path in the guest" path: String! "Size in bytes" size: Int! "URL at which the artifact may be downloaded, or null if pruned" url: String } """ A cursor for enumerating a list of jobs If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type JobCursor { results: [Job!]! cursor: Cursor } type JobGroup { id: Int! created: Time! note: String owner: Entity! @access(scope: PROFILE, kind: RO) jobs: [Job!]! triggers: [Trigger!]! } enum TaskStatus { PENDING RUNNING SUCCESS FAILED SKIPPED } type Task { id: Int! created: Time! updated: Time! name: String! status: TaskStatus! log: Log @access(scope: LOGS, kind: RO) job: Job! } enum TriggerCondition { SUCCESS FAILURE ALWAYS } """ Triggers run upon the completion of all of the jobs in a job group. Note that these triggers are distinct from the ones defined by an individual job's build manifest, but are similar in functionality. """ interface Trigger { condition: TriggerCondition! } type EmailTrigger implements Trigger { condition: TriggerCondition! to: String! cc: String inReplyTo: String } type WebhookTrigger implements Trigger { condition: TriggerCondition! url: String! } interface Secret { id: Int! created: Time! uuid: String! name: String "Set when this secret was copied from another user account" fromUser: Entity } """ A cursor for enumerating a list of secrets If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type SecretCursor { results: [Secret!]! cursor: Cursor } type SSHKey implements Secret { id: Int! created: Time! uuid: String! name: String fromUser: Entity privateKey: Binary! @worker } type PGPKey implements Secret { id: Int! created: Time! uuid: String! name: String fromUser: Entity privateKey: Binary! @worker } type SecretFile implements Secret { id: Int! created: Time! uuid: String! name: String fromUser: Entity path: String! mode: Int! data: Binary! @worker } type OAuthClient { uuid: String! } enum WebhookEvent { JOB_CREATED } interface WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! """ If this webhook was registered by an authorized OAuth 2.0 client, this field is non-null. """ client: OAuthClient @private "All deliveries which have been sent to this webhook." deliveries(cursor: Cursor): WebhookDeliveryCursor! "Returns a sample payload for this subscription, for testing purposes" sample(event: WebhookEvent!): String! } type UserWebhookSubscription implements WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! client: OAuthClient @private deliveries(cursor: Cursor): WebhookDeliveryCursor! sample(event: WebhookEvent!): String! } type WebhookDelivery { uuid: String! date: Time! event: WebhookEvent! subscription: WebhookSubscription! requestBody: String! """ These details are provided only after a response is received from the remote server. If a response is sent whose Content-Type is not text/*, or cannot be decoded as UTF-8, the response body will be null. It will be truncated after 64 KiB. """ responseBody: String responseHeaders: String responseStatus: Int } interface WebhookPayload { uuid: String! event: WebhookEvent! date: Time! } type JobEvent implements WebhookPayload { uuid: String! event: WebhookEvent! date: Time! job: Job! } """ A cursor for enumerating a list of webhook deliveries If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type WebhookDeliveryCursor { results: [WebhookDelivery!]! cursor: Cursor } """ A cursor for enumerating a list of webhook subscriptions If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type WebhookSubscriptionCursor { results: [WebhookSubscription!]! cursor: Cursor } type Query { "Returns API version information." version: Version! "Returns the authenticated user." me: User! @access(scope: PROFILE, kind: RO) "Returns a specific user." userByID(id: Int!): User @access(scope: PROFILE, kind: RO) userByName(username: String!): User @access(scope: PROFILE, kind: RO) "Returns jobs submitted by the authenticated user." jobs(cursor: Cursor): JobCursor! @access(scope: JOBS, kind: RO) "Returns information about a specific job." job(id: Int!): Job @access(scope: JOBS, kind: RO) "Returns secrets owned by the authenticated user." secrets(cursor: Cursor): SecretCursor! @access(scope: SECRETS, kind: RO) """ Returns a list of user webhook subscriptions. For clients authenticated with a personal access token, this returns all webhooks configured by all GraphQL clients for your account. For clients authenticated with an OAuth 2.0 access token, this returns only webhooks registered for your client. """ userWebhooks(cursor: Cursor): WebhookSubscriptionCursor! "Returns details of a user webhook subscription by its ID." userWebhook(id: Int!): WebhookSubscription """ Returns information about the webhook currently being processed. This is not valid during normal queries over HTTP, and will return an error if used outside of a webhook context. """ webhook: WebhookPayload! } enum TriggerType { EMAIL WEBHOOK } input EmailTriggerInput { to: String! cc: String inReplyTo: String } input WebhookTriggerInput { url: String! } input TriggerInput { type: TriggerType! condition: TriggerCondition! email: EmailTriggerInput webhook: WebhookTriggerInput } input UserWebhookInput { url: String! events: [WebhookEvent!]! query: String! } type Mutation { """ Submits a new job to the queue. 'secrets' may be set to false to disable secrets for this build. If unspecified, secrets are enabled if at least one is specified in the manifest and the SECRETS:RO grant is available. Enabling secrets requires the SECRETS:RO grant. 'execute' may be set to false to defer queueing this job. Builds are executed immediately if unspecified. """ submit(manifest: String!, tags: [String!] note: String, secrets: Boolean, execute: Boolean, visibility: Visibility): Job! @access(scope: JOBS, kind: RW) "Queues a pending job." start(jobID: Int!): Job @access(scope: JOBS, kind: RW) "Cancels a submitted job." cancel(jobId: Int!): Job @access(scope: JOBS, kind: RW) """ Creates a job group from several pending jobs. 'execute' may be set to false to defer queueing this job. The job group is executed immediately if unspecified. """ createGroup(jobIds: [Int!]! triggers: [TriggerInput!], execute: Boolean, note: String): JobGroup! @access(scope: JOBS, kind: RW) "Starts a pending job group." startGroup(groupId: Int!): JobGroup @access(scope: JOBS, kind: RW) "Copies a secret to the target user account." shareSecret(uuid: String!, user: String!): Secret! @access(scope: SECRETS, kind: RW) ### ### The following resolvers are for internal worker use "Claims a job." claim(jobId: Int!): Job @worker "Updates job status." updateJob(jobId: Int!, status: JobStatus!): Job @worker "Updates task status." updateTask(taskId: Int!, status: TaskStatus!): Job @worker "Uploads a build artifact." createArtifact(jobId: Int!, path: String!, contents: File!): Artifact @worker """ Creates a new user webhook subscription. When an event from the provided list of events occurs, the 'query' parameter (a GraphQL query) will be evaluated and the results will be sent to the provided URL as the body of an HTTP POST request. The list of events must include at least one event, and no duplicates. This query is evaluated in the webhook context, such that query { webhook } may be used to access details of the event which trigged the webhook. The query may not make any mutations. """ createUserWebhook(config: UserWebhookInput!): WebhookSubscription! """ Deletes a user webhook. Any events already queued may still be delivered after this request completes. Clients authenticated with a personal access token may delete any webhook registered for their account, but authorized OAuth 2.0 clients may only delete their own webhooks. Manually deleting a webhook configured by a third-party client may cause unexpected behavior with the third-party integration. """ deleteUserWebhook(id: Int!): WebhookSubscription! """ Deletes the authenticated user's account. Internal use only. """ deleteUser: Int! @internal } hut-0.6.0/srht/buildssrht/strings.go000066400000000000000000000046601463710650600175160ustar00rootroot00000000000000package buildssrht import ( "fmt" "strings" "git.sr.ht/~xenrox/hut/termfmt" ) func (status JobStatus) Icon() string { switch status { case JobStatusPending, JobStatusQueued: return "○" case JobStatusRunning: return "●" case JobStatusSuccess: return "✔" case JobStatusFailed: return "✗" case JobStatusTimeout: return "⏱️" case JobStatusCancelled: return "🛑" default: panic(fmt.Sprintf("unknown job status: %q", status)) } } func (status JobStatus) TermStyle() termfmt.Style { switch status { case JobStatusPending, JobStatusQueued, JobStatusRunning: return termfmt.Blue case JobStatusSuccess: return termfmt.Green case JobStatusFailed, JobStatusTimeout: return termfmt.Red case JobStatusCancelled: return termfmt.Yellow default: panic(fmt.Sprintf("unknown job status: %q", status)) } } func (status JobStatus) TermIcon() string { return status.TermStyle().String(status.Icon()) } func (status JobStatus) TermString() string { return status.TermStyle().Sprintf("%s %s", status.Icon(), string(status)) } func (status TaskStatus) Icon() string { switch status { case TaskStatusPending: return "○" case TaskStatusRunning: return "●" case TaskStatusSuccess: return "✔" case TaskStatusFailed: return "✗" case TaskStatusSkipped: return "⏩" default: panic(fmt.Sprintf("unknown task status: %q", status)) } } func (status TaskStatus) TermStyle() termfmt.Style { switch status { case TaskStatusPending, TaskStatusRunning: return termfmt.Blue case TaskStatusSuccess: return termfmt.Green case TaskStatusFailed: return termfmt.Red case TaskStatusSkipped: return termfmt.Yellow default: panic(fmt.Sprintf("unknown task status: %q", status)) } } func (status TaskStatus) TermIcon() string { return status.TermStyle().String(status.Icon()) } func ParseUserEvents(events []string) ([]WebhookEvent, error) { var whEvents []WebhookEvent for _, event := range events { switch strings.ToLower(event) { case "job_created": whEvents = append(whEvents, WebhookEventJobCreated) default: return whEvents, fmt.Errorf("invalid event: %q", event) } } return whEvents, nil } func ParseVisibility(s string) (Visibility, error) { switch strings.ToLower(s) { case "unlisted": return VisibilityUnlisted, nil case "private": return VisibilityPrivate, nil case "public": return VisibilityPublic, nil default: return "", fmt.Errorf("invalid visibility: %s", s) } } hut-0.6.0/srht/generate.go000066400000000000000000000024141463710650600154270ustar00rootroot00000000000000//go:build generate // +build generate package srht import ( _ "git.sr.ht/~emersion/gqlclient/cmd/gqlclientgen" ) //go:generate go run git.sr.ht/~emersion/gqlclient/cmd/gqlclientgen -s pastesrht/schema.graphqls -q pastesrht/operations.graphql -o pastesrht/gql.go //go:generate go run git.sr.ht/~emersion/gqlclient/cmd/gqlclientgen -s buildssrht/schema.graphqls -q buildssrht/operations.graphql -o buildssrht/gql.go //go:generate go run git.sr.ht/~emersion/gqlclient/cmd/gqlclientgen -s gitsrht/schema.graphqls -q gitsrht/operations.graphql -o gitsrht/gql.go //go:generate go run git.sr.ht/~emersion/gqlclient/cmd/gqlclientgen -s pagessrht/schema.graphqls -q pagessrht/operations.graphql -o pagessrht/gql.go //go:generate go run git.sr.ht/~emersion/gqlclient/cmd/gqlclientgen -s metasrht/schema.graphqls -q metasrht/operations.graphql -o metasrht/gql.go //go:generate go run git.sr.ht/~emersion/gqlclient/cmd/gqlclientgen -s listssrht/schema.graphqls -q listssrht/operations.graphql -o listssrht/gql.go //go:generate go run git.sr.ht/~emersion/gqlclient/cmd/gqlclientgen -s hgsrht/schema.graphqls -q hgsrht/operations.graphql -o hgsrht/gql.go //go:generate go run git.sr.ht/~emersion/gqlclient/cmd/gqlclientgen -s todosrht/schema.graphqls -q todosrht/operations.graphql -o todosrht/gql.go hut-0.6.0/srht/gitsrht/000077500000000000000000000000001463710650600147715ustar00rootroot00000000000000hut-0.6.0/srht/gitsrht/gql.go000066400000000000000000001001021463710650600160750ustar00rootroot00000000000000// Code generated by gqlclientgen - DO NOT EDIT. package gitsrht import ( "context" "encoding/json" "fmt" gqlclient "git.sr.ht/~emersion/gqlclient" ) type ACL struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Repository *Repository `json:"repository"` Entity *Entity `json:"entity"` Mode *AccessMode `json:"mode,omitempty"` } // A cursor for enumerating access control list entries // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type ACLCursor struct { Results []ACL `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type AccessKind string const ( AccessKindRo AccessKind = "RO" AccessKindRw AccessKind = "RW" ) type AccessMode string const ( // Read-only AccessModeRo AccessMode = "RO" // Read/write AccessModeRw AccessMode = "RW" ) type AccessScope string const ( AccessScopeProfile AccessScope = "PROFILE" AccessScopeRepositories AccessScope = "REPOSITORIES" AccessScopeObjects AccessScope = "OBJECTS" AccessScopeAcls AccessScope = "ACLS" ) // Arbitrary file attached to a git repository type Artifact struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Filename string `json:"filename"` Checksum string `json:"checksum"` Size int32 `json:"size"` Url string `json:"url"` } // A cursor for enumerating artifacts // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type ArtifactCursor struct { Results []Artifact `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type BinaryBlob struct { Type ObjectType `json:"type"` Id string `json:"id"` ShortId string `json:"shortId"` Raw string `json:"raw"` Base64 string `json:"base64"` } func (*BinaryBlob) isObject() {} func (*BinaryBlob) isBlob() {} type Blob struct { Id string `json:"id"` // Underlying value of the GraphQL interface Value BlobValue `json:"-"` } func (base *Blob) UnmarshalJSON(b []byte) error { type Raw Blob var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "TextBlob": base.Value = new(TextBlob) case "BinaryBlob": base.Value = new(BinaryBlob) case "": return nil default: return fmt.Errorf("gqlclient: interface Blob: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // BlobValue is one of: TextBlob | BinaryBlob type BlobValue interface { isBlob() } type Commit struct { Type ObjectType `json:"type"` Id string `json:"id"` ShortId string `json:"shortId"` Raw string `json:"raw"` Author *Signature `json:"author"` Committer *Signature `json:"committer"` Message string `json:"message"` Tree *Tree `json:"tree"` Parents []Commit `json:"parents"` Diff string `json:"diff"` } func (*Commit) isObject() {} // A cursor for enumerating commits // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type CommitCursor struct { Results []Commit `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type Cursor string type Entity struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` // The canonical name of this entity. For users, this is their username // prefixed with '~'. Additional entity types will be supported in the future. CanonicalName string `json:"canonicalName"` // Returns a specific repository owned by the entity. Repository *Repository `json:"repository,omitempty"` // Returns a list of repositories owned by the entity. Repositories *RepositoryCursor `json:"repositories"` // Underlying value of the GraphQL interface Value EntityValue `json:"-"` } func (base *Entity) UnmarshalJSON(b []byte) error { type Raw Entity var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "User": base.Value = new(User) case "": return nil default: return fmt.Errorf("gqlclient: interface Entity: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // EntityValue is one of: User type EntityValue interface { isEntity() } // Describes the status of optional features type Features struct { Artifacts bool `json:"artifacts"` } type Filter struct { // Number of results to return. Count *int32 `json:"count,omitempty"` // Search terms. The exact meaning varies by usage, but generally these are // compatible with the web UI's search syntax. Search *string `json:"search,omitempty"` } type OAuthClient struct { Uuid string `json:"uuid"` } type Object struct { Type ObjectType `json:"type"` Id string `json:"id"` ShortId string `json:"shortId"` // Raw git object, base64 encoded Raw string `json:"raw"` // Underlying value of the GraphQL interface Value ObjectValue `json:"-"` } func (base *Object) UnmarshalJSON(b []byte) error { type Raw Object var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "Commit": base.Value = new(Commit) case "Tree": base.Value = new(Tree) case "TextBlob": base.Value = new(TextBlob) case "BinaryBlob": base.Value = new(BinaryBlob) case "Tag": base.Value = new(Tag) case "": return nil default: return fmt.Errorf("gqlclient: interface Object: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // ObjectValue is one of: Commit | Tree | TextBlob | BinaryBlob | Tag type ObjectValue interface { isObject() } type ObjectType string const ( ObjectTypeCommit ObjectType = "COMMIT" ObjectTypeTree ObjectType = "TREE" ObjectTypeBlob ObjectType = "BLOB" ObjectTypeTag ObjectType = "TAG" ) type Reference struct { Name string `json:"name"` Target string `json:"target"` Follow *Object `json:"follow,omitempty"` Artifacts *ArtifactCursor `json:"artifacts"` } // A cursor for enumerating a list of references // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type ReferenceCursor struct { Results []Reference `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type RepoInput struct { Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` Visibility *Visibility `json:"visibility,omitempty"` // Updates the custom README associated with this repository. Note that the // provided HTML will be sanitized when displayed on the web; see // https://man.sr.ht/markdown/#post-processing Readme *string `json:"readme,omitempty"` // Updates the repository HEAD reference, which serves as the default branch. // Must be a valid branch name. HEAD *string `json:"HEAD,omitempty"` } type Repository struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` Owner *Entity `json:"owner"` Name string `json:"name"` Description *string `json:"description,omitempty"` Visibility Visibility `json:"visibility"` // The repository's custom README, if set. // // NOTICE: This returns unsanitized HTML. It is the client's responsibility to // sanitize this for display on the web, if so desired. Readme *string `json:"readme,omitempty"` // The access that applies to this user for this repository Access AccessMode `json:"access"` Acls *ACLCursor `json:"acls"` Objects []*Object `json:"objects"` References *ReferenceCursor `json:"references"` // The HEAD reference for this repository (equivalent to the default branch) HEAD *Reference `json:"HEAD,omitempty"` // Returns a list of comments sorted by committer time (similar to `git log`'s // default ordering). // // If `from` is specified, it is interpreted as a revspec to start logging // from. A clever reader may notice that using commits[-1].from + "^" as the // from parameter is equivalent to passing the cursor to the next call. Log *CommitCursor `json:"log"` // Returns a tree entry for a given path, at the given revspec. Path *TreeEntry `json:"path,omitempty"` // Returns the commit for a given revspec. Revparse_single *Commit `json:"revparse_single,omitempty"` } // A cursor for enumerating a list of repositories // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type RepositoryCursor struct { Results []Repository `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type RepositoryEvent struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` Repository *Repository `json:"repository"` } func (*RepositoryEvent) isWebhookPayload() {} // Instance specific settings type Settings struct { SshUser string `json:"sshUser"` } type Signature struct { Name string `json:"name"` Email string `json:"email"` Time gqlclient.Time `json:"time"` } type Tag struct { Type ObjectType `json:"type"` Id string `json:"id"` ShortId string `json:"shortId"` Raw string `json:"raw"` Target *Object `json:"target"` Name string `json:"name"` Tagger *Signature `json:"tagger"` Message *string `json:"message,omitempty"` } func (*Tag) isObject() {} type TextBlob struct { Type ObjectType `json:"type"` Id string `json:"id"` ShortId string `json:"shortId"` Raw string `json:"raw"` Text string `json:"text"` } func (*TextBlob) isObject() {} func (*TextBlob) isBlob() {} type Tree struct { Type ObjectType `json:"type"` Id string `json:"id"` ShortId string `json:"shortId"` Raw string `json:"raw"` Entries *TreeEntryCursor `json:"entries"` Entry *TreeEntry `json:"entry,omitempty"` } func (*Tree) isObject() {} type TreeEntry struct { Id string `json:"id"` Name string `json:"name"` Object *Object `json:"object"` // Unix-style file mode, i.e. 0755 or 0644 (octal) Mode int32 `json:"mode"` } // A cursor for enumerating tree entries // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type TreeEntryCursor struct { Results []TreeEntry `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type User struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` CanonicalName string `json:"canonicalName"` Username string `json:"username"` Email string `json:"email"` Url *string `json:"url,omitempty"` Location *string `json:"location,omitempty"` Bio *string `json:"bio,omitempty"` Repository *Repository `json:"repository,omitempty"` Repositories *RepositoryCursor `json:"repositories"` } func (*User) isEntity() {} type UserWebhookInput struct { Url string `json:"url"` Events []WebhookEvent `json:"events"` Query string `json:"query"` } type UserWebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` Client *OAuthClient `json:"client,omitempty"` Deliveries *WebhookDeliveryCursor `json:"deliveries"` Sample string `json:"sample"` } func (*UserWebhookSubscription) isWebhookSubscription() {} type Version struct { Major int32 `json:"major"` Minor int32 `json:"minor"` Patch int32 `json:"patch"` // If this API version is scheduled for deprecation, this is the date on which // it will stop working; or null if this API version is not scheduled for // deprecation. DeprecationDate gqlclient.Time `json:"deprecationDate,omitempty"` // Optional features Features *Features `json:"features"` // Config settings Settings *Settings `json:"settings"` } type Visibility string const ( // Visible to everyone, listed on your profile VisibilityPublic Visibility = "PUBLIC" // Visible to everyone (if they know the URL), not listed on your profile VisibilityUnlisted Visibility = "UNLISTED" // Not visible to anyone except those explicitly added to the access list VisibilityPrivate Visibility = "PRIVATE" ) type WebhookDelivery struct { Uuid string `json:"uuid"` Date gqlclient.Time `json:"date"` Event WebhookEvent `json:"event"` Subscription *WebhookSubscription `json:"subscription"` RequestBody string `json:"requestBody"` // These details are provided only after a response is received from the // remote server. If a response is sent whose Content-Type is not text/*, or // cannot be decoded as UTF-8, the response body will be null. It will be // truncated after 64 KiB. ResponseBody *string `json:"responseBody,omitempty"` ResponseHeaders *string `json:"responseHeaders,omitempty"` ResponseStatus *int32 `json:"responseStatus,omitempty"` } // A cursor for enumerating a list of webhook deliveries // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type WebhookDeliveryCursor struct { Results []WebhookDelivery `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type WebhookEvent string const ( WebhookEventRepoCreated WebhookEvent = "REPO_CREATED" WebhookEventRepoUpdate WebhookEvent = "REPO_UPDATE" WebhookEventRepoDeleted WebhookEvent = "REPO_DELETED" ) type WebhookPayload struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` // Underlying value of the GraphQL interface Value WebhookPayloadValue `json:"-"` } func (base *WebhookPayload) UnmarshalJSON(b []byte) error { type Raw WebhookPayload var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "RepositoryEvent": base.Value = new(RepositoryEvent) case "": return nil default: return fmt.Errorf("gqlclient: interface WebhookPayload: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // WebhookPayloadValue is one of: RepositoryEvent type WebhookPayloadValue interface { isWebhookPayload() } type WebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` // If this webhook was registered by an authorized OAuth 2.0 client, this // field is non-null. Client *OAuthClient `json:"client,omitempty"` // All deliveries which have been sent to this webhook. Deliveries *WebhookDeliveryCursor `json:"deliveries"` // Returns a sample payload for this subscription, for testing purposes Sample string `json:"sample"` // Underlying value of the GraphQL interface Value WebhookSubscriptionValue `json:"-"` } func (base *WebhookSubscription) UnmarshalJSON(b []byte) error { type Raw WebhookSubscription var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "UserWebhookSubscription": base.Value = new(UserWebhookSubscription) case "": return nil default: return fmt.Errorf("gqlclient: interface WebhookSubscription: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // WebhookSubscriptionValue is one of: UserWebhookSubscription type WebhookSubscriptionValue interface { isWebhookSubscription() } // A cursor for enumerating a list of webhook subscriptions // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type WebhookSubscriptionCursor struct { Results []WebhookSubscription `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } func RepositoryIDByName(client *gqlclient.Client, ctx context.Context, name string) (me *User, err error) { op := gqlclient.NewOperation("query repositoryIDByName ($name: String!) {\n\tme {\n\t\trepository(name: $name) {\n\t\t\tid\n\t\t}\n\t}\n}\n") op.Var("name", name) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func RepositoryIDByUser(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) { op := gqlclient.NewOperation("query repositoryIDByUser ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\trepository(name: $name) {\n\t\t\tid\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func ListArtifacts(client *gqlclient.Client, ctx context.Context, name string) (me *User, err error) { op := gqlclient.NewOperation("query listArtifacts ($name: String!) {\n\tme {\n\t\trepository(name: $name) {\n\t\t\t... artifacts\n\t\t}\n\t}\n}\nfragment artifacts on Repository {\n\treferences {\n\t\tresults {\n\t\t\tname\n\t\t\tartifacts {\n\t\t\t\tresults {\n\t\t\t\t\tid\n\t\t\t\t\tfilename\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("name", name) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func ListArtifactsByUser(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) { op := gqlclient.NewOperation("query listArtifactsByUser ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\trepository(name: $name) {\n\t\t\t... artifacts\n\t\t}\n\t}\n}\nfragment artifacts on Repository {\n\treferences {\n\t\tresults {\n\t\t\tname\n\t\t\tartifacts {\n\t\t\t\tresults {\n\t\t\t\t\tid\n\t\t\t\t\tfilename\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func RepositoryByName(client *gqlclient.Client, ctx context.Context, name string) (me *User, err error) { op := gqlclient.NewOperation("query repositoryByName ($name: String!) {\n\tme {\n\t\trepository(name: $name) {\n\t\t\t... repository\n\t\t}\n\t}\n}\nfragment repository on Repository {\n\tname\n\tdescription\n\tvisibility\n\treferences {\n\t\tresults {\n\t\t\tname\n\t\t}\n\t}\n\tlog {\n\t\tresults {\n\t\t\tshortId\n\t\t\tauthor {\n\t\t\t\tname\n\t\t\t\temail\n\t\t\t\ttime\n\t\t\t}\n\t\t\tmessage\n\t\t}\n\t}\n}\n") op.Var("name", name) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func RepositoryByUser(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) { op := gqlclient.NewOperation("query repositoryByUser ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\trepository(name: $name) {\n\t\t\t... repository\n\t\t}\n\t}\n}\nfragment repository on Repository {\n\tname\n\tdescription\n\tvisibility\n\treferences {\n\t\tresults {\n\t\t\tname\n\t\t}\n\t}\n\tlog {\n\t\tresults {\n\t\t\tshortId\n\t\t\tauthor {\n\t\t\t\tname\n\t\t\t\temail\n\t\t\t\ttime\n\t\t\t}\n\t\t\tmessage\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func Repositories(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (repositories *RepositoryCursor, err error) { op := gqlclient.NewOperation("query repositories ($cursor: Cursor) {\n\trepositories(cursor: $cursor) {\n\t\t... repos\n\t}\n}\nfragment repos on RepositoryCursor {\n\tresults {\n\t\tname\n\t\tdescription\n\t\tvisibility\n\t\towner {\n\t\t\tcanonicalName\n\t\t}\n\t}\n\tcursor\n}\n") op.Var("cursor", cursor) var respData struct { Repositories *RepositoryCursor } err = client.Execute(ctx, op, &respData) return respData.Repositories, err } func RepositoriesByUser(client *gqlclient.Client, ctx context.Context, username string, cursor *Cursor) (user *User, err error) { op := gqlclient.NewOperation("query repositoriesByUser ($username: String!, $cursor: Cursor) {\n\tuser(username: $username) {\n\t\trepositories(cursor: $cursor) {\n\t\t\t... repos\n\t\t}\n\t}\n}\nfragment repos on RepositoryCursor {\n\tresults {\n\t\tname\n\t\tdescription\n\t\tvisibility\n\t\towner {\n\t\t\tcanonicalName\n\t\t}\n\t}\n\tcursor\n}\n") op.Var("username", username) op.Var("cursor", cursor) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func ExportRepository(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) { op := gqlclient.NewOperation("query exportRepository ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\trepository(name: $name) {\n\t\t\t... repositoryExport\n\t\t}\n\t}\n}\nfragment repositoryExport on Repository {\n\tname\n\towner {\n\t\tcanonicalName\n\t}\n\tdescription\n\tvisibility\n\treadme\n\tHEAD {\n\t\tname\n\t}\n}\n") op.Var("username", username) op.Var("name", name) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func ExportRepositories(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (repositories *RepositoryCursor, err error) { op := gqlclient.NewOperation("query exportRepositories ($cursor: Cursor) {\n\trepositories(cursor: $cursor) {\n\t\tresults {\n\t\t\t... repositoryExport\n\t\t}\n\t\tcursor\n\t}\n}\nfragment repositoryExport on Repository {\n\tname\n\towner {\n\t\tcanonicalName\n\t}\n\tdescription\n\tvisibility\n\treadme\n\tHEAD {\n\t\tname\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { Repositories *RepositoryCursor } err = client.Execute(ctx, op, &respData) return respData.Repositories, err } func SshSettings(client *gqlclient.Client, ctx context.Context) (version *Version, err error) { op := gqlclient.NewOperation("query sshSettings {\n\tversion {\n\t\tsettings {\n\t\t\tsshUser\n\t\t}\n\t}\n}\n") var respData struct { Version *Version } err = client.Execute(ctx, op, &respData) return respData.Version, err } func CompleteRepositories(client *gqlclient.Client, ctx context.Context) (repositories *RepositoryCursor, err error) { op := gqlclient.NewOperation("query completeRepositories {\n\trepositories {\n\t\tresults {\n\t\t\tname\n\t\t}\n\t}\n}\n") var respData struct { Repositories *RepositoryCursor } err = client.Execute(ctx, op, &respData) return respData.Repositories, err } func RevsByRepoName(client *gqlclient.Client, ctx context.Context, name string) (me *User, err error) { op := gqlclient.NewOperation("query revsByRepoName ($name: String!) {\n\tme {\n\t\trepository(name: $name) {\n\t\t\t... revs\n\t\t}\n\t}\n}\nfragment revs on Repository {\n\treferences {\n\t\tresults {\n\t\t\tname\n\t\t}\n\t}\n}\n") op.Var("name", name) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func RevsByUser(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) { op := gqlclient.NewOperation("query revsByUser ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\trepository(name: $name) {\n\t\t\t... revs\n\t\t}\n\t}\n}\nfragment revs on Repository {\n\treferences {\n\t\tresults {\n\t\t\tname\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func AclByRepoName(client *gqlclient.Client, ctx context.Context, name string, cursor *Cursor) (me *User, err error) { op := gqlclient.NewOperation("query aclByRepoName ($name: String!, $cursor: Cursor) {\n\tme {\n\t\t... acl\n\t}\n}\nfragment acl on User {\n\trepository(name: $name) {\n\t\tacls(cursor: $cursor) {\n\t\t\tresults {\n\t\t\t\tid\n\t\t\t\tcreated\n\t\t\t\tentity {\n\t\t\t\t\tcanonicalName\n\t\t\t\t}\n\t\t\t\tmode\n\t\t\t}\n\t\t\tcursor\n\t\t}\n\t}\n}\n") op.Var("name", name) op.Var("cursor", cursor) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func AclByUser(client *gqlclient.Client, ctx context.Context, username string, name string, cursor *Cursor) (user *User, err error) { op := gqlclient.NewOperation("query aclByUser ($username: String!, $name: String!, $cursor: Cursor) {\n\tuser(username: $username) {\n\t\t... acl\n\t}\n}\nfragment acl on User {\n\trepository(name: $name) {\n\t\tacls(cursor: $cursor) {\n\t\t\tresults {\n\t\t\t\tid\n\t\t\t\tcreated\n\t\t\t\tentity {\n\t\t\t\t\tcanonicalName\n\t\t\t\t}\n\t\t\t\tmode\n\t\t\t}\n\t\t\tcursor\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) op.Var("cursor", cursor) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func UserWebhooks(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (userWebhooks *WebhookSubscriptionCursor, err error) { op := gqlclient.NewOperation("query userWebhooks ($cursor: Cursor) {\n\tuserWebhooks(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\turl\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { UserWebhooks *WebhookSubscriptionCursor } err = client.Execute(ctx, op, &respData) return respData.UserWebhooks, err } func CompleteCoMaintainers(client *gqlclient.Client, ctx context.Context, name string) (me *User, err error) { op := gqlclient.NewOperation("query completeCoMaintainers ($name: String!) {\n\tme {\n\t\trepository(name: $name) {\n\t\t\tacls {\n\t\t\t\tresults {\n\t\t\t\t\tentity {\n\t\t\t\t\t\tcanonicalName\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("name", name) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func UploadArtifact(client *gqlclient.Client, ctx context.Context, repoId int32, revspec string, file gqlclient.Upload) (uploadArtifact *Artifact, err error) { op := gqlclient.NewOperation("mutation uploadArtifact ($repoId: Int!, $revspec: String!, $file: Upload!) {\n\tuploadArtifact(repoId: $repoId, revspec: $revspec, file: $file) {\n\t\tfilename\n\t}\n}\n") op.Var("repoId", repoId) op.Var("revspec", revspec) op.Var("file", file) var respData struct { UploadArtifact *Artifact } err = client.Execute(ctx, op, &respData) return respData.UploadArtifact, err } func DeleteArtifact(client *gqlclient.Client, ctx context.Context, id int32) (deleteArtifact *Artifact, err error) { op := gqlclient.NewOperation("mutation deleteArtifact ($id: Int!) {\n\tdeleteArtifact(id: $id) {\n\t\tfilename\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteArtifact *Artifact } err = client.Execute(ctx, op, &respData) return respData.DeleteArtifact, err } func CreateRepository(client *gqlclient.Client, ctx context.Context, name string, visibility Visibility, description *string, cloneUrl *string) (createRepository *Repository, err error) { op := gqlclient.NewOperation("mutation createRepository ($name: String!, $visibility: Visibility!, $description: String, $cloneUrl: String) {\n\tcreateRepository(name: $name, visibility: $visibility, description: $description, cloneUrl: $cloneUrl) {\n\t\tid\n\t\towner {\n\t\t\tcanonicalName\n\t\t}\n\t\tname\n\t}\n}\n") op.Var("name", name) op.Var("visibility", visibility) op.Var("description", description) op.Var("cloneUrl", cloneUrl) var respData struct { CreateRepository *Repository } err = client.Execute(ctx, op, &respData) return respData.CreateRepository, err } func DeleteRepository(client *gqlclient.Client, ctx context.Context, id int32) (deleteRepository *Repository, err error) { op := gqlclient.NewOperation("mutation deleteRepository ($id: Int!) {\n\tdeleteRepository(id: $id) {\n\t\tname\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteRepository *Repository } err = client.Execute(ctx, op, &respData) return respData.DeleteRepository, err } func UpdateACL(client *gqlclient.Client, ctx context.Context, repoId int32, mode AccessMode, entity string) (updateACL *ACL, err error) { op := gqlclient.NewOperation("mutation updateACL ($repoId: Int!, $mode: AccessMode!, $entity: ID!) {\n\tupdateACL(repoId: $repoId, mode: $mode, entity: $entity) {\n\t\tentity {\n\t\t\tcanonicalName\n\t\t}\n\t}\n}\n") op.Var("repoId", repoId) op.Var("mode", mode) op.Var("entity", entity) var respData struct { UpdateACL *ACL } err = client.Execute(ctx, op, &respData) return respData.UpdateACL, err } func DeleteACL(client *gqlclient.Client, ctx context.Context, id int32) (deleteACL *ACL, err error) { op := gqlclient.NewOperation("mutation deleteACL ($id: Int!) {\n\tdeleteACL(id: $id) {\n\t\tentity {\n\t\t\tcanonicalName\n\t\t}\n\t\trepository {\n\t\t\tname\n\t\t}\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteACL *ACL } err = client.Execute(ctx, op, &respData) return respData.DeleteACL, err } func DeleteUserWebhook(client *gqlclient.Client, ctx context.Context, id int32) (deleteUserWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation deleteUserWebhook ($id: Int!) {\n\tdeleteUserWebhook(id: $id) {\n\t\tid\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteUserWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.DeleteUserWebhook, err } func CreateUserWebhook(client *gqlclient.Client, ctx context.Context, config UserWebhookInput) (createUserWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation createUserWebhook ($config: UserWebhookInput!) {\n\tcreateUserWebhook(config: $config) {\n\t\tid\n\t}\n}\n") op.Var("config", config) var respData struct { CreateUserWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.CreateUserWebhook, err } func UpdateRepository(client *gqlclient.Client, ctx context.Context, id int32, input RepoInput) (updateRepository *Repository, err error) { op := gqlclient.NewOperation("mutation updateRepository ($id: Int!, $input: RepoInput!) {\n\tupdateRepository(id: $id, input: $input) {\n\t\tname\n\t}\n}\n") op.Var("id", id) op.Var("input", input) var respData struct { UpdateRepository *Repository } err = client.Execute(ctx, op, &respData) return respData.UpdateRepository, err } func ClearCustomReadme(client *gqlclient.Client, ctx context.Context, id int32) (updateRepository *Repository, err error) { op := gqlclient.NewOperation("mutation clearCustomReadme ($id: Int!) {\n\tupdateRepository(id: $id, input: {readme:null}) {\n\t\tname\n\t}\n}\n") op.Var("id", id) var respData struct { UpdateRepository *Repository } err = client.Execute(ctx, op, &respData) return respData.UpdateRepository, err } func ClearDescription(client *gqlclient.Client, ctx context.Context, id int32) (updateRepository *Repository, err error) { op := gqlclient.NewOperation("mutation clearDescription ($id: Int!) {\n\tupdateRepository(id: $id, input: {description:null}) {\n\t\tname\n\t}\n}\n") op.Var("id", id) var respData struct { UpdateRepository *Repository } err = client.Execute(ctx, op, &respData) return respData.UpdateRepository, err } hut-0.6.0/srht/gitsrht/methods.go000066400000000000000000000007731463710650600167720ustar00rootroot00000000000000package gitsrht import "strings" func (cursor ReferenceCursor) Tags() []string { return cursor.ReferencesByType("tags") } func (cursor ReferenceCursor) Heads() []string { return cursor.ReferencesByType("heads") } func (cursor ReferenceCursor) ReferencesByType(refType string) []string { var refList []string for _, ref := range cursor.Results { split := strings.SplitN(ref.Name, "/", 3) if len(split) == 3 && split[1] == refType { refList = append(refList, split[2]) } } return refList } hut-0.6.0/srht/gitsrht/operations.graphql000066400000000000000000000127251463710650600205430ustar00rootroot00000000000000query repositoryIDByName($name: String!) { me { repository(name: $name) { id } } } query repositoryIDByUser($username: String!, $name: String!) { user(username: $username) { repository(name: $name) { id } } } query listArtifacts($name: String!) { me { repository(name: $name) { ...artifacts } } } query listArtifactsByUser($username: String!, $name: String!) { user(username: $username) { repository(name: $name) { ...artifacts } } } fragment artifacts on Repository { references { results { name artifacts { results { id filename } } } } } query repositoryByName($name: String!) { me { repository(name: $name) { ...repository } } } query repositoryByUser($username: String!, $name: String!) { user(username: $username) { repository(name: $name) { ...repository } } } fragment repository on Repository { name description visibility references { results { name } } log { results { shortId author { name email time } message } } } query repositories($cursor: Cursor) { repositories(cursor: $cursor) { ...repos } } query repositoriesByUser($username: String!, $cursor: Cursor) { user(username: $username) { repositories(cursor: $cursor) { ...repos } } } fragment repos on RepositoryCursor { results { name description visibility owner { canonicalName } } cursor } query exportRepository($username: String!, $name: String!) { user(username: $username) { repository(name: $name) { ...repositoryExport } } } query exportRepositories($cursor: Cursor) { repositories(cursor: $cursor) { results { ...repositoryExport } cursor } } fragment repositoryExport on Repository { name owner { canonicalName } description visibility readme HEAD { name } } query sshSettings { version { settings { sshUser } } } query completeRepositories { repositories { results { name } } } query revsByRepoName($name: String!) { me { repository(name: $name) { ...revs } } } query revsByUser($username: String!, $name: String!) { user(username: $username) { repository(name: $name) { ...revs } } } fragment revs on Repository { references { results { name } } } query aclByRepoName($name: String!, $cursor: Cursor) { me { ...acl } } query aclByUser($username: String!, $name: String!, $cursor: Cursor) { user(username: $username) { ...acl } } fragment acl on User { repository(name: $name) { acls(cursor: $cursor) { results { id created entity { canonicalName } mode } cursor } } } query userWebhooks($cursor: Cursor) { userWebhooks(cursor: $cursor) { results { id url } cursor } } query completeCoMaintainers($name: String!) { me { repository(name: $name) { acls { results { entity { canonicalName } } } } } } mutation uploadArtifact($repoId: Int!, $revspec: String!, $file: Upload!) { uploadArtifact(repoId: $repoId, revspec: $revspec, file: $file) { filename } } mutation deleteArtifact($id: Int!) { deleteArtifact(id: $id) { filename } } mutation createRepository( $name: String! $visibility: Visibility! $description: String $cloneUrl: String ) { createRepository( name: $name visibility: $visibility description: $description cloneUrl: $cloneUrl ) { id owner { canonicalName } name } } mutation deleteRepository($id: Int!) { deleteRepository(id: $id) { name } } mutation updateACL($repoId: Int!, $mode: AccessMode!, $entity: ID!) { updateACL(repoId: $repoId, mode: $mode, entity: $entity) { entity { canonicalName } } } mutation deleteACL($id: Int!) { deleteACL(id: $id) { entity { canonicalName } repository { name } } } mutation deleteUserWebhook($id: Int!) { deleteUserWebhook(id: $id) { id } } mutation createUserWebhook($config: UserWebhookInput!) { createUserWebhook(config: $config) { id } } mutation updateRepository($id: Int!, $input: RepoInput!) { updateRepository(id: $id, input: $input) { name } } mutation clearCustomReadme($id: Int!) { updateRepository(id: $id, input: { readme: null }) { name } } mutation clearDescription($id: Int!) { updateRepository(id: $id, input: { description: null }) { name } } hut-0.6.0/srht/gitsrht/schema.graphqls000066400000000000000000000354051463710650600200030ustar00rootroot00000000000000# This schema definition is available in the public domain, or under the terms # of CC-0, at your choice. scalar Cursor scalar Time scalar Upload "Used to provide a human-friendly description of an access scope" directive @scopehelp(details: String!) on ENUM_VALUE """ This is used to decorate fields which are only accessible with a personal access token, and are not available to clients using OAuth 2.0 access tokens. """ directive @private on FIELD_DEFINITION """ This used to decorate fields which are for internal use, and are not available to normal API users. """ directive @internal on FIELD_DEFINITION enum AccessScope { PROFILE @scopehelp(details: "profile information") REPOSITORIES @scopehelp(details: "repository metadata") OBJECTS @scopehelp(details: "git objects & references") ACLS @scopehelp(details: "access control lists") } enum AccessKind { RO @scopehelp(details: "read") RW @scopehelp(details: "read and write") } """ Decorates fields for which access requires a particular OAuth 2.0 scope with read or write access. """ directive @access(scope: AccessScope!, kind: AccessKind!) on FIELD_DEFINITION # https://semver.org type Version { major: Int! minor: Int! patch: Int! """ If this API version is scheduled for deprecation, this is the date on which it will stop working; or null if this API version is not scheduled for deprecation. """ deprecationDate: Time "Optional features" features: Features! "Config settings" settings: Settings! } "Describes the status of optional features" type Features { artifacts: Boolean! } "Instance specific settings" type Settings { sshUser: String! } enum AccessMode { "Read-only" RO "Read/write" RW } enum Visibility { "Visible to everyone, listed on your profile" PUBLIC "Visible to everyone (if they know the URL), not listed on your profile" UNLISTED "Not visible to anyone except those explicitly added to the access list" PRIVATE } interface Entity { id: Int! created: Time! updated: Time! """ The canonical name of this entity. For users, this is their username prefixed with '~'. Additional entity types will be supported in the future. """ canonicalName: String! "Returns a specific repository owned by the entity." repository(name: String!): Repository @access(scope: REPOSITORIES, kind: RO) "Returns a list of repositories owned by the entity." repositories(cursor: Cursor, filter: Filter): RepositoryCursor! @access(scope: REPOSITORIES, kind: RO) } type User implements Entity { id: Int! created: Time! updated: Time! canonicalName: String! username: String! email: String! url: String location: String bio: String repository(name: String!): Repository @access(scope: REPOSITORIES, kind: RO) repositories(cursor: Cursor, filter: Filter): RepositoryCursor! @access(scope: REPOSITORIES, kind: RO) } type Repository { id: Int! created: Time! updated: Time! owner: Entity! @access(scope: PROFILE, kind: RO) name: String! description: String visibility: Visibility! """ The repository's custom README, if set. NOTICE: This returns unsanitized HTML. It is the client's responsibility to sanitize this for display on the web, if so desired. """ readme: String "The access that applies to this user for this repository" access: AccessMode! @access(scope: ACLS, kind: RO) # Only available to the repository owner acls(cursor: Cursor): ACLCursor! @access(scope: ACLS, kind: RO) ## Plumbing API: objects(ids: [String!]): [Object]! @access(scope: OBJECTS, kind: RO) references(cursor: Cursor): ReferenceCursor! @access(scope: OBJECTS, kind: RO) ## Porcelain API: # NOTE: revspecs are git-compatible, e.g. "HEAD~4", "master", "9790b10") "The HEAD reference for this repository (equivalent to the default branch)" HEAD: Reference @access(scope: OBJECTS, kind: RO) """ Returns a list of comments sorted by committer time (similar to `git log`'s default ordering). If `from` is specified, it is interpreted as a revspec to start logging from. A clever reader may notice that using commits[-1].from + "^" as the from parameter is equivalent to passing the cursor to the next call. """ log(cursor: Cursor, from: String): CommitCursor! @access(scope: OBJECTS, kind: RO) "Returns a tree entry for a given path, at the given revspec." path(revspec: String = "HEAD", path: String!): TreeEntry @access(scope: OBJECTS, kind: RO) "Returns the commit for a given revspec." revparse_single(revspec: String!): Commit @access(scope: OBJECTS, kind: RO) } type OAuthClient { uuid: String! } enum WebhookEvent { REPO_CREATED REPO_UPDATE REPO_DELETED } interface WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! """ If this webhook was registered by an authorized OAuth 2.0 client, this field is non-null. """ client: OAuthClient @private "All deliveries which have been sent to this webhook." deliveries(cursor: Cursor): WebhookDeliveryCursor! "Returns a sample payload for this subscription, for testing purposes" sample(event: WebhookEvent!): String! } type UserWebhookSubscription implements WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! client: OAuthClient @private deliveries(cursor: Cursor): WebhookDeliveryCursor! sample(event: WebhookEvent): String! } type WebhookDelivery { uuid: String! date: Time! event: WebhookEvent! subscription: WebhookSubscription! requestBody: String! """ These details are provided only after a response is received from the remote server. If a response is sent whose Content-Type is not text/*, or cannot be decoded as UTF-8, the response body will be null. It will be truncated after 64 KiB. """ responseBody: String responseHeaders: String responseStatus: Int } interface WebhookPayload { uuid: String! event: WebhookEvent! date: Time! } type RepositoryEvent implements WebhookPayload { uuid: String! event: WebhookEvent! date: Time! repository: Repository! } """ A cursor for enumerating a list of repositories If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type RepositoryCursor { results: [Repository!]! cursor: Cursor } """ A cursor for enumerating access control list entries If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type ACLCursor { results: [ACL!]! cursor: Cursor } """ A cursor for enumerating a list of references If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type ReferenceCursor { results: [Reference!]! cursor: Cursor } """ A cursor for enumerating commits If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type CommitCursor { results: [Commit!]! cursor: Cursor } """ A cursor for enumerating tree entries If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type TreeEntryCursor { results: [TreeEntry!]! cursor: Cursor } """ A cursor for enumerating artifacts If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type ArtifactCursor { results: [Artifact!]! cursor: Cursor } """ A cursor for enumerating a list of webhook deliveries If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type WebhookDeliveryCursor { results: [WebhookDelivery!]! cursor: Cursor } """ A cursor for enumerating a list of webhook subscriptions If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type WebhookSubscriptionCursor { results: [WebhookSubscription!]! cursor: Cursor } type ACL { id: Int! created: Time! repository: Repository! entity: Entity! @access(scope: PROFILE, kind: RO) mode: AccessMode } "Arbitrary file attached to a git repository" type Artifact { id: Int! created: Time! filename: String! checksum: String! size: Int! url: String! } type Reference { name: String! target: String! follow: Object artifacts(cursor: Cursor): ArtifactCursor! } enum ObjectType { COMMIT TREE BLOB TAG } interface Object { type: ObjectType! id: String! shortId: String! "Raw git object, base64 encoded" raw: String! } type Signature { name: String! email: String! time: Time! } type Commit implements Object { type: ObjectType! id: String! shortId: String! raw: String! author: Signature! committer: Signature! message: String! tree: Tree! parents: [Commit!]! diff: String! } type Tree implements Object { type: ObjectType! id: String! shortId: String! raw: String! # TODO: add globbing entries(cursor: Cursor): TreeEntryCursor! entry(path: String): TreeEntry } type TreeEntry { id: String! name: String! object: Object! "Unix-style file mode, i.e. 0755 or 0644 (octal)" mode: Int! } interface Blob { id: String! } type TextBlob implements Object & Blob { type: ObjectType! id: String! shortId: String! raw: String! # TODO: Consider adding a range specifier text: String! } type BinaryBlob implements Object & Blob { type: ObjectType! id: String! shortId: String! raw: String! # TODO: Consider adding a range specifier base64: String! } type Tag implements Object { type: ObjectType! id: String! shortId: String! raw: String! target: Object! name: String! tagger: Signature! message: String } input Filter { "Number of results to return." count: Int = 20 """ Search terms. The exact meaning varies by usage, but generally these are compatible with the web UI's search syntax. """ search: String } type Query { "Returns API version information." version: Version! "Returns the authenticated user." me: User! @access(scope: PROFILE, kind: RO) "Returns a specific user." user(username: String!): User @access(scope: PROFILE, kind: RO) """ Returns repositories that the authenticated user has access to. NOTE: in this version of the API, only repositories owned by the authenticated user are returned, but in the future the default behavior will be to return all repositories that the user either (1) has been given explicit access to via ACLs or (2) has implicit access to either by ownership or group membership. """ repositories(cursor: Cursor, filter: Filter): RepositoryCursor @access(scope: REPOSITORIES, kind: RO) """ Returns a list of user webhook subscriptions. For clients authenticated with a personal access token, this returns all webhooks configured by all GraphQL clients for your account. For clients authenticated with an OAuth 2.0 access token, this returns only webhooks registered for your client. """ userWebhooks(cursor: Cursor): WebhookSubscriptionCursor! "Returns details of a user webhook subscription by its ID." userWebhook(id: Int!): WebhookSubscription """ Returns information about the webhook currently being processed. This is not valid during normal queries over HTTP, and will return an error if used outside of a webhook context. """ webhook: WebhookPayload! } input RepoInput { # Omit these fields to leave them unchanged, or set them to null to clear # their value. name: String description: String visibility: Visibility """ Updates the custom README associated with this repository. Note that the provided HTML will be sanitized when displayed on the web; see https://man.sr.ht/markdown/#post-processing """ readme: String """ Updates the repository HEAD reference, which serves as the default branch. Must be a valid branch name. """ HEAD: String } input UserWebhookInput { url: String! events: [WebhookEvent!]! query: String! } type Mutation { """ Creates a new git repository. If the cloneUrl parameter is specified, the repository will be cloned from the given URL. """ createRepository(name: String!, visibility: Visibility!, description: String, cloneUrl: String): Repository @access(scope: REPOSITORIES, kind: RW) "Updates the metadata for a git repository" updateRepository(id: Int!, input: RepoInput!): Repository @access(scope: REPOSITORIES, kind: RW) "Deletes a git repository" deleteRepository(id: Int!): Repository @access(scope: REPOSITORIES, kind: RW) "Adds or updates a user in the access control list" updateACL(repoId: Int!, mode: AccessMode!, entity: ID!): ACL! @access(scope: ACLS, kind: RW) "Deletes an entry from the access control list" deleteACL(id: Int!): ACL @access(scope: ACLS, kind: RW) """ Uploads an artifact. revspec must match a specific git tag, and the filename must be unique among artifacts for this repository. """ uploadArtifact(repoId: Int!, revspec: String!, file: Upload!): Artifact! @access(scope: OBJECTS, kind: RW) "Deletes an artifact." deleteArtifact(id: Int!): Artifact @access(scope: OBJECTS, kind: RW) """ Creates a new user webhook subscription. When an event from the provided list of events occurs, the 'query' parameter (a GraphQL query) will be evaluated and the results will be sent to the provided URL as the body of an HTTP POST request. The list of events must include at least one event, and no duplicates. This query is evaluated in the webhook context, such that query { webhook } may be used to access details of the event which trigged the webhook. The query may not make any mutations. """ createUserWebhook(config: UserWebhookInput!): WebhookSubscription! """ Deletes a user webhook. Any events already queued may still be delivered after this request completes. Clients authenticated with a personal access token may delete any webhook registered for their account, but authorized OAuth 2.0 clients may only delete their own webhooks. Manually deleting a webhook configured by a third-party client may cause unexpected behavior with the third-party integration. """ deleteUserWebhook(id: Int!): WebhookSubscription! """ Deletes the authenticated user's account. Internal use only. """ deleteUser: Int! @internal } hut-0.6.0/srht/gitsrht/strings.go000066400000000000000000000026551463710650600170210ustar00rootroot00000000000000package gitsrht import ( "fmt" "strings" "git.sr.ht/~xenrox/hut/termfmt" ) func (visibility Visibility) TermString() string { var style termfmt.Style switch visibility { case VisibilityPublic: case VisibilityUnlisted: style = termfmt.Blue case VisibilityPrivate: style = termfmt.Red default: panic(fmt.Sprintf("unknown visibility: %q", visibility)) } return style.String(strings.ToLower(string(visibility))) } func ParseVisibility(s string) (Visibility, error) { switch strings.ToLower(s) { case "unlisted": return VisibilityUnlisted, nil case "private": return VisibilityPrivate, nil case "public": return VisibilityPublic, nil default: return "", fmt.Errorf("invalid visibility: %s", s) } } func ParseAccessMode(s string) (AccessMode, error) { switch strings.ToLower(s) { case "ro": return AccessModeRo, nil case "rw": return AccessModeRw, nil default: return "", fmt.Errorf("invalid access mode: %s", s) } } func ParseEvents(events []string) ([]WebhookEvent, error) { var whEvents []WebhookEvent for _, event := range events { switch strings.ToLower(event) { case "repo_created": whEvents = append(whEvents, WebhookEventRepoCreated) case "repo_update": whEvents = append(whEvents, WebhookEventRepoUpdate) case "repo_deleted": whEvents = append(whEvents, WebhookEventRepoDeleted) default: return whEvents, fmt.Errorf("invalid event: %q", event) } } return whEvents, nil } hut-0.6.0/srht/hgsrht/000077500000000000000000000000001463710650600146045ustar00rootroot00000000000000hut-0.6.0/srht/hgsrht/gql.go000066400000000000000000000542761463710650600157340ustar00rootroot00000000000000// Code generated by gqlclientgen - DO NOT EDIT. package hgsrht import ( "context" "encoding/json" "fmt" gqlclient "git.sr.ht/~emersion/gqlclient" ) type ACL struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Repository *Repository `json:"repository"` Entity *Entity `json:"entity"` Mode *AccessMode `json:"mode,omitempty"` } // A cursor for enumerating access control list entries // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type ACLCursor struct { Results []*ACL `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type AccessKind string const ( AccessKindRo AccessKind = "RO" AccessKindRw AccessKind = "RW" ) type AccessMode string const ( // Read-only AccessModeRo AccessMode = "RO" // Read/write AccessModeRw AccessMode = "RW" ) type AccessScope string const ( AccessScopeProfile AccessScope = "PROFILE" AccessScopeRepositories AccessScope = "REPOSITORIES" AccessScopeRevisions AccessScope = "REVISIONS" AccessScopeAcls AccessScope = "ACLS" ) type Cursor string type Entity struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` // The canonical name of this entity. For users, this is their username // prefixed with '~'. Additional entity types will be supported in the future. CanonicalName string `json:"canonicalName"` // Returns a specific repository owned by the entity. Repository *Repository `json:"repository,omitempty"` // Returns a list of repositories owned by the entity. Repositories *RepositoryCursor `json:"repositories"` // Underlying value of the GraphQL interface Value EntityValue `json:"-"` } func (base *Entity) UnmarshalJSON(b []byte) error { type Raw Entity var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "User": base.Value = new(User) case "": return nil default: return fmt.Errorf("gqlclient: interface Entity: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // EntityValue is one of: User type EntityValue interface { isEntity() } // Describes the status of optional features type Features struct { Artifacts bool `json:"artifacts"` } type NamedRevision struct { Name string `json:"name"` Id string `json:"id"` } // A cursor for enumerating bookmarks, tags, and branches // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type NamedRevisionCursor struct { Results []*NamedRevision `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type OAuthClient struct { Uuid string `json:"uuid"` } type RepoInput struct { // Omit these fields to leave them unchanged, or set them to null to clear // their value. Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` Visibility *Visibility `json:"visibility,omitempty"` // Updates the custom README associated with this repository. Note that the // provided HTML will be sanitized when displayed on the web; see // https://man.sr.ht/markdown/#post-processing Readme *string `json:"readme,omitempty"` // Controls whether this repository is a non-publishing repository. NonPublishing *bool `json:"nonPublishing,omitempty"` } type Repository struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` Owner *Entity `json:"owner"` Name string `json:"name"` Description *string `json:"description,omitempty"` Visibility Visibility `json:"visibility"` // The repository's custom README, if set. // // NOTICE: This returns unsanitized HTML. It is the client's responsibility to // sanitize this for display on the web, if so desired. Readme *string `json:"readme,omitempty"` // Whether or not this repository is a non-publishing repository. NonPublishing bool `json:"nonPublishing"` AccessControlList *ACLCursor `json:"accessControlList"` // The tip reference for this repository (latest commit) Tip *Revision `json:"tip,omitempty"` // Returns the list of open heads in the repository (like `hg heads`) // If `rev` is specified, return only open heads on the branch associated with // the given revision (like `hg heads REV`) Heads *RevisionCursor `json:"heads"` // Returns a list of commits (like `hg log`) // If `rev` is specified, only show the given commit (like `hg log --rev REV`) Log *RevisionCursor `json:"log"` // Returns a list of bookmarks Bookmarks *NamedRevisionCursor `json:"bookmarks"` // Returns a list of branches Branches *NamedRevisionCursor `json:"branches"` // Returns a list of tags Tags *NamedRevisionCursor `json:"tags"` } // A cursor for enumerating a list of repositories // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type RepositoryCursor struct { Results []*Repository `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type RepositoryEvent struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` Repository *Repository `json:"repository"` } func (*RepositoryEvent) isWebhookPayload() {} type Revision struct { Id string `json:"id"` Branch string `json:"branch"` Tags []*string `json:"tags"` Author string `json:"author"` Description string `json:"description"` } // A cursor for enumerating revisions // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type RevisionCursor struct { Results []*Revision `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } // Instance specific settings type Settings struct { SshUser string `json:"sshUser"` } type Tag struct { Name string `json:"name"` Id string `json:"id"` } type User struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` CanonicalName string `json:"canonicalName"` Username string `json:"username"` Email string `json:"email"` Url *string `json:"url,omitempty"` Location *string `json:"location,omitempty"` Bio *string `json:"bio,omitempty"` Repository *Repository `json:"repository,omitempty"` Repositories *RepositoryCursor `json:"repositories"` } func (*User) isEntity() {} type UserWebhookInput struct { Url string `json:"url"` Events []WebhookEvent `json:"events"` Query string `json:"query"` } type UserWebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` Client *OAuthClient `json:"client,omitempty"` Deliveries *WebhookDeliveryCursor `json:"deliveries"` Sample string `json:"sample"` } func (*UserWebhookSubscription) isWebhookSubscription() {} type Version struct { Major int32 `json:"major"` Minor int32 `json:"minor"` Patch int32 `json:"patch"` // If this API version is scheduled for deprecation, this is the date on which // it will stop working; or null if this API version is not scheduled for // deprecation. DeprecationDate gqlclient.Time `json:"deprecationDate,omitempty"` // Optional features Features *Features `json:"features"` // Config settings Settings *Settings `json:"settings"` } type Visibility string const ( // Visible to everyone, listed on your profile VisibilityPublic Visibility = "PUBLIC" // Visible to everyone (if they know the URL), not listed on your profile VisibilityUnlisted Visibility = "UNLISTED" // Not visible to anyone except those explicitly added to the access list VisibilityPrivate Visibility = "PRIVATE" ) type WebhookDelivery struct { Uuid string `json:"uuid"` Date gqlclient.Time `json:"date"` Event WebhookEvent `json:"event"` Subscription *WebhookSubscription `json:"subscription"` RequestBody string `json:"requestBody"` // These details are provided only after a response is received from the // remote server. If a response is sent whose Content-Type is not text/*, or // cannot be decoded as UTF-8, the response body will be null. It will be // truncated after 64 KiB. ResponseBody *string `json:"responseBody,omitempty"` ResponseHeaders *string `json:"responseHeaders,omitempty"` ResponseStatus *int32 `json:"responseStatus,omitempty"` } // A cursor for enumerating a list of webhook deliveries // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type WebhookDeliveryCursor struct { Results []WebhookDelivery `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type WebhookEvent string const ( WebhookEventRepoCreated WebhookEvent = "REPO_CREATED" WebhookEventRepoUpdate WebhookEvent = "REPO_UPDATE" WebhookEventRepoDeleted WebhookEvent = "REPO_DELETED" ) type WebhookPayload struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` // Underlying value of the GraphQL interface Value WebhookPayloadValue `json:"-"` } func (base *WebhookPayload) UnmarshalJSON(b []byte) error { type Raw WebhookPayload var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "RepositoryEvent": base.Value = new(RepositoryEvent) case "": return nil default: return fmt.Errorf("gqlclient: interface WebhookPayload: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // WebhookPayloadValue is one of: RepositoryEvent type WebhookPayloadValue interface { isWebhookPayload() } type WebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` // If this webhook was registered by an authorized OAuth 2.0 client, this // field is non-null. Client *OAuthClient `json:"client,omitempty"` // All deliveries which have been sent to this webhook. Deliveries *WebhookDeliveryCursor `json:"deliveries"` // Returns a sample payload for this subscription, for testing purposes Sample string `json:"sample"` // Underlying value of the GraphQL interface Value WebhookSubscriptionValue `json:"-"` } func (base *WebhookSubscription) UnmarshalJSON(b []byte) error { type Raw WebhookSubscription var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "UserWebhookSubscription": base.Value = new(UserWebhookSubscription) case "": return nil default: return fmt.Errorf("gqlclient: interface WebhookSubscription: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // WebhookSubscriptionValue is one of: UserWebhookSubscription type WebhookSubscriptionValue interface { isWebhookSubscription() } // A cursor for enumerating a list of webhook subscriptions // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type WebhookSubscriptionCursor struct { Results []WebhookSubscription `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } func RepositoryIDByName(client *gqlclient.Client, ctx context.Context, name string) (me *User, err error) { op := gqlclient.NewOperation("query repositoryIDByName ($name: String!) {\n\tme {\n\t\trepository(name: $name) {\n\t\t\tid\n\t\t}\n\t}\n}\n") op.Var("name", name) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func RepositoryIDByUser(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) { op := gqlclient.NewOperation("query repositoryIDByUser ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\trepository(name: $name) {\n\t\t\tid\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func Repositories(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (repositories *RepositoryCursor, err error) { op := gqlclient.NewOperation("query repositories ($cursor: Cursor) {\n\trepositories(cursor: $cursor) {\n\t\t... repos\n\t}\n}\nfragment repos on RepositoryCursor {\n\tresults {\n\t\tname\n\t\tdescription\n\t\tvisibility\n\t\towner {\n\t\t\tcanonicalName\n\t\t}\n\t}\n\tcursor\n}\n") op.Var("cursor", cursor) var respData struct { Repositories *RepositoryCursor } err = client.Execute(ctx, op, &respData) return respData.Repositories, err } func RepositoriesByUser(client *gqlclient.Client, ctx context.Context, username string, cursor *Cursor) (user *User, err error) { op := gqlclient.NewOperation("query repositoriesByUser ($username: String!, $cursor: Cursor) {\n\tuser(username: $username) {\n\t\trepositories(cursor: $cursor) {\n\t\t\t... repos\n\t\t}\n\t}\n}\nfragment repos on RepositoryCursor {\n\tresults {\n\t\tname\n\t\tdescription\n\t\tvisibility\n\t\towner {\n\t\t\tcanonicalName\n\t\t}\n\t}\n\tcursor\n}\n") op.Var("username", username) op.Var("cursor", cursor) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func ExportRepository(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) { op := gqlclient.NewOperation("query exportRepository ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\trepository(name: $name) {\n\t\t\t... repositoryExport\n\t\t}\n\t}\n}\nfragment repositoryExport on Repository {\n\tname\n\towner {\n\t\tcanonicalName\n\t}\n\tdescription\n\tvisibility\n\treadme\n\tnonPublishing\n}\n") op.Var("username", username) op.Var("name", name) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func ExportRepositories(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (repositories *RepositoryCursor, err error) { op := gqlclient.NewOperation("query exportRepositories ($cursor: Cursor) {\n\trepositories(cursor: $cursor) {\n\t\tresults {\n\t\t\t... repositoryExport\n\t\t}\n\t\tcursor\n\t}\n}\nfragment repositoryExport on Repository {\n\tname\n\towner {\n\t\tcanonicalName\n\t}\n\tdescription\n\tvisibility\n\treadme\n\tnonPublishing\n}\n") op.Var("cursor", cursor) var respData struct { Repositories *RepositoryCursor } err = client.Execute(ctx, op, &respData) return respData.Repositories, err } func UserWebhooks(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (userWebhooks *WebhookSubscriptionCursor, err error) { op := gqlclient.NewOperation("query userWebhooks ($cursor: Cursor) {\n\tuserWebhooks(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\turl\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { UserWebhooks *WebhookSubscriptionCursor } err = client.Execute(ctx, op, &respData) return respData.UserWebhooks, err } func SshSettings(client *gqlclient.Client, ctx context.Context) (version *Version, err error) { op := gqlclient.NewOperation("query sshSettings {\n\tversion {\n\t\tsettings {\n\t\t\tsshUser\n\t\t}\n\t}\n}\n") var respData struct { Version *Version } err = client.Execute(ctx, op, &respData) return respData.Version, err } func CompleteRepositories(client *gqlclient.Client, ctx context.Context) (repositories *RepositoryCursor, err error) { op := gqlclient.NewOperation("query completeRepositories {\n\trepositories {\n\t\tresults {\n\t\t\tname\n\t\t}\n\t}\n}\n") var respData struct { Repositories *RepositoryCursor } err = client.Execute(ctx, op, &respData) return respData.Repositories, err } func AclByRepoName(client *gqlclient.Client, ctx context.Context, name string, cursor *Cursor) (me *User, err error) { op := gqlclient.NewOperation("query aclByRepoName ($name: String!, $cursor: Cursor) {\n\tme {\n\t\t... acl\n\t}\n}\nfragment acl on User {\n\trepository(name: $name) {\n\t\taccessControlList(cursor: $cursor) {\n\t\t\tresults {\n\t\t\t\tid\n\t\t\t\tcreated\n\t\t\t\tentity {\n\t\t\t\t\tcanonicalName\n\t\t\t\t}\n\t\t\t\tmode\n\t\t\t}\n\t\t\tcursor\n\t\t}\n\t}\n}\n") op.Var("name", name) op.Var("cursor", cursor) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func AclByUser(client *gqlclient.Client, ctx context.Context, username string, name string, cursor *Cursor) (user *User, err error) { op := gqlclient.NewOperation("query aclByUser ($username: String!, $name: String!, $cursor: Cursor) {\n\tuser(username: $username) {\n\t\t... acl\n\t}\n}\nfragment acl on User {\n\trepository(name: $name) {\n\t\taccessControlList(cursor: $cursor) {\n\t\t\tresults {\n\t\t\t\tid\n\t\t\t\tcreated\n\t\t\t\tentity {\n\t\t\t\t\tcanonicalName\n\t\t\t\t}\n\t\t\t\tmode\n\t\t\t}\n\t\t\tcursor\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) op.Var("cursor", cursor) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func CreateRepository(client *gqlclient.Client, ctx context.Context, name string, visibility Visibility, description string) (createRepository *Repository, err error) { op := gqlclient.NewOperation("mutation createRepository ($name: String!, $visibility: Visibility!, $description: String!) {\n\tcreateRepository(name: $name, visibility: $visibility, description: $description) {\n\t\tid\n\t\tname\n\t\towner {\n\t\t\tcanonicalName\n\t\t}\n\t}\n}\n") op.Var("name", name) op.Var("visibility", visibility) op.Var("description", description) var respData struct { CreateRepository *Repository } err = client.Execute(ctx, op, &respData) return respData.CreateRepository, err } func UpdateRepository(client *gqlclient.Client, ctx context.Context, id int32, input RepoInput) (updateRepository *Repository, err error) { op := gqlclient.NewOperation("mutation updateRepository ($id: Int!, $input: RepoInput!) {\n\tupdateRepository(id: $id, input: $input) {\n\t\tname\n\t}\n}\n") op.Var("id", id) op.Var("input", input) var respData struct { UpdateRepository *Repository } err = client.Execute(ctx, op, &respData) return respData.UpdateRepository, err } func ClearCustomReadme(client *gqlclient.Client, ctx context.Context, id int32) (updateRepository *Repository, err error) { op := gqlclient.NewOperation("mutation clearCustomReadme ($id: Int!) {\n\tupdateRepository(id: $id, input: {readme:null}) {\n\t\tname\n\t}\n}\n") op.Var("id", id) var respData struct { UpdateRepository *Repository } err = client.Execute(ctx, op, &respData) return respData.UpdateRepository, err } func ClearDescription(client *gqlclient.Client, ctx context.Context, id int32) (updateRepository *Repository, err error) { op := gqlclient.NewOperation("mutation clearDescription ($id: Int!) {\n\tupdateRepository(id: $id, input: {description:null}) {\n\t\tname\n\t}\n}\n") op.Var("id", id) var respData struct { UpdateRepository *Repository } err = client.Execute(ctx, op, &respData) return respData.UpdateRepository, err } func DeleteRepository(client *gqlclient.Client, ctx context.Context, id int32) (deleteRepository *Repository, err error) { op := gqlclient.NewOperation("mutation deleteRepository ($id: Int!) {\n\tdeleteRepository(id: $id) {\n\t\tname\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteRepository *Repository } err = client.Execute(ctx, op, &respData) return respData.DeleteRepository, err } func CreateUserWebhook(client *gqlclient.Client, ctx context.Context, config UserWebhookInput) (createUserWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation createUserWebhook ($config: UserWebhookInput!) {\n\tcreateUserWebhook(config: $config) {\n\t\tid\n\t}\n}\n") op.Var("config", config) var respData struct { CreateUserWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.CreateUserWebhook, err } func DeleteUserWebhook(client *gqlclient.Client, ctx context.Context, id int32) (deleteUserWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation deleteUserWebhook ($id: Int!) {\n\tdeleteUserWebhook(id: $id) {\n\t\tid\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteUserWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.DeleteUserWebhook, err } func UpdateACL(client *gqlclient.Client, ctx context.Context, repoId int32, mode AccessMode, entity string) (updateACL *ACL, err error) { op := gqlclient.NewOperation("mutation updateACL ($repoId: Int!, $mode: AccessMode!, $entity: ID!) {\n\tupdateACL(repoId: $repoId, mode: $mode, entity: $entity) {\n\t\tentity {\n\t\t\tcanonicalName\n\t\t}\n\t}\n}\n") op.Var("repoId", repoId) op.Var("mode", mode) op.Var("entity", entity) var respData struct { UpdateACL *ACL } err = client.Execute(ctx, op, &respData) return respData.UpdateACL, err } func DeleteACL(client *gqlclient.Client, ctx context.Context, id int32) (deleteACL *ACL, err error) { op := gqlclient.NewOperation("mutation deleteACL ($id: Int!) {\n\tdeleteACL(id: $id) {\n\t\tentity {\n\t\t\tcanonicalName\n\t\t}\n\t\trepository {\n\t\t\tname\n\t\t}\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteACL *ACL } err = client.Execute(ctx, op, &respData) return respData.DeleteACL, err } hut-0.6.0/srht/hgsrht/operations.graphql000066400000000000000000000065721463710650600203610ustar00rootroot00000000000000query repositoryIDByName($name: String!) { me { repository(name: $name) { id } } } query repositoryIDByUser($username: String!, $name: String!) { user(username: $username) { repository(name: $name) { id } } } query repositories($cursor: Cursor) { repositories(cursor: $cursor) { ...repos } } query repositoriesByUser($username: String!, $cursor: Cursor) { user(username: $username) { repositories(cursor: $cursor) { ...repos } } } fragment repos on RepositoryCursor { results { name description visibility owner { canonicalName } } cursor } query exportRepository($username: String!, $name: String!) { user(username: $username) { repository(name: $name) { ...repositoryExport } } } query exportRepositories($cursor: Cursor) { repositories(cursor: $cursor) { results { ...repositoryExport } cursor } } fragment repositoryExport on Repository { name owner { canonicalName } description visibility readme nonPublishing } query userWebhooks($cursor: Cursor) { userWebhooks(cursor: $cursor) { results { id url } cursor } } query sshSettings { version { settings { sshUser } } } query completeRepositories { repositories { results { name } } } query aclByRepoName($name: String!, $cursor: Cursor) { me { ...acl } } query aclByUser($username: String!, $name: String!, $cursor: Cursor) { user(username: $username) { ...acl } } fragment acl on User { repository(name: $name) { accessControlList(cursor: $cursor) { results { id created entity { canonicalName } mode } cursor } } } mutation createRepository( $name: String! $visibility: Visibility! $description: String! ) { createRepository( name: $name visibility: $visibility description: $description ) { id name owner { canonicalName } } } mutation updateRepository($id: Int!, $input: RepoInput!) { updateRepository(id: $id, input: $input) { name } } mutation clearCustomReadme($id: Int!) { updateRepository(id: $id, input: { readme: null }) { name } } mutation clearDescription($id: Int!) { updateRepository(id: $id, input: { description: null }) { name } } mutation deleteRepository($id: Int!) { deleteRepository(id: $id) { name } } mutation createUserWebhook($config: UserWebhookInput!) { createUserWebhook(config: $config) { id } } mutation deleteUserWebhook($id: Int!) { deleteUserWebhook(id: $id) { id } } mutation updateACL($repoId: Int!, $mode: AccessMode!, $entity: ID!) { updateACL(repoId: $repoId, mode: $mode, entity: $entity) { entity { canonicalName } } } mutation deleteACL($id: Int!) { deleteACL(id: $id) { entity { canonicalName } repository { name } } } hut-0.6.0/srht/hgsrht/schema.graphqls000066400000000000000000000276731463710650600176260ustar00rootroot00000000000000scalar Cursor scalar Time scalar Upload "Used to provide a human-friendly description of an access scope" directive @scopehelp(details: String!) on ENUM_VALUE """ This is used to decorate fields which are only accessible with a personal access token, and are not available to clients using OAuth 2.0 access tokens. """ directive @private on FIELD_DEFINITION """ This used to decorate fields which are for internal use, and are not available to normal API users. """ directive @internal on FIELD_DEFINITION enum AccessScope { PROFILE @scopehelp(details: "profile information") REPOSITORIES @scopehelp(details: "repository metadata") REVISIONS @scopehelp(details: "hg revisions & data") ACLS @scopehelp(details: "access control lists") } enum AccessKind { RO @scopehelp(details: "read") RW @scopehelp(details: "read and write") } """ Decorates fields for which access requires a particular OAuth 2.0 scope with read or write access. For the meta.sr.ht API, you have access to all public information without any special permissions - user profile information, public keys, and so on. """ directive @access(scope: AccessScope!, kind: AccessKind!) on FIELD_DEFINITION # https://semver.org type Version { major: Int! minor: Int! patch: Int! """ If this API version is scheduled for deprecation, this is the date on which it will stop working; or null if this API version is not scheduled for deprecation. """ deprecationDate: Time "Optional features" features: Features! "Config settings" settings: Settings! } "Describes the status of optional features" type Features { artifacts: Boolean! } "Instance specific settings" type Settings { sshUser: String! } enum AccessMode { "Read-only" RO "Read/write" RW } enum Visibility { "Visible to everyone, listed on your profile" PUBLIC "Visible to everyone (if they know the URL), not listed on your profile" UNLISTED "Not visible to anyone except those explicitly added to the access list" PRIVATE } interface Entity { id: Int! created: Time! updated: Time! """ The canonical name of this entity. For users, this is their username prefixed with '~'. Additional entity types will be supported in the future. """ canonicalName: String! "Returns a specific repository owned by the entity." repository(name: String!): Repository @access(scope: REPOSITORIES, kind: RO) "Returns a list of repositories owned by the entity." repositories(cursor: Cursor): RepositoryCursor! @access(scope: REPOSITORIES, kind: RO) } type User implements Entity { id: Int! created: Time! updated: Time! canonicalName: String! username: String! email: String! url: String location: String bio: String repository(name: String!): Repository @access(scope: REPOSITORIES, kind: RO) repositories(cursor: Cursor): RepositoryCursor! @access(scope: REPOSITORIES, kind: RO) } type Repository { id: Int! created: Time! updated: Time! owner: Entity! @access(scope: PROFILE, kind: RO) name: String! description: String visibility: Visibility! """ The repository's custom README, if set. NOTICE: This returns unsanitized HTML. It is the client's responsibility to sanitize this for display on the web, if so desired. """ readme: String "Whether or not this repository is a non-publishing repository." nonPublishing: Boolean! accessControlList(cursor: Cursor): ACLCursor! @access(scope: ACLS, kind: RO) # Mercurial API "The tip reference for this repository (latest commit)" tip: Revision @access(scope: REVISIONS, kind: RO) """ Returns the list of open heads in the repository (like `hg heads`) If `rev` is specified, return only open heads on the branch associated with the given revision (like `hg heads REV`) """ heads(cursor: Cursor, rev: String): RevisionCursor! @access(scope: REVISIONS, kind: RO) """ Returns a list of commits (like `hg log`) If `rev` is specified, only show the given commit (like `hg log --rev REV`) """ log(cursor: Cursor, rev: String): RevisionCursor! @access(scope: REVISIONS, kind: RO) "Returns a list of bookmarks" bookmarks(cursor: Cursor): NamedRevisionCursor! @access(scope: REVISIONS, kind: RO) "Returns a list of branches" branches(cursor: Cursor): NamedRevisionCursor! @access(scope: REVISIONS, kind: RO) "Returns a list of tags" tags(cursor: Cursor): NamedRevisionCursor! @access(scope: REVISIONS, kind: RO) } type OAuthClient { uuid: String! } enum WebhookEvent { REPO_CREATED REPO_UPDATE REPO_DELETED } interface WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! """ If this webhook was registered by an authorized OAuth 2.0 client, this field is non-null. """ client: OAuthClient @private "All deliveries which have been sent to this webhook." deliveries(cursor: Cursor): WebhookDeliveryCursor! "Returns a sample payload for this subscription, for testing purposes" sample(event: WebhookEvent!): String! } type UserWebhookSubscription implements WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! client: OAuthClient @private deliveries(cursor: Cursor): WebhookDeliveryCursor! sample(event: WebhookEvent!): String! } type WebhookDelivery { uuid: String! date: Time! event: WebhookEvent! subscription: WebhookSubscription! requestBody: String! """ These details are provided only after a response is received from the remote server. If a response is sent whose Content-Type is not text/*, or cannot be decoded as UTF-8, the response body will be null. It will be truncated after 64 KiB. """ responseBody: String responseHeaders: String responseStatus: Int } interface WebhookPayload { uuid: String! event: WebhookEvent! date: Time! } type RepositoryEvent implements WebhookPayload { uuid: String! event: WebhookEvent! date: Time! repository: Repository! } """ A cursor for enumerating a list of repositories If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type RepositoryCursor { results: [Repository]! cursor: Cursor } """ A cursor for enumerating access control list entries If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type ACLCursor { results: [ACL]! cursor: Cursor } """ A cursor for enumerating revisions If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type RevisionCursor { results: [Revision]! cursor: Cursor } """ A cursor for enumerating bookmarks, tags, and branches If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type NamedRevisionCursor { results: [NamedRevision]! cursor: Cursor } """ A cursor for enumerating a list of webhook deliveries If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type WebhookDeliveryCursor { results: [WebhookDelivery!]! cursor: Cursor } """ A cursor for enumerating a list of webhook subscriptions If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type WebhookSubscriptionCursor { results: [WebhookSubscription!]! cursor: Cursor } type ACL { id: Int! created: Time! repository: Repository! entity: Entity! @access(scope: PROFILE, kind: RO) mode: AccessMode } type Revision { id: String! branch: String! tags: [String]! author: String! description: String! } type NamedRevision { name: String! id: String! } type Tag { name: String! id: String! } type Query { "Returns API version information." version: Version! "Returns the authenticated user." me: User! @access(scope: PROFILE, kind: RO) "Returns a specific user." user(username: String!): User @access(scope: PROFILE, kind: RO) """ Returns repositories that the authenticated user has access to. NOTE: in this version of the API, only repositories owned by the authenticated user are returned, but in the future the default behavior will be to return all repositories that the user either (1) has been given explicit access to via ACLs or (2) has implicit access to either by ownership or group membership. """ repositories(cursor: Cursor): RepositoryCursor @access(scope: REPOSITORIES, kind: RO) """ Returns a list of user webhook subscriptions. For clients authenticated with a personal access token, this returns all webhooks configured by all GraphQL clients for your account. For clients authenticated with an OAuth 2.0 access token, this returns only webhooks registered for your client. """ userWebhooks(cursor: Cursor): WebhookSubscriptionCursor! "Returns details of a user webhook subscription by its ID." userWebhook(id: Int!): WebhookSubscription """ Returns information about the webhook currently being processed. This is not valid during normal queries over HTTP, and will return an error if used outside of a webhook context. """ webhook: WebhookPayload! } input RepoInput { """ Omit these fields to leave them unchanged, or set them to null to clear their value. """ name: String description: String visibility: Visibility """ Updates the custom README associated with this repository. Note that the provided HTML will be sanitized when displayed on the web; see https://man.sr.ht/markdown/#post-processing """ readme: String "Controls whether this repository is a non-publishing repository." nonPublishing: Boolean } input UserWebhookInput { url: String! events: [WebhookEvent!]! query: String! } type Mutation { "Creates a new mercurial repository" createRepository(name: String!, visibility: Visibility!, description: String): Repository! @access(scope: REPOSITORIES, kind: RW) "Updates the metadata for a mercurial repository" updateRepository(id: Int!, input: RepoInput!): Repository! @access(scope: REPOSITORIES, kind: RW) "Deletes a mercurial repository" deleteRepository(id: Int!): Repository! @access(scope: REPOSITORIES, kind: RW) "Adds or updates a user in the access control list" updateACL(repoId: Int!, mode: AccessMode!, entity: ID!): ACL! @access(scope: ACLS, kind: RW) "Deletes an entry from the access control list" deleteACL(id: Int!): ACL! @access(scope: ACLS, kind: RW) """ Creates a new user webhook subscription. When an event from the provided list of events occurs, the 'query' parameter (a GraphQL query) will be evaluated and the results will be sent to the provided URL as the body of an HTTP POST request. The list of events must include at least one event, and no duplicates. This query is evaluated in the webhook context, such that query { webhook } may be used to access details of the event which trigged the webhook. The query may not make any mutations. """ createUserWebhook(config: UserWebhookInput!): WebhookSubscription! """ Deletes a user webhook. Any events already queued may still be delivered after this request completes. Clients authenticated with a personal access token may delete any webhook registered for their account, but authorized OAuth 2.0 clients may only delete their own webhooks. Manually deleting a webhook configured by a third-party client may cause unexpected behavior with the third-party integration. """ deleteUserWebhook(id: Int!): WebhookSubscription! """ Deletes the authenticated user's account. Internal use only. """ deleteUser: Int! @internal } hut-0.6.0/srht/hgsrht/strings.go000066400000000000000000000026601463710650600166300ustar00rootroot00000000000000package hgsrht import ( "fmt" "strings" "git.sr.ht/~xenrox/hut/termfmt" ) func (visibility Visibility) TermString() string { var style termfmt.Style switch visibility { case VisibilityPublic: case VisibilityUnlisted: style = termfmt.Blue case VisibilityPrivate: style = termfmt.Red default: panic(fmt.Sprintf("unknown visibility: %q", visibility)) } return style.String(strings.ToLower(string(visibility))) } func ParseVisibility(s string) (Visibility, error) { switch strings.ToLower(s) { case "unlisted": return VisibilityUnlisted, nil case "private": return VisibilityPrivate, nil case "public": return VisibilityPublic, nil default: return "", fmt.Errorf("invalid visibility: %s", s) } } func ParseUserEvents(events []string) ([]WebhookEvent, error) { var whEvents []WebhookEvent for _, event := range events { switch strings.ToLower(event) { case "repo_created": whEvents = append(whEvents, WebhookEventRepoCreated) case "repo_update": whEvents = append(whEvents, WebhookEventRepoUpdate) case "repo_deleted": whEvents = append(whEvents, WebhookEventRepoDeleted) default: return whEvents, fmt.Errorf("invalid event: %q", event) } } return whEvents, nil } func ParseAccessMode(s string) (AccessMode, error) { switch strings.ToLower(s) { case "ro": return AccessModeRo, nil case "rw": return AccessModeRw, nil default: return "", fmt.Errorf("invalid access mode: %s", s) } } hut-0.6.0/srht/listssrht/000077500000000000000000000000001463710650600153445ustar00rootroot00000000000000hut-0.6.0/srht/listssrht/gql.go000066400000000000000000001245511463710650600164660ustar00rootroot00000000000000// Code generated by gqlclientgen - DO NOT EDIT. package listssrht import ( "context" "encoding/json" "fmt" gqlclient "git.sr.ht/~emersion/gqlclient" ) type ACL struct { // Permission to browse or subscribe to emails Browse bool `json:"browse"` // Permission to reply to existing threads Reply bool `json:"reply"` // Permission to start new threads Post bool `json:"post"` // Permission to moderate the list Moderate bool `json:"moderate"` // Underlying value of the GraphQL interface Value ACLValue `json:"-"` } func (base *ACL) UnmarshalJSON(b []byte) error { type Raw ACL var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "MailingListACL": base.Value = new(MailingListACL) case "GeneralACL": base.Value = new(GeneralACL) case "": return nil default: return fmt.Errorf("gqlclient: interface ACL: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // ACLValue is one of: MailingListACL | GeneralACL type ACLValue interface { isACL() } type ACLInput struct { Browse bool `json:"browse"` Reply bool `json:"reply"` Post bool `json:"post"` Moderate bool `json:"moderate"` } type AccessKind string const ( AccessKindRo AccessKind = "RO" AccessKindRw AccessKind = "RW" ) type AccessScope string const ( AccessScopeAcls AccessScope = "ACLS" AccessScopeEmails AccessScope = "EMAILS" AccessScopeLists AccessScope = "LISTS" AccessScopePatches AccessScope = "PATCHES" AccessScopeProfile AccessScope = "PROFILE" AccessScopeSubscriptions AccessScope = "SUBSCRIPTIONS" ) type ActivitySubscription struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` // Underlying value of the GraphQL interface Value ActivitySubscriptionValue `json:"-"` } func (base *ActivitySubscription) UnmarshalJSON(b []byte) error { type Raw ActivitySubscription var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "MailingListSubscription": base.Value = new(MailingListSubscription) case "": return nil default: return fmt.Errorf("gqlclient: interface ActivitySubscription: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // ActivitySubscriptionValue is one of: MailingListSubscription type ActivitySubscriptionValue interface { isActivitySubscription() } // A cursor for enumerating subscriptions // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type ActivitySubscriptionCursor struct { Results []ActivitySubscription `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } // A byte range. type ByteRange struct { // Inclusive start byte offset. Start int32 `json:"start"` // Exclusive end byte offset. End int32 `json:"end"` } // Opaque string type Cursor string type Email struct { Id int32 `json:"id"` // The entity which sent this email. Will be a User if it can be associated // with an account, or a Mailbox otherwise. Sender *Entity `json:"sender"` // Time we received this email (non-forgable). Received gqlclient.Time `json:"received"` // Time given by Date header (forgable). Date gqlclient.Time `json:"date,omitempty"` // The Subject header. Subject string `json:"subject"` // The Message-ID header, without angle brackets. MessageID string `json:"messageID"` // The In-Reply-To header, if present, without angle brackets. InReplyTo *string `json:"inReplyTo,omitempty"` // Provides the value (or values) of a specific header from this email. Note // that the returned value is coerced to UTF-8 and may be lossy under certain // circumstances. Header []string `json:"header"` // Retrieves the value of an address list header, such as To or Cc. AddressList []Mailbox `json:"addressList"` // The decoded text/plain message part of the email, i.e. email body. Body string `json:"body"` // A URL from which the full raw message envelope may be downloaded. Envelope URL `json:"envelope"` Thread *Thread `json:"thread"` Parent *Email `json:"parent,omitempty"` Patch *Patch `json:"patch,omitempty"` Patchset *Patchset `json:"patchset,omitempty"` List *MailingList `json:"list"` } // A cursor for enumerating emails // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type EmailCursor struct { Results []Email `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type EmailEvent struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` Email *Email `json:"email"` } func (*EmailEvent) isWebhookPayload() {} type Entity struct { CanonicalName string `json:"canonicalName"` // Underlying value of the GraphQL interface Value EntityValue `json:"-"` } func (base *Entity) UnmarshalJSON(b []byte) error { type Raw Entity var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "User": base.Value = new(User) case "Mailbox": base.Value = new(Mailbox) case "": return nil default: return fmt.Errorf("gqlclient: interface Entity: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // EntityValue is one of: User | Mailbox type EntityValue interface { isEntity() } // An ACL entry that applies "generally", for example the rights which apply to // all subscribers to a list. type GeneralACL struct { Browse bool `json:"browse"` Reply bool `json:"reply"` Post bool `json:"post"` Moderate bool `json:"moderate"` } func (*GeneralACL) isACL() {} // A mailbox not associated with a registered user type Mailbox struct { CanonicalName string `json:"canonicalName"` Name string `json:"name"` Address string `json:"address"` } func (*Mailbox) isEntity() {} type MailingList struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` Name string `json:"name"` Owner *Entity `json:"owner"` Description *string `json:"description,omitempty"` Visibility Visibility `json:"visibility"` // List of globs for permitted or rejected mimetypes on this list // e.g. text/* PermitMime []string `json:"permitMime"` RejectMime []string `json:"rejectMime"` // List of threads on this list in order of most recently bumped Threads *ThreadCursor `json:"threads"` // List of emails received on this list in reverse chronological order Emails *EmailCursor `json:"emails"` // List of patches received on this list in order of most recently bumped Patches *PatchsetCursor `json:"patches"` // True if an import operation is underway for this list Importing bool `json:"importing"` // The access that applies to this user for this list Access *ACL `json:"access"` // The user's subscription for this list, if any Subscription *MailingListSubscription `json:"subscription,omitempty"` // URLs to application/mbox archives for this mailing list Archive URL `json:"archive"` Last30days URL `json:"last30days"` // Access control list entries for this mailing list Acl *MailingListACLCursor `json:"acl"` DefaultACL *GeneralACL `json:"defaultACL"` // Returns a list of mailing list webhook subscriptions. For clients // authenticated with a personal access token, this returns all webhooks // configured by all GraphQL clients for your account. For clients // authenticated with an OAuth 2.0 access token, this returns only webhooks // registered for your client. Webhooks *WebhookSubscriptionCursor `json:"webhooks"` // Returns details of a mailing list webhook subscription by its ID. Webhook *WebhookSubscription `json:"webhook,omitempty"` } // These ACLs are configured for specific entities, and may be used to expand or // constrain the rights of a participant. type MailingListACL struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` List *MailingList `json:"list"` Entity *Entity `json:"entity"` Browse bool `json:"browse"` Reply bool `json:"reply"` Post bool `json:"post"` Moderate bool `json:"moderate"` } func (*MailingListACL) isACL() {} // A cursor for enumerating ACL entries // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type MailingListACLCursor struct { Results []MailingListACL `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } // A cursor for enumerating mailing lists // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type MailingListCursor struct { Results []MailingList `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type MailingListEvent struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` List *MailingList `json:"list"` } func (*MailingListEvent) isWebhookPayload() {} type MailingListInput struct { Description *string `json:"description,omitempty"` Visibility *Visibility `json:"visibility,omitempty"` // List of globs for permitted or rejected mimetypes on this list // e.g. text/* PermitMime []string `json:"permitMime,omitempty"` RejectMime []string `json:"rejectMime,omitempty"` } type MailingListSubscription struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` List *MailingList `json:"list"` } func (*MailingListSubscription) isActivitySubscription() {} type MailingListWebhookInput struct { Url string `json:"url"` Events []WebhookEvent `json:"events"` Query string `json:"query"` } type MailingListWebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` Client *OAuthClient `json:"client,omitempty"` Deliveries *WebhookDeliveryCursor `json:"deliveries"` Sample string `json:"sample"` List *MailingList `json:"list"` } func (*MailingListWebhookSubscription) isWebhookSubscription() {} type OAuthClient struct { Uuid string `json:"uuid"` } // Information parsed from the subject line of a patch, such that the following: // // [PATCH myproject v2 3/4] Add foo to bar // // Will produce: // // index: 3 // count: 4 // version: 2 // prefix: "myproject" // subject: "Add foo to bar" type Patch struct { Index *int32 `json:"index,omitempty"` Count *int32 `json:"count,omitempty"` Version *int32 `json:"version,omitempty"` Prefix *string `json:"prefix,omitempty"` Subject *string `json:"subject,omitempty"` } type Patchset struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` Subject string `json:"subject"` Version int32 `json:"version"` Prefix *string `json:"prefix,omitempty"` Status PatchsetStatus `json:"status"` Submitter *Entity `json:"submitter"` CoverLetter *Email `json:"coverLetter,omitempty"` Thread *Thread `json:"thread"` SupersededBy *Patchset `json:"supersededBy,omitempty"` List *MailingList `json:"list"` Patches *EmailCursor `json:"patches"` Tools []PatchsetTool `json:"tools"` // URL to an application/mbox archive of only the patches in this thread Mbox URL `json:"mbox"` } // A cursor for enumerating patchsets // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type PatchsetCursor struct { Results []Patchset `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type PatchsetEvent struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` Patchset *Patchset `json:"patchset"` } func (*PatchsetEvent) isWebhookPayload() {} type PatchsetStatus string const ( PatchsetStatusUnknown PatchsetStatus = "UNKNOWN" PatchsetStatusProposed PatchsetStatus = "PROPOSED" PatchsetStatusNeedsRevision PatchsetStatus = "NEEDS_REVISION" PatchsetStatusSuperseded PatchsetStatus = "SUPERSEDED" PatchsetStatusApproved PatchsetStatus = "APPROVED" PatchsetStatusRejected PatchsetStatus = "REJECTED" PatchsetStatusApplied PatchsetStatus = "APPLIED" ) // Used to add some kind of indicator for a third-party process associated with // a patchset, such as a CI service validating the change. type PatchsetTool struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` Icon ToolIcon `json:"icon"` Details string `json:"details"` Patchset *Patchset `json:"patchset"` } type Thread struct { Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` Subject string `json:"subject"` Replies int32 `json:"replies"` Participants int32 `json:"participants"` Sender *Entity `json:"sender"` Root *Email `json:"root"` List *MailingList `json:"list"` // Replies to this thread, in chronological order Descendants *EmailCursor `json:"descendants"` // A mailto: URI for replying to the latest message in this thread Mailto string `json:"mailto"` // URL to an application/mbox archive of this thread Mbox URL `json:"mbox"` // Thread parsed as a tree. // // The returned list is never empty. The first item is guaranteed to be the root // message. The blocks are sorted in topological order. Blocks []ThreadBlock `json:"blocks"` } // A block of text in an email thread. // // Blocks are parts of a message's body that aren't quotes of the parent message. // A block can be a reply to a parent block, in which case the parentStart and // parentEnd fields indicate which part of the parent message is replied to. A // block can have replies, each of which will be represented by a block in the // children field. type ThreadBlock struct { // Unique identifier for this block. Key string `json:"key"` // The block's plain-text content. Body string `json:"body"` // Index of the parent block (if any) in Thread.blocks. Parent *int32 `json:"parent,omitempty"` // Replies to this block. // // The list items are indexes into Thread.blocks. Children []int32 `json:"children"` // The email this block comes from. Source *Email `json:"source"` // The range of this block in the source email body. SourceRange *ByteRange `json:"sourceRange"` // If this block is a reply to a particular chunk of the parent block, this // field indicates the range of that chunk in the parent's email body. ParentRange *ByteRange `json:"parentRange,omitempty"` } // A cursor for enumerating threads // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type ThreadCursor struct { Results []Thread `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type ToolIcon string const ( ToolIconPending ToolIcon = "PENDING" ToolIconWaiting ToolIcon = "WAITING" ToolIconSuccess ToolIcon = "SUCCESS" ToolIconFailed ToolIcon = "FAILED" ToolIconCancelled ToolIcon = "CANCELLED" ) // URL from which some secondary data may be retrieved. You must provide the // same Authentication header to this address as you did to the GraphQL resolver // which provided it. The URL is not guaranteed to be consistent for an extended // length of time; applications should submit a new GraphQL query each time they // wish to access the data at the provided URL. type URL string // A registered user type User struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` CanonicalName string `json:"canonicalName"` Username string `json:"username"` Email string `json:"email"` Url *string `json:"url,omitempty"` Location *string `json:"location,omitempty"` Bio *string `json:"bio,omitempty"` List *MailingList `json:"list,omitempty"` Lists *MailingListCursor `json:"lists"` Emails *EmailCursor `json:"emails"` Threads *ThreadCursor `json:"threads"` Patches *PatchsetCursor `json:"patches"` } func (*User) isEntity() {} type UserWebhookInput struct { Url string `json:"url"` Events []WebhookEvent `json:"events"` Query string `json:"query"` } type UserWebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` Client *OAuthClient `json:"client,omitempty"` Deliveries *WebhookDeliveryCursor `json:"deliveries"` Sample string `json:"sample"` } func (*UserWebhookSubscription) isWebhookSubscription() {} type Version struct { Major int32 `json:"major"` Minor int32 `json:"minor"` Patch int32 `json:"patch"` // If this API version is scheduled for deprecation, this is the date on which // it will stop working; or null if this API version is not scheduled for // deprecation. DeprecationDate gqlclient.Time `json:"deprecationDate,omitempty"` } type Visibility string const ( VisibilityPublic Visibility = "PUBLIC" VisibilityUnlisted Visibility = "UNLISTED" VisibilityPrivate Visibility = "PRIVATE" ) type WebhookDelivery struct { Uuid string `json:"uuid"` Date gqlclient.Time `json:"date"` Event WebhookEvent `json:"event"` Subscription *WebhookSubscription `json:"subscription"` RequestBody string `json:"requestBody"` // These details are provided only after a response is received from the // remote server. If a response is sent whose Content-Type is not text/*, or // cannot be decoded as UTF-8, the response body will be null. It will be // truncated after 64 KiB. ResponseBody *string `json:"responseBody,omitempty"` ResponseHeaders *string `json:"responseHeaders,omitempty"` ResponseStatus *int32 `json:"responseStatus,omitempty"` } // A cursor for enumerating a list of webhook deliveries // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type WebhookDeliveryCursor struct { Results []WebhookDelivery `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type WebhookEvent string const ( WebhookEventListCreated WebhookEvent = "LIST_CREATED" WebhookEventListUpdated WebhookEvent = "LIST_UPDATED" WebhookEventListDeleted WebhookEvent = "LIST_DELETED" WebhookEventEmailReceived WebhookEvent = "EMAIL_RECEIVED" WebhookEventPatchsetReceived WebhookEvent = "PATCHSET_RECEIVED" ) type WebhookPayload struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` // Underlying value of the GraphQL interface Value WebhookPayloadValue `json:"-"` } func (base *WebhookPayload) UnmarshalJSON(b []byte) error { type Raw WebhookPayload var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "MailingListEvent": base.Value = new(MailingListEvent) case "EmailEvent": base.Value = new(EmailEvent) case "PatchsetEvent": base.Value = new(PatchsetEvent) case "": return nil default: return fmt.Errorf("gqlclient: interface WebhookPayload: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // WebhookPayloadValue is one of: MailingListEvent | EmailEvent | PatchsetEvent type WebhookPayloadValue interface { isWebhookPayload() } type WebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` // If this webhook was registered by an authorized OAuth 2.0 client, this // field is non-null. Client *OAuthClient `json:"client,omitempty"` // All deliveries which have been sent to this webhook. Deliveries *WebhookDeliveryCursor `json:"deliveries"` // Returns a sample payload for this subscription, for testing purposes Sample string `json:"sample"` // Underlying value of the GraphQL interface Value WebhookSubscriptionValue `json:"-"` } func (base *WebhookSubscription) UnmarshalJSON(b []byte) error { type Raw WebhookSubscription var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "UserWebhookSubscription": base.Value = new(UserWebhookSubscription) case "MailingListWebhookSubscription": base.Value = new(MailingListWebhookSubscription) case "": return nil default: return fmt.Errorf("gqlclient: interface WebhookSubscription: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // WebhookSubscriptionValue is one of: UserWebhookSubscription | MailingListWebhookSubscription type WebhookSubscriptionValue interface { isWebhookSubscription() } // A cursor for enumerating a list of webhook subscriptions // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type WebhookSubscriptionCursor struct { Results []WebhookSubscription `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } func DeleteMailingList(client *gqlclient.Client, ctx context.Context, id int32) (deleteMailingList *MailingList, err error) { op := gqlclient.NewOperation("mutation deleteMailingList ($id: Int!) {\n\tdeleteMailingList(id: $id) {\n\t\tname\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteMailingList *MailingList } err = client.Execute(ctx, op, &respData) return respData.DeleteMailingList, err } func MailingLists(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (me *User, err error) { op := gqlclient.NewOperation("query mailingLists ($cursor: Cursor) {\n\tme {\n\t\tlists(cursor: $cursor) {\n\t\t\t... lists\n\t\t}\n\t}\n}\nfragment lists on MailingListCursor {\n\tresults {\n\t\tname\n\t\tdescription\n\t\tvisibility\n\t}\n\tcursor\n}\n") op.Var("cursor", cursor) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func ExportMailingList(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) { op := gqlclient.NewOperation("query exportMailingList ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\tlist(name: $name) {\n\t\t\t... mailingListExport\n\t\t}\n\t}\n}\nfragment mailingListExport on MailingList {\n\tname\n\tdescription\n\tvisibility\n\tpermitMime\n\trejectMime\n\tarchive\n}\n") op.Var("username", username) op.Var("name", name) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func ExportMailingLists(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (me *User, err error) { op := gqlclient.NewOperation("query exportMailingLists ($cursor: Cursor) {\n\tme {\n\t\tlists(cursor: $cursor) {\n\t\t\tresults {\n\t\t\t\t... mailingListExport\n\t\t\t}\n\t\t\tcursor\n\t\t}\n\t}\n}\nfragment mailingListExport on MailingList {\n\tname\n\tdescription\n\tvisibility\n\tpermitMime\n\trejectMime\n\tarchive\n}\n") op.Var("cursor", cursor) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func MailingListsByUser(client *gqlclient.Client, ctx context.Context, username string, cursor *Cursor) (user *User, err error) { op := gqlclient.NewOperation("query mailingListsByUser ($username: String!, $cursor: Cursor) {\n\tuser(username: $username) {\n\t\tlists(cursor: $cursor) {\n\t\t\t... lists\n\t\t}\n\t}\n}\nfragment lists on MailingListCursor {\n\tresults {\n\t\tname\n\t\tdescription\n\t\tvisibility\n\t}\n\tcursor\n}\n") op.Var("username", username) op.Var("cursor", cursor) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func MailingListIDByName(client *gqlclient.Client, ctx context.Context, name string) (me *User, err error) { op := gqlclient.NewOperation("query mailingListIDByName ($name: String!) {\n\tme {\n\t\tlist(name: $name) {\n\t\t\tid\n\t\t}\n\t}\n}\n") op.Var("name", name) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func MailingListIDByUser(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) { op := gqlclient.NewOperation("query mailingListIDByUser ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\tlist(name: $name) {\n\t\t\tid\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func ListPatches(client *gqlclient.Client, ctx context.Context, name string, cursor *Cursor) (me *User, err error) { op := gqlclient.NewOperation("query listPatches ($name: String!, $cursor: Cursor) {\n\tme {\n\t\tlist(name: $name) {\n\t\t\t... patchsetsByList\n\t\t}\n\t}\n}\nfragment patchsetsByList on MailingList {\n\tpatches(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\tsubject\n\t\t\tstatus\n\t\t\tcreated\n\t\t\tversion\n\t\t\tprefix\n\t\t\tsubmitter {\n\t\t\t\tcanonicalName\n\t\t\t}\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("name", name) op.Var("cursor", cursor) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func ListPatchesByUser(client *gqlclient.Client, ctx context.Context, username string, name string, cursor *Cursor) (user *User, err error) { op := gqlclient.NewOperation("query listPatchesByUser ($username: String!, $name: String!, $cursor: Cursor) {\n\tuser(username: $username) {\n\t\tlist(name: $name) {\n\t\t\t... patchsetsByList\n\t\t}\n\t}\n}\nfragment patchsetsByList on MailingList {\n\tpatches(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\tsubject\n\t\t\tstatus\n\t\t\tcreated\n\t\t\tversion\n\t\t\tprefix\n\t\t\tsubmitter {\n\t\t\t\tcanonicalName\n\t\t\t}\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("username", username) op.Var("name", name) op.Var("cursor", cursor) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func Patches(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (me *User, err error) { op := gqlclient.NewOperation("query patches ($cursor: Cursor) {\n\tme {\n\t\t... patchsets\n\t}\n}\nfragment patchsets on User {\n\tpatches(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\tsubject\n\t\t\tstatus\n\t\t\tcreated\n\t\t\tversion\n\t\t\tprefix\n\t\t\tlist {\n\t\t\t\tname\n\t\t\t\towner {\n\t\t\t\t\tcanonicalName\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func PatchesByUser(client *gqlclient.Client, ctx context.Context, username string, cursor *Cursor) (user *User, err error) { op := gqlclient.NewOperation("query patchesByUser ($username: String!, $cursor: Cursor) {\n\tuser(username: $username) {\n\t\t... patchsets\n\t}\n}\nfragment patchsets on User {\n\tpatches(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\tsubject\n\t\t\tstatus\n\t\t\tcreated\n\t\t\tversion\n\t\t\tprefix\n\t\t\tlist {\n\t\t\t\tname\n\t\t\t\towner {\n\t\t\t\t\tcanonicalName\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("username", username) op.Var("cursor", cursor) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func PatchsetById(client *gqlclient.Client, ctx context.Context, id int32, cursor *Cursor) (patchset *Patchset, err error) { op := gqlclient.NewOperation("query patchsetById ($id: Int!, $cursor: Cursor) {\n\tpatchset(id: $id) {\n\t\tpatches(cursor: $cursor) {\n\t\t\tresults {\n\t\t\t\tdate\n\t\t\t\tbody\n\t\t\t\tsubject\n\t\t\t\theader(want: \"From\")\n\t\t\t}\n\t\t\tcursor\n\t\t}\n\t}\n}\n") op.Var("id", id) op.Var("cursor", cursor) var respData struct { Patchset *Patchset } err = client.Execute(ctx, op, &respData) return respData.Patchset, err } func CompletePatchsetId(client *gqlclient.Client, ctx context.Context, name string) (me *User, err error) { op := gqlclient.NewOperation("query completePatchsetId ($name: String!) {\n\tme {\n\t\tlist(name: $name) {\n\t\t\t... completePatchset\n\t\t}\n\t}\n}\nfragment completePatchset on MailingList {\n\tpatches {\n\t\tresults {\n\t\t\tid\n\t\t\tsubject\n\t\t\tstatus\n\t\t\tversion\n\t\t\tprefix\n\t\t}\n\t}\n}\n") op.Var("name", name) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func CompletePatchsetIdByUser(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) { op := gqlclient.NewOperation("query completePatchsetIdByUser ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\tlist(name: $name) {\n\t\t\t... completePatchset\n\t\t}\n\t}\n}\nfragment completePatchset on MailingList {\n\tpatches {\n\t\tresults {\n\t\t\tid\n\t\t\tsubject\n\t\t\tstatus\n\t\t\tversion\n\t\t\tprefix\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func AclByListName(client *gqlclient.Client, ctx context.Context, name string, cursor *Cursor) (me *User, err error) { op := gqlclient.NewOperation("query aclByListName ($name: String!, $cursor: Cursor) {\n\tme {\n\t\t... acl\n\t}\n}\nfragment acl on User {\n\tlist(name: $name) {\n\t\tdefaultACL {\n\t\t\tbrowse\n\t\t\treply\n\t\t\tpost\n\t\t\tmoderate\n\t\t}\n\t\tacl(cursor: $cursor) {\n\t\t\tresults {\n\t\t\t\tid\n\t\t\t\tcreated\n\t\t\t\tentity {\n\t\t\t\t\tcanonicalName\n\t\t\t\t}\n\t\t\t\tbrowse\n\t\t\t\treply\n\t\t\t\tpost\n\t\t\t\tmoderate\n\t\t\t}\n\t\t\tcursor\n\t\t}\n\t}\n}\n") op.Var("name", name) op.Var("cursor", cursor) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func AclByUser(client *gqlclient.Client, ctx context.Context, username string, name string, cursor *Cursor) (user *User, err error) { op := gqlclient.NewOperation("query aclByUser ($username: String!, $name: String!, $cursor: Cursor) {\n\tuser(username: $username) {\n\t\t... acl\n\t}\n}\nfragment acl on User {\n\tlist(name: $name) {\n\t\tdefaultACL {\n\t\t\tbrowse\n\t\t\treply\n\t\t\tpost\n\t\t\tmoderate\n\t\t}\n\t\tacl(cursor: $cursor) {\n\t\t\tresults {\n\t\t\t\tid\n\t\t\t\tcreated\n\t\t\t\tentity {\n\t\t\t\t\tcanonicalName\n\t\t\t\t}\n\t\t\t\tbrowse\n\t\t\t\treply\n\t\t\t\tpost\n\t\t\t\tmoderate\n\t\t\t}\n\t\t\tcursor\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) op.Var("cursor", cursor) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func UserWebhooks(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (userWebhooks *WebhookSubscriptionCursor, err error) { op := gqlclient.NewOperation("query userWebhooks ($cursor: Cursor) {\n\tuserWebhooks(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\turl\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { UserWebhooks *WebhookSubscriptionCursor } err = client.Execute(ctx, op, &respData) return respData.UserWebhooks, err } func Archive(client *gqlclient.Client, ctx context.Context, name string) (me *User, err error) { op := gqlclient.NewOperation("query archive ($name: String!) {\n\tme {\n\t\tlist(name: $name) {\n\t\t\tarchive\n\t\t}\n\t}\n}\n") op.Var("name", name) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func ArchiveByUser(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) { op := gqlclient.NewOperation("query archiveByUser ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\tlist(name: $name) {\n\t\t\tarchive\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func CompleteLists(client *gqlclient.Client, ctx context.Context) (me *User, err error) { op := gqlclient.NewOperation("query completeLists {\n\tme {\n\t\tlists {\n\t\t\tresults {\n\t\t\t\tname\n\t\t\t}\n\t\t}\n\t}\n}\n") var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func MailingListWebhooks(client *gqlclient.Client, ctx context.Context, name string, cursor *Cursor) (me *User, err error) { op := gqlclient.NewOperation("query mailingListWebhooks ($name: String!, $cursor: Cursor) {\n\tme {\n\t\tlist(name: $name) {\n\t\t\t... mailingListWebhooks\n\t\t}\n\t}\n}\nfragment mailingListWebhooks on MailingList {\n\twebhooks(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\turl\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("name", name) op.Var("cursor", cursor) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func MailingListWebhooksByUser(client *gqlclient.Client, ctx context.Context, username string, name string, cursor *Cursor) (user *User, err error) { op := gqlclient.NewOperation("query mailingListWebhooksByUser ($username: String!, $name: String!, $cursor: Cursor) {\n\tuser(username: $username) {\n\t\tlist(name: $name) {\n\t\t\t... mailingListWebhooks\n\t\t}\n\t}\n}\nfragment mailingListWebhooks on MailingList {\n\twebhooks(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\turl\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("username", username) op.Var("name", name) op.Var("cursor", cursor) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func Subscriptions(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (subscriptions *ActivitySubscriptionCursor, err error) { op := gqlclient.NewOperation("query subscriptions ($cursor: Cursor) {\n\tsubscriptions(cursor: $cursor) {\n\t\tresults {\n\t\t\tcreated\n\t\t\t__typename\n\t\t\t... on MailingListSubscription {\n\t\t\t\tlist {\n\t\t\t\t\tname\n\t\t\t\t\towner {\n\t\t\t\t\t\tcanonicalName\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { Subscriptions *ActivitySubscriptionCursor } err = client.Execute(ctx, op, &respData) return respData.Subscriptions, err } func MailingListDescription(client *gqlclient.Client, ctx context.Context, name string) (me *User, err error) { op := gqlclient.NewOperation("query mailingListDescription ($name: String!) {\n\tme {\n\t\tlist(name: $name) {\n\t\t\tdescription\n\t\t}\n\t}\n}\n") op.Var("name", name) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func MailingListDescriptionByUser(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) { op := gqlclient.NewOperation("query mailingListDescriptionByUser ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\tlist(name: $name) {\n\t\t\tdescription\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func MailingListSubscribe(client *gqlclient.Client, ctx context.Context, listID int32) (mailingListSubscribe *MailingListSubscription, err error) { op := gqlclient.NewOperation("mutation mailingListSubscribe ($listID: Int!) {\n\tmailingListSubscribe(listID: $listID) {\n\t\tlist {\n\t\t\tname\n\t\t\towner {\n\t\t\t\tcanonicalName\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("listID", listID) var respData struct { MailingListSubscribe *MailingListSubscription } err = client.Execute(ctx, op, &respData) return respData.MailingListSubscribe, err } func MailingListUnsubscribe(client *gqlclient.Client, ctx context.Context, listID int32) (mailingListUnsubscribe *MailingListSubscription, err error) { op := gqlclient.NewOperation("mutation mailingListUnsubscribe ($listID: Int!) {\n\tmailingListUnsubscribe(listID: $listID) {\n\t\tlist {\n\t\t\tname\n\t\t\towner {\n\t\t\t\tcanonicalName\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("listID", listID) var respData struct { MailingListUnsubscribe *MailingListSubscription } err = client.Execute(ctx, op, &respData) return respData.MailingListUnsubscribe, err } func UpdatePatchset(client *gqlclient.Client, ctx context.Context, id int32, status PatchsetStatus) (updatePatchset *Patchset, err error) { op := gqlclient.NewOperation("mutation updatePatchset ($id: Int!, $status: PatchsetStatus!) {\n\tupdatePatchset(id: $id, status: $status) {\n\t\tsubmitter {\n\t\t\tcanonicalName\n\t\t}\n\t\tsubject\n\t}\n}\n") op.Var("id", id) op.Var("status", status) var respData struct { UpdatePatchset *Patchset } err = client.Execute(ctx, op, &respData) return respData.UpdatePatchset, err } func DeleteACL(client *gqlclient.Client, ctx context.Context, id int32) (deleteACL *MailingListACL, err error) { op := gqlclient.NewOperation("mutation deleteACL ($id: Int!) {\n\tdeleteACL(id: $id) {\n\t\tentity {\n\t\t\tcanonicalName\n\t\t}\n\t\tlist {\n\t\t\tname\n\t\t}\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteACL *MailingListACL } err = client.Execute(ctx, op, &respData) return respData.DeleteACL, err } func CreateMailingList(client *gqlclient.Client, ctx context.Context, name string, description *string, visibility Visibility) (createMailingList *MailingList, err error) { op := gqlclient.NewOperation("mutation createMailingList ($name: String!, $description: String, $visibility: Visibility!) {\n\tcreateMailingList(name: $name, description: $description, visibility: $visibility) {\n\t\tid\n\t\tname\n\t}\n}\n") op.Var("name", name) op.Var("description", description) op.Var("visibility", visibility) var respData struct { CreateMailingList *MailingList } err = client.Execute(ctx, op, &respData) return respData.CreateMailingList, err } func UpdateMailingList(client *gqlclient.Client, ctx context.Context, id int32, input MailingListInput) (updateMailingList *MailingList, err error) { op := gqlclient.NewOperation("mutation updateMailingList ($id: Int!, $input: MailingListInput!) {\n\tupdateMailingList(id: $id, input: $input) {\n\t\tid\n\t}\n}\n") op.Var("id", id) op.Var("input", input) var respData struct { UpdateMailingList *MailingList } err = client.Execute(ctx, op, &respData) return respData.UpdateMailingList, err } func ClearDescription(client *gqlclient.Client, ctx context.Context, id int32) (updateMailingList *MailingList, err error) { op := gqlclient.NewOperation("mutation clearDescription ($id: Int!) {\n\tupdateMailingList(id: $id, input: {description:null}) {\n\t\tid\n\t}\n}\n") op.Var("id", id) var respData struct { UpdateMailingList *MailingList } err = client.Execute(ctx, op, &respData) return respData.UpdateMailingList, err } func ImportMailingListSpool(client *gqlclient.Client, ctx context.Context, id int32, spool gqlclient.Upload) (importMailingListSpool bool, err error) { op := gqlclient.NewOperation("mutation importMailingListSpool ($id: Int!, $spool: Upload!) {\n\timportMailingListSpool(listID: $id, spool: $spool)\n}\n") op.Var("id", id) op.Var("spool", spool) var respData struct { ImportMailingListSpool bool } err = client.Execute(ctx, op, &respData) return respData.ImportMailingListSpool, err } func CreateUserWebhook(client *gqlclient.Client, ctx context.Context, config UserWebhookInput) (createUserWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation createUserWebhook ($config: UserWebhookInput!) {\n\tcreateUserWebhook(config: $config) {\n\t\tid\n\t}\n}\n") op.Var("config", config) var respData struct { CreateUserWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.CreateUserWebhook, err } func DeleteUserWebhook(client *gqlclient.Client, ctx context.Context, id int32) (deleteUserWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation deleteUserWebhook ($id: Int!) {\n\tdeleteUserWebhook(id: $id) {\n\t\tid\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteUserWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.DeleteUserWebhook, err } func CreateMailingListWebhook(client *gqlclient.Client, ctx context.Context, listId int32, config MailingListWebhookInput) (createMailingListWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation createMailingListWebhook ($listId: Int!, $config: MailingListWebhookInput!) {\n\tcreateMailingListWebhook(listId: $listId, config: $config) {\n\t\tid\n\t}\n}\n") op.Var("listId", listId) op.Var("config", config) var respData struct { CreateMailingListWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.CreateMailingListWebhook, err } func DeleteMailingListWebhook(client *gqlclient.Client, ctx context.Context, id int32) (deleteMailingListWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation deleteMailingListWebhook ($id: Int!) {\n\tdeleteMailingListWebhook(id: $id) {\n\t\tid\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteMailingListWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.DeleteMailingListWebhook, err } hut-0.6.0/srht/listssrht/operations.graphql000066400000000000000000000166421463710650600211200ustar00rootroot00000000000000mutation deleteMailingList($id: Int!) { deleteMailingList(id: $id) { name } } query mailingLists($cursor: Cursor) { me { lists(cursor: $cursor) { ...lists } } } query exportMailingList($username: String!, $name: String!) { user(username: $username) { list(name: $name) { ...mailingListExport } } } query exportMailingLists($cursor: Cursor) { me { lists(cursor: $cursor) { results { ...mailingListExport } cursor } } } fragment mailingListExport on MailingList { name description visibility permitMime rejectMime archive } query mailingListsByUser($username: String!, $cursor: Cursor) { user(username: $username) { lists(cursor: $cursor) { ...lists } } } fragment lists on MailingListCursor { results { name description visibility } cursor } query mailingListIDByName($name: String!) { me { list(name: $name) { id } } } query mailingListIDByUser($username: String!, $name: String!) { user(username: $username) { list(name: $name) { id } } } query listPatches($name: String!, $cursor: Cursor) { me { list(name: $name) { ...patchsetsByList } } } query listPatchesByUser($username: String!, $name: String!, $cursor: Cursor) { user(username: $username) { list(name: $name) { ...patchsetsByList } } } fragment patchsetsByList on MailingList { patches(cursor: $cursor) { results { id subject status created version prefix submitter { canonicalName } } cursor } } query patches($cursor: Cursor) { me { ...patchsets } } query patchesByUser($username: String!, $cursor: Cursor) { user(username: $username) { ...patchsets } } fragment patchsets on User { patches(cursor: $cursor) { results { id subject status created version prefix list { name owner { canonicalName } } } cursor } } query patchsetById($id: Int!, $cursor: Cursor) { patchset(id: $id) { patches(cursor: $cursor) { results { date body subject header(want: "From") } cursor } } } query completePatchsetId($name: String!) { me { list(name: $name) { ...completePatchset } } } query completePatchsetIdByUser($username: String!, $name: String!) { user(username: $username) { list(name: $name) { ...completePatchset } } } fragment completePatchset on MailingList { patches { results { id subject status version prefix } } } query aclByListName($name: String!, $cursor: Cursor) { me { ...acl } } query aclByUser($username: String!, $name: String!, $cursor: Cursor) { user(username: $username) { ...acl } } fragment acl on User { list(name: $name) { defaultACL { browse reply post moderate } acl(cursor: $cursor) { results { id created entity { canonicalName } browse reply post moderate } cursor } } } query userWebhooks($cursor: Cursor) { userWebhooks(cursor: $cursor) { results { id url } cursor } } query archive($name: String!) { me { list(name: $name) { archive } } } query archiveByUser($username: String!, $name: String!) { user(username: $username) { list(name: $name) { archive } } } query completeLists { me { lists { results { name } } } } query mailingListWebhooks($name: String!, $cursor: Cursor) { me { list(name: $name) { ...mailingListWebhooks } } } query mailingListWebhooksByUser( $username: String! $name: String! $cursor: Cursor ) { user(username: $username) { list(name: $name) { ...mailingListWebhooks } } } fragment mailingListWebhooks on MailingList { webhooks(cursor: $cursor) { results { id url } cursor } } query subscriptions($cursor: Cursor) { subscriptions(cursor: $cursor) { results { created __typename ... on MailingListSubscription { list { name owner { canonicalName } } } } cursor } } query mailingListDescription($name: String!) { me { list(name: $name) { description } } } query mailingListDescriptionByUser($username: String!, $name: String!) { user(username: $username) { list(name: $name) { description } } } mutation mailingListSubscribe($listID: Int!) { mailingListSubscribe(listID: $listID) { list { name owner { canonicalName } } } } mutation mailingListUnsubscribe($listID: Int!) { mailingListUnsubscribe(listID: $listID) { list { name owner { canonicalName } } } } mutation updatePatchset($id: Int!, $status: PatchsetStatus!) { updatePatchset(id: $id, status: $status) { submitter { canonicalName } subject } } mutation deleteACL($id: Int!) { deleteACL(id: $id) { entity { canonicalName } list { name } } } mutation createMailingList( $name: String! $description: String $visibility: Visibility! ) { createMailingList( name: $name description: $description visibility: $visibility ) { id name } } mutation updateMailingList($id: Int!, $input: MailingListInput!) { updateMailingList(id: $id, input: $input) { id } } mutation clearDescription($id: Int!) { updateMailingList(id: $id, input: { description: null }) { id } } mutation importMailingListSpool($id: Int!, $spool: Upload!) { importMailingListSpool(listID: $id, spool: $spool) } mutation createUserWebhook($config: UserWebhookInput!) { createUserWebhook(config: $config) { id } } mutation deleteUserWebhook($id: Int!) { deleteUserWebhook(id: $id) { id } } mutation createMailingListWebhook( $listId: Int! $config: MailingListWebhookInput! ) { createMailingListWebhook(listId: $listId, config: $config) { id } } mutation deleteMailingListWebhook($id: Int!) { deleteMailingListWebhook(id: $id) { id } } hut-0.6.0/srht/listssrht/schema.graphqls000066400000000000000000000501641463710650600203550ustar00rootroot00000000000000# This schema definition is available in the public domain, or under the terms # of CC-0, at your choice. "String of the format %Y-%m-%dT%H:%M:%SZ" scalar Time "Opaque string" scalar Cursor """ URL from which some secondary data may be retrieved. You must provide the same Authentication header to this address as you did to the GraphQL resolver which provided it. The URL is not guaranteed to be consistent for an extended length of time; applications should submit a new GraphQL query each time they wish to access the data at the provided URL. """ scalar URL scalar Upload "Used to provide a human-friendly description of an access scope" directive @scopehelp(details: String!) on ENUM_VALUE """ This is used to decorate fields which are only accessible with a personal access token, and are not available to clients using OAuth 2.0 access tokens. """ directive @private on FIELD_DEFINITION """ This is used to decorate fields which are for internal use, and are not available to normal API users. """ directive @internal on FIELD_DEFINITION enum AccessScope { ACLS @scopehelp(details: "access control lists") EMAILS @scopehelp(details: "emails") LISTS @scopehelp(details: "mailing lists") PATCHES @scopehelp(details: "patches") PROFILE @scopehelp(details: "profile information") SUBSCRIPTIONS @scopehelp(details: "tracker & ticket subscriptions") } enum AccessKind { RO @scopehelp(details: "read") RW @scopehelp(details: "read and write") } """ Decorates fields for which access requires a particular OAuth 2.0 scope with read or write access. """ directive @access(scope: AccessScope!, kind: AccessKind!) on FIELD_DEFINITION # https://semver.org type Version { major: Int! minor: Int! patch: Int! """ If this API version is scheduled for deprecation, this is the date on which it will stop working; or null if this API version is not scheduled for deprecation. """ deprecationDate: Time } interface Entity { canonicalName: String! } "A registered user" type User implements Entity { id: Int! created: Time! updated: Time! canonicalName: String! username: String! email: String! url: String location: String bio: String list(name: String!): MailingList @access(scope: LISTS, kind: RO) lists(cursor: Cursor): MailingListCursor! @access(scope: LISTS, kind: RO) emails(cursor: Cursor): EmailCursor! @access(scope: EMAILS, kind: RO) threads(cursor: Cursor): ThreadCursor! @access(scope: EMAILS, kind: RO) patches(cursor: Cursor): PatchsetCursor! @access(scope: PATCHES, kind: RO) } "A mailbox not associated with a registered user" type Mailbox implements Entity { canonicalName: String! name: String! address: String! } enum Visibility { PUBLIC UNLISTED PRIVATE } type MailingList { id: Int! created: Time! updated: Time! name: String! owner: Entity! @access(scope: PROFILE, kind: RO) # Markdown description: String visibility: Visibility! """ List of globs for permitted or rejected mimetypes on this list e.g. text/* """ permitMime: [String!]! rejectMime: [String!]! "List of threads on this list in order of most recently bumped" threads(cursor: Cursor): ThreadCursor! @access(scope: EMAILS, kind: RO) "List of emails received on this list in reverse chronological order" emails(cursor: Cursor): EmailCursor! @access(scope: EMAILS, kind: RO) "List of patches received on this list in order of most recently bumped" patches(cursor: Cursor): PatchsetCursor! @access(scope: PATCHES, kind: RO) "True if an import operation is underway for this list" importing: Boolean! "The access that applies to this user for this list" access: ACL! @access(scope: ACLS, kind: RO) "The user's subscription for this list, if any" subscription: MailingListSubscription @access(scope: SUBSCRIPTIONS, kind: RO) "URLs to application/mbox archives for this mailing list" archive: URL! last30days: URL! # # The following resolvers are only available to the list owner: "Access control list entries for this mailing list" acl(cursor: Cursor): MailingListACLCursor! @access(scope: ACLS, kind: RO) defaultACL: GeneralACL! """ Returns a list of mailing list webhook subscriptions. For clients authenticated with a personal access token, this returns all webhooks configured by all GraphQL clients for your account. For clients authenticated with an OAuth 2.0 access token, this returns only webhooks registered for your client. """ webhooks(cursor: Cursor): WebhookSubscriptionCursor! "Returns details of a mailing list webhook subscription by its ID." webhook(id: Int!): WebhookSubscription } type OAuthClient { uuid: String! } enum WebhookEvent { LIST_CREATED LIST_UPDATED LIST_DELETED EMAIL_RECEIVED PATCHSET_RECEIVED } interface WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! """ If this webhook was registered by an authorized OAuth 2.0 client, this field is non-null. """ client: OAuthClient @private "All deliveries which have been sent to this webhook." deliveries(cursor: Cursor): WebhookDeliveryCursor! "Returns a sample payload for this subscription, for testing purposes" sample(event: WebhookEvent!): String! } type UserWebhookSubscription implements WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! client: OAuthClient @private deliveries(cursor: Cursor): WebhookDeliveryCursor! sample(event: WebhookEvent!): String! } type MailingListWebhookSubscription implements WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! client: OAuthClient @private deliveries(cursor: Cursor): WebhookDeliveryCursor! sample(event: WebhookEvent!): String! list: MailingList! } type WebhookDelivery { uuid: String! date: Time! event: WebhookEvent! subscription: WebhookSubscription! requestBody: String! """ These details are provided only after a response is received from the remote server. If a response is sent whose Content-Type is not text/*, or cannot be decoded as UTF-8, the response body will be null. It will be truncated after 64 KiB. """ responseBody: String responseHeaders: String responseStatus: Int } interface WebhookPayload { uuid: String! event: WebhookEvent! date: Time! } type MailingListEvent implements WebhookPayload { uuid: String! event: WebhookEvent! date: Time! list: MailingList! } type EmailEvent implements WebhookPayload { uuid: String! event: WebhookEvent! date: Time! email: Email! } type PatchsetEvent implements WebhookPayload { uuid: String! event: WebhookEvent! date: Time! patchset: Patchset! } interface ACL { "Permission to browse or subscribe to emails" browse: Boolean! "Permission to reply to existing threads" reply: Boolean! "Permission to start new threads" post: Boolean! "Permission to moderate the list" moderate: Boolean! } """ These ACLs are configured for specific entities, and may be used to expand or constrain the rights of a participant. """ type MailingListACL implements ACL { id: Int! created: Time! list: MailingList! @access(scope: LISTS, kind: RO) entity: Entity! @access(scope: PROFILE, kind: RO) browse: Boolean! reply: Boolean! post: Boolean! moderate: Boolean! } """ An ACL entry that applies "generally", for example the rights which apply to all subscribers to a list. """ type GeneralACL implements ACL { browse: Boolean! reply: Boolean! post: Boolean! moderate: Boolean! } type Thread { created: Time! updated: Time! subject: String! replies: Int! participants: Int! sender: Entity! root: Email! list: MailingList! @access(scope: LISTS, kind: RO) "Replies to this thread, in chronological order" descendants(cursor: Cursor): EmailCursor! "A mailto: URI for replying to the latest message in this thread" mailto: String! "URL to an application/mbox archive of this thread" mbox: URL! """ Thread parsed as a tree. The returned list is never empty. The first item is guaranteed to be the root message. The blocks are sorted in topological order. """ blocks: [ThreadBlock!]! } """ A block of text in an email thread. Blocks are parts of a message's body that aren't quotes of the parent message. A block can be a reply to a parent block, in which case the parentStart and parentEnd fields indicate which part of the parent message is replied to. A block can have replies, each of which will be represented by a block in the children field. """ type ThreadBlock { "Unique identifier for this block." key: String! "The block's plain-text content." body: String! "Index of the parent block (if any) in Thread.blocks." parent: Int """ Replies to this block. The list items are indexes into Thread.blocks. """ children: [Int!]! "The email this block comes from." source: Email! "The range of this block in the source email body." sourceRange: ByteRange! """ If this block is a reply to a particular chunk of the parent block, this field indicates the range of that chunk in the parent's email body. """ parentRange: ByteRange } """ A byte range. """ type ByteRange { "Inclusive start byte offset." start: Int! "Exclusive end byte offset." end: Int! } type Email { id: Int! """ The entity which sent this email. Will be a User if it can be associated with an account, or a Mailbox otherwise. """ sender: Entity! "Time we received this email (non-forgable)." received: Time! "Time given by Date header (forgable)." date: Time "The Subject header." subject: String! "The Message-ID header, without angle brackets." messageID: String! "The In-Reply-To header, if present, without angle brackets." inReplyTo: String """ Provides the value (or values) of a specific header from this email. Note that the returned value is coerced to UTF-8 and may be lossy under certain circumstances. """ header(want: String!): [String!]! "Retrieves the value of an address list header, such as To or Cc." addressList(want: String!): [Mailbox!]! "The decoded text/plain message part of the email, i.e. email body." body: String! "A URL from which the full raw message envelope may be downloaded." envelope: URL! thread: Thread! parent: Email patch: Patch patchset: Patchset @access(scope: PATCHES, kind: RO) list: MailingList! @access(scope: LISTS, kind: RO) } """ Information parsed from the subject line of a patch, such that the following: [PATCH myproject v2 3/4] Add foo to bar Will produce: index: 3 count: 4 version: 2 prefix: "myproject" subject: "Add foo to bar" """ type Patch { index: Int count: Int version: Int prefix: String subject: String } enum PatchsetStatus { UNKNOWN PROPOSED NEEDS_REVISION SUPERSEDED APPROVED REJECTED APPLIED } type Patchset { id: Int! created: Time! updated: Time! subject: String! version: Int! prefix: String status: PatchsetStatus! submitter: Entity! coverLetter: Email @access(scope: EMAILS, kind: RO) thread: Thread! @access(scope: EMAILS, kind: RO) supersededBy: Patchset list: MailingList! @access(scope: LISTS, kind: RO) patches(cursor: Cursor): EmailCursor! @access(scope: EMAILS, kind: RO) tools: [PatchsetTool!]! "URL to an application/mbox archive of only the patches in this thread" mbox: URL! } enum ToolIcon { PENDING WAITING SUCCESS FAILED CANCELLED } """ Used to add some kind of indicator for a third-party process associated with a patchset, such as a CI service validating the change. """ type PatchsetTool { id: Int! created: Time! updated: Time! icon: ToolIcon! details: String! patchset: Patchset! } interface ActivitySubscription { id: Int! created: Time! } type MailingListSubscription implements ActivitySubscription { id: Int! created: Time! list: MailingList! @access(scope: LISTS, kind: RO) } """ A cursor for enumerating ACL entries If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type MailingListACLCursor { results: [MailingListACL!]! cursor: Cursor } """ A cursor for enumerating mailing lists If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type MailingListCursor { results: [MailingList!]! cursor: Cursor } """ A cursor for enumerating threads If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type ThreadCursor { results: [Thread!]! cursor: Cursor } """ A cursor for enumerating emails If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type EmailCursor { results: [Email!]! cursor: Cursor } """ A cursor for enumerating patchsets If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type PatchsetCursor { results: [Patchset!]! cursor: Cursor } """ A cursor for enumerating subscriptions If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type ActivitySubscriptionCursor { results: [ActivitySubscription!]! cursor: Cursor } """ A cursor for enumerating a list of webhook deliveries If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type WebhookDeliveryCursor { results: [WebhookDelivery!]! cursor: Cursor } """ A cursor for enumerating a list of webhook subscriptions If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type WebhookSubscriptionCursor { results: [WebhookSubscription!]! cursor: Cursor } type Query { "Returns API version information" version: Version! "Returns the authenticated user" me: User! @access(scope: PROFILE, kind: RO) "Looks up a specific user" user(username: String!): User @access(scope: PROFILE, kind: RO) "Looks up a specific email by its ID" email(id: Int!): Email @access(scope: EMAILS, kind: RO) """ Looks up a specific email by its Message-ID header, including the angle brackets ('<' and '>'). """ message(messageID: String!): Email @access(scope: EMAILS, kind: RO) "Looks up a patchset by ID" patchset(id: Int!): Patchset @access(scope: EMAILS, kind: RO) "List of subscriptions of the authenticated user" subscriptions(cursor: Cursor): ActivitySubscriptionCursor @access(scope: SUBSCRIPTIONS, kind: RO) """ Returns a list of user webhook subscriptions. For clients authenticated with a personal access token, this returns all webhooks configured by all GraphQL clients for your account. For clients authenticated with an OAuth 2.0 access token, this returns only webhooks registered for your client. """ userWebhooks(cursor: Cursor): WebhookSubscriptionCursor! "Returns details of a user webhook subscription by its ID." userWebhook(id: Int!): WebhookSubscription """ Returns information about the webhook currently being processed. This is not valid during normal queries over HTTP, and will return an error if used outside of a webhook context. """ webhook: WebhookPayload! } # You may omit any fields to leave them unchanged. # TODO: Allow users to change the name of a mailing list input MailingListInput { description: String visibility: Visibility """ List of globs for permitted or rejected mimetypes on this list e.g. text/* """ permitMime: [String!] rejectMime: [String!] } # All fields are required input ACLInput { browse: Boolean! reply: Boolean! post: Boolean! moderate: Boolean! } input UserWebhookInput { url: String! events: [WebhookEvent!]! query: String! } input MailingListWebhookInput { url: String! events: [WebhookEvent!]! query: String! } type Mutation { "Creates a new mailing list" createMailingList( name: String!, description: String, visibility: Visibility!): MailingList! @access(scope: LISTS, kind: RW) "Updates a mailing list." updateMailingList( id: Int!, input: MailingListInput!): MailingList @access(scope: LISTS, kind: RW) "Deletes a mailing list" deleteMailingList(id: Int!): MailingList @access(scope: LISTS, kind: RW) "Adds or updates the ACL for a user on a mailing list" updateUserACL( listID: Int!, userID: Int!, input: ACLInput!): MailingListACL @access(scope: ACLS, kind: RW) "Adds or updates the ACL for an email address on a mailing list" updateSenderACL( listID: Int!, address: String!, input: ACLInput!): MailingListACL @access(scope: ACLS, kind: RW) """ Updates the default ACL for a mailing list, which applies to users and senders for whom a more specific ACL does not exist. """ updateMailingListACL( listID: Int!, input: ACLInput!): MailingList @access(scope: ACLS, kind: RW) """ Removes a mailing list ACL. Following this, the default mailing list ACL will apply to this user. """ deleteACL(id: Int!): MailingListACL @access(scope: ACLS, kind: RW) "Updates the status of a patchset" updatePatchset(id: Int!, status: PatchsetStatus!): Patchset @access(scope: PATCHES, kind: RW) "Create a new patchset tool" createTool(patchsetID: Int!, details: String!, icon: ToolIcon!): PatchsetTool @access(scope: PATCHES, kind: RW) "Updates the status of a patchset tool by its ID" updateTool(id: Int!, details: String, icon: ToolIcon): PatchsetTool @access(scope: PATCHES, kind: RW) "Creates a mailing list subscription" mailingListSubscribe(listID: Int!): MailingListSubscription @access(scope: SUBSCRIPTIONS, kind: RW) "Deletes a mailing list subscription" mailingListUnsubscribe(listID: Int!): MailingListSubscription @access(scope: SUBSCRIPTIONS, kind: RW) "Imports a mail spool (must be in the Mbox format)" importMailingListSpool(listID: Int!, spool: Upload!): Boolean! @access(scope: LISTS, kind: RW) """ Creates a new user webhook subscription. When an event from the provided list of events occurs, the 'query' parameter (a GraphQL query) will be evaluated and the results will be sent to the provided URL as the body of an HTTP POST request. The list of events must include at least one event, and no duplicates. This query is evaluated in the webhook context, such that query { webhook } may be used to access details of the event which trigged the webhook. The query may not make any mutations. """ createUserWebhook(config: UserWebhookInput!): WebhookSubscription! """ Deletes a user webhook. Any events already queued may still be delivered after this request completes. Clients authenticated with a personal access token may delete any webhook registered for their account, but authorized OAuth 2.0 clients may only delete their own webhooks. Manually deleting a webhook configured by a third-party client may cause unexpected behavior with the third-party integration. """ deleteUserWebhook(id: Int!): WebhookSubscription! "Creates a new mailing list webhook." createMailingListWebhook(listId: Int!, config: MailingListWebhookInput!): WebhookSubscription! "Deletes a mailing list webhook." deleteMailingListWebhook(id: Int!): WebhookSubscription! """ Triggers user webhooks for an email. The result can be null if the user does not have browse access to the archived email. In this case, no webhook will be triggered. """ triggerUserEmailWebhooks(emailId: Int!): Email @internal triggerListEmailWebhooks(listId: Int!, emailId: Int!): Email! @internal """ Deletes the authenticated user's account. Internal use only. """ deleteUser: Int! @internal } hut-0.6.0/srht/listssrht/strings.go000066400000000000000000000067141463710650600173740ustar00rootroot00000000000000package listssrht import ( "fmt" "strings" "git.sr.ht/~xenrox/hut/termfmt" ) func (visibility Visibility) TermString() string { var style termfmt.Style switch visibility { case VisibilityPublic: case VisibilityUnlisted: style = termfmt.Blue case VisibilityPrivate: style = termfmt.Red default: panic(fmt.Sprintf("unknown visibility: %q", visibility)) } return style.String(strings.ToLower(string(visibility))) } func ParseVisibility(s string) (Visibility, error) { switch strings.ToLower(s) { case "unlisted": return VisibilityUnlisted, nil case "private": return VisibilityPrivate, nil case "public": return VisibilityPublic, nil default: return "", fmt.Errorf("invalid visibility: %s", s) } } func (status PatchsetStatus) TermString() string { var style termfmt.Style switch status { case PatchsetStatusUnknown: case PatchsetStatusProposed: style = termfmt.Blue case PatchsetStatusNeedsRevision: style = termfmt.Yellow status = "needs revision" case PatchsetStatusSuperseded: style = termfmt.Dim case PatchsetStatusApproved: style = termfmt.Green case PatchsetStatusRejected: style = termfmt.Red case PatchsetStatusApplied: style = termfmt.Bold default: panic(fmt.Sprintf("unknown status: %q", status)) } return style.String(strings.ToLower(string(status))) } func ParsePatchsetStatus(s string) (PatchsetStatus, error) { switch strings.ToLower(s) { case "unknown": return PatchsetStatusUnknown, nil case "proposed": return PatchsetStatusProposed, nil case "needs_revision": return PatchsetStatusNeedsRevision, nil case "superseded": return PatchsetStatusSuperseded, nil case "approved": return PatchsetStatusApproved, nil case "rejected": return PatchsetStatusRejected, nil case "applied": return PatchsetStatusApplied, nil default: return "", fmt.Errorf("invalid patchset status: %s", s) } } func (acl GeneralACL) TermString() string { return fmt.Sprintf("%s browse %s reply %s post %s moderate", PermissionIcon(acl.Browse), PermissionIcon(acl.Reply), PermissionIcon(acl.Post), PermissionIcon(acl.Moderate)) } func PermissionIcon(permission bool) string { if permission { return termfmt.Green.Sprint("✔") } return termfmt.Red.Sprint("✗") } func ParseUserEvents(events []string) ([]WebhookEvent, error) { var whEvents []WebhookEvent for _, event := range events { switch strings.ToLower(event) { case "list_created": whEvents = append(whEvents, WebhookEventListCreated) case "list_updated": whEvents = append(whEvents, WebhookEventListUpdated) case "list_deleted": whEvents = append(whEvents, WebhookEventListDeleted) case "email_received": whEvents = append(whEvents, WebhookEventEmailReceived) case "patchset_received": whEvents = append(whEvents, WebhookEventPatchsetReceived) default: return whEvents, fmt.Errorf("invalid event: %q", event) } } return whEvents, nil } func ParseMailingListWebhookEvents(events []string) ([]WebhookEvent, error) { var whEvents []WebhookEvent for _, event := range events { switch strings.ToLower(event) { case "list_updated": whEvents = append(whEvents, WebhookEventListUpdated) case "list_deleted": whEvents = append(whEvents, WebhookEventListDeleted) case "email_received": whEvents = append(whEvents, WebhookEventEmailReceived) case "patchset_received": whEvents = append(whEvents, WebhookEventPatchsetReceived) default: return whEvents, fmt.Errorf("invalid event: %q", event) } } return whEvents, nil } hut-0.6.0/srht/metasrht/000077500000000000000000000000001463710650600151345ustar00rootroot00000000000000hut-0.6.0/srht/metasrht/gql.go000066400000000000000000000556741463710650600162670ustar00rootroot00000000000000// Code generated by gqlclientgen - DO NOT EDIT. package metasrht import ( "context" "encoding/json" "fmt" gqlclient "git.sr.ht/~emersion/gqlclient" ) type AccessKind string const ( AccessKindRo AccessKind = "RO" AccessKindRw AccessKind = "RW" ) type AccessScope string const ( AccessScopeAuditLog AccessScope = "AUDIT_LOG" AccessScopeBilling AccessScope = "BILLING" AccessScopePgpKeys AccessScope = "PGP_KEYS" AccessScopeSshKeys AccessScope = "SSH_KEYS" AccessScopeProfile AccessScope = "PROFILE" ) // A cursor for enumerating a list of audit log entries // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type AuditLogCursor struct { Results []AuditLogEntry `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type AuditLogEntry struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` IpAddress string `json:"ipAddress"` EventType string `json:"eventType"` Details *string `json:"details,omitempty"` } type Cursor string type Entity struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` // The canonical name of this entity. For users, this is their username // prefixed with '~'. Additional entity types will be supported in the future. CanonicalName string `json:"canonicalName"` // Underlying value of the GraphQL interface Value EntityValue `json:"-"` } func (base *Entity) UnmarshalJSON(b []byte) error { type Raw Entity var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "User": base.Value = new(User) case "": return nil default: return fmt.Errorf("gqlclient: interface Entity: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // EntityValue is one of: User type EntityValue interface { isEntity() } type Invoice struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Cents int32 `json:"cents"` ValidThru gqlclient.Time `json:"validThru"` Source *string `json:"source,omitempty"` } // A cursor for enumerating a list of invoices // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type InvoiceCursor struct { Results []Invoice `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type OAuthClient struct { Id int32 `json:"id"` Uuid string `json:"uuid"` RedirectUrl string `json:"redirectUrl"` Name string `json:"name"` Description *string `json:"description,omitempty"` Url *string `json:"url,omitempty"` Owner *Entity `json:"owner"` } type OAuthClientRegistration struct { Client *OAuthClient `json:"client"` Secret string `json:"secret"` } type OAuthGrant struct { Id int32 `json:"id"` Client *OAuthClient `json:"client"` Issued gqlclient.Time `json:"issued"` Expires gqlclient.Time `json:"expires"` TokenHash string `json:"tokenHash"` Grants *string `json:"grants,omitempty"` } type OAuthGrantRegistration struct { Grant *OAuthGrant `json:"grant"` Grants string `json:"grants"` Secret string `json:"secret"` RefreshToken string `json:"refreshToken"` } type OAuthPersonalToken struct { Id int32 `json:"id"` Issued gqlclient.Time `json:"issued"` Expires gqlclient.Time `json:"expires"` Comment *string `json:"comment,omitempty"` Grants *string `json:"grants,omitempty"` } type OAuthPersonalTokenRegistration struct { Token *OAuthPersonalToken `json:"token"` Secret string `json:"secret"` } type PGPKey struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` User *User `json:"user"` Key string `json:"key"` Fingerprint string `json:"fingerprint"` } // A cursor for enumerating a list of PGP keys // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type PGPKeyCursor struct { Results []PGPKey `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type PGPKeyEvent struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` Key *PGPKey `json:"key"` } func (*PGPKeyEvent) isWebhookPayload() {} type ProfileUpdateEvent struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` Profile *User `json:"profile"` } func (*ProfileUpdateEvent) isWebhookPayload() {} type ProfileWebhookInput struct { Url string `json:"url"` Events []WebhookEvent `json:"events"` Query string `json:"query"` } type ProfileWebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` Client *OAuthClient `json:"client,omitempty"` Deliveries *WebhookDeliveryCursor `json:"deliveries"` Sample string `json:"sample"` } func (*ProfileWebhookSubscription) isWebhookSubscription() {} type SSHKey struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` LastUsed gqlclient.Time `json:"lastUsed,omitempty"` User *User `json:"user"` Key string `json:"key"` Fingerprint string `json:"fingerprint"` Comment *string `json:"comment,omitempty"` Username string `json:"username"` } // A cursor for enumerating a list of SSH keys // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type SSHKeyCursor struct { Results []SSHKey `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type SSHKeyEvent struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` Key *SSHKey `json:"key"` } func (*SSHKeyEvent) isWebhookPayload() {} type User struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` CanonicalName string `json:"canonicalName"` Username string `json:"username"` Email string `json:"email"` Url *string `json:"url,omitempty"` Location *string `json:"location,omitempty"` Bio *string `json:"bio,omitempty"` UserType UserType `json:"userType"` SuspensionNotice *string `json:"suspensionNotice,omitempty"` SshKeys *SSHKeyCursor `json:"sshKeys"` PgpKeys *PGPKeyCursor `json:"pgpKeys"` } func (*User) isEntity() {} // Omit these fields to leave them unchanged, or set them to null to clear // their value. type UserInput struct { Url *string `json:"url,omitempty"` Location *string `json:"location,omitempty"` Bio *string `json:"bio,omitempty"` // Note: changing the user's email address will not take effect immediately; // the user is sent an email to confirm the change first. Email *string `json:"email,omitempty"` } type UserType string const ( UserTypeUnconfirmed UserType = "UNCONFIRMED" UserTypeActiveNonPaying UserType = "ACTIVE_NON_PAYING" UserTypeActiveFree UserType = "ACTIVE_FREE" UserTypeActivePaying UserType = "ACTIVE_PAYING" UserTypeActiveDelinquent UserType = "ACTIVE_DELINQUENT" UserTypeAdmin UserType = "ADMIN" UserTypeSuspended UserType = "SUSPENDED" ) type Version struct { Major int32 `json:"major"` Minor int32 `json:"minor"` Patch int32 `json:"patch"` // If this API version is scheduled for deprecation, this is the date on which // it will stop working; or null if this API version is not scheduled for // deprecation. DeprecationDate gqlclient.Time `json:"deprecationDate,omitempty"` } type WebhookDelivery struct { Uuid string `json:"uuid"` Date gqlclient.Time `json:"date"` Event WebhookEvent `json:"event"` Subscription *WebhookSubscription `json:"subscription"` RequestBody string `json:"requestBody"` // These details are provided only after a response is received from the // remote server. If a response is sent whose Content-Type is not text/*, or // cannot be decoded as UTF-8, the response body will be null. It will be // truncated after 64 KiB. ResponseBody *string `json:"responseBody,omitempty"` ResponseHeaders *string `json:"responseHeaders,omitempty"` ResponseStatus *int32 `json:"responseStatus,omitempty"` } // A cursor for enumerating a list of webhook deliveries // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type WebhookDeliveryCursor struct { Results []WebhookDelivery `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type WebhookEvent string const ( // Used for user profile webhooks WebhookEventProfileUpdate WebhookEvent = "PROFILE_UPDATE" WebhookEventPgpKeyAdded WebhookEvent = "PGP_KEY_ADDED" WebhookEventPgpKeyRemoved WebhookEvent = "PGP_KEY_REMOVED" WebhookEventSshKeyAdded WebhookEvent = "SSH_KEY_ADDED" WebhookEventSshKeyRemoved WebhookEvent = "SSH_KEY_REMOVED" ) type WebhookPayload struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` // Underlying value of the GraphQL interface Value WebhookPayloadValue `json:"-"` } func (base *WebhookPayload) UnmarshalJSON(b []byte) error { type Raw WebhookPayload var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "ProfileUpdateEvent": base.Value = new(ProfileUpdateEvent) case "PGPKeyEvent": base.Value = new(PGPKeyEvent) case "SSHKeyEvent": base.Value = new(SSHKeyEvent) case "": return nil default: return fmt.Errorf("gqlclient: interface WebhookPayload: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // WebhookPayloadValue is one of: ProfileUpdateEvent | PGPKeyEvent | SSHKeyEvent type WebhookPayloadValue interface { isWebhookPayload() } type WebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` // If this webhook was registered by an authorized OAuth 2.0 client, this // field is non-null. Client *OAuthClient `json:"client,omitempty"` // All deliveries which have been sent to this webhook. Deliveries *WebhookDeliveryCursor `json:"deliveries"` // Returns a sample payload for this subscription, for testing purposes Sample string `json:"sample"` // Underlying value of the GraphQL interface Value WebhookSubscriptionValue `json:"-"` } func (base *WebhookSubscription) UnmarshalJSON(b []byte) error { type Raw WebhookSubscription var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "ProfileWebhookSubscription": base.Value = new(ProfileWebhookSubscription) case "": return nil default: return fmt.Errorf("gqlclient: interface WebhookSubscription: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // WebhookSubscriptionValue is one of: ProfileWebhookSubscription type WebhookSubscriptionValue interface { isWebhookSubscription() } // A cursor for enumerating a list of webhook subscriptions // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type WebhookSubscriptionCursor struct { Results []WebhookSubscription `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } func FetchMe(client *gqlclient.Client, ctx context.Context) (me *User, err error) { op := gqlclient.NewOperation("query fetchMe {\n\tme {\n\t\t... user\n\t}\n}\nfragment user on User {\n\tcanonicalName\n\temail\n\turl\n\tlocation\n\tbio\n}\n") var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func FetchUser(client *gqlclient.Client, ctx context.Context, username string) (userByName *User, err error) { op := gqlclient.NewOperation("query fetchUser ($username: String!) {\n\tuserByName(username: $username) {\n\t\t... user\n\t}\n}\nfragment user on User {\n\tcanonicalName\n\temail\n\turl\n\tlocation\n\tbio\n}\n") op.Var("username", username) var respData struct { UserByName *User } err = client.Execute(ctx, op, &respData) return respData.UserByName, err } func ListSSHKeys(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (me *User, err error) { op := gqlclient.NewOperation("query listSSHKeys ($cursor: Cursor) {\n\tme {\n\t\t... sshKeys\n\t}\n}\nfragment sshKeys on User {\n\tsshKeys(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\tfingerprint\n\t\t\tcomment\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func ListSSHKeysByUser(client *gqlclient.Client, ctx context.Context, username string, cursor *Cursor) (userByName *User, err error) { op := gqlclient.NewOperation("query listSSHKeysByUser ($username: String!, $cursor: Cursor) {\n\tuserByName(username: $username) {\n\t\t... sshKeys\n\t}\n}\nfragment sshKeys on User {\n\tsshKeys(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\tfingerprint\n\t\t\tcomment\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("username", username) op.Var("cursor", cursor) var respData struct { UserByName *User } err = client.Execute(ctx, op, &respData) return respData.UserByName, err } func ListRawSSHKeys(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (me *User, err error) { op := gqlclient.NewOperation("query listRawSSHKeys ($cursor: Cursor) {\n\tme {\n\t\t... sshKeysRaw\n\t}\n}\nfragment sshKeysRaw on User {\n\tsshKeys(cursor: $cursor) {\n\t\tresults {\n\t\t\tkey\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func ListRawSSHKeysByUser(client *gqlclient.Client, ctx context.Context, username string, cursor *Cursor) (userByName *User, err error) { op := gqlclient.NewOperation("query listRawSSHKeysByUser ($username: String!, $cursor: Cursor) {\n\tuserByName(username: $username) {\n\t\t... sshKeysRaw\n\t}\n}\nfragment sshKeysRaw on User {\n\tsshKeys(cursor: $cursor) {\n\t\tresults {\n\t\t\tkey\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("username", username) op.Var("cursor", cursor) var respData struct { UserByName *User } err = client.Execute(ctx, op, &respData) return respData.UserByName, err } func ListPGPKeys(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (me *User, err error) { op := gqlclient.NewOperation("query listPGPKeys ($cursor: Cursor) {\n\tme {\n\t\t... pgpKeys\n\t}\n}\nfragment pgpKeys on User {\n\tpgpKeys(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\tfingerprint\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func ListPGPKeysByUser(client *gqlclient.Client, ctx context.Context, username string, cursor *Cursor) (userByName *User, err error) { op := gqlclient.NewOperation("query listPGPKeysByUser ($username: String!, $cursor: Cursor) {\n\tuserByName(username: $username) {\n\t\t... pgpKeys\n\t}\n}\nfragment pgpKeys on User {\n\tpgpKeys(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\tfingerprint\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("username", username) op.Var("cursor", cursor) var respData struct { UserByName *User } err = client.Execute(ctx, op, &respData) return respData.UserByName, err } func ListRawPGPKeys(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (me *User, err error) { op := gqlclient.NewOperation("query listRawPGPKeys ($cursor: Cursor) {\n\tme {\n\t\t... pgpKeysRaw\n\t}\n}\nfragment pgpKeysRaw on User {\n\tpgpKeys(cursor: $cursor) {\n\t\tresults {\n\t\t\tkey\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func ListRawPGPKeysByUser(client *gqlclient.Client, ctx context.Context, username string, cursor *Cursor) (userByName *User, err error) { op := gqlclient.NewOperation("query listRawPGPKeysByUser ($username: String!, $cursor: Cursor) {\n\tuserByName(username: $username) {\n\t\t... pgpKeysRaw\n\t}\n}\nfragment pgpKeysRaw on User {\n\tpgpKeys(cursor: $cursor) {\n\t\tresults {\n\t\t\tkey\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("username", username) op.Var("cursor", cursor) var respData struct { UserByName *User } err = client.Execute(ctx, op, &respData) return respData.UserByName, err } func AuditLog(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (auditLog *AuditLogCursor, err error) { op := gqlclient.NewOperation("query auditLog ($cursor: Cursor) {\n\tauditLog(cursor: $cursor) {\n\t\tresults {\n\t\t\tcreated\n\t\t\tipAddress\n\t\t\teventType\n\t\t\tdetails\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { AuditLog *AuditLogCursor } err = client.Execute(ctx, op, &respData) return respData.AuditLog, err } func UserWebhooks(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (profileWebhooks *WebhookSubscriptionCursor, err error) { op := gqlclient.NewOperation("query userWebhooks ($cursor: Cursor) {\n\tprofileWebhooks(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\turl\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { ProfileWebhooks *WebhookSubscriptionCursor } err = client.Execute(ctx, op, &respData) return respData.ProfileWebhooks, err } func PersonalAccessTokens(client *gqlclient.Client, ctx context.Context) (personalAccessTokens []OAuthPersonalToken, err error) { op := gqlclient.NewOperation("query personalAccessTokens {\n\tpersonalAccessTokens {\n\t\tissued\n\t\texpires\n\t\tcomment\n\t\tgrants\n\t}\n}\n") var respData struct { PersonalAccessTokens []OAuthPersonalToken } err = client.Execute(ctx, op, &respData) return respData.PersonalAccessTokens, err } func Bio(client *gqlclient.Client, ctx context.Context) (me *User, err error) { op := gqlclient.NewOperation("query bio {\n\tme {\n\t\tbio\n\t}\n}\n") var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func CreateSSHKey(client *gqlclient.Client, ctx context.Context, key string) (createSSHKey *SSHKey, err error) { op := gqlclient.NewOperation("mutation createSSHKey ($key: String!) {\n\tcreateSSHKey(key: $key) {\n\t\tfingerprint\n\t\tcomment\n\t}\n}\n") op.Var("key", key) var respData struct { CreateSSHKey *SSHKey } err = client.Execute(ctx, op, &respData) return respData.CreateSSHKey, err } func CreatePGPKey(client *gqlclient.Client, ctx context.Context, key string) (createPGPKey *PGPKey, err error) { op := gqlclient.NewOperation("mutation createPGPKey ($key: String!) {\n\tcreatePGPKey(key: $key) {\n\t\tfingerprint\n\t}\n}\n") op.Var("key", key) var respData struct { CreatePGPKey *PGPKey } err = client.Execute(ctx, op, &respData) return respData.CreatePGPKey, err } func DeleteSSHKey(client *gqlclient.Client, ctx context.Context, id int32) (deleteSSHKey *SSHKey, err error) { op := gqlclient.NewOperation("mutation deleteSSHKey ($id: Int!) {\n\tdeleteSSHKey(id: $id) {\n\t\tfingerprint\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteSSHKey *SSHKey } err = client.Execute(ctx, op, &respData) return respData.DeleteSSHKey, err } func DeletePGPKey(client *gqlclient.Client, ctx context.Context, id int32) (deletePGPKey *PGPKey, err error) { op := gqlclient.NewOperation("mutation deletePGPKey ($id: Int!) {\n\tdeletePGPKey(id: $id) {\n\t\tfingerprint\n\t}\n}\n") op.Var("id", id) var respData struct { DeletePGPKey *PGPKey } err = client.Execute(ctx, op, &respData) return respData.DeletePGPKey, err } func CreateUserWebhook(client *gqlclient.Client, ctx context.Context, config ProfileWebhookInput) (createWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation createUserWebhook ($config: ProfileWebhookInput!) {\n\tcreateWebhook(config: $config) {\n\t\tid\n\t}\n}\n") op.Var("config", config) var respData struct { CreateWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.CreateWebhook, err } func DeleteUserWebhook(client *gqlclient.Client, ctx context.Context, id int32) (deleteWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation deleteUserWebhook ($id: Int!) {\n\tdeleteWebhook(id: $id) {\n\t\tid\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.DeleteWebhook, err } func UpdateUser(client *gqlclient.Client, ctx context.Context, input *UserInput) (updateUser *User, err error) { op := gqlclient.NewOperation("mutation updateUser ($input: UserInput) {\n\tupdateUser(input: $input) {\n\t\tcanonicalName\n\t}\n}\n") op.Var("input", input) var respData struct { UpdateUser *User } err = client.Execute(ctx, op, &respData) return respData.UpdateUser, err } func ClearUserLocation(client *gqlclient.Client, ctx context.Context) (updateUser *User, err error) { op := gqlclient.NewOperation("mutation clearUserLocation {\n\tupdateUser(input: {location:null}) {\n\t\tcanonicalName\n\t}\n}\n") var respData struct { UpdateUser *User } err = client.Execute(ctx, op, &respData) return respData.UpdateUser, err } func ClearUserURL(client *gqlclient.Client, ctx context.Context) (updateUser *User, err error) { op := gqlclient.NewOperation("mutation clearUserURL {\n\tupdateUser(input: {url:null}) {\n\t\tcanonicalName\n\t}\n}\n") var respData struct { UpdateUser *User } err = client.Execute(ctx, op, &respData) return respData.UpdateUser, err } func ClearBio(client *gqlclient.Client, ctx context.Context) (updateUser *User, err error) { op := gqlclient.NewOperation("mutation clearBio {\n\tupdateUser(input: {bio:null}) {\n\t\tcanonicalName\n\t}\n}\n") var respData struct { UpdateUser *User } err = client.Execute(ctx, op, &respData) return respData.UpdateUser, err } hut-0.6.0/srht/metasrht/operations.graphql000066400000000000000000000061431463710650600207030ustar00rootroot00000000000000query fetchMe { me { ...user } } query fetchUser($username: String!) { userByName(username: $username) { ...user } } fragment user on User { canonicalName email url location bio } query listSSHKeys($cursor: Cursor) { me { ...sshKeys } } query listSSHKeysByUser($username: String!, $cursor: Cursor) { userByName(username: $username) { ...sshKeys } } fragment sshKeys on User { sshKeys(cursor: $cursor) { results { id fingerprint comment } cursor } } query listRawSSHKeys($cursor: Cursor) { me { ...sshKeysRaw } } query listRawSSHKeysByUser($username: String!, $cursor: Cursor) { userByName(username: $username) { ...sshKeysRaw } } fragment sshKeysRaw on User { sshKeys(cursor: $cursor) { results { key } cursor } } query listPGPKeys($cursor: Cursor) { me { ...pgpKeys } } query listPGPKeysByUser($username: String!, $cursor: Cursor) { userByName(username: $username) { ...pgpKeys } } fragment pgpKeys on User { pgpKeys(cursor: $cursor) { results { id fingerprint } cursor } } query listRawPGPKeys($cursor: Cursor) { me { ...pgpKeysRaw } } query listRawPGPKeysByUser($username: String!, $cursor: Cursor) { userByName(username: $username) { ...pgpKeysRaw } } fragment pgpKeysRaw on User { pgpKeys(cursor: $cursor) { results { key } cursor } } query auditLog($cursor: Cursor) { auditLog(cursor: $cursor) { results { created ipAddress eventType details } cursor } } query userWebhooks($cursor: Cursor) { profileWebhooks(cursor: $cursor) { results { id url } cursor } } query personalAccessTokens { personalAccessTokens { issued expires comment grants } } query bio { me { bio } } mutation createSSHKey($key: String!) { createSSHKey(key: $key) { fingerprint comment } } mutation createPGPKey($key: String!) { createPGPKey(key: $key) { fingerprint } } mutation deleteSSHKey($id: Int!) { deleteSSHKey(id: $id) { fingerprint } } mutation deletePGPKey($id: Int!) { deletePGPKey(id: $id) { fingerprint } } mutation createUserWebhook($config: ProfileWebhookInput!) { createWebhook(config: $config) { id } } mutation deleteUserWebhook($id: Int!) { deleteWebhook(id: $id) { id } } mutation updateUser($input: UserInput) { updateUser(input: $input) { canonicalName } } mutation clearUserLocation { updateUser(input: { location: null }) { canonicalName } } mutation clearUserURL { updateUser(input: { url: null }) { canonicalName } } mutation clearBio { updateUser(input: { bio: null }) { canonicalName } } hut-0.6.0/srht/metasrht/schema.graphqls000066400000000000000000000353221463710650600201440ustar00rootroot00000000000000# This schema definition is available in the public domain, or under the terms # of CC-0, at your choice. scalar Cursor scalar Time """ This is used to decorate fields which are only accessible with a personal access token, and are not available to clients using OAuth 2.0 access tokens. """ directive @private on FIELD_DEFINITION """ This used to decorate fields which are for internal use, and are not available to normal API users. """ directive @internal on FIELD_DEFINITION directive @anoninternal on FIELD_DEFINITION """ Used to provide a human-friendly description of an access scope. """ directive @scopehelp(details: String!) on ENUM_VALUE enum AccessScope { AUDIT_LOG @scopehelp(details: "audit log") BILLING @scopehelp(details: "billing history") PGP_KEYS @scopehelp(details: "PGP keys") SSH_KEYS @scopehelp(details: "SSH keys") PROFILE @scopehelp(details: "profile information") } enum AccessKind { RO @scopehelp(details: "read") RW @scopehelp(details: "read and write") } """ Decorates fields for which access requires a particular OAuth 2.0 scope with read or write access. For the meta.sr.ht API, you have access to all public information without any special permissions - user profile information, public keys, and so on. """ directive @access(scope: AccessScope!, kind: AccessKind!) on FIELD_DEFINITION # https://semver.org type Version { major: Int! minor: Int! patch: Int! """ If this API version is scheduled for deprecation, this is the date on which it will stop working; or null if this API version is not scheduled for deprecation. """ deprecationDate: Time } interface Entity { id: Int! created: Time! updated: Time! """ The canonical name of this entity. For users, this is their username prefixed with '~'. Additional entity types will be supported in the future. """ canonicalName: String! } enum UserType { UNCONFIRMED ACTIVE_NON_PAYING ACTIVE_FREE ACTIVE_PAYING ACTIVE_DELINQUENT ADMIN SUSPENDED } type User implements Entity { id: Int! created: Time! updated: Time! canonicalName: String! username: String! email: String! url: String location: String bio: String userType: UserType! @private suspensionNotice: String @internal sshKeys(cursor: Cursor): SSHKeyCursor! @access(scope: SSH_KEYS, kind: RO) pgpKeys(cursor: Cursor): PGPKeyCursor! @access(scope: PGP_KEYS, kind: RO) } type AuditLogEntry { id: Int! created: Time! ipAddress: String! eventType: String! details: String } type SSHKey { id: Int! created: Time! lastUsed: Time user: User! @access(scope: PROFILE, kind: RO) key: String! fingerprint: String! comment: String # TODO: replace with user.username username: String! @anoninternal } type PGPKey { id: Int! created: Time! user: User! @access(scope: PROFILE, kind: RO) key: String! fingerprint: String! } type Invoice { id: Int! created: Time! cents: Int! validThru: Time! source: String } type OAuthGrant { id: Int! client: OAuthClient! issued: Time! expires: Time! tokenHash: String! @internal grants: String } type OAuthGrantRegistration { grant: OAuthGrant! grants: String! secret: String! refreshToken: String! } type OAuthClient { id: Int! uuid: String! redirectUrl: String! name: String! description: String url: String owner: Entity! @access(scope: PROFILE, kind: RO) } type OAuthClientRegistration { client: OAuthClient! secret: String! } type OAuthPersonalToken { id: Int! issued: Time! expires: Time! comment: String grants: String } type OAuthPersonalTokenRegistration { token: OAuthPersonalToken! secret: String! } enum WebhookEvent { "Used for user profile webhooks" PROFILE_UPDATE PGP_KEY_ADDED PGP_KEY_REMOVED SSH_KEY_ADDED SSH_KEY_REMOVED } interface WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! """ If this webhook was registered by an authorized OAuth 2.0 client, this field is non-null. """ client: OAuthClient @private "All deliveries which have been sent to this webhook." deliveries(cursor: Cursor): WebhookDeliveryCursor! "Returns a sample payload for this subscription, for testing purposes" sample(event: WebhookEvent!): String! } type ProfileWebhookSubscription implements WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! client: OAuthClient @private deliveries(cursor: Cursor): WebhookDeliveryCursor! sample(event: WebhookEvent!): String! } type WebhookDelivery { uuid: String! date: Time! event: WebhookEvent! subscription: WebhookSubscription! requestBody: String! """ These details are provided only after a response is received from the remote server. If a response is sent whose Content-Type is not text/*, or cannot be decoded as UTF-8, the response body will be null. It will be truncated after 64 KiB. """ responseBody: String responseHeaders: String responseStatus: Int } interface WebhookPayload { uuid: String! event: WebhookEvent! date: Time! } type ProfileUpdateEvent implements WebhookPayload { uuid: String! event: WebhookEvent! date: Time! profile: User! } type PGPKeyEvent implements WebhookPayload { uuid: String! event: WebhookEvent! date: Time! key: PGPKey! } type SSHKeyEvent implements WebhookPayload { uuid: String! event: WebhookEvent! date: Time! key: SSHKey! } """ A cursor for enumerating a list of audit log entries If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type AuditLogCursor { results: [AuditLogEntry!]! cursor: Cursor } """ A cursor for enumerating a list of invoices If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type InvoiceCursor { results: [Invoice!]! cursor: Cursor } """ A cursor for enumerating a list of SSH keys If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type SSHKeyCursor { results: [SSHKey!]! cursor: Cursor } """ A cursor for enumerating a list of PGP keys If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type PGPKeyCursor { results: [PGPKey!]! cursor: Cursor } """ A cursor for enumerating a list of webhook deliveries If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type WebhookDeliveryCursor { results: [WebhookDelivery!]! cursor: Cursor } """ A cursor for enumerating a list of webhook subscriptions If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type WebhookSubscriptionCursor { results: [WebhookSubscription!]! cursor: Cursor } type Query { "Returns API version information." version: Version! "Returns the authenticated user." me: User! @access(scope: PROFILE, kind: RO) "Returns a specific user" userByName(username: String!): User @access(scope: PROFILE, kind: RO) userByEmail(email: String!): User @access(scope: PROFILE, kind: RO) "Returns a specific SSH key by its fingerprint, in hexadecimal" sshKeyByFingerprint(fingerprint: String!): SSHKey @access(scope: SSH_KEYS, kind: RO) "Returns a specific PGP key by its fingerprint, in hexadecimal." pgpKeyByFingerprint(fingerprint: String!): PGPKey @access(scope: PGP_KEYS, kind: RO) "Returns invoices for the authenticated user." invoices(cursor: Cursor): InvoiceCursor! @access(scope: BILLING, kind: RO) "Returns the audit log for the authenticated user." auditLog(cursor: Cursor): AuditLogCursor! @access(scope: AUDIT_LOG, kind: RO) """ Returns a list of user profile webhook subscriptions. For clients authenticated with a personal access token, this returns all webhooks configured by all GraphQL clients for your account. For clients authenticated with an OAuth 2.0 access token, this returns only webhooks registered for your client. """ profileWebhooks(cursor: Cursor): WebhookSubscriptionCursor! "Returns details of a user profile webhook subscription by its ID." profileWebhook(id: Int!): WebhookSubscription """ Returns information about the webhook currently being processed. This is not valid during normal queries over HTTP, and will return an error if used outside of a webhook context. """ webhook: WebhookPayload! "Returns the current OAuth grant in use, if any" myOauthGrant: OAuthGrant "Returns OAuth grants issued for the authenticated user" oauthGrants: [OAuthGrant!]! @private "List of OAuth clients this user administrates" oauthClients: [OAuthClient!]! @private "Returns a list of personal OAuth tokens issued" personalAccessTokens: [OAuthPersonalToken!]! @private ### ### ### The following resolvers are for internal use. ### ### ### "Returns a specific user by ID" userByID(id: Int!): User @anoninternal "Returns a specific user by username" user(username: String!): User @anoninternal "Returns a specific OAuth client (by database ID)" oauthClientByID(id: Int!): OAuthClient @internal "Returns a specific OAuth client (by UUID)" oauthClientByUUID(uuid: String!): OAuthClient @internal """ Returns the revocation status of a given OAuth 2.0 token hash (SHA-512). If the token or client ID has been revoked, this returns true, and the key should not be trusted. Client ID is optional for personal access tokens. """ tokenRevocationStatus(hash: String!, clientId: String): Boolean! @internal # TODO: replace with sshKeyByFingerprint sshKeyByFingerprintInternal(fingerprint: String!): SSHKey @anoninternal } """ Omit these fields to leave them unchanged, or set them to null to clear their value. """ input UserInput { url: String location: String bio: String """ Note: changing the user's email address will not take effect immediately; the user is sent an email to confirm the change first. """ email: String } input ProfileWebhookInput { url: String! events: [WebhookEvent!]! query: String! } type Mutation { updateUser(input: UserInput): User! @access(scope: PROFILE, kind: RW) createPGPKey(key: String!): PGPKey! @access(scope: PGP_KEYS, kind: RW) deletePGPKey(id: Int!): PGPKey @access(scope: PGP_KEYS, kind: RW) createSSHKey(key: String!): SSHKey! @access(scope: SSH_KEYS, kind: RW) deleteSSHKey(id: Int!): SSHKey @access(scope: SSH_KEYS, kind: RW) """ Causes the "last used" time of this SSH key to be set to the current time. """ updateSSHKeyLastUsed(id: Int!): SSHKey! @access(scope: SSH_KEYS, kind: RO) @internal """ Creates a new user profile webhook subscription. When an event from the provided list of events occurs, the 'query' parameter (a GraphQL query) will be evaluated and the results will be sent to the provided URL as the body of an HTTP POST request. The list of events must include at least one event, and no duplicates. This query is evaluated in the webhook context, such that query { webhook } may be used to access details of the event which trigged the webhook. The query may not make any mutations. """ createWebhook(config: ProfileWebhookInput!): WebhookSubscription! """ Deletes a user profile webhook. Any events already queued may still be delivered after this request completes. Clients authenticated with a personal access token may delete any webhook registered for their account, but authorized OAuth 2.0 clients may only delete their own webhooks. Manually deleting a webhook configured by a third-party client may cause unexpected behavior with the third-party integration. """ deleteWebhook(id: Int!): WebhookSubscription! ### ### ### The following resolvers are for internal use. ### ### ### "Registers a new account." registerAccount(email: String!, username: String!, password: String!, pgpKey: String): User @anoninternal """ Registers an OAuth client. Only OAuth 2.0 confidental clients are supported. """ registerOAuthClient( redirectUri: String!, clientName: String!, clientDescription: String, clientUrl: String): OAuthClientRegistration! @internal """ Revokes this OAuth client, revoking all tokens for it and preventing future use. """ revokeOAuthClient(uuid: String!): OAuthClient @internal "Revokes a specific OAuth grant." revokeOAuthGrant(hash: String!): OAuthGrant @internal "Issues an OAuth personal access token." issuePersonalAccessToken(grants: String, comment: String): OAuthPersonalTokenRegistration! @internal "Revokes a personal access token." revokePersonalAccessToken(id: Int!): OAuthPersonalToken @internal """ Issues an OAuth 2.0 authorization code. Used after the user has consented to the access grant request. """ issueAuthorizationCode(clientUUID: String!, grants: String!): String! @internal """ Completes the OAuth 2.0 grant process and issues an OAuth token for a specific OAuth client. """ issueOAuthGrant(authorization: String!, clientUUID: String, clientSecret: String!, redirectUri: String): OAuthGrantRegistration @internal """ Refreshes an existing OAuth 2.0 grant. This invalidates the previous grant and returns a new one. """ refreshOAuthGrant(refreshToken: String!, clientUUID: String!, clientSecret: String!, grants: String): OAuthGrantRegistration @internal """ Send a notification email. The 'address' parameter must be a single RFC 5322 address (e.g. "Barry Gibbs ", or "bg@example.com"). The 'message' parameter must be a RFC 5322 compliant Internet message with the special requirement that it must not contain any recipients (i.e. no 'To', 'Cc', or 'Bcc' header). The message will be signed with the site key. If the address is that of a registered user it will be encrypted according to the user's privacy settings. """ sendEmail(address: String!, message: String!): Boolean! @anoninternal """ Re-sends the account confirmation email to this user. """ resendConfirmation(username: String!): String! @internal """ Deletes the authenticated user's account. """ deleteUser(reserve: Boolean!): Int! @internal } hut-0.6.0/srht/metasrht/strings.go000066400000000000000000000013201463710650600171500ustar00rootroot00000000000000package metasrht import ( "fmt" "strings" ) func ParseUserEvents(events []string) ([]WebhookEvent, error) { var whEvents []WebhookEvent for _, event := range events { switch strings.ToLower(event) { case "profile_update": whEvents = append(whEvents, WebhookEventProfileUpdate) case "pgp_key_added": whEvents = append(whEvents, WebhookEventPgpKeyAdded) case "pgp_key_removed": whEvents = append(whEvents, WebhookEventPgpKeyRemoved) case "ssh_key_added": whEvents = append(whEvents, WebhookEventSshKeyAdded) case "ssh_key_removed": whEvents = append(whEvents, WebhookEventSshKeyRemoved) default: return whEvents, fmt.Errorf("invalid event: %q", event) } } return whEvents, nil } hut-0.6.0/srht/pagessrht/000077500000000000000000000000001463710650600153055ustar00rootroot00000000000000hut-0.6.0/srht/pagessrht/gql.go000066400000000000000000000263561463710650600164330ustar00rootroot00000000000000// Code generated by gqlclientgen - DO NOT EDIT. package pagessrht import ( "context" "encoding/json" "fmt" gqlclient "git.sr.ht/~emersion/gqlclient" ) type AccessKind string const ( AccessKindRo AccessKind = "RO" AccessKindRw AccessKind = "RW" ) type AccessScope string const ( AccessScopeProfile AccessScope = "PROFILE" AccessScopeSites AccessScope = "SITES" AccessScopePages AccessScope = "PAGES" ) type Cursor string type Entity struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` // The canonical name of this entity. For users, this is their username // prefixed with '~'. Additional entity types will be supported in the future. CanonicalName string `json:"canonicalName"` // Underlying value of the GraphQL interface Value EntityValue `json:"-"` } func (base *Entity) UnmarshalJSON(b []byte) error { type Raw Entity var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "User": base.Value = new(User) case "": return nil default: return fmt.Errorf("gqlclient: interface Entity: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // EntityValue is one of: User type EntityValue interface { isEntity() } // Provides a way to configure options for a set of files matching the glob // pattern. type FileConfig struct { Glob string `json:"glob"` Options FileOptions `json:"options"` } // Options for a file being served. type FileOptions struct { // Value of the Cache-Control header to be used when serving the file. CacheControl *string `json:"cacheControl,omitempty"` } type OAuthClient struct { Uuid string `json:"uuid"` } type Protocol string const ( ProtocolHttps Protocol = "HTTPS" ProtocolGemini Protocol = "GEMINI" ) // A published website type Site struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` // Domain name the site services Domain string `json:"domain"` // The site protocol Protocol Protocol `json:"protocol"` // SHA-256 checksum of the source tarball (uncompressed) Version string `json:"version"` // Path to the file to serve for 404 Not Found responses NotFound *string `json:"notFound,omitempty"` } type SiteConfig struct { // Path to the file to serve for 404 Not Found responses NotFound *string `json:"notFound,omitempty"` FileConfigs []FileConfig `json:"fileConfigs,omitempty"` } // A cursor for enumerating site entries // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type SiteCursor struct { Results []Site `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type SiteEvent struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` Site *Site `json:"site"` } func (*SiteEvent) isWebhookPayload() {} type User struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` CanonicalName string `json:"canonicalName"` Username string `json:"username"` Email string `json:"email"` Url *string `json:"url,omitempty"` Location *string `json:"location,omitempty"` Bio *string `json:"bio,omitempty"` } func (*User) isEntity() {} type UserWebhookInput struct { Url string `json:"url"` Events []WebhookEvent `json:"events"` Query string `json:"query"` } type UserWebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` Client *OAuthClient `json:"client,omitempty"` Deliveries *WebhookDeliveryCursor `json:"deliveries"` Sample string `json:"sample"` } func (*UserWebhookSubscription) isWebhookSubscription() {} type Version struct { Major int32 `json:"major"` Minor int32 `json:"minor"` Patch int32 `json:"patch"` // If this API version is scheduled for deprecation, this is the date on which // it will stop working; or null if this API version is not scheduled for // deprecation. DeprecationDate gqlclient.Time `json:"deprecationDate,omitempty"` } type WebhookDelivery struct { Uuid string `json:"uuid"` Date gqlclient.Time `json:"date"` Event WebhookEvent `json:"event"` Subscription *WebhookSubscription `json:"subscription"` RequestBody string `json:"requestBody"` // These details are provided only after a response is received from the // remote server. If a response is sent whose Content-Type is not text/*, or // cannot be decoded as UTF-8, the response body will be null. It will be // truncated after 64 KiB. ResponseBody *string `json:"responseBody,omitempty"` ResponseHeaders *string `json:"responseHeaders,omitempty"` ResponseStatus *int32 `json:"responseStatus,omitempty"` } // A cursor for enumerating a list of webhook deliveries // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type WebhookDeliveryCursor struct { Results []WebhookDelivery `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type WebhookEvent string const ( WebhookEventSitePublished WebhookEvent = "SITE_PUBLISHED" WebhookEventSiteUnpublished WebhookEvent = "SITE_UNPUBLISHED" ) type WebhookPayload struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` // Underlying value of the GraphQL interface Value WebhookPayloadValue `json:"-"` } func (base *WebhookPayload) UnmarshalJSON(b []byte) error { type Raw WebhookPayload var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "SiteEvent": base.Value = new(SiteEvent) case "": return nil default: return fmt.Errorf("gqlclient: interface WebhookPayload: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // WebhookPayloadValue is one of: SiteEvent type WebhookPayloadValue interface { isWebhookPayload() } type WebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` // If this webhook was registered by an authorized OAuth 2.0 client, this // field is non-null. Client *OAuthClient `json:"client,omitempty"` // All deliveries which have been sent to this webhook. Deliveries *WebhookDeliveryCursor `json:"deliveries"` // Returns a sample payload for this subscription, for testing purposes Sample string `json:"sample"` // Underlying value of the GraphQL interface Value WebhookSubscriptionValue `json:"-"` } func (base *WebhookSubscription) UnmarshalJSON(b []byte) error { type Raw WebhookSubscription var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "UserWebhookSubscription": base.Value = new(UserWebhookSubscription) case "": return nil default: return fmt.Errorf("gqlclient: interface WebhookSubscription: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // WebhookSubscriptionValue is one of: UserWebhookSubscription type WebhookSubscriptionValue interface { isWebhookSubscription() } // A cursor for enumerating a list of webhook subscriptions // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type WebhookSubscriptionCursor struct { Results []WebhookSubscription `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } func Publish(client *gqlclient.Client, ctx context.Context, domain string, content gqlclient.Upload, protocol Protocol, subdirectory string, siteConfig SiteConfig) (publish *Site, err error) { op := gqlclient.NewOperation("mutation publish ($domain: String!, $content: Upload!, $protocol: Protocol!, $subdirectory: String!, $siteConfig: SiteConfig!) {\n\tpublish(domain: $domain, content: $content, protocol: $protocol, subdirectory: $subdirectory, siteConfig: $siteConfig) {\n\t\tdomain\n\t}\n}\n") op.Var("domain", domain) op.Var("content", content) op.Var("protocol", protocol) op.Var("subdirectory", subdirectory) op.Var("siteConfig", siteConfig) var respData struct { Publish *Site } err = client.Execute(ctx, op, &respData) return respData.Publish, err } func Unpublish(client *gqlclient.Client, ctx context.Context, domain string, protocol Protocol) (unpublish *Site, err error) { op := gqlclient.NewOperation("mutation unpublish ($domain: String!, $protocol: Protocol!) {\n\tunpublish(domain: $domain, protocol: $protocol) {\n\t\tdomain\n\t}\n}\n") op.Var("domain", domain) op.Var("protocol", protocol) var respData struct { Unpublish *Site } err = client.Execute(ctx, op, &respData) return respData.Unpublish, err } func CreateUserWebhook(client *gqlclient.Client, ctx context.Context, config UserWebhookInput) (createUserWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation createUserWebhook ($config: UserWebhookInput!) {\n\tcreateUserWebhook(config: $config) {\n\t\tid\n\t}\n}\n") op.Var("config", config) var respData struct { CreateUserWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.CreateUserWebhook, err } func DeleteUserWebhook(client *gqlclient.Client, ctx context.Context, id int32) (deleteUserWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation deleteUserWebhook ($id: Int!) {\n\tdeleteUserWebhook(id: $id) {\n\t\tid\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteUserWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.DeleteUserWebhook, err } func Sites(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (sites *SiteCursor, err error) { op := gqlclient.NewOperation("query sites ($cursor: Cursor) {\n\tsites(cursor: $cursor) {\n\t\tresults {\n\t\t\tdomain\n\t\t\tprotocol\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { Sites *SiteCursor } err = client.Execute(ctx, op, &respData) return respData.Sites, err } func UserWebhooks(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (userWebhooks *WebhookSubscriptionCursor, err error) { op := gqlclient.NewOperation("query userWebhooks ($cursor: Cursor) {\n\tuserWebhooks(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\turl\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { UserWebhooks *WebhookSubscriptionCursor } err = client.Execute(ctx, op, &respData) return respData.UserWebhooks, err } hut-0.6.0/srht/pagessrht/operations.graphql000066400000000000000000000017431463710650600210550ustar00rootroot00000000000000mutation publish( $domain: String! $content: Upload! $protocol: Protocol! $subdirectory: String! $siteConfig: SiteConfig! ) { publish( domain: $domain content: $content protocol: $protocol subdirectory: $subdirectory siteConfig: $siteConfig ) { domain } } mutation unpublish($domain: String!, $protocol: Protocol!) { unpublish(domain: $domain, protocol: $protocol) { domain } } mutation createUserWebhook($config: UserWebhookInput!) { createUserWebhook(config: $config) { id } } mutation deleteUserWebhook($id: Int!) { deleteUserWebhook(id: $id) { id } } query sites($cursor: Cursor) { sites(cursor: $cursor) { results { domain protocol } cursor } } query userWebhooks($cursor: Cursor) { userWebhooks(cursor: $cursor) { results { id url } cursor } } hut-0.6.0/srht/pagessrht/schema.graphqls000066400000000000000000000204501463710650600203110ustar00rootroot00000000000000scalar Cursor scalar Time scalar Upload "Used to provide a human-friendly description of an access scope" directive @scopehelp(details: String!) on ENUM_VALUE """ This is used to decorate fields which are only accessible with a personal access token, and are not available to clients using OAuth 2.0 access tokens. """ directive @private on FIELD_DEFINITION """ This is used to decorate fields which are for internal use, and are not available to normal API users. """ directive @internal on FIELD_DEFINITION enum AccessScope { PROFILE @scopehelp(details: "profile information") SITES @scopehelp(details: "registered sites") PAGES @scopehelp(details: "contents of registered sites") } enum AccessKind { RO @scopehelp(details: "read") RW @scopehelp(details: "read and write") } """ Decorates fields for which access requires a particular OAuth 0.0 scope with read or write access. """ directive @access(scope: AccessScope!, kind: AccessKind!) on FIELD_DEFINITION enum Protocol { HTTPS GEMINI } # https://semver.org type Version { major: Int! minor: Int! patch: Int! """ If this API version is scheduled for deprecation, this is the date on which it will stop working; or null if this API version is not scheduled for deprecation. """ deprecationDate: Time } interface Entity { id: Int! created: Time! updated: Time! """ The canonical name of this entity. For users, this is their username prefixed with '~'. Additional entity types will be supported in the future. """ canonicalName: String! } type User implements Entity { id: Int! created: Time! updated: Time! canonicalName: String! username: String! email: String! url: String location: String bio: String } "A published website" type Site { id: Int! created: Time! updated: Time! "Domain name the site services" domain: String! "The site protocol" protocol: Protocol! "SHA-256 checksum of the source tarball (uncompressed)" version: String! "Path to the file to serve for 404 Not Found responses" notFound: String } """ A cursor for enumerating site entries If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type SiteCursor { results: [Site!]! cursor: Cursor } """ Options for a file being served. """ input FileOptions { "Value of the Cache-Control header to be used when serving the file." cacheControl: String } """ Provides a way to configure options for a set of files matching the glob pattern. """ input FileConfig { glob: String! options: FileOptions! } input SiteConfig { "Path to the file to serve for 404 Not Found responses" notFound: String fileConfigs: [FileConfig!] } type OAuthClient { uuid: String! } enum WebhookEvent { SITE_PUBLISHED SITE_UNPUBLISHED } interface WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! """ If this webhook was registered by an authorized OAuth 2.0 client, this field is non-null. """ client: OAuthClient @private "All deliveries which have been sent to this webhook." deliveries(cursor: Cursor): WebhookDeliveryCursor! "Returns a sample payload for this subscription, for testing purposes" sample(event: WebhookEvent!): String! } type UserWebhookSubscription implements WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! client: OAuthClient @private deliveries(cursor: Cursor): WebhookDeliveryCursor! sample(event: WebhookEvent!): String! } type WebhookDelivery { uuid: String! date: Time! event: WebhookEvent! subscription: WebhookSubscription! requestBody: String! """ These details are provided only after a response is received from the remote server. If a response is sent whose Content-Type is not text/*, or cannot be decoded as UTF-8, the response body will be null. It will be truncated after 64 KiB. """ responseBody: String responseHeaders: String responseStatus: Int } interface WebhookPayload { uuid: String! event: WebhookEvent! date: Time! } type SiteEvent implements WebhookPayload { uuid: String! event: WebhookEvent! date: Time! site: Site! } """ A cursor for enumerating a list of webhook deliveries If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type WebhookDeliveryCursor { results: [WebhookDelivery!]! cursor: Cursor } """ A cursor for enumerating a list of webhook subscriptions If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type WebhookSubscriptionCursor { results: [WebhookSubscription!]! cursor: Cursor } type Query { "Returns API version information." version: Version! "Returns the authenticated user." me: User! @access(scope: PROFILE, kind: RO) "Returns a list of registered sites on your account." sites(cursor: Cursor): SiteCursor! @access(scope: SITES, kind: RO) """ Returns a list of user webhook subscriptions. For clients authenticated with a personal access token, this returns all webhooks configured by all GraphQL clients for your account. For clients authenticated with an OAuth 2.0 access token, this returns only webhooks registered for your client. """ userWebhooks(cursor: Cursor): WebhookSubscriptionCursor! "Returns details of a user webhook subscription by its ID." userWebhook(id: Int!): WebhookSubscription """ Returns information about the webhook currently being processed. This is not valid during normal queries over HTTP, and will return an error if used outside of a webhook context. """ webhook: WebhookPayload! } input UserWebhookInput { url: String! events: [WebhookEvent!]! query: String! } type Mutation { """ Publishes a website. If the domain already exists on your account, it is updated to a new version. If the domain already exists under someone else's account, the request is rejected. If the domain does not exist, a new site is created. Every user is given exclusive use over the 'username.srht.site' domain, and it requires no special configuration to use. Users may also bring their own domain name, in which case they should consult the configuration docs: https://man.sr.ht/pages.sr.ht 'content' must be a .tar.gz file. It must contain only directories and regular files of mode 644. Symlinks are not supported. No error is returned for an invalid tarball; the invalid data is simply discarded. If protocol is unset, HTTPS is presumed. If subdirectory is set, only the specified directory is updated. The rest of the files are unchanged. """ publish(domain: String!, content: Upload!, protocol: Protocol, subdirectory: String, siteConfig: SiteConfig): Site! @access(scope: PAGES, kind: RW) """ Deletes a previously published website. If protocol is unset, HTTPS is presumed. """ unpublish(domain: String!, protocol: Protocol): Site @access(scope: SITES, kind: RW) """ Creates a new user webhook subscription. When an event from the provided list of events occurs, the 'query' parameter (a GraphQL query) will be evaluated and the results will be sent to the provided URL as the body of an HTTP POST request. The list of events must include at least one event, and no duplicates. This query is evaluated in the webhook context, such that query { webhook } may be used to access details of the event which trigged the webhook. The query may not make any mutations. """ createUserWebhook(config: UserWebhookInput!): WebhookSubscription! """ Deletes a user webhook. Any events already queued may still be delivered after this request completes. Clients authenticated with a personal access token may delete any webhook registered for their account, but authorized OAuth 2.0 clients may only delete their own webhooks. Manually deleting a webhook configured by a third-party client may cause unexpected behavior with the third-party integration. """ deleteUserWebhook(id: Int!): WebhookSubscription! """ Deletes the authenticated user's account. Internal use only. """ deleteUser: Int! @internal } hut-0.6.0/srht/pagessrht/strings.go000066400000000000000000000013051463710650600173240ustar00rootroot00000000000000package pagessrht import ( "fmt" "strings" ) func ParseProtocol(s string) (Protocol, error) { switch strings.ToLower(s) { case "https": return ProtocolHttps, nil case "gemini": return ProtocolGemini, nil default: return "", fmt.Errorf("invalid protocol: %s", s) } } func ParseEvents(events []string) ([]WebhookEvent, error) { var whEvents []WebhookEvent for _, event := range events { switch strings.ToLower(event) { case "site_published": whEvents = append(whEvents, WebhookEventSitePublished) case "site_unpublished": whEvents = append(whEvents, WebhookEventSiteUnpublished) default: return whEvents, fmt.Errorf("invalid event: %q", event) } } return whEvents, nil } hut-0.6.0/srht/pastesrht/000077500000000000000000000000001463710650600153225ustar00rootroot00000000000000hut-0.6.0/srht/pastesrht/gql.go000066400000000000000000000317171463710650600164450ustar00rootroot00000000000000// Code generated by gqlclientgen - DO NOT EDIT. package pastesrht import ( "context" "encoding/json" "fmt" gqlclient "git.sr.ht/~emersion/gqlclient" ) type AccessKind string const ( AccessKindRo AccessKind = "RO" AccessKindRw AccessKind = "RW" ) type AccessScope string const ( AccessScopeProfile AccessScope = "PROFILE" AccessScopePastes AccessScope = "PASTES" ) type Cursor string type Entity struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` // The canonical name of this entity. For users, this is their username // prefixed with '~'. Additional entity types will be supported in the future. CanonicalName string `json:"canonicalName"` Pastes *PasteCursor `json:"pastes"` // Underlying value of the GraphQL interface Value EntityValue `json:"-"` } func (base *Entity) UnmarshalJSON(b []byte) error { type Raw Entity var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "User": base.Value = new(User) case "": return nil default: return fmt.Errorf("gqlclient: interface Entity: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // EntityValue is one of: User type EntityValue interface { isEntity() } type File struct { Filename *string `json:"filename,omitempty"` Hash string `json:"hash"` Contents URL `json:"contents"` } type OAuthClient struct { Uuid string `json:"uuid"` } type Paste struct { Id string `json:"id"` Created gqlclient.Time `json:"created"` Visibility Visibility `json:"visibility"` Files []File `json:"files"` User *Entity `json:"user"` } // A cursor for enumerating pastes // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type PasteCursor struct { Results []Paste `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type PasteEvent struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` Paste *Paste `json:"paste"` } func (*PasteEvent) isWebhookPayload() {} // URL from which some secondary data may be retrieved. You must provide the // same Authentication header to this address as you did to the GraphQL resolver // which provided it. The URL is not guaranteed to be consistent for an extended // length of time; applications should submit a new GraphQL query each time they // wish to access the data at the provided URL. type URL string type User struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` CanonicalName string `json:"canonicalName"` Pastes *PasteCursor `json:"pastes"` Username string `json:"username"` } func (*User) isEntity() {} type UserWebhookInput struct { Url string `json:"url"` Events []WebhookEvent `json:"events"` Query string `json:"query"` } type UserWebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` Client *OAuthClient `json:"client,omitempty"` Deliveries *WebhookDeliveryCursor `json:"deliveries"` Sample string `json:"sample"` } func (*UserWebhookSubscription) isWebhookSubscription() {} type Version struct { Major int32 `json:"major"` Minor int32 `json:"minor"` Patch int32 `json:"patch"` // If this API version is scheduled for deprecation, this is the date on which // it will stop working; or null if this API version is not scheduled for // deprecation. DeprecationDate gqlclient.Time `json:"deprecationDate,omitempty"` } type Visibility string const ( // Visible to everyone, listed on your profile VisibilityPublic Visibility = "PUBLIC" // Visible to everyone (if they know the URL), not listed on your profile VisibilityUnlisted Visibility = "UNLISTED" // Not visible to anyone except those explicitly added to the access list VisibilityPrivate Visibility = "PRIVATE" ) type WebhookDelivery struct { Uuid string `json:"uuid"` Date gqlclient.Time `json:"date"` Event WebhookEvent `json:"event"` Subscription *WebhookSubscription `json:"subscription"` RequestBody string `json:"requestBody"` // These details are provided only after a response is received from the // remote server. If a response is sent whose Content-Type is not text/*, or // cannot be decoded as UTF-8, the response body will be null. It will be // truncated after 64 KiB. ResponseBody *string `json:"responseBody,omitempty"` ResponseHeaders *string `json:"responseHeaders,omitempty"` ResponseStatus *int32 `json:"responseStatus,omitempty"` } // A cursor for enumerating a list of webhook deliveries // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type WebhookDeliveryCursor struct { Results []WebhookDelivery `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type WebhookEvent string const ( WebhookEventPasteCreated WebhookEvent = "PASTE_CREATED" WebhookEventPasteUpdated WebhookEvent = "PASTE_UPDATED" WebhookEventPasteDeleted WebhookEvent = "PASTE_DELETED" ) type WebhookPayload struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` // Underlying value of the GraphQL interface Value WebhookPayloadValue `json:"-"` } func (base *WebhookPayload) UnmarshalJSON(b []byte) error { type Raw WebhookPayload var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "PasteEvent": base.Value = new(PasteEvent) case "": return nil default: return fmt.Errorf("gqlclient: interface WebhookPayload: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // WebhookPayloadValue is one of: PasteEvent type WebhookPayloadValue interface { isWebhookPayload() } type WebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` // If this webhook was registered by an authorized OAuth 2.0 client, this // field is non-null. Client *OAuthClient `json:"client,omitempty"` // All deliveries which have been sent to this webhook. Deliveries *WebhookDeliveryCursor `json:"deliveries"` // Returns a sample payload for this subscription, for testing purposes Sample string `json:"sample"` // Underlying value of the GraphQL interface Value WebhookSubscriptionValue `json:"-"` } func (base *WebhookSubscription) UnmarshalJSON(b []byte) error { type Raw WebhookSubscription var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "UserWebhookSubscription": base.Value = new(UserWebhookSubscription) case "": return nil default: return fmt.Errorf("gqlclient: interface WebhookSubscription: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // WebhookSubscriptionValue is one of: UserWebhookSubscription type WebhookSubscriptionValue interface { isWebhookSubscription() } // A cursor for enumerating a list of webhook subscriptions // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type WebhookSubscriptionCursor struct { Results []WebhookSubscription `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } func CreatePaste(client *gqlclient.Client, ctx context.Context, files []gqlclient.Upload, visibility Visibility) (create *Paste, err error) { op := gqlclient.NewOperation("mutation createPaste ($files: [Upload!]!, $visibility: Visibility!) {\n\tcreate(files: $files, visibility: $visibility) {\n\t\tid\n\t\tuser {\n\t\t\tcanonicalName\n\t\t}\n\t}\n}\n") op.Var("files", files) op.Var("visibility", visibility) var respData struct { Create *Paste } err = client.Execute(ctx, op, &respData) return respData.Create, err } func Delete(client *gqlclient.Client, ctx context.Context, id string) (delete *Paste, err error) { op := gqlclient.NewOperation("mutation delete ($id: String!) {\n\tdelete(id: $id) {\n\t\tid\n\t}\n}\n") op.Var("id", id) var respData struct { Delete *Paste } err = client.Execute(ctx, op, &respData) return respData.Delete, err } func Update(client *gqlclient.Client, ctx context.Context, id string, visibility Visibility) (update *Paste, err error) { op := gqlclient.NewOperation("mutation update ($id: String!, $visibility: Visibility!) {\n\tupdate(id: $id, visibility: $visibility) {\n\t\tid\n\t}\n}\n") op.Var("id", id) op.Var("visibility", visibility) var respData struct { Update *Paste } err = client.Execute(ctx, op, &respData) return respData.Update, err } func CreateUserWebhook(client *gqlclient.Client, ctx context.Context, config UserWebhookInput) (createUserWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation createUserWebhook ($config: UserWebhookInput!) {\n\tcreateUserWebhook(config: $config) {\n\t\tid\n\t}\n}\n") op.Var("config", config) var respData struct { CreateUserWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.CreateUserWebhook, err } func DeleteUserWebhook(client *gqlclient.Client, ctx context.Context, id int32) (deleteUserWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation deleteUserWebhook ($id: Int!) {\n\tdeleteUserWebhook(id: $id) {\n\t\tid\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteUserWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.DeleteUserWebhook, err } func Pastes(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (pastes *PasteCursor, err error) { op := gqlclient.NewOperation("query pastes ($cursor: Cursor) {\n\tpastes(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\tcreated\n\t\t\tvisibility\n\t\t\tfiles {\n\t\t\t\tfilename\n\t\t\t}\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { Pastes *PasteCursor } err = client.Execute(ctx, op, &respData) return respData.Pastes, err } func PasteContents(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (pastes *PasteCursor, err error) { op := gqlclient.NewOperation("query pasteContents ($cursor: Cursor) {\n\tpastes(cursor: $cursor) {\n\t\tresults {\n\t\t\t... pasteContents\n\t\t}\n\t\tcursor\n\t}\n}\nfragment pasteContents on Paste {\n\tid\n\tcreated\n\tvisibility\n\tfiles {\n\t\tfilename\n\t\tcontents\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { Pastes *PasteCursor } err = client.Execute(ctx, op, &respData) return respData.Pastes, err } func PasteContentsByID(client *gqlclient.Client, ctx context.Context, id string) (paste *Paste, err error) { op := gqlclient.NewOperation("query pasteContentsByID ($id: String!) {\n\tpaste(id: $id) {\n\t\t... pasteContents\n\t}\n}\nfragment pasteContents on Paste {\n\tid\n\tcreated\n\tvisibility\n\tfiles {\n\t\tfilename\n\t\tcontents\n\t}\n}\n") op.Var("id", id) var respData struct { Paste *Paste } err = client.Execute(ctx, op, &respData) return respData.Paste, err } func PasteCompletionList(client *gqlclient.Client, ctx context.Context) (pastes *PasteCursor, err error) { op := gqlclient.NewOperation("query pasteCompletionList {\n\tpastes {\n\t\tresults {\n\t\t\tid\n\t\t\tfiles {\n\t\t\t\tfilename\n\t\t\t}\n\t\t}\n\t}\n}\n") var respData struct { Pastes *PasteCursor } err = client.Execute(ctx, op, &respData) return respData.Pastes, err } func ShowPaste(client *gqlclient.Client, ctx context.Context, id string) (paste *Paste, err error) { op := gqlclient.NewOperation("query showPaste ($id: String!) {\n\tpaste(id: $id) {\n\t\tid\n\t\tcreated\n\t\tvisibility\n\t\tfiles {\n\t\t\tfilename\n\t\t\tcontents\n\t\t}\n\t\tuser {\n\t\t\tcanonicalName\n\t\t}\n\t}\n}\n") op.Var("id", id) var respData struct { Paste *Paste } err = client.Execute(ctx, op, &respData) return respData.Paste, err } func UserWebhooks(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (userWebhooks *WebhookSubscriptionCursor, err error) { op := gqlclient.NewOperation("query userWebhooks ($cursor: Cursor) {\n\tuserWebhooks(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\turl\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { UserWebhooks *WebhookSubscriptionCursor } err = client.Execute(ctx, op, &respData) return respData.UserWebhooks, err } hut-0.6.0/srht/pastesrht/operations.graphql000066400000000000000000000033111463710650600210630ustar00rootroot00000000000000mutation createPaste($files: [Upload!]!, $visibility: Visibility!) { create(files: $files, visibility: $visibility) { id user { canonicalName } } } mutation delete($id: String!) { delete(id: $id) { id } } mutation update($id: String!, $visibility: Visibility!) { update(id: $id, visibility: $visibility) { id } } mutation createUserWebhook($config: UserWebhookInput!) { createUserWebhook(config: $config) { id } } mutation deleteUserWebhook($id: Int!) { deleteUserWebhook(id: $id) { id } } query pastes($cursor: Cursor) { pastes(cursor: $cursor) { results { id created visibility files { filename } } cursor } } query pasteContents($cursor: Cursor) { pastes(cursor: $cursor) { results { ...pasteContents } cursor } } query pasteContentsByID($id: String!) { paste(id: $id) { ...pasteContents } } fragment pasteContents on Paste { id created visibility files { filename contents } } query pasteCompletionList { pastes { results { id files { filename } } } } query showPaste($id: String!) { paste(id: $id) { id created visibility files { filename contents } user { canonicalName } } } query userWebhooks($cursor: Cursor) { userWebhooks(cursor: $cursor) { results { id url } cursor } } hut-0.6.0/srht/pastesrht/schema.graphqls000066400000000000000000000201401463710650600203220ustar00rootroot00000000000000# This schema definition is available in the public domain, or under the terms # of CC-0, at your choice. scalar Cursor scalar Time scalar Upload """ URL from which some secondary data may be retrieved. You must provide the same Authentication header to this address as you did to the GraphQL resolver which provided it. The URL is not guaranteed to be consistent for an extended length of time; applications should submit a new GraphQL query each time they wish to access the data at the provided URL. """ scalar URL """ This is used to decorate fields which are only accessible with a personal access token, and are not available to clients using OAuth 2.0 access tokens. """ directive @private on FIELD_DEFINITION "Used to provide a human-friendly description of an access scope." directive @scopehelp(details: String!) on ENUM_VALUE """ This used to decorate fields which are for internal use, and are not available to normal API users. """ directive @internal on FIELD_DEFINITION enum AccessScope { PROFILE @scopehelp(details: "profile information") PASTES @scopehelp(details: "pastes") } enum AccessKind { RO @scopehelp(details: "read") RW @scopehelp(details: "read and write") } """ Decorates fields for which access requires a particular OAuth 2.0 scope with read or write access. For the meta.sr.ht API, you have access to all public information without any special permissions - user profile information, public keys, and so on. """ directive @access(scope: AccessScope!, kind: AccessKind!) on FIELD_DEFINITION enum Visibility { "Visible to everyone, listed on your profile" PUBLIC "Visible to everyone (if they know the URL), not listed on your profile" UNLISTED "Not visible to anyone except those explicitly added to the access list" PRIVATE } # https://semver.org type Version { major: Int! minor: Int! patch: Int! """ If this API version is scheduled for deprecation, this is the date on which it will stop working; or null if this API version is not scheduled for deprecation. """ deprecationDate: Time } interface Entity { id: Int! created: Time! """ The canonical name of this entity. For users, this is their username prefixed with '~'. Additional entity types will be supported in the future. """ canonicalName: String! pastes(cursor: Cursor): PasteCursor! @access(scope: PASTES, kind: RO) } type User implements Entity { id: Int! created: Time! canonicalName: String! pastes(cursor: Cursor): PasteCursor! @access(scope: PASTES, kind: RO) username: String! } type Paste { id: String! created: Time! visibility: Visibility! files: [File!]! user: Entity! @access(scope: PROFILE, kind: RO) } type File { filename: String hash: String! contents: URL! } """ A cursor for enumerating pastes If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type PasteCursor { results: [Paste!]! cursor: Cursor } type OAuthClient { uuid: String! } enum WebhookEvent { PASTE_CREATED PASTE_UPDATED PASTE_DELETED } interface WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! """ If this webhook was registered by an authorized OAuth 2.0 client, this field is non-null. """ client: OAuthClient @private "All deliveries which have been sent to this webhook." deliveries(cursor: Cursor): WebhookDeliveryCursor! "Returns a sample payload for this subscription, for testing purposes" sample(event: WebhookEvent!): String! } type UserWebhookSubscription implements WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! client: OAuthClient @private deliveries(cursor: Cursor): WebhookDeliveryCursor! sample(event: WebhookEvent!): String! } type WebhookDelivery { uuid: String! date: Time! event: WebhookEvent! subscription: WebhookSubscription! requestBody: String! """ These details are provided only after a response is received from the remote server. If a response is sent whose Content-Type is not text/*, or cannot be decoded as UTF-8, the response body will be null. It will be truncated after 64 KiB. """ responseBody: String responseHeaders: String responseStatus: Int } interface WebhookPayload { uuid: String! event: WebhookEvent! date: Time! } type PasteEvent implements WebhookPayload { uuid: String! event: WebhookEvent! date: Time! paste: Paste! } """ A cursor for enumerating a list of webhook deliveries If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type WebhookDeliveryCursor { results: [WebhookDelivery!]! cursor: Cursor } """ A cursor for enumerating a list of webhook subscriptions If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type WebhookSubscriptionCursor { results: [WebhookSubscription!]! cursor: Cursor } type Query { "Returns API version information." version: Version! "Returns the authenticated user." me: User! @access(scope: PROFILE, kind: RO) "Returns a specific user." user(username: String!): User @access(scope: PROFILE, kind: RO) "Returns a list of pastes created by the authenticated user." pastes(cursor: Cursor): PasteCursor @access(scope: PASTES, kind: RO) "Returns a paste by its ID." paste(id: String!): Paste @access(scope: PASTES, kind: RO) """ Returns a list of user webhook subscriptions. For clients authenticated with a personal access token, this returns all webhooks configured by all GraphQL clients for your account. For clients authenticated with an OAuth 2.0 access token, this returns only webhooks registered for your client. """ userWebhooks(cursor: Cursor): WebhookSubscriptionCursor! "Returns details of a user webhook subscription by its ID." userWebhook(id: Int!): WebhookSubscription """ Returns information about the webhook currently being processed. This is not valid during normal queries over HTTP, and will return an error if used outside of a webhook context. """ webhook: WebhookPayload! } input UserWebhookInput { url: String! events: [WebhookEvent!]! query: String! } type Mutation { """ Creates a new paste from a list of files. The files uploaded must have a content type of text/* and must be decodable as UTF-8. Note that the web UI will replace CRLF with LF in uploads; the GraphQL API does not. """ create( files: [Upload!]!, visibility: Visibility!, ): Paste! @access(scope: PASTES, kind: RW) "Updates the visibility of a paste." update(id: String!, visibility: Visibility!): Paste @access(scope: PASTES, kind: RW) "Deletes a paste by its ID." delete(id: String!): Paste @access(scope: PASTES, kind: RW) """ Creates a new user webhook subscription. When an event from the provided list of events occurs, the 'query' parameter (a GraphQL query) will be evaluated and the results will be sent to the provided URL as the body of an HTTP POST request. The list of events must include at least one event, and no duplicates. This query is evaluated in the webhook context, such that query { webhook } may be used to access details of the event which trigged the webhook. The query may not make any mutations. """ createUserWebhook(config: UserWebhookInput!): WebhookSubscription! """ Deletes a user webhook. Any events already queued may still be delivered after this request completes. Clients authenticated with a personal access token may delete any webhook registered for their account, but authorized OAuth 2.0 clients may only delete their own webhooks. Manually deleting a webhook configured by a third-party client may cause unexpected behavior with the third-party integration. """ deleteUserWebhook(id: Int!): WebhookSubscription! """ Deletes the authenticated user's account. Internal use only. """ deleteUser: Int! @internal } hut-0.6.0/srht/pastesrht/strings.go000066400000000000000000000023211463710650600173400ustar00rootroot00000000000000package pastesrht import ( "fmt" "strings" "git.sr.ht/~xenrox/hut/termfmt" ) func (visibility Visibility) TermString() string { var style termfmt.Style switch visibility { case VisibilityPublic: case VisibilityUnlisted: style = termfmt.Blue case VisibilityPrivate: style = termfmt.Red default: panic(fmt.Sprintf("unknown visibility: %q", visibility)) } return style.String(strings.ToLower(string(visibility))) } func ParseVisibility(s string) (Visibility, error) { switch strings.ToLower(s) { case "unlisted": return VisibilityUnlisted, nil case "private": return VisibilityPrivate, nil case "public": return VisibilityPublic, nil default: return "", fmt.Errorf("invalid visibility: %s", s) } } func ParseEvents(events []string) ([]WebhookEvent, error) { var whEvents []WebhookEvent for _, event := range events { switch strings.ToLower(event) { case "paste_created": whEvents = append(whEvents, WebhookEventPasteCreated) case "paste_updated": whEvents = append(whEvents, WebhookEventPasteUpdated) case "paste_deleted": whEvents = append(whEvents, WebhookEventPasteDeleted) default: return whEvents, fmt.Errorf("invalid event: %q", event) } } return whEvents, nil } hut-0.6.0/srht/todosrht/000077500000000000000000000000001463710650600151535ustar00rootroot00000000000000hut-0.6.0/srht/todosrht/gql.go000066400000000000000000002047151463710650600162760ustar00rootroot00000000000000// Code generated by gqlclientgen - DO NOT EDIT. package todosrht import ( "context" "encoding/json" "fmt" gqlclient "git.sr.ht/~emersion/gqlclient" ) type ACL struct { // Permission to view tickets Browse bool `json:"browse"` // Permission to submit tickets Submit bool `json:"submit"` // Permission to comment on tickets Comment bool `json:"comment"` // Permission to edit tickets Edit bool `json:"edit"` // Permission to resolve, re-open, transfer, or label tickets Triage bool `json:"triage"` // Underlying value of the GraphQL interface Value ACLValue `json:"-"` } func (base *ACL) UnmarshalJSON(b []byte) error { type Raw ACL var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "TrackerACL": base.Value = new(TrackerACL) case "DefaultACL": base.Value = new(DefaultACL) case "": return nil default: return fmt.Errorf("gqlclient: interface ACL: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // ACLValue is one of: TrackerACL | DefaultACL type ACLValue interface { isACL() } // A cursor for enumerating access control list entries // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type ACLCursor struct { Results []TrackerACL `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type ACLInput struct { // Permission to view tickets Browse bool `json:"browse"` // Permission to submit tickets Submit bool `json:"submit"` // Permission to comment on tickets Comment bool `json:"comment"` // Permission to edit tickets Edit bool `json:"edit"` // Permission to resolve, re-open, transfer, or label tickets Triage bool `json:"triage"` } type AccessKind string const ( AccessKindRo AccessKind = "RO" AccessKindRw AccessKind = "RW" ) type AccessScope string const ( AccessScopeProfile AccessScope = "PROFILE" AccessScopeTrackers AccessScope = "TRACKERS" AccessScopeTickets AccessScope = "TICKETS" AccessScopeAcls AccessScope = "ACLS" AccessScopeEvents AccessScope = "EVENTS" AccessScopeSubscriptions AccessScope = "SUBSCRIPTIONS" ) type ActivitySubscription struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` // Underlying value of the GraphQL interface Value ActivitySubscriptionValue `json:"-"` } func (base *ActivitySubscription) UnmarshalJSON(b []byte) error { type Raw ActivitySubscription var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "TrackerSubscription": base.Value = new(TrackerSubscription) case "TicketSubscription": base.Value = new(TicketSubscription) case "": return nil default: return fmt.Errorf("gqlclient: interface ActivitySubscription: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // ActivitySubscriptionValue is one of: TrackerSubscription | TicketSubscription type ActivitySubscriptionValue interface { isActivitySubscription() } // A cursor for enumerating subscriptions // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type ActivitySubscriptionCursor struct { Results []ActivitySubscription `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type Assignment struct { EventType EventType `json:"eventType"` Ticket *Ticket `json:"ticket"` Assigner *Entity `json:"assigner"` Assignee *Entity `json:"assignee"` } func (*Assignment) isEventDetail() {} type Authenticity string const ( // The server vouches for this information as entered verbatim by the // attributed entity. AuthenticityAuthentic Authenticity = "AUTHENTIC" // The server does not vouch for this information as entered by the attributed // entity, no authentication was provided. AuthenticityUnauthenticated Authenticity = "UNAUTHENTICATED" // The server has evidence that the information has likely been manipulated by // a third-party. AuthenticityTampered Authenticity = "TAMPERED" ) type Comment struct { EventType EventType `json:"eventType"` Ticket *Ticket `json:"ticket"` Author *Entity `json:"author"` Text string `json:"text"` Authenticity Authenticity `json:"authenticity"` // If this comment has been edited, this field points to the new revision. SupersededBy *Comment `json:"supersededBy,omitempty"` } func (*Comment) isEventDetail() {} type Created struct { EventType EventType `json:"eventType"` Ticket *Ticket `json:"ticket"` Author *Entity `json:"author"` } func (*Created) isEventDetail() {} type Cursor string // These ACL policies are applied non-specifically, e.g. the default ACL for all // authenticated users. type DefaultACL struct { Browse bool `json:"browse"` Submit bool `json:"submit"` Comment bool `json:"comment"` Edit bool `json:"edit"` Triage bool `json:"triage"` } func (*DefaultACL) isACL() {} type EmailAddress struct { CanonicalName string `json:"canonicalName"` // "jdoe@example.org" of "Jane Doe " Mailbox string `json:"mailbox"` // "Jane Doe" of "Jane Doe " Name *string `json:"name,omitempty"` } func (*EmailAddress) isEntity() {} type EmailCmd string const ( EmailCmdResolve EmailCmd = "RESOLVE" EmailCmdReopen EmailCmd = "REOPEN" EmailCmdLabel EmailCmd = "LABEL" EmailCmdUnlabel EmailCmd = "UNLABEL" ) type Entity struct { CanonicalName string `json:"canonicalName"` // Underlying value of the GraphQL interface Value EntityValue `json:"-"` } func (base *Entity) UnmarshalJSON(b []byte) error { type Raw Entity var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "User": base.Value = new(User) case "EmailAddress": base.Value = new(EmailAddress) case "ExternalUser": base.Value = new(ExternalUser) case "": return nil default: return fmt.Errorf("gqlclient: interface Entity: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // EntityValue is one of: User | EmailAddress | ExternalUser type EntityValue interface { isEntity() } // Represents an event which affects a ticket. Multiple changes can occur in a // single event, and are enumerated in the "changes" field. type Event struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Changes []EventDetail `json:"changes"` Ticket *Ticket `json:"ticket"` } type EventCreated struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` NewEvent *Event `json:"newEvent"` } func (*EventCreated) isWebhookPayload() {} // A cursor for enumerating events // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type EventCursor struct { Results []Event `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type EventDetail struct { EventType EventType `json:"eventType"` Ticket *Ticket `json:"ticket"` // Underlying value of the GraphQL interface Value EventDetailValue `json:"-"` } func (base *EventDetail) UnmarshalJSON(b []byte) error { type Raw EventDetail var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "Created": base.Value = new(Created) case "Assignment": base.Value = new(Assignment) case "Comment": base.Value = new(Comment) case "LabelUpdate": base.Value = new(LabelUpdate) case "StatusChange": base.Value = new(StatusChange) case "UserMention": base.Value = new(UserMention) case "TicketMention": base.Value = new(TicketMention) case "": return nil default: return fmt.Errorf("gqlclient: interface EventDetail: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // EventDetailValue is one of: Created | Assignment | Comment | LabelUpdate | StatusChange | UserMention | TicketMention type EventDetailValue interface { isEventDetail() } type EventType string const ( EventTypeCreated EventType = "CREATED" EventTypeComment EventType = "COMMENT" EventTypeStatusChange EventType = "STATUS_CHANGE" EventTypeLabelAdded EventType = "LABEL_ADDED" EventTypeLabelRemoved EventType = "LABEL_REMOVED" EventTypeAssignedUser EventType = "ASSIGNED_USER" EventTypeUnassignedUser EventType = "UNASSIGNED_USER" EventTypeUserMentioned EventType = "USER_MENTIONED" EventTypeTicketMentioned EventType = "TICKET_MENTIONED" ) type ExternalUser struct { CanonicalName string `json:"canonicalName"` // : // e.g. github:ddevault ExternalId string `json:"externalId"` // The canonical external URL for this user, e.g. https://github.com/ddevault ExternalUrl *string `json:"externalUrl,omitempty"` } func (*ExternalUser) isEntity() {} // This is used for importing tickets from third-party services, and may only be // used by the tracker owner. It causes a ticket submission, update, or comment // to be attributed to an external user and appear as if it were submitted at a // specific time. type ImportInput struct { Created gqlclient.Time `json:"created"` // External user ID. By convention this should be "service:username", e.g. // "codeberg:ddevault". ExternalId string `json:"externalId"` // A URL at which the user's external profile may be found, e.g. // "https://codeberg.org/ddevault". ExternalUrl string `json:"externalUrl"` } type Label struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Name string `json:"name"` Tracker *Tracker `json:"tracker"` // In CSS hexadecimal format BackgroundColor string `json:"backgroundColor"` ForegroundColor string `json:"foregroundColor"` Tickets *TicketCursor `json:"tickets"` } // A cursor for enumerating labels // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type LabelCursor struct { Results []Label `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type LabelEvent struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` Label *Label `json:"label"` } func (*LabelEvent) isWebhookPayload() {} type LabelUpdate struct { EventType EventType `json:"eventType"` Ticket *Ticket `json:"ticket"` Labeler *Entity `json:"labeler"` Label *Label `json:"label"` } func (*LabelUpdate) isEventDetail() {} type OAuthClient struct { Uuid string `json:"uuid"` } type StatusChange struct { EventType EventType `json:"eventType"` Ticket *Ticket `json:"ticket"` Editor *Entity `json:"editor"` OldStatus TicketStatus `json:"oldStatus"` NewStatus TicketStatus `json:"newStatus"` OldResolution TicketResolution `json:"oldResolution"` NewResolution TicketResolution `json:"newResolution"` } func (*StatusChange) isEventDetail() {} type SubmitCommentEmailInput struct { Text string `json:"text"` SenderId int32 `json:"senderId"` Cmd *EmailCmd `json:"cmd,omitempty"` Resolution *TicketResolution `json:"resolution,omitempty"` LabelIds []int32 `json:"labelIds,omitempty"` MessageId string `json:"messageId"` } // You may omit the status or resolution fields to leave them unchanged (or if // you do not have permission to change them). "resolution" is required if // status is RESOLVED. type SubmitCommentInput struct { Text string `json:"text"` Status *TicketStatus `json:"status,omitempty"` Resolution *TicketResolution `json:"resolution,omitempty"` // For use by the tracker owner only Import *ImportInput `json:"import,omitempty"` } type SubmitTicketEmailInput struct { Subject string `json:"subject"` Body *string `json:"body,omitempty"` SenderId int32 `json:"senderId"` MessageId string `json:"messageId"` } type SubmitTicketInput struct { Subject string `json:"subject"` Body *string `json:"body,omitempty"` Created gqlclient.Time `json:"created,omitempty"` ExternalId *string `json:"externalId,omitempty"` ExternalUrl *string `json:"externalUrl,omitempty"` } type Ticket struct { // The ticket ID is unique within each tracker, but is not globally unique. // The first ticket opened on a given tracker will have ID 1, then 2, and so // on. Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` Submitter *Entity `json:"submitter"` Tracker *Tracker `json:"tracker"` // Canonical ticket reference string; may be used in comments to identify the // ticket from anywhere. Ref string `json:"ref"` Subject string `json:"subject"` Body *string `json:"body,omitempty"` Status TicketStatus `json:"status"` Resolution TicketResolution `json:"resolution"` Authenticity Authenticity `json:"authenticity"` Labels []Label `json:"labels"` Assignees []Entity `json:"assignees"` Events *EventCursor `json:"events"` // If the authenticated user is subscribed to this ticket, this is that // subscription. Subscription *TicketSubscription `json:"subscription,omitempty"` // Returns a list of ticket webhook subscriptions. For clients // authenticated with a personal access token, this returns all webhooks // configured by all GraphQL clients for your account. For clients // authenticated with an OAuth 2.0 access token, this returns only webhooks // registered for your client. Webhooks *WebhookSubscriptionCursor `json:"webhooks"` // Returns details of a ticket webhook subscription by its ID. Webhook *WebhookSubscription `json:"webhook,omitempty"` } // A cursor for enumerating tickets // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type TicketCursor struct { Results []Ticket `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type TicketDeletedEvent struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` TrackerId int32 `json:"trackerId"` TicketId int32 `json:"ticketId"` } func (*TicketDeletedEvent) isWebhookPayload() {} type TicketEvent struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` Ticket *Ticket `json:"ticket"` } func (*TicketEvent) isWebhookPayload() {} type TicketMention struct { EventType EventType `json:"eventType"` Ticket *Ticket `json:"ticket"` Author *Entity `json:"author"` Mentioned *Ticket `json:"mentioned"` } func (*TicketMention) isEventDetail() {} type TicketResolution string const ( TicketResolutionUnresolved TicketResolution = "UNRESOLVED" TicketResolutionClosed TicketResolution = "CLOSED" TicketResolutionFixed TicketResolution = "FIXED" TicketResolutionImplemented TicketResolution = "IMPLEMENTED" TicketResolutionWontFix TicketResolution = "WONT_FIX" TicketResolutionByDesign TicketResolution = "BY_DESIGN" TicketResolutionInvalid TicketResolution = "INVALID" TicketResolutionDuplicate TicketResolution = "DUPLICATE" TicketResolutionNotOurBug TicketResolution = "NOT_OUR_BUG" ) type TicketStatus string const ( TicketStatusReported TicketStatus = "REPORTED" TicketStatusConfirmed TicketStatus = "CONFIRMED" TicketStatusInProgress TicketStatus = "IN_PROGRESS" TicketStatusPending TicketStatus = "PENDING" TicketStatusResolved TicketStatus = "RESOLVED" ) // A ticket subscription will notify a participant when activity occurs on a // ticket. type TicketSubscription struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Ticket *Ticket `json:"ticket"` } func (*TicketSubscription) isActivitySubscription() {} type TicketWebhookInput struct { Url string `json:"url"` Events []WebhookEvent `json:"events"` Query string `json:"query"` } type TicketWebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` Client *OAuthClient `json:"client,omitempty"` Deliveries *WebhookDeliveryCursor `json:"deliveries"` Sample string `json:"sample"` Ticket *Ticket `json:"ticket"` } func (*TicketWebhookSubscription) isWebhookSubscription() {} type Tracker struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` Owner *Entity `json:"owner"` Name string `json:"name"` Description *string `json:"description,omitempty"` Visibility Visibility `json:"visibility"` Ticket *Ticket `json:"ticket"` Tickets *TicketCursor `json:"tickets"` Label *Label `json:"label,omitempty"` Labels *LabelCursor `json:"labels"` // If the authenticated user is subscribed to this tracker, this is that // subscription. Subscription *TrackerSubscription `json:"subscription,omitempty"` // The access control list entry (or the default ACL) which describes the // authenticated user's permissions with respect to this tracker. Acl *ACL `json:"acl,omitempty"` DefaultACL *DefaultACL `json:"defaultACL"` Acls *ACLCursor `json:"acls"` // Returns a URL from which the tracker owner may download a gzipped JSON // archive of the tracker. Export URL `json:"export"` // Returns a list of tracker webhook subscriptions. For clients // authenticated with a personal access token, this returns all webhooks // configured by all GraphQL clients for your account. For clients // authenticated with an OAuth 2.0 access token, this returns only webhooks // registered for your client. Webhooks *WebhookSubscriptionCursor `json:"webhooks"` // Returns details of a tracker webhook subscription by its ID. Webhook *WebhookSubscription `json:"webhook,omitempty"` } // These ACLs are configured for specific entities, and may be used to expand or // constrain the rights of a participant. type TrackerACL struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Tracker *Tracker `json:"tracker"` Entity *Entity `json:"entity"` Browse bool `json:"browse"` Submit bool `json:"submit"` Comment bool `json:"comment"` Edit bool `json:"edit"` Triage bool `json:"triage"` } func (*TrackerACL) isACL() {} // A cursor for enumerating trackers // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type TrackerCursor struct { Results []Tracker `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type TrackerEvent struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` Tracker *Tracker `json:"tracker"` } func (*TrackerEvent) isWebhookPayload() {} // You may omit any fields to leave them unchanged. type TrackerInput struct { Description *string `json:"description,omitempty"` Visibility *Visibility `json:"visibility,omitempty"` } // A tracker subscription will notify a participant of all activity for a // tracker, including all new tickets and their events. type TrackerSubscription struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Tracker *Tracker `json:"tracker"` } func (*TrackerSubscription) isActivitySubscription() {} type TrackerWebhookInput struct { Url string `json:"url"` Events []WebhookEvent `json:"events"` Query string `json:"query"` } type TrackerWebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` Client *OAuthClient `json:"client,omitempty"` Deliveries *WebhookDeliveryCursor `json:"deliveries"` Sample string `json:"sample"` Tracker *Tracker `json:"tracker"` } func (*TrackerWebhookSubscription) isWebhookSubscription() {} type URL string // You may omit any fields to leave them unchanged. type UpdateLabelInput struct { Name *string `json:"name,omitempty"` ForegroundColor *string `json:"foregroundColor,omitempty"` BackgroundColor *string `json:"backgroundColor,omitempty"` } // "resolution" is required if status is RESOLVED. type UpdateStatusInput struct { Status TicketStatus `json:"status"` Resolution *TicketResolution `json:"resolution,omitempty"` // For use by the tracker owner only Import *ImportInput `json:"import,omitempty"` } // You may omit any fields to leave them unchanged. To remove the ticket body, // set it to null. type UpdateTicketInput struct { Subject *string `json:"subject,omitempty"` Body *string `json:"body,omitempty"` // For use by the tracker owner only Import *ImportInput `json:"import,omitempty"` } type User struct { Id int32 `json:"id"` Created gqlclient.Time `json:"created"` Updated gqlclient.Time `json:"updated"` CanonicalName string `json:"canonicalName"` Username string `json:"username"` Email string `json:"email"` Url *string `json:"url,omitempty"` Location *string `json:"location,omitempty"` Bio *string `json:"bio,omitempty"` // Returns a specific tracker. Tracker *Tracker `json:"tracker,omitempty"` Trackers *TrackerCursor `json:"trackers"` } func (*User) isEntity() {} type UserMention struct { EventType EventType `json:"eventType"` Ticket *Ticket `json:"ticket"` Author *Entity `json:"author"` Mentioned *Entity `json:"mentioned"` } func (*UserMention) isEventDetail() {} type UserWebhookInput struct { Url string `json:"url"` Events []WebhookEvent `json:"events"` Query string `json:"query"` } type UserWebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` Client *OAuthClient `json:"client,omitempty"` Deliveries *WebhookDeliveryCursor `json:"deliveries"` Sample string `json:"sample"` } func (*UserWebhookSubscription) isWebhookSubscription() {} type Version struct { Major int32 `json:"major"` Minor int32 `json:"minor"` Patch int32 `json:"patch"` // If this API version is scheduled for deprecation, this is the date on which // it will stop working; or null if this API version is not scheduled for // deprecation. DeprecationDate gqlclient.Time `json:"deprecationDate,omitempty"` } type Visibility string const ( VisibilityPublic Visibility = "PUBLIC" VisibilityUnlisted Visibility = "UNLISTED" VisibilityPrivate Visibility = "PRIVATE" ) type WebhookDelivery struct { Uuid string `json:"uuid"` Date gqlclient.Time `json:"date"` Event WebhookEvent `json:"event"` Subscription *WebhookSubscription `json:"subscription"` RequestBody string `json:"requestBody"` // These details are provided only after a response is received from the // remote server. If a response is sent whose Content-Type is not text/*, or // cannot be decoded as UTF-8, the response body will be null. It will be // truncated after 64 KiB. ResponseBody *string `json:"responseBody,omitempty"` ResponseHeaders *string `json:"responseHeaders,omitempty"` ResponseStatus *int32 `json:"responseStatus,omitempty"` } // A cursor for enumerating a list of webhook deliveries // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type WebhookDeliveryCursor struct { Results []WebhookDelivery `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } type WebhookEvent string const ( WebhookEventTrackerCreated WebhookEvent = "TRACKER_CREATED" WebhookEventTrackerUpdate WebhookEvent = "TRACKER_UPDATE" WebhookEventTrackerDeleted WebhookEvent = "TRACKER_DELETED" WebhookEventTicketCreated WebhookEvent = "TICKET_CREATED" WebhookEventTicketUpdate WebhookEvent = "TICKET_UPDATE" WebhookEventTicketDeleted WebhookEvent = "TICKET_DELETED" WebhookEventLabelCreated WebhookEvent = "LABEL_CREATED" WebhookEventLabelUpdate WebhookEvent = "LABEL_UPDATE" WebhookEventLabelDeleted WebhookEvent = "LABEL_DELETED" WebhookEventEventCreated WebhookEvent = "EVENT_CREATED" ) type WebhookPayload struct { Uuid string `json:"uuid"` Event WebhookEvent `json:"event"` Date gqlclient.Time `json:"date"` // Underlying value of the GraphQL interface Value WebhookPayloadValue `json:"-"` } func (base *WebhookPayload) UnmarshalJSON(b []byte) error { type Raw WebhookPayload var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "TrackerEvent": base.Value = new(TrackerEvent) case "TicketEvent": base.Value = new(TicketEvent) case "TicketDeletedEvent": base.Value = new(TicketDeletedEvent) case "EventCreated": base.Value = new(EventCreated) case "LabelEvent": base.Value = new(LabelEvent) case "": return nil default: return fmt.Errorf("gqlclient: interface WebhookPayload: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // WebhookPayloadValue is one of: TrackerEvent | TicketEvent | TicketDeletedEvent | EventCreated | LabelEvent type WebhookPayloadValue interface { isWebhookPayload() } type WebhookSubscription struct { Id int32 `json:"id"` Events []WebhookEvent `json:"events"` Query string `json:"query"` Url string `json:"url"` // If this webhook was registered by an authorized OAuth 2.0 client, this // field is non-null. Client *OAuthClient `json:"client,omitempty"` // All deliveries which have been sent to this webhook. Deliveries *WebhookDeliveryCursor `json:"deliveries"` // Returns a sample payload for this subscription, for testing purposes Sample string `json:"sample"` // Underlying value of the GraphQL interface Value WebhookSubscriptionValue `json:"-"` } func (base *WebhookSubscription) UnmarshalJSON(b []byte) error { type Raw WebhookSubscription var data struct { *Raw TypeName string `json:"__typename"` } data.Raw = (*Raw)(base) err := json.Unmarshal(b, &data) if err != nil { return err } switch data.TypeName { case "UserWebhookSubscription": base.Value = new(UserWebhookSubscription) case "TrackerWebhookSubscription": base.Value = new(TrackerWebhookSubscription) case "TicketWebhookSubscription": base.Value = new(TicketWebhookSubscription) case "": return nil default: return fmt.Errorf("gqlclient: interface WebhookSubscription: unknown __typename %q", data.TypeName) } return json.Unmarshal(b, base.Value) } // WebhookSubscriptionValue is one of: UserWebhookSubscription | TrackerWebhookSubscription | TicketWebhookSubscription type WebhookSubscriptionValue interface { isWebhookSubscription() } // A cursor for enumerating a list of webhook subscriptions // // If there are additional results available, the cursor object may be passed // back into the same endpoint to retrieve another page. If the cursor is null, // there are no remaining results to return. type WebhookSubscriptionCursor struct { Results []WebhookSubscription `json:"results"` Cursor *Cursor `json:"cursor,omitempty"` } func Trackers(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (trackers *TrackerCursor, err error) { op := gqlclient.NewOperation("query trackers ($cursor: Cursor) {\n\ttrackers(cursor: $cursor) {\n\t\t... trackers\n\t}\n}\nfragment trackers on TrackerCursor {\n\tresults {\n\t\tname\n\t\tdescription\n\t\tvisibility\n\t}\n\tcursor\n}\n") op.Var("cursor", cursor) var respData struct { Trackers *TrackerCursor } err = client.Execute(ctx, op, &respData) return respData.Trackers, err } func TrackersByUser(client *gqlclient.Client, ctx context.Context, username string, cursor *Cursor) (user *User, err error) { op := gqlclient.NewOperation("query trackersByUser ($username: String!, $cursor: Cursor) {\n\tuser(username: $username) {\n\t\ttrackers(cursor: $cursor) {\n\t\t\t... trackers\n\t\t}\n\t}\n}\nfragment trackers on TrackerCursor {\n\tresults {\n\t\tname\n\t\tdescription\n\t\tvisibility\n\t}\n\tcursor\n}\n") op.Var("username", username) op.Var("cursor", cursor) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func ExportTracker(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) { op := gqlclient.NewOperation("query exportTracker ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\ttracker(name: $name) {\n\t\t\t... trackerExport\n\t\t}\n\t}\n}\nfragment trackerExport on Tracker {\n\tname\n\tdescription\n\tvisibility\n\texport\n}\n") op.Var("username", username) op.Var("name", name) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func ExportTrackers(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (trackers *TrackerCursor, err error) { op := gqlclient.NewOperation("query exportTrackers ($cursor: Cursor) {\n\ttrackers(cursor: $cursor) {\n\t\tresults {\n\t\t\t... trackerExport\n\t\t}\n\t\tcursor\n\t}\n}\nfragment trackerExport on Tracker {\n\tname\n\tdescription\n\tvisibility\n\texport\n}\n") op.Var("cursor", cursor) var respData struct { Trackers *TrackerCursor } err = client.Execute(ctx, op, &respData) return respData.Trackers, err } func TrackerIDByName(client *gqlclient.Client, ctx context.Context, name string) (me *User, err error) { op := gqlclient.NewOperation("query trackerIDByName ($name: String!) {\n\tme {\n\t\ttracker(name: $name) {\n\t\t\tid\n\t\t}\n\t}\n}\n") op.Var("name", name) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func TrackerIDByUser(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) { op := gqlclient.NewOperation("query trackerIDByUser ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\ttracker(name: $name) {\n\t\t\tid\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func Tickets(client *gqlclient.Client, ctx context.Context, name string, cursor *Cursor) (me *User, err error) { op := gqlclient.NewOperation("query tickets ($name: String!, $cursor: Cursor) {\n\tme {\n\t\ttracker(name: $name) {\n\t\t\ttickets(cursor: $cursor) {\n\t\t\t\t... tickets\n\t\t\t}\n\t\t}\n\t}\n}\nfragment tickets on TicketCursor {\n\tresults {\n\t\tid\n\t\tsubject\n\t\tstatus\n\t\tresolution\n\t\tcreated\n\t\tsubmitter {\n\t\t\tcanonicalName\n\t\t}\n\t\tlabels {\n\t\t\tname\n\t\t\tbackgroundColor\n\t\t\tforegroundColor\n\t\t}\n\t}\n\tcursor\n}\n") op.Var("name", name) op.Var("cursor", cursor) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func TicketsByUser(client *gqlclient.Client, ctx context.Context, username string, name string, cursor *Cursor) (user *User, err error) { op := gqlclient.NewOperation("query ticketsByUser ($username: String!, $name: String!, $cursor: Cursor) {\n\tuser(username: $username) {\n\t\ttracker(name: $name) {\n\t\t\ttickets(cursor: $cursor) {\n\t\t\t\t... tickets\n\t\t\t}\n\t\t}\n\t}\n}\nfragment tickets on TicketCursor {\n\tresults {\n\t\tid\n\t\tsubject\n\t\tstatus\n\t\tresolution\n\t\tcreated\n\t\tsubmitter {\n\t\t\tcanonicalName\n\t\t}\n\t\tlabels {\n\t\t\tname\n\t\t\tbackgroundColor\n\t\t\tforegroundColor\n\t\t}\n\t}\n\tcursor\n}\n") op.Var("username", username) op.Var("name", name) op.Var("cursor", cursor) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func Labels(client *gqlclient.Client, ctx context.Context, name string, cursor *Cursor) (me *User, err error) { op := gqlclient.NewOperation("query labels ($name: String!, $cursor: Cursor) {\n\tme {\n\t\ttracker(name: $name) {\n\t\t\tlabels(cursor: $cursor) {\n\t\t\t\t... labels\n\t\t\t}\n\t\t}\n\t}\n}\nfragment labels on LabelCursor {\n\tresults {\n\t\tname\n\t\tbackgroundColor\n\t\tforegroundColor\n\t}\n\tcursor\n}\n") op.Var("name", name) op.Var("cursor", cursor) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func LabelsByUser(client *gqlclient.Client, ctx context.Context, username string, name string, cursor *Cursor) (user *User, err error) { op := gqlclient.NewOperation("query labelsByUser ($username: String!, $name: String!, $cursor: Cursor) {\n\tuser(username: $username) {\n\t\ttracker(name: $name) {\n\t\t\tlabels(cursor: $cursor) {\n\t\t\t\t... labels\n\t\t\t}\n\t\t}\n\t}\n}\nfragment labels on LabelCursor {\n\tresults {\n\t\tname\n\t\tbackgroundColor\n\t\tforegroundColor\n\t}\n\tcursor\n}\n") op.Var("username", username) op.Var("name", name) op.Var("cursor", cursor) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func AclByTrackerName(client *gqlclient.Client, ctx context.Context, name string, cursor *Cursor) (me *User, err error) { op := gqlclient.NewOperation("query aclByTrackerName ($name: String!, $cursor: Cursor) {\n\tme {\n\t\ttracker(name: $name) {\n\t\t\t... acl\n\t\t}\n\t}\n}\nfragment acl on Tracker {\n\tdefaultACL {\n\t\tbrowse\n\t\tsubmit\n\t\tcomment\n\t\tedit\n\t\ttriage\n\t}\n\tacls(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\tcreated\n\t\t\tentity {\n\t\t\t\tcanonicalName\n\t\t\t}\n\t\t\tbrowse\n\t\t\tsubmit\n\t\t\tcomment\n\t\t\tedit\n\t\t\ttriage\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("name", name) op.Var("cursor", cursor) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func AclByUser(client *gqlclient.Client, ctx context.Context, username string, name string, cursor *Cursor) (user *User, err error) { op := gqlclient.NewOperation("query aclByUser ($username: String!, $name: String!, $cursor: Cursor) {\n\tuser(username: $username) {\n\t\ttracker(name: $name) {\n\t\t\t... acl\n\t\t}\n\t}\n}\nfragment acl on Tracker {\n\tdefaultACL {\n\t\tbrowse\n\t\tsubmit\n\t\tcomment\n\t\tedit\n\t\ttriage\n\t}\n\tacls(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\tcreated\n\t\t\tentity {\n\t\t\t\tcanonicalName\n\t\t\t}\n\t\t\tbrowse\n\t\t\tsubmit\n\t\t\tcomment\n\t\t\tedit\n\t\t\ttriage\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("username", username) op.Var("name", name) op.Var("cursor", cursor) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func UserIDByName(client *gqlclient.Client, ctx context.Context, username string) (user *User, err error) { op := gqlclient.NewOperation("query userIDByName ($username: String!) {\n\tuser(username: $username) {\n\t\tid\n\t}\n}\n") op.Var("username", username) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func Assignees(client *gqlclient.Client, ctx context.Context, name string, id int32) (me *User, err error) { op := gqlclient.NewOperation("query assignees ($name: String!, $id: Int!) {\n\tme {\n\t\ttracker(name: $name) {\n\t\t\tticket(id: $id) {\n\t\t\t\tassignees {\n\t\t\t\t\tcanonicalName\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("name", name) op.Var("id", id) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func AssigneesByUser(client *gqlclient.Client, ctx context.Context, username string, name string, id int32) (user *User, err error) { op := gqlclient.NewOperation("query assigneesByUser ($username: String!, $name: String!, $id: Int!) {\n\tuser(username: $username) {\n\t\ttracker(name: $name) {\n\t\t\tticket(id: $id) {\n\t\t\t\tassignees {\n\t\t\t\t\tcanonicalName\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) op.Var("id", id) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func CompleteTicketId(client *gqlclient.Client, ctx context.Context, name string, subscription bool) (me *User, err error) { op := gqlclient.NewOperation("query completeTicketId ($name: String!, $subscription: Boolean!) {\n\tme {\n\t\ttracker(name: $name) {\n\t\t\t... completeTicket\n\t\t}\n\t}\n}\nfragment completeTicket on Tracker {\n\ttickets {\n\t\tresults {\n\t\t\tid\n\t\t\tsubject\n\t\t\t... on Ticket @include(if: $subscription) {\n\t\t\t\tsubscription {\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("name", name) op.Var("subscription", subscription) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func CompleteTicketIdByUser(client *gqlclient.Client, ctx context.Context, username string, name string, subscription bool) (user *User, err error) { op := gqlclient.NewOperation("query completeTicketIdByUser ($username: String!, $name: String!, $subscription: Boolean!) {\n\tuser(username: $username) {\n\t\ttracker(name: $name) {\n\t\t\t... completeTicket\n\t\t}\n\t}\n}\nfragment completeTicket on Tracker {\n\ttickets {\n\t\tresults {\n\t\t\tid\n\t\t\tsubject\n\t\t\t... on Ticket @include(if: $subscription) {\n\t\t\t\tsubscription {\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) op.Var("subscription", subscription) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func CompleteTicketAssign(client *gqlclient.Client, ctx context.Context, name string, id int32) (me *User, err error) { op := gqlclient.NewOperation("query completeTicketAssign ($name: String!, $id: Int!) {\n\tme {\n\t\tcanonicalName\n\t\ttracker(name: $name) {\n\t\t\tticket(id: $id) {\n\t\t\t\tassignees {\n\t\t\t\t\tcanonicalName\n\t\t\t\t}\n\t\t\t}\n\t\t\ttickets {\n\t\t\t\tresults {\n\t\t\t\t\tassignees {\n\t\t\t\t\t\tcanonicalName\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("name", name) op.Var("id", id) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func CompleteTicketAssignByUser(client *gqlclient.Client, ctx context.Context, username string, name string, id int32) (me *User, user *User, err error) { op := gqlclient.NewOperation("query completeTicketAssignByUser ($username: String!, $name: String!, $id: Int!) {\n\tme {\n\t\tcanonicalName\n\t}\n\tuser(username: $username) {\n\t\ttracker(name: $name) {\n\t\t\tticket(id: $id) {\n\t\t\t\tassignees {\n\t\t\t\t\tcanonicalName\n\t\t\t\t}\n\t\t\t}\n\t\t\ttickets {\n\t\t\t\tresults {\n\t\t\t\t\tassignees {\n\t\t\t\t\t\tcanonicalName\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) op.Var("id", id) var respData struct { Me *User User *User } err = client.Execute(ctx, op, &respData) return respData.Me, respData.User, err } func TrackerNames(client *gqlclient.Client, ctx context.Context) (trackers *TrackerCursor, err error) { op := gqlclient.NewOperation("query trackerNames {\n\ttrackers {\n\t\tresults {\n\t\t\tname\n\t\t}\n\t}\n}\n") var respData struct { Trackers *TrackerCursor } err = client.Execute(ctx, op, &respData) return respData.Trackers, err } func TicketWebhooks(client *gqlclient.Client, ctx context.Context, name string, id int32, cursor *Cursor) (me *User, err error) { op := gqlclient.NewOperation("query ticketWebhooks ($name: String!, $id: Int!, $cursor: Cursor) {\n\tme {\n\t\ttracker(name: $name) {\n\t\t\tticket(id: $id) {\n\t\t\t\t... ticketWebhooks\n\t\t\t}\n\t\t}\n\t}\n}\nfragment ticketWebhooks on Ticket {\n\twebhooks(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\turl\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("name", name) op.Var("id", id) op.Var("cursor", cursor) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func TicketWebhooksByUser(client *gqlclient.Client, ctx context.Context, username string, name string, id int32, cursor *Cursor) (user *User, err error) { op := gqlclient.NewOperation("query ticketWebhooksByUser ($username: String!, $name: String!, $id: Int!, $cursor: Cursor) {\n\tuser(username: $username) {\n\t\ttracker(name: $name) {\n\t\t\tticket(id: $id) {\n\t\t\t\t... ticketWebhooks\n\t\t\t}\n\t\t}\n\t}\n}\nfragment ticketWebhooks on Ticket {\n\twebhooks(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\turl\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("username", username) op.Var("name", name) op.Var("id", id) op.Var("cursor", cursor) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func UserWebhooks(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (userWebhooks *WebhookSubscriptionCursor, err error) { op := gqlclient.NewOperation("query userWebhooks ($cursor: Cursor) {\n\tuserWebhooks(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\turl\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("cursor", cursor) var respData struct { UserWebhooks *WebhookSubscriptionCursor } err = client.Execute(ctx, op, &respData) return respData.UserWebhooks, err } func TrackerWebhooks(client *gqlclient.Client, ctx context.Context, name string, cursor *Cursor) (me *User, err error) { op := gqlclient.NewOperation("query trackerWebhooks ($name: String!, $cursor: Cursor) {\n\tme {\n\t\ttracker(name: $name) {\n\t\t\t... trackerWebhooks\n\t\t}\n\t}\n}\nfragment trackerWebhooks on Tracker {\n\twebhooks(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\turl\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("name", name) op.Var("cursor", cursor) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func TrackerWebhooksByUser(client *gqlclient.Client, ctx context.Context, username string, name string, cursor *Cursor) (user *User, err error) { op := gqlclient.NewOperation("query trackerWebhooksByUser ($username: String!, $name: String!, $cursor: Cursor) {\n\tuser(username: $username) {\n\t\ttracker(name: $name) {\n\t\t\t... trackerWebhooks\n\t\t}\n\t}\n}\nfragment trackerWebhooks on Tracker {\n\twebhooks(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\turl\n\t\t}\n\t\tcursor\n\t}\n}\n") op.Var("username", username) op.Var("name", name) op.Var("cursor", cursor) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func TicketByName(client *gqlclient.Client, ctx context.Context, name string, id int32) (me *User, err error) { op := gqlclient.NewOperation("query ticketByName ($name: String!, $id: Int!) {\n\tme {\n\t\ttracker(name: $name) {\n\t\t\tticket(id: $id) {\n\t\t\t\t... ticket\n\t\t\t}\n\t\t}\n\t}\n}\nfragment ticket on Ticket {\n\tcreated\n\tupdated\n\tsubmitter {\n\t\tcanonicalName\n\t}\n\tsubject\n\tbody\n\tstatus\n\tresolution\n\tlabels {\n\t\tname\n\t\tbackgroundColor\n\t\tforegroundColor\n\t}\n\tassignees {\n\t\tcanonicalName\n\t}\n\tevents {\n\t\tresults {\n\t\t\tcreated\n\t\t\tchanges {\n\t\t\t\t__typename\n\t\t\t\t... on Comment {\n\t\t\t\t\tauthor {\n\t\t\t\t\t\tcanonicalName\n\t\t\t\t\t}\n\t\t\t\t\ttext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("name", name) op.Var("id", id) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func TicketByUser(client *gqlclient.Client, ctx context.Context, username string, tracker string, id int32) (user *User, err error) { op := gqlclient.NewOperation("query ticketByUser ($username: String!, $tracker: String!, $id: Int!) {\n\tuser(username: $username) {\n\t\ttracker(name: $tracker) {\n\t\t\tticket(id: $id) {\n\t\t\t\t... ticket\n\t\t\t}\n\t\t}\n\t}\n}\nfragment ticket on Ticket {\n\tcreated\n\tupdated\n\tsubmitter {\n\t\tcanonicalName\n\t}\n\tsubject\n\tbody\n\tstatus\n\tresolution\n\tlabels {\n\t\tname\n\t\tbackgroundColor\n\t\tforegroundColor\n\t}\n\tassignees {\n\t\tcanonicalName\n\t}\n\tevents {\n\t\tresults {\n\t\t\tcreated\n\t\t\tchanges {\n\t\t\t\t__typename\n\t\t\t\t... on Comment {\n\t\t\t\t\tauthor {\n\t\t\t\t\t\tcanonicalName\n\t\t\t\t\t}\n\t\t\t\t\ttext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("tracker", tracker) op.Var("id", id) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func TicketBodyByName(client *gqlclient.Client, ctx context.Context, name string, id int32) (me *User, err error) { op := gqlclient.NewOperation("query ticketBodyByName ($name: String!, $id: Int!) {\n\tme {\n\t\ttracker(name: $name) {\n\t\t\tid\n\t\t\tticket(id: $id) {\n\t\t\t\t... ticketBody\n\t\t\t}\n\t\t}\n\t}\n}\nfragment ticketBody on Ticket {\n\tid\n\tsubject\n\tbody\n}\n") op.Var("name", name) op.Var("id", id) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func TicketBodyByUser(client *gqlclient.Client, ctx context.Context, username string, tracker string, id int32) (user *User, err error) { op := gqlclient.NewOperation("query ticketBodyByUser ($username: String!, $tracker: String!, $id: Int!) {\n\tuser(username: $username) {\n\t\ttracker(name: $tracker) {\n\t\t\tid\n\t\t\tticket(id: $id) {\n\t\t\t\t... ticketBody\n\t\t\t}\n\t\t}\n\t}\n}\nfragment ticketBody on Ticket {\n\tid\n\tsubject\n\tbody\n}\n") op.Var("username", username) op.Var("tracker", tracker) op.Var("id", id) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func LabelIDByName(client *gqlclient.Client, ctx context.Context, trackerName string, labelName string) (me *User, err error) { op := gqlclient.NewOperation("query labelIDByName ($trackerName: String!, $labelName: String!) {\n\tme {\n\t\ttracker(name: $trackerName) {\n\t\t\tlabel(name: $labelName) {\n\t\t\t\tid\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("trackerName", trackerName) op.Var("labelName", labelName) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func LabelIDByUser(client *gqlclient.Client, ctx context.Context, username string, trackerName string, labelName string) (user *User, err error) { op := gqlclient.NewOperation("query labelIDByUser ($username: String!, $trackerName: String!, $labelName: String!) {\n\tuser(username: $username) {\n\t\ttracker(name: $trackerName) {\n\t\t\tlabel(name: $labelName) {\n\t\t\t\tid\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("trackerName", trackerName) op.Var("labelName", labelName) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func CompleteLabel(client *gqlclient.Client, ctx context.Context, name string) (me *User, err error) { op := gqlclient.NewOperation("query completeLabel ($name: String!) {\n\tme {\n\t\ttracker(name: $name) {\n\t\t\tlabels {\n\t\t\t\tresults {\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("name", name) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func CompleteLabelByUser(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) { op := gqlclient.NewOperation("query completeLabelByUser ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\ttracker(name: $name) {\n\t\t\tlabels {\n\t\t\t\tresults {\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func CompleteTicketLabel(client *gqlclient.Client, ctx context.Context, name string, id int32) (me *User, err error) { op := gqlclient.NewOperation("query completeTicketLabel ($name: String!, $id: Int!) {\n\tme {\n\t\ttracker(name: $name) {\n\t\t\tlabels {\n\t\t\t\tresults {\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t\tticket(id: $id) {\n\t\t\t\tlabels {\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("name", name) op.Var("id", id) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func CompleteTicketLabelByUser(client *gqlclient.Client, ctx context.Context, username string, name string, id int32) (user *User, err error) { op := gqlclient.NewOperation("query completeTicketLabelByUser ($username: String!, $name: String!, $id: Int!) {\n\tuser(username: $username) {\n\t\ttracker(name: $name) {\n\t\t\tlabels {\n\t\t\t\tresults {\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t\tticket(id: $id) {\n\t\t\t\tlabels {\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) op.Var("id", id) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func CompleteTicketUnlabel(client *gqlclient.Client, ctx context.Context, name string, id int32) (me *User, err error) { op := gqlclient.NewOperation("query completeTicketUnlabel ($name: String!, $id: Int!) {\n\tme {\n\t\ttracker(name: $name) {\n\t\t\tticket(id: $id) {\n\t\t\t\tlabels {\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("name", name) op.Var("id", id) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func CompleteTicketUnlabelByUser(client *gqlclient.Client, ctx context.Context, username string, name string, id int32) (user *User, err error) { op := gqlclient.NewOperation("query completeTicketUnlabelByUser ($username: String!, $name: String!, $id: Int!) {\n\tuser(username: $username) {\n\t\ttracker(name: $name) {\n\t\t\tticket(id: $id) {\n\t\t\t\tlabels {\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) op.Var("id", id) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func TrackerDescription(client *gqlclient.Client, ctx context.Context, name string) (me *User, err error) { op := gqlclient.NewOperation("query trackerDescription ($name: String!) {\n\tme {\n\t\ttracker(name: $name) {\n\t\t\tdescription\n\t\t}\n\t}\n}\n") op.Var("name", name) var respData struct { Me *User } err = client.Execute(ctx, op, &respData) return respData.Me, err } func TrackerDescriptionByUser(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) { op := gqlclient.NewOperation("query trackerDescriptionByUser ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\ttracker(name: $name) {\n\t\t\tdescription\n\t\t}\n\t}\n}\n") op.Var("username", username) op.Var("name", name) var respData struct { User *User } err = client.Execute(ctx, op, &respData) return respData.User, err } func DeleteTracker(client *gqlclient.Client, ctx context.Context, id int32) (deleteTracker *Tracker, err error) { op := gqlclient.NewOperation("mutation deleteTracker ($id: Int!) {\n\tdeleteTracker(id: $id) {\n\t\tname\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteTracker *Tracker } err = client.Execute(ctx, op, &respData) return respData.DeleteTracker, err } func SubmitComment(client *gqlclient.Client, ctx context.Context, trackerId int32, ticketId int32, input SubmitCommentInput) (submitComment *Event, err error) { op := gqlclient.NewOperation("mutation submitComment ($trackerId: Int!, $ticketId: Int!, $input: SubmitCommentInput!) {\n\tsubmitComment(trackerId: $trackerId, ticketId: $ticketId, input: $input) {\n\t\tticket {\n\t\t\tsubject\n\t\t}\n\t}\n}\n") op.Var("trackerId", trackerId) op.Var("ticketId", ticketId) op.Var("input", input) var respData struct { SubmitComment *Event } err = client.Execute(ctx, op, &respData) return respData.SubmitComment, err } func UpdateTicketStatus(client *gqlclient.Client, ctx context.Context, trackerId int32, ticketId int32, input UpdateStatusInput) (updateTicketStatus *Event, err error) { op := gqlclient.NewOperation("mutation updateTicketStatus ($trackerId: Int!, $ticketId: Int!, $input: UpdateStatusInput!) {\n\tupdateTicketStatus(trackerId: $trackerId, ticketId: $ticketId, input: $input) {\n\t\tticket {\n\t\t\tsubject\n\t\t}\n\t}\n}\n") op.Var("trackerId", trackerId) op.Var("ticketId", ticketId) op.Var("input", input) var respData struct { UpdateTicketStatus *Event } err = client.Execute(ctx, op, &respData) return respData.UpdateTicketStatus, err } func DeleteLabel(client *gqlclient.Client, ctx context.Context, id int32) (deleteLabel *Label, err error) { op := gqlclient.NewOperation("mutation deleteLabel ($id: Int!) {\n\tdeleteLabel(id: $id) {\n\t\tname\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteLabel *Label } err = client.Execute(ctx, op, &respData) return respData.DeleteLabel, err } func CreateLabel(client *gqlclient.Client, ctx context.Context, trackerId int32, name string, foregroundColor string, backgroundColor string) (createLabel *Label, err error) { op := gqlclient.NewOperation("mutation createLabel ($trackerId: Int!, $name: String!, $foregroundColor: String!, $backgroundColor: String!) {\n\tcreateLabel(trackerId: $trackerId, name: $name, foregroundColor: $foregroundColor, backgroundColor: $backgroundColor) {\n\t\tname\n\t\tbackgroundColor\n\t\tforegroundColor\n\t}\n}\n") op.Var("trackerId", trackerId) op.Var("name", name) op.Var("foregroundColor", foregroundColor) op.Var("backgroundColor", backgroundColor) var respData struct { CreateLabel *Label } err = client.Execute(ctx, op, &respData) return respData.CreateLabel, err } func UpdateLabel(client *gqlclient.Client, ctx context.Context, id int32, input UpdateLabelInput) (updateLabel *Label, err error) { op := gqlclient.NewOperation("mutation updateLabel ($id: Int!, $input: UpdateLabelInput!) {\n\tupdateLabel(id: $id, input: $input) {\n\t\tname\n\t}\n}\n") op.Var("id", id) op.Var("input", input) var respData struct { UpdateLabel *Label } err = client.Execute(ctx, op, &respData) return respData.UpdateLabel, err } func DeleteACL(client *gqlclient.Client, ctx context.Context, id int32) (deleteACL *TrackerACL, err error) { op := gqlclient.NewOperation("mutation deleteACL ($id: Int!) {\n\tdeleteACL(id: $id) {\n\t\ttracker {\n\t\t\tname\n\t\t}\n\t\tentity {\n\t\t\tcanonicalName\n\t\t}\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteACL *TrackerACL } err = client.Execute(ctx, op, &respData) return respData.DeleteACL, err } func TrackerSubscribe(client *gqlclient.Client, ctx context.Context, trackerId int32) (trackerSubscribe *TrackerSubscription, err error) { op := gqlclient.NewOperation("mutation trackerSubscribe ($trackerId: Int!) {\n\ttrackerSubscribe(trackerId: $trackerId) {\n\t\ttracker {\n\t\t\tname\n\t\t\towner {\n\t\t\t\tcanonicalName\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("trackerId", trackerId) var respData struct { TrackerSubscribe *TrackerSubscription } err = client.Execute(ctx, op, &respData) return respData.TrackerSubscribe, err } func TrackerUnsubscribe(client *gqlclient.Client, ctx context.Context, trackerId int32) (trackerUnsubscribe *TrackerSubscription, err error) { op := gqlclient.NewOperation("mutation trackerUnsubscribe ($trackerId: Int!) {\n\ttrackerUnsubscribe(trackerId: $trackerId, tickets: false) {\n\t\ttracker {\n\t\t\tname\n\t\t\towner {\n\t\t\t\tcanonicalName\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("trackerId", trackerId) var respData struct { TrackerUnsubscribe *TrackerSubscription } err = client.Execute(ctx, op, &respData) return respData.TrackerUnsubscribe, err } func TicketSubscribe(client *gqlclient.Client, ctx context.Context, trackerId int32, ticketId int32) (ticketSubscribe *TicketSubscription, err error) { op := gqlclient.NewOperation("mutation ticketSubscribe ($trackerId: Int!, $ticketId: Int!) {\n\tticketSubscribe(trackerId: $trackerId, ticketId: $ticketId) {\n\t\tticket {\n\t\t\tid\n\t\t}\n\t}\n}\n") op.Var("trackerId", trackerId) op.Var("ticketId", ticketId) var respData struct { TicketSubscribe *TicketSubscription } err = client.Execute(ctx, op, &respData) return respData.TicketSubscribe, err } func TicketUnsubscribe(client *gqlclient.Client, ctx context.Context, trackerId int32, ticketId int32) (ticketUnsubscribe *TicketSubscription, err error) { op := gqlclient.NewOperation("mutation ticketUnsubscribe ($trackerId: Int!, $ticketId: Int!) {\n\tticketUnsubscribe(trackerId: $trackerId, ticketId: $ticketId) {\n\t\tticket {\n\t\t\tid\n\t\t}\n\t}\n}\n") op.Var("trackerId", trackerId) op.Var("ticketId", ticketId) var respData struct { TicketUnsubscribe *TicketSubscription } err = client.Execute(ctx, op, &respData) return respData.TicketUnsubscribe, err } func AssignUser(client *gqlclient.Client, ctx context.Context, trackerId int32, ticketId int32, userId int32) (assignUser *Event, err error) { op := gqlclient.NewOperation("mutation assignUser ($trackerId: Int!, $ticketId: Int!, $userId: Int!) {\n\tassignUser(trackerId: $trackerId, ticketId: $ticketId, userId: $userId) {\n\t\tticket {\n\t\t\tsubject\n\t\t}\n\t}\n}\n") op.Var("trackerId", trackerId) op.Var("ticketId", ticketId) op.Var("userId", userId) var respData struct { AssignUser *Event } err = client.Execute(ctx, op, &respData) return respData.AssignUser, err } func UnassignUser(client *gqlclient.Client, ctx context.Context, trackerId int32, ticketId int32, userId int32) (unassignUser *Event, err error) { op := gqlclient.NewOperation("mutation unassignUser ($trackerId: Int!, $ticketId: Int!, $userId: Int!) {\n\tunassignUser(trackerId: $trackerId, ticketId: $ticketId, userId: $userId) {\n\t\tticket {\n\t\t\tsubject\n\t\t}\n\t}\n}\n") op.Var("trackerId", trackerId) op.Var("ticketId", ticketId) op.Var("userId", userId) var respData struct { UnassignUser *Event } err = client.Execute(ctx, op, &respData) return respData.UnassignUser, err } func CreateTracker(client *gqlclient.Client, ctx context.Context, name string, description *string, visibility Visibility) (createTracker *Tracker, err error) { op := gqlclient.NewOperation("mutation createTracker ($name: String!, $description: String, $visibility: Visibility!) {\n\tcreateTracker(name: $name, description: $description, visibility: $visibility) {\n\t\tname\n\t}\n}\n") op.Var("name", name) op.Var("description", description) op.Var("visibility", visibility) var respData struct { CreateTracker *Tracker } err = client.Execute(ctx, op, &respData) return respData.CreateTracker, err } func ImportTracker(client *gqlclient.Client, ctx context.Context, name string, description *string, visibility Visibility, dump gqlclient.Upload) (createTracker *Tracker, err error) { op := gqlclient.NewOperation("mutation importTracker ($name: String!, $description: String, $visibility: Visibility!, $dump: Upload!) {\n\tcreateTracker(name: $name, description: $description, visibility: $visibility, importUpload: $dump) {\n\t\tid\n\t}\n}\n") op.Var("name", name) op.Var("description", description) op.Var("visibility", visibility) op.Var("dump", dump) var respData struct { CreateTracker *Tracker } err = client.Execute(ctx, op, &respData) return respData.CreateTracker, err } func DeleteTicket(client *gqlclient.Client, ctx context.Context, trackerId int32, ticketId int32) (deleteTicket *Ticket, err error) { op := gqlclient.NewOperation("mutation deleteTicket ($trackerId: Int!, $ticketId: Int!) {\n\tdeleteTicket(trackerId: $trackerId, ticketId: $ticketId) {\n\t\tsubject\n\t}\n}\n") op.Var("trackerId", trackerId) op.Var("ticketId", ticketId) var respData struct { DeleteTicket *Ticket } err = client.Execute(ctx, op, &respData) return respData.DeleteTicket, err } func CreateTicketWebhook(client *gqlclient.Client, ctx context.Context, trackerId int32, ticketId int32, config TicketWebhookInput) (createTicketWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation createTicketWebhook ($trackerId: Int!, $ticketId: Int!, $config: TicketWebhookInput!) {\n\tcreateTicketWebhook(trackerId: $trackerId, ticketId: $ticketId, config: $config) {\n\t\tid\n\t}\n}\n") op.Var("trackerId", trackerId) op.Var("ticketId", ticketId) op.Var("config", config) var respData struct { CreateTicketWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.CreateTicketWebhook, err } func DeleteTicketWebhook(client *gqlclient.Client, ctx context.Context, id int32) (deleteTicketWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation deleteTicketWebhook ($id: Int!) {\n\tdeleteTicketWebhook(id: $id) {\n\t\tid\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteTicketWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.DeleteTicketWebhook, err } func CreateUserWebhook(client *gqlclient.Client, ctx context.Context, config UserWebhookInput) (createUserWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation createUserWebhook ($config: UserWebhookInput!) {\n\tcreateUserWebhook(config: $config) {\n\t\tid\n\t}\n}\n") op.Var("config", config) var respData struct { CreateUserWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.CreateUserWebhook, err } func DeleteUserWebhook(client *gqlclient.Client, ctx context.Context, id int32) (deleteUserWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation deleteUserWebhook ($id: Int!) {\n\tdeleteUserWebhook(id: $id) {\n\t\tid\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteUserWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.DeleteUserWebhook, err } func CreateTrackerWebhook(client *gqlclient.Client, ctx context.Context, trackerId int32, config TrackerWebhookInput) (createTrackerWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation createTrackerWebhook ($trackerId: Int!, $config: TrackerWebhookInput!) {\n\tcreateTrackerWebhook(trackerId: $trackerId, config: $config) {\n\t\tid\n\t}\n}\n") op.Var("trackerId", trackerId) op.Var("config", config) var respData struct { CreateTrackerWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.CreateTrackerWebhook, err } func DeleteTrackerWebhook(client *gqlclient.Client, ctx context.Context, id int32) (deleteTrackerWebhook *WebhookSubscription, err error) { op := gqlclient.NewOperation("mutation deleteTrackerWebhook ($id: Int!) {\n\tdeleteTrackerWebhook(id: $id) {\n\t\tid\n\t}\n}\n") op.Var("id", id) var respData struct { DeleteTrackerWebhook *WebhookSubscription } err = client.Execute(ctx, op, &respData) return respData.DeleteTrackerWebhook, err } func SubmitTicket(client *gqlclient.Client, ctx context.Context, trackerId int32, input SubmitTicketInput) (submitTicket *Ticket, err error) { op := gqlclient.NewOperation("mutation submitTicket ($trackerId: Int!, $input: SubmitTicketInput!) {\n\tsubmitTicket(trackerId: $trackerId, input: $input) {\n\t\tid\n\t}\n}\n") op.Var("trackerId", trackerId) op.Var("input", input) var respData struct { SubmitTicket *Ticket } err = client.Execute(ctx, op, &respData) return respData.SubmitTicket, err } func LabelTicket(client *gqlclient.Client, ctx context.Context, trackerId int32, ticketId int32, labelId int32) (labelTicket *Event, err error) { op := gqlclient.NewOperation("mutation labelTicket ($trackerId: Int!, $ticketId: Int!, $labelId: Int!) {\n\tlabelTicket(trackerId: $trackerId, ticketId: $ticketId, labelId: $labelId) {\n\t\tticket {\n\t\t\tsubject\n\t\t}\n\t}\n}\n") op.Var("trackerId", trackerId) op.Var("ticketId", ticketId) op.Var("labelId", labelId) var respData struct { LabelTicket *Event } err = client.Execute(ctx, op, &respData) return respData.LabelTicket, err } func UnlabelTicket(client *gqlclient.Client, ctx context.Context, trackerId int32, ticketId int32, labelId int32) (unlabelTicket *Event, err error) { op := gqlclient.NewOperation("mutation unlabelTicket ($trackerId: Int!, $ticketId: Int!, $labelId: Int!) {\n\tunlabelTicket(trackerId: $trackerId, ticketId: $ticketId, labelId: $labelId) {\n\t\tticket {\n\t\t\tsubject\n\t\t}\n\t}\n}\n") op.Var("trackerId", trackerId) op.Var("ticketId", ticketId) op.Var("labelId", labelId) var respData struct { UnlabelTicket *Event } err = client.Execute(ctx, op, &respData) return respData.UnlabelTicket, err } func UpdateTicket(client *gqlclient.Client, ctx context.Context, trackerId int32, ticketId int32, input UpdateTicketInput) (updateTicket *Ticket, err error) { op := gqlclient.NewOperation("mutation updateTicket ($trackerId: Int!, $ticketId: Int!, $input: UpdateTicketInput!) {\n\tupdateTicket(trackerId: $trackerId, ticketId: $ticketId, input: $input) {\n\t\tid\n\t}\n}\n") op.Var("trackerId", trackerId) op.Var("ticketId", ticketId) op.Var("input", input) var respData struct { UpdateTicket *Ticket } err = client.Execute(ctx, op, &respData) return respData.UpdateTicket, err } func UpdateTracker(client *gqlclient.Client, ctx context.Context, id int32, input TrackerInput) (updateTracker *Tracker, err error) { op := gqlclient.NewOperation("mutation updateTracker ($id: Int!, $input: TrackerInput!) {\n\tupdateTracker(id: $id, input: $input) {\n\t\tname\n\t}\n}\n") op.Var("id", id) op.Var("input", input) var respData struct { UpdateTracker *Tracker } err = client.Execute(ctx, op, &respData) return respData.UpdateTracker, err } func ClearDescription(client *gqlclient.Client, ctx context.Context, id int32) (updateTracker *Tracker, err error) { op := gqlclient.NewOperation("mutation clearDescription ($id: Int!) {\n\tupdateTracker(id: $id, input: {description:null}) {\n\t\tname\n\t}\n}\n") op.Var("id", id) var respData struct { UpdateTracker *Tracker } err = client.Execute(ctx, op, &respData) return respData.UpdateTracker, err } hut-0.6.0/srht/todosrht/operations.graphql000066400000000000000000000364301463710650600207240ustar00rootroot00000000000000query trackers($cursor: Cursor) { trackers(cursor: $cursor) { ...trackers } } query trackersByUser($username: String!, $cursor: Cursor) { user(username: $username) { trackers(cursor: $cursor) { ...trackers } } } fragment trackers on TrackerCursor { results { name description visibility } cursor } query exportTracker($username: String!, $name: String!) { user(username: $username) { tracker(name: $name) { ...trackerExport } } } query exportTrackers($cursor: Cursor) { trackers(cursor: $cursor) { results { ...trackerExport } cursor } } fragment trackerExport on Tracker { name description visibility export } query trackerIDByName($name: String!) { me { tracker(name: $name) { id } } } query trackerIDByUser($username: String!, $name: String!) { user(username: $username) { tracker(name: $name) { id } } } query tickets($name: String!, $cursor: Cursor) { me { tracker(name: $name) { tickets(cursor: $cursor) { ...tickets } } } } query ticketsByUser($username: String!, $name: String!, $cursor: Cursor) { user(username: $username) { tracker(name: $name) { tickets(cursor: $cursor) { ...tickets } } } } fragment tickets on TicketCursor { results { id subject status resolution created submitter { canonicalName } labels { name backgroundColor foregroundColor } } cursor } query labels($name: String!, $cursor: Cursor) { me { tracker(name: $name) { labels(cursor: $cursor) { ...labels } } } } query labelsByUser($username: String!, $name: String!, $cursor: Cursor) { user(username: $username) { tracker(name: $name) { labels(cursor: $cursor) { ...labels } } } } fragment labels on LabelCursor { results { name backgroundColor foregroundColor } cursor } query aclByTrackerName($name: String!, $cursor: Cursor) { me { tracker(name: $name) { ...acl } } } query aclByUser($username: String!, $name: String!, $cursor: Cursor) { user(username: $username) { tracker(name: $name) { ...acl } } } fragment acl on Tracker { defaultACL { browse submit comment edit triage } acls(cursor: $cursor) { results { id created entity { canonicalName } browse submit comment edit triage } cursor } } query userIDByName($username: String!) { user(username: $username) { id } } query assignees($name: String!, $id: Int!) { me { tracker(name: $name) { ticket(id: $id) { assignees { canonicalName } } } } } query assigneesByUser($username: String!, $name: String!, $id: Int!) { user(username: $username) { tracker(name: $name) { ticket(id: $id) { assignees { canonicalName } } } } } query completeTicketId($name: String!, $subscription: Boolean!) { me { tracker(name: $name) { ...completeTicket } } } query completeTicketIdByUser( $username: String! $name: String! $subscription: Boolean! ) { user(username: $username) { tracker(name: $name) { ...completeTicket } } } fragment completeTicket on Tracker { tickets { results { id subject ... on Ticket @include(if: $subscription) { subscription { id } } } } } query completeTicketAssign($name: String!, $id: Int!) { me { canonicalName tracker(name: $name) { ticket(id: $id) { assignees { canonicalName } } tickets { results { assignees { canonicalName } } } } } } query completeTicketAssignByUser( $username: String! $name: String! $id: Int! ) { me { canonicalName } user(username: $username) { tracker(name: $name) { ticket(id: $id) { assignees { canonicalName } } tickets { results { assignees { canonicalName } } } } } } query trackerNames { trackers { results { name } } } query ticketWebhooks($name: String!, $id: Int!, $cursor: Cursor) { me { tracker(name: $name) { ticket(id: $id) { ...ticketWebhooks } } } } query ticketWebhooksByUser( $username: String! $name: String! $id: Int! $cursor: Cursor ) { user(username: $username) { tracker(name: $name) { ticket(id: $id) { ...ticketWebhooks } } } } fragment ticketWebhooks on Ticket { webhooks(cursor: $cursor) { results { id url } cursor } } query userWebhooks($cursor: Cursor) { userWebhooks(cursor: $cursor) { results { id url } cursor } } query trackerWebhooks($name: String!, $cursor: Cursor) { me { tracker(name: $name) { ...trackerWebhooks } } } query trackerWebhooksByUser( $username: String! $name: String! $cursor: Cursor ) { user(username: $username) { tracker(name: $name) { ...trackerWebhooks } } } fragment trackerWebhooks on Tracker { webhooks(cursor: $cursor) { results { id url } cursor } } query ticketByName($name: String!, $id: Int!) { me { tracker(name: $name) { ticket(id: $id) { ...ticket } } } } query ticketByUser($username: String!, $tracker: String!, $id: Int!) { user(username: $username) { tracker(name: $tracker) { ticket(id: $id) { ...ticket } } } } fragment ticket on Ticket { created updated submitter { canonicalName } subject body status resolution labels { name backgroundColor foregroundColor } assignees { canonicalName } events { results { created changes { __typename ... on Comment { author { canonicalName } text } } } } } query ticketBodyByName($name: String!, $id: Int!) { me { tracker(name: $name) { id ticket(id: $id) { ...ticketBody } } } } query ticketBodyByUser($username: String!, $tracker: String!, $id: Int!) { user(username: $username) { tracker(name: $tracker) { id ticket(id: $id) { ...ticketBody } } } } fragment ticketBody on Ticket { id subject body } query labelIDByName($trackerName: String!, $labelName: String!) { me { tracker(name: $trackerName) { label(name: $labelName) { id } } } } query labelIDByUser( $username: String! $trackerName: String! $labelName: String! ) { user(username: $username) { tracker(name: $trackerName) { label(name: $labelName) { id } } } } query completeLabel($name: String!) { me { tracker(name: $name) { labels { results { name } } } } } query completeLabelByUser($username: String!, $name: String!) { user(username: $username) { tracker(name: $name) { labels { results { name } } } } } query completeTicketLabel($name: String!, $id: Int!) { me { tracker(name: $name) { labels { results { name } } ticket(id: $id) { labels { name } } } } } query completeTicketLabelByUser($username: String!, $name: String!, $id: Int!) { user(username: $username) { tracker(name: $name) { labels { results { name } } ticket(id: $id) { labels { name } } } } } query completeTicketUnlabel($name: String!, $id: Int!) { me { tracker(name: $name) { ticket(id: $id) { labels { name } } } } } query completeTicketUnlabelByUser( $username: String! $name: String! $id: Int! ) { user(username: $username) { tracker(name: $name) { ticket(id: $id) { labels { name } } } } } query trackerDescription($name: String!) { me { tracker(name: $name) { description } } } query trackerDescriptionByUser($username: String!, $name: String!) { user(username: $username) { tracker(name: $name) { description } } } mutation deleteTracker($id: Int!) { deleteTracker(id: $id) { name } } mutation submitComment( $trackerId: Int! $ticketId: Int! $input: SubmitCommentInput! ) { submitComment(trackerId: $trackerId, ticketId: $ticketId, input: $input) { ticket { subject } } } mutation updateTicketStatus( $trackerId: Int! $ticketId: Int! $input: UpdateStatusInput! ) { updateTicketStatus( trackerId: $trackerId ticketId: $ticketId input: $input ) { ticket { subject } } } mutation deleteLabel($id: Int!) { deleteLabel(id: $id) { name } } mutation createLabel( $trackerId: Int! $name: String! $foregroundColor: String! $backgroundColor: String! ) { createLabel( trackerId: $trackerId name: $name foregroundColor: $foregroundColor backgroundColor: $backgroundColor ) { name backgroundColor foregroundColor } } mutation updateLabel($id: Int!, $input: UpdateLabelInput!) { updateLabel(id: $id, input: $input) { name } } mutation deleteACL($id: Int!) { deleteACL(id: $id) { tracker { name } entity { canonicalName } } } mutation trackerSubscribe($trackerId: Int!) { trackerSubscribe(trackerId: $trackerId) { tracker { name owner { canonicalName } } } } mutation trackerUnsubscribe($trackerId: Int!) { # TODO: Wait for API to implement "tickets" trackerUnsubscribe(trackerId: $trackerId, tickets: false) { tracker { name owner { canonicalName } } } } mutation ticketSubscribe($trackerId: Int!, $ticketId: Int!) { ticketSubscribe(trackerId: $trackerId, ticketId: $ticketId) { ticket { id } } } mutation ticketUnsubscribe($trackerId: Int!, $ticketId: Int!) { ticketUnsubscribe(trackerId: $trackerId, ticketId: $ticketId) { ticket { id } } } mutation assignUser($trackerId: Int!, $ticketId: Int!, $userId: Int!) { assignUser(trackerId: $trackerId, ticketId: $ticketId, userId: $userId) { ticket { subject } } } mutation unassignUser($trackerId: Int!, $ticketId: Int!, $userId: Int!) { unassignUser(trackerId: $trackerId, ticketId: $ticketId, userId: $userId) { ticket { subject } } } mutation createTracker( $name: String! $description: String $visibility: Visibility! ) { createTracker( name: $name description: $description visibility: $visibility ) { name } } mutation importTracker( $name: String! $description: String $visibility: Visibility! $dump: Upload! ) { createTracker( name: $name description: $description visibility: $visibility importUpload: $dump ) { id } } mutation deleteTicket($trackerId: Int!, $ticketId: Int!) { deleteTicket(trackerId: $trackerId, ticketId: $ticketId) { subject } } mutation createTicketWebhook( $trackerId: Int! $ticketId: Int! $config: TicketWebhookInput! ) { createTicketWebhook( trackerId: $trackerId ticketId: $ticketId config: $config ) { id } } mutation deleteTicketWebhook($id: Int!) { deleteTicketWebhook(id: $id) { id } } mutation createUserWebhook($config: UserWebhookInput!) { createUserWebhook(config: $config) { id } } mutation deleteUserWebhook($id: Int!) { deleteUserWebhook(id: $id) { id } } mutation createTrackerWebhook($trackerId: Int!, $config: TrackerWebhookInput!) { createTrackerWebhook(trackerId: $trackerId, config: $config) { id } } mutation deleteTrackerWebhook($id: Int!) { deleteTrackerWebhook(id: $id) { id } } mutation submitTicket($trackerId: Int!, $input: SubmitTicketInput!) { submitTicket(trackerId: $trackerId, input: $input) { id } } mutation labelTicket($trackerId: Int!, $ticketId: Int!, $labelId: Int!) { labelTicket(trackerId: $trackerId, ticketId: $ticketId, labelId: $labelId) { ticket { subject } } } mutation unlabelTicket($trackerId: Int!, $ticketId: Int!, $labelId: Int!) { unlabelTicket( trackerId: $trackerId ticketId: $ticketId labelId: $labelId ) { ticket { subject } } } mutation updateTicket( $trackerId: Int! $ticketId: Int! $input: UpdateTicketInput! ) { updateTicket(trackerId: $trackerId, ticketId: $ticketId, input: $input) { id } } mutation updateTracker($id: Int!, $input: TrackerInput!) { updateTracker(id: $id, input: $input) { name } } mutation clearDescription($id: Int!) { updateTracker(id: $id, input: { description: null }) { name } } hut-0.6.0/srht/todosrht/schema.graphqls000066400000000000000000000631561463710650600201710ustar00rootroot00000000000000# This schema definition is available in the public domain, or under the terms # of CC-0, at your choice. scalar Cursor scalar Time scalar URL scalar Upload "Used to provide a human-friendly description of an access scope" directive @scopehelp(details: String!) on ENUM_VALUE """ This is used to decorate fields which are only accessible with a personal access token, and are not available to clients using OAuth 2.0 access tokens. """ directive @private on FIELD_DEFINITION """ This is used to decorate fields which are for internal use, and are not available to normal API users. """ directive @internal on FIELD_DEFINITION enum AccessScope { PROFILE @scopehelp(details: "profile information") TRACKERS @scopehelp(details: "trackers") TICKETS @scopehelp(details: "tickets") ACLS @scopehelp(details: "access control lists") EVENTS @scopehelp(details: "events") SUBSCRIPTIONS @scopehelp(details: "tracker & ticket subscriptions") } enum AccessKind { RO @scopehelp(details: "read") RW @scopehelp(details: "read and write") } """ Decorates fields for which access requires a particular OAuth 2.0 scope with read or write access. """ directive @access(scope: AccessScope!, kind: AccessKind!) on FIELD_DEFINITION # https://semver.org type Version { major: Int! minor: Int! patch: Int! """ If this API version is scheduled for deprecation, this is the date on which it will stop working; or null if this API version is not scheduled for deprecation. """ deprecationDate: Time } interface Entity { canonicalName: String! } type User implements Entity { id: Int! created: Time! updated: Time! canonicalName: String! username: String! email: String! url: String location: String bio: String "Returns a specific tracker." tracker(name: String!): Tracker @access(scope: TRACKERS, kind: RO) trackers(cursor: Cursor): TrackerCursor! @access(scope: TRACKERS, kind: RO) } type EmailAddress implements Entity { canonicalName: String! """ "jdoe@example.org" of "Jane Doe " """ mailbox: String! """ "Jane Doe" of "Jane Doe " """ name: String } type ExternalUser implements Entity { canonicalName: String! """ : e.g. github:ddevault """ externalId: String! "The canonical external URL for this user, e.g. https://github.com/ddevault" externalUrl: String } enum Visibility { PUBLIC UNLISTED PRIVATE } type Tracker { id: Int! created: Time! updated: Time! owner: Entity! @access(scope: PROFILE, kind: RO) name: String! description: String visibility: Visibility! ticket(id: Int!): Ticket! @access(scope: TICKETS, kind: RO) tickets(cursor: Cursor): TicketCursor! @access(scope: TICKETS, kind: RO) label(name: String!): Label labels(cursor: Cursor): LabelCursor! """ If the authenticated user is subscribed to this tracker, this is that subscription. """ subscription: TrackerSubscription @access(scope: SUBSCRIPTIONS, kind: RO) """ The access control list entry (or the default ACL) which describes the authenticated user's permissions with respect to this tracker. """ acl: ACL # Only available to the tracker owner: defaultACL: DefaultACL! acls(cursor: Cursor): ACLCursor! @access(scope: ACLS, kind: RO) """ Returns a URL from which the tracker owner may download a gzipped JSON archive of the tracker. """ export: URL! """ Returns a list of tracker webhook subscriptions. For clients authenticated with a personal access token, this returns all webhooks configured by all GraphQL clients for your account. For clients authenticated with an OAuth 2.0 access token, this returns only webhooks registered for your client. """ webhooks(cursor: Cursor): WebhookSubscriptionCursor! "Returns details of a tracker webhook subscription by its ID." webhook(id: Int!): WebhookSubscription } type OAuthClient { uuid: String! } enum WebhookEvent { TRACKER_CREATED TRACKER_UPDATE TRACKER_DELETED TICKET_CREATED TICKET_UPDATE TICKET_DELETED LABEL_CREATED LABEL_UPDATE LABEL_DELETED EVENT_CREATED } interface WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! """ If this webhook was registered by an authorized OAuth 2.0 client, this field is non-null. """ client: OAuthClient @private "All deliveries which have been sent to this webhook." deliveries(cursor: Cursor): WebhookDeliveryCursor! "Returns a sample payload for this subscription, for testing purposes" sample(event: WebhookEvent!): String! } type UserWebhookSubscription implements WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! client: OAuthClient @private deliveries(cursor: Cursor): WebhookDeliveryCursor! sample(event: WebhookEvent!): String! } type TrackerWebhookSubscription implements WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! client: OAuthClient @private deliveries(cursor: Cursor): WebhookDeliveryCursor! sample(event: WebhookEvent!): String! tracker: Tracker! } type TicketWebhookSubscription implements WebhookSubscription { id: Int! events: [WebhookEvent!]! query: String! url: String! client: OAuthClient @private deliveries(cursor: Cursor): WebhookDeliveryCursor! sample(event: WebhookEvent!): String! ticket: Ticket! } type WebhookDelivery { uuid: String! date: Time! event: WebhookEvent! subscription: WebhookSubscription! requestBody: String! """ These details are provided only after a response is received from the remote server. If a response is sent whose Content-Type is not text/*, or cannot be decoded as UTF-8, the response body will be null. It will be truncated after 64 KiB. """ responseBody: String responseHeaders: String responseStatus: Int } interface WebhookPayload { uuid: String! event: WebhookEvent! date: Time! } type TrackerEvent implements WebhookPayload { uuid: String! event: WebhookEvent! date: Time! tracker: Tracker! } type TicketEvent implements WebhookPayload { uuid: String! event: WebhookEvent! date: Time! ticket: Ticket! } type TicketDeletedEvent implements WebhookPayload { uuid: String! event: WebhookEvent! date: Time! trackerId: Int! ticketId: Int! } type EventCreated implements WebhookPayload { uuid: String! event: WebhookEvent! date: Time! newEvent: Event! } type LabelEvent implements WebhookPayload { uuid: String! event: WebhookEvent! date: Time! label: Label! } enum TicketStatus { REPORTED CONFIRMED IN_PROGRESS PENDING RESOLVED } enum TicketResolution { UNRESOLVED CLOSED FIXED IMPLEMENTED WONT_FIX BY_DESIGN INVALID DUPLICATE NOT_OUR_BUG } enum Authenticity { """ The server vouches for this information as entered verbatim by the attributed entity. """ AUTHENTIC """ The server does not vouch for this information as entered by the attributed entity, no authentication was provided. """ UNAUTHENTICATED """ The server has evidence that the information has likely been manipulated by a third-party. """ TAMPERED } type Ticket { """ The ticket ID is unique within each tracker, but is not globally unique. The first ticket opened on a given tracker will have ID 1, then 2, and so on. """ id: Int! created: Time! updated: Time! submitter: Entity! @access(scope: PROFILE, kind: RO) tracker: Tracker! @access(scope: TRACKERS, kind: RO) """ Canonical ticket reference string; may be used in comments to identify the ticket from anywhere. """ ref: String! subject: String! body: String status: TicketStatus! resolution: TicketResolution! authenticity: Authenticity! labels: [Label!]! assignees: [Entity!]! @access(scope: PROFILE, kind: RO) events(cursor: Cursor): EventCursor! @access(scope: EVENTS, kind: RO) """ If the authenticated user is subscribed to this ticket, this is that subscription. """ subscription: TicketSubscription @access(scope: SUBSCRIPTIONS, kind: RO) """ Returns a list of ticket webhook subscriptions. For clients authenticated with a personal access token, this returns all webhooks configured by all GraphQL clients for your account. For clients authenticated with an OAuth 2.0 access token, this returns only webhooks registered for your client. """ webhooks(cursor: Cursor): WebhookSubscriptionCursor! "Returns details of a ticket webhook subscription by its ID." webhook(id: Int!): WebhookSubscription } interface ACL { "Permission to view tickets" browse: Boolean! "Permission to submit tickets" submit: Boolean! "Permission to comment on tickets" comment: Boolean! "Permission to edit tickets" edit: Boolean! "Permission to resolve, re-open, transfer, or label tickets" triage: Boolean! } """ These ACLs are configured for specific entities, and may be used to expand or constrain the rights of a participant. """ type TrackerACL implements ACL { id: Int! created: Time! tracker: Tracker! @access(scope: TRACKERS, kind: RO) entity: Entity! @access(scope: PROFILE, kind: RO) browse: Boolean! submit: Boolean! comment: Boolean! edit: Boolean! triage: Boolean! } """ These ACL policies are applied non-specifically, e.g. the default ACL for all authenticated users. """ type DefaultACL implements ACL { browse: Boolean! submit: Boolean! comment: Boolean! edit: Boolean! triage: Boolean! } type Label { id: Int! created: Time! name: String! tracker: Tracker! @access(scope: TRACKERS, kind: RO) "In CSS hexadecimal format" backgroundColor: String! foregroundColor: String! tickets(cursor: Cursor): TicketCursor! @access(scope: TICKETS, kind: RO) } enum EventType { CREATED COMMENT STATUS_CHANGE LABEL_ADDED LABEL_REMOVED ASSIGNED_USER UNASSIGNED_USER USER_MENTIONED TICKET_MENTIONED } """ Represents an event which affects a ticket. Multiple changes can occur in a single event, and are enumerated in the "changes" field. """ type Event { id: Int! created: Time! changes: [EventDetail!]! ticket: Ticket! @access(scope: TICKETS, kind: RO) } interface EventDetail { eventType: EventType! ticket: Ticket! @access(scope: TICKETS, kind: RO) } type Created implements EventDetail { eventType: EventType! ticket: Ticket! @access(scope: TICKETS, kind: RO) author: Entity! @access(scope: PROFILE, kind: RO) } type Assignment implements EventDetail { eventType: EventType! ticket: Ticket! @access(scope: TICKETS, kind: RO) assigner: Entity! @access(scope: PROFILE, kind: RO) assignee: Entity! @access(scope: PROFILE, kind: RO) } type Comment implements EventDetail { eventType: EventType! ticket: Ticket! @access(scope: TICKETS, kind: RO) author: Entity! @access(scope: PROFILE, kind: RO) text: String! authenticity: Authenticity! "If this comment has been edited, this field points to the new revision." supersededBy: Comment } type LabelUpdate implements EventDetail { eventType: EventType! ticket: Ticket! @access(scope: TICKETS, kind: RO) labeler: Entity! @access(scope: PROFILE, kind: RO) label: Label! } type StatusChange implements EventDetail { eventType: EventType! ticket: Ticket! @access(scope: TICKETS, kind: RO) editor: Entity! @access(scope: PROFILE, kind: RO) oldStatus: TicketStatus! newStatus: TicketStatus! oldResolution: TicketResolution! newResolution: TicketResolution! } type UserMention implements EventDetail { eventType: EventType! ticket: Ticket! @access(scope: TICKETS, kind: RO) author: Entity! @access(scope: PROFILE, kind: RO) mentioned: Entity! @access(scope: PROFILE, kind: RO) } type TicketMention implements EventDetail { eventType: EventType! ticket: Ticket! @access(scope: TICKETS, kind: RO) author: Entity! @access(scope: PROFILE, kind: RO) mentioned: Ticket! @access(scope: TICKETS, kind: RO) } interface ActivitySubscription { id: Int! created: Time! } """ A tracker subscription will notify a participant of all activity for a tracker, including all new tickets and their events. """ type TrackerSubscription implements ActivitySubscription { id: Int! created: Time! tracker: Tracker! @access(scope: TRACKERS, kind: RO) } """ A ticket subscription will notify a participant when activity occurs on a ticket. """ type TicketSubscription implements ActivitySubscription { id: Int! created: Time! ticket: Ticket! @access(scope: TICKETS, kind: RO) } """ A cursor for enumerating trackers If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type TrackerCursor { results: [Tracker!]! cursor: Cursor } """ A cursor for enumerating tickets If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type TicketCursor { results: [Ticket!]! cursor: Cursor } """ A cursor for enumerating labels If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type LabelCursor { results: [Label!]! cursor: Cursor } """ A cursor for enumerating access control list entries If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type ACLCursor { results: [TrackerACL!]! cursor: Cursor } """ A cursor for enumerating events If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type EventCursor { results: [Event!]! cursor: Cursor } """ A cursor for enumerating subscriptions If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type ActivitySubscriptionCursor { results: [ActivitySubscription!]! cursor: Cursor } """ A cursor for enumerating a list of webhook deliveries If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type WebhookDeliveryCursor { results: [WebhookDelivery!]! cursor: Cursor } """ A cursor for enumerating a list of webhook subscriptions If there are additional results available, the cursor object may be passed back into the same endpoint to retrieve another page. If the cursor is null, there are no remaining results to return. """ type WebhookSubscriptionCursor { results: [WebhookSubscription!]! cursor: Cursor } type Query { "Returns API version information." version: Version! "Returns the authenticated user." me: User! @access(scope: PROFILE, kind: RO) "Returns a specific user." user(username: String!): User @access(scope: PROFILE, kind: RO) """ Returns trackers that the authenticated user has access to. NOTE: in this version of the API, only trackers owned by the authenticated user are returned, but in the future the default behavior will be to return all trackers that the user either (1) has been given explicit access to via ACLs or (2) has implicit access to either by ownership or group membership. """ trackers(cursor: Cursor): TrackerCursor @access(scope: TRACKERS, kind: RO) """ List of events which the authenticated user is subscribed to or implicated in, ordered by the event date (recent events first). """ events(cursor: Cursor): EventCursor @access(scope: EVENTS, kind: RO) "List of subscriptions of the authenticated user." subscriptions(cursor: Cursor): ActivitySubscriptionCursor @access(scope: SUBSCRIPTIONS, kind: RO) """ Returns a list of user webhook subscriptions. For clients authenticated with a personal access token, this returns all webhooks configured by all GraphQL clients for your account. For clients authenticated with an OAuth 2.0 access token, this returns only webhooks registered for your client. """ userWebhooks(cursor: Cursor): WebhookSubscriptionCursor! "Returns details of a user webhook subscription by its ID." userWebhook(id: Int!): WebhookSubscription """ Returns information about the webhook currently being processed. This is not valid during normal queries over HTTP, and will return an error if used outside of a webhook context. """ webhook: WebhookPayload! } "You may omit any fields to leave them unchanged." # TODO: Allow users to change the name of a tracker input TrackerInput { description: String visibility: Visibility } "You may omit any fields to leave them unchanged." input UpdateLabelInput { name: String foregroundColor: String backgroundColor: String } input ACLInput { "Permission to view tickets" browse: Boolean! "Permission to submit tickets" submit: Boolean! "Permission to comment on tickets" comment: Boolean! "Permission to edit tickets" edit: Boolean! "Permission to resolve, re-open, transfer, or label tickets" triage: Boolean! } """ This is used for importing tickets from third-party services, and may only be used by the tracker owner. It causes a ticket submission, update, or comment to be attributed to an external user and appear as if it were submitted at a specific time. """ input ImportInput { created: Time! """ External user ID. By convention this should be "service:username", e.g. "codeberg:ddevault". """ externalId: String! """ A URL at which the user's external profile may be found, e.g. "https://codeberg.org/ddevault". """ externalUrl: String! } input SubmitTicketInput { subject: String! body: String # These fields are meant for use when importing tickets from third-party # services, and may only be used by the tracker owner. # TODO: Use ImportInput here created: Time externalId: String externalUrl: String } # For internal use only. input SubmitTicketEmailInput { subject: String! body: String senderId: Int! messageId: String! } # For internal use only. enum EmailCmd { RESOLVE REOPEN LABEL UNLABEL } # For internal use only. input SubmitCommentEmailInput { text: String! senderId: Int! cmd: EmailCmd resolution: TicketResolution labelIds: [Int!] messageId: String! } """ You may omit any fields to leave them unchanged. To remove the ticket body, set it to null. """ input UpdateTicketInput { subject: String body: String "For use by the tracker owner only" import: ImportInput } """ You may omit the status or resolution fields to leave them unchanged (or if you do not have permission to change them). "resolution" is required if status is RESOLVED. """ input SubmitCommentInput { text: String! status: TicketStatus resolution: TicketResolution "For use by the tracker owner only" import: ImportInput } """ "resolution" is required if status is RESOLVED. """ input UpdateStatusInput { status: TicketStatus! resolution: TicketResolution "For use by the tracker owner only" import: ImportInput } input UserWebhookInput { url: String! events: [WebhookEvent!]! query: String! } input TrackerWebhookInput { url: String! events: [WebhookEvent!]! query: String! } input TicketWebhookInput { url: String! events: [WebhookEvent!]! query: String! } type Mutation { """ Creates a new bug tracker. If specified, the 'import' field specifies a gzipped dump of a tracker to populate tickets from; see Tracker.export. """ createTracker( name: String!, description: String, visibility: Visibility!, importUpload: Upload): Tracker! @access(scope: TRACKERS, kind: RW) "Updates an existing bug tracker" updateTracker( id: Int!, input: TrackerInput!): Tracker! @access(scope: TRACKERS, kind: RW) "Deletes a bug tracker" deleteTracker(id: Int!): Tracker! @access(scope: TRACKERS, kind: RW) "Adds or updates the ACL for a specific user on a bug tracker" updateUserACL( trackerId: Int!, userId: Int!, input: ACLInput!): TrackerACL! @access(scope: ACLS, kind: RW) # "Adds or updates the ACL for an email address on a bug tracker" # TODO: This requires internal changes #updateSenderACL( # trackerId: Int!, # address: String!, # input: ACLInput!): TrackerACL! @access(scope: ACLS, kind: RW) """ Updates the default ACL for a bug tracker, which applies to users and senders for whom a more specific ACL does not exist. """ updateTrackerACL( trackerId: Int!, input: ACLInput!): DefaultACL! @access(scope: ACLS, kind: RW) """ Removes a tracker ACL. Following this change, the default tracker ACL will apply to this user. """ deleteACL(id: Int!): TrackerACL! @access(scope: ACLS, kind: RW) "Subscribes to all email notifications for a tracker" trackerSubscribe( trackerId: Int!): TrackerSubscription! @access(scope: SUBSCRIPTIONS, kind: RW) """ Unsubscribes from email notifications for a tracker. If "tickets" is true, also unsubscribe from all tickets on this tracker. """ trackerUnsubscribe( trackerId: Int!, tickets: Boolean!): TrackerSubscription! @access(scope: SUBSCRIPTIONS, kind: RW) "Subscribes to all email notifications for a ticket" ticketSubscribe( trackerId: Int!, ticketId: Int!): TicketSubscription! @access(scope: SUBSCRIPTIONS, kind: RW) "Unsubscribes from email notifications for a ticket" ticketUnsubscribe( trackerId: Int!, ticketId: Int!): TicketSubscription! @access(scope: SUBSCRIPTIONS, kind: RW) """ Creates a new ticket label for a tracker. The colors must be in CSS hexadecimal RGB format "#RRGGBB", i.e. "#000000" for black and "#FF0000" for red. """ createLabel(trackerId: Int!, name: String!, foregroundColor: String!, backgroundColor: String!): Label! @access(scope: TRACKERS, kind: RW) "Changes the name or colors for a label." updateLabel(id: Int!, input: UpdateLabelInput!): Label! @access(scope: TRACKERS, kind: RW) """ Deletes a label, removing it from any tickets which currently have it applied. """ deleteLabel(id: Int!): Label! @access(scope: TRACKERS, kind: RW) "Creates a new ticket." submitTicket(trackerId: Int!, input: SubmitTicketInput!): Ticket! @access(scope: TICKETS, kind: RW) "Deletes a ticket." deleteTicket(trackerId: Int!, ticketId: Int!): Ticket! @access(scope: TICKETS, kind: RW) # Creates a new ticket from an incoming email. (For internal use only) submitTicketEmail(trackerId: Int!, input: SubmitTicketEmailInput!): Ticket! @internal # Creates a new comment from an incoming email. (For internal use only) submitCommentEmail(trackerId: Int!, ticketId: Int!, input: SubmitCommentEmailInput!): Event! @internal "Updates a ticket's subject or body" updateTicket(trackerId: Int!, ticketId: Int!, input: UpdateTicketInput!): Ticket! @access(scope: TICKETS, kind: RW) "Updates the status or resolution of a ticket" updateTicketStatus(trackerId: Int!, ticketId: Int!, input: UpdateStatusInput!): Event! @access(scope: TICKETS, kind: RW) "Submits a comment for a ticket" submitComment(trackerId: Int!, ticketId: Int!, input: SubmitCommentInput!): Event! @access(scope: TICKETS, kind: RW) "Adds a user to the list of assigned users for a ticket" assignUser(trackerId: Int!, ticketId: Int!, userId: Int!): Event! @access(scope: TICKETS, kind: RW) "Removes a user from the list of assigned users for a ticket" unassignUser(trackerId: Int!, ticketId: Int!, userId: Int!): Event! @access(scope: TICKETS, kind: RW) "Adds a label to the list of labels for a ticket" labelTicket(trackerId: Int!, ticketId: Int!, labelId: Int!): Event! @access(scope: TICKETS, kind: RW) "Removes a list from the list of labels for a ticket" unlabelTicket(trackerId: Int!, ticketId: Int!, labelId: Int!): Event! @access(scope: TICKETS, kind: RW) "Imports a gzipped JSON dump of tracker data" importTrackerDump(trackerId: Int!, dump: Upload!): Boolean! @access(scope: TRACKERS, kind: RW) """ Creates a new user webhook subscription. When an event from the provided list of events occurs, the 'query' parameter (a GraphQL query) will be evaluated and the results will be sent to the provided URL as the body of an HTTP POST request. The list of events must include at least one event, and no duplicates. This query is evaluated in the webhook context, such that query { webhook } may be used to access details of the event which trigged the webhook. The query may not make any mutations. """ createUserWebhook(config: UserWebhookInput!): WebhookSubscription! """ Deletes a user webhook. Any events already queued may still be delivered after this request completes. Clients authenticated with a personal access token may delete any webhook registered for their account, but authorized OAuth 2.0 clients may only delete their own webhooks. Manually deleting a webhook configured by a third-party client may cause unexpected behavior with the third-party integration. """ deleteUserWebhook(id: Int!): WebhookSubscription! "Creates a new tracker webhook." createTrackerWebhook(trackerId: Int!, config: TrackerWebhookInput!): WebhookSubscription! "Deletes a tracker webhook." deleteTrackerWebhook(id: Int!): WebhookSubscription! "Creates a new ticket webhook." createTicketWebhook(trackerId: Int!, ticketId: Int!, config: TicketWebhookInput!): WebhookSubscription! "Deletes a ticket webhook." deleteTicketWebhook(id: Int!): WebhookSubscription! """ Deletes the authenticated user's account. Internal use only. """ deleteUser: Int! @internal } hut-0.6.0/srht/todosrht/strings.go000066400000000000000000000114671463710650600172040ustar00rootroot00000000000000package todosrht import ( "fmt" "strings" "git.sr.ht/~xenrox/hut/termfmt" ) func (visibility Visibility) TermString() string { var style termfmt.Style switch visibility { case VisibilityPublic: case VisibilityUnlisted: style = termfmt.Blue case VisibilityPrivate: style = termfmt.Red default: panic(fmt.Sprintf("unknown visibility: %q", visibility)) } return style.String(strings.ToLower(string(visibility))) } func (status TicketStatus) TermString() string { var style termfmt.Style var s string switch status { case TicketStatusReported, TicketStatusConfirmed, TicketStatusInProgress, TicketStatusPending: s = "open" style = termfmt.Red case TicketStatusResolved: s = "closed" style = termfmt.Green default: panic(fmt.Sprintf("unknown status: %q", status)) } return style.String(s) } func (label Label) TermString() string { return termfmt.HexString(fmt.Sprintf(" %s ", label.Name), label.ForegroundColor, label.BackgroundColor) } func ParseVisibility(s string) (Visibility, error) { switch strings.ToLower(s) { case "unlisted": return VisibilityUnlisted, nil case "private": return VisibilityPrivate, nil case "public": return VisibilityPublic, nil default: return "", fmt.Errorf("invalid visibility: %s", s) } } func ParseTicketStatus(s string) (TicketStatus, error) { switch strings.ToLower(s) { case "reported": return TicketStatusReported, nil case "confirmed": return TicketStatusConfirmed, nil case "in_progress": return TicketStatusInProgress, nil case "pending": return TicketStatusPending, nil case "resolved": return TicketStatusResolved, nil default: return "", fmt.Errorf("invalid ticket status: %s", s) } } func ParseTicketResolution(s string) (TicketResolution, error) { switch strings.ToLower(s) { case "unresolved": return TicketResolutionUnresolved, nil case "fixed": return TicketResolutionFixed, nil case "closed": return TicketResolutionClosed, nil case "implemented": return TicketResolutionImplemented, nil case "wont_fix": return TicketResolutionWontFix, nil case "by_design": return TicketResolutionByDesign, nil case "invalid": return TicketResolutionInvalid, nil case "duplicate": return TicketResolutionDuplicate, nil case "not_out_bug": return TicketResolutionNotOurBug, nil default: return "", fmt.Errorf("invalid ticket resolution: %s", s) } } func (acl DefaultACL) TermString() string { return fmt.Sprintf("%s browse %s submit %s comment %s edit %s triage", PermissionIcon(acl.Browse), PermissionIcon(acl.Submit), PermissionIcon(acl.Comment), PermissionIcon(acl.Edit), PermissionIcon(acl.Triage)) } func PermissionIcon(permission bool) string { if permission { return termfmt.Green.Sprint("✔") } return termfmt.Red.Sprint("✗") } func ParseTicketWebhookEvents(events []string) ([]WebhookEvent, error) { var whEvents []WebhookEvent for _, event := range events { switch strings.ToLower(event) { case "ticket_update": whEvents = append(whEvents, WebhookEventTicketUpdate) case "ticket_deleted": whEvents = append(whEvents, WebhookEventTicketDeleted) case "event_created": whEvents = append(whEvents, WebhookEventEventCreated) default: return whEvents, fmt.Errorf("invalid event: %q", event) } } return whEvents, nil } func ParseUserEvents(events []string) ([]WebhookEvent, error) { var whEvents []WebhookEvent for _, event := range events { switch strings.ToLower(event) { case "tracker_created": whEvents = append(whEvents, WebhookEventTrackerCreated) case "tracker_update": whEvents = append(whEvents, WebhookEventTrackerUpdate) case "tracker_deleted": whEvents = append(whEvents, WebhookEventTrackerDeleted) case "ticket_created": whEvents = append(whEvents, WebhookEventTicketCreated) default: return whEvents, fmt.Errorf("invalid event: %q", event) } } return whEvents, nil } func ParseTrackerWebhookEvents(events []string) ([]WebhookEvent, error) { var whEvents []WebhookEvent for _, event := range events { switch strings.ToLower(event) { case "tracker_update": whEvents = append(whEvents, WebhookEventTrackerUpdate) case "tracker_deleted": whEvents = append(whEvents, WebhookEventTrackerDeleted) case "label_created": whEvents = append(whEvents, WebhookEventLabelCreated) case "label_update": whEvents = append(whEvents, WebhookEventLabelUpdate) case "label_deleted": whEvents = append(whEvents, WebhookEventLabelDeleted) case "ticket_created": whEvents = append(whEvents, WebhookEventTicketCreated) case "ticket_update": whEvents = append(whEvents, WebhookEventTicketUpdate) case "ticket_deleted": whEvents = append(whEvents, WebhookEventTicketDeleted) case "event_created": whEvents = append(whEvents, WebhookEventEventCreated) default: return whEvents, fmt.Errorf("invalid event: %q", event) } } return whEvents, nil } hut-0.6.0/termfmt/000077500000000000000000000000001463710650600140035ustar00rootroot00000000000000hut-0.6.0/termfmt/formatting.go000066400000000000000000000043401463710650600165050ustar00rootroot00000000000000package termfmt import ( "fmt" "log" "os" "strconv" "strings" "sync" "golang.org/x/term" ) var initIsTerminal sync.Once var isTerminal bool type Style string type RGB struct { Red, Green, Blue uint8 } const ( Bold Style = "bold" Dim Style = "dim" Red Style = "red" Green Style = "green" Yellow Style = "yellow" Blue Style = "blue" DarkYellow Style = "dark-yellow" ) func (style Style) String(s string) string { if !IsTerminal() { return s } switch style { case Bold: return fmt.Sprintf("\033[01m%s\033[0m", s) case Dim: return fmt.Sprintf("\033[02m%s\033[0m", s) case Red: return fmt.Sprintf("\033[91m%s\033[0m", s) case Green: return fmt.Sprintf("\033[92m%s\033[0m", s) case Yellow: return fmt.Sprintf("\033[93m%s\033[0m", s) case Blue: return fmt.Sprintf("\033[94m%s\033[0m", s) case DarkYellow: return fmt.Sprintf("\033[33m%s\033[0m", s) default: return s } } func HexString(s string, fg string, bg string) string { if !IsTerminal() { return s } return RGBString(s, HexToRGB(fg), HexToRGB(bg)) } func RGBString(s string, fg, bg RGB) string { if !IsTerminal() { return s } return fmt.Sprintf("\033[38;2;%d;%d;%dm\033[48;2;%d;%d;%dm%s\033[0m", fg.Red, fg.Green, fg.Blue, bg.Red, bg.Green, bg.Blue, s) } func (style Style) Sprint(args ...interface{}) string { return style.String(fmt.Sprint(args...)) } func (style Style) Sprintf(format string, args ...interface{}) string { return style.String(fmt.Sprintf(format, args...)) } func HexToRGB(hex string) RGB { var rgb RGB hex = strings.TrimPrefix(hex, "#") if len(hex) != 6 { log.Fatalf("not a valid hex color %q", hex) } for i := 0; i < 3; i++ { v, err := strconv.ParseUint(hex[i*2:i*2+2], 16, 8) if err != nil { log.Fatal(err) } switch i { case 0: rgb.Red = uint8(v) case 1: rgb.Green = uint8(v) case 2: rgb.Blue = uint8(v) } } return rgb } func ReplaceLine() string { if !IsTerminal() { return "\n" } return "\x1b[1K\r" } func InitIsTerminal(b bool) { initIsTerminal.Do(func() { isTerminal = b }) } func IsTerminal() bool { initIsTerminal.Do(func() { isTerminal = term.IsTerminal(int(os.Stdout.Fd())) }) return isTerminal } func Bell() { if IsTerminal() { fmt.Print("\a") } } hut-0.6.0/todo.go000066400000000000000000001772211463710650600136330ustar00rootroot00000000000000package main import ( "bufio" "context" "errors" "fmt" "io" "log" "math" "os" "strings" "git.sr.ht/~xenrox/hut/srht/todosrht" "git.sr.ht/~xenrox/hut/termfmt" "github.com/dustin/go-humanize" "github.com/spf13/cobra" ) func newTodoCommand() *cobra.Command { cmd := &cobra.Command{ Use: "todo", Short: "Use the todo API", } cmd.AddCommand(newTodoListCommand()) cmd.AddCommand(newTodoDeleteCommand()) cmd.AddCommand(newTodoUpdateCommand()) cmd.AddCommand(newTodoSubscribeCommand()) cmd.AddCommand(newTodoUnsubscribeCommand()) cmd.AddCommand(newTodoCreateCommand()) cmd.AddCommand(newTodoTicketCommand()) cmd.AddCommand(newTodoLabelCommand()) cmd.AddCommand(newTodoACLCommand()) cmd.AddCommand(newTodoWebhookCommand()) cmd.AddCommand(newTodoUserWebhookCommand()) cmd.PersistentFlags().StringP("tracker", "t", "", "name of tracker") cmd.RegisterFlagCompletionFunc("tracker", completeTracker) return cmd } func newTodoListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("todo", cmd) var cursor *todosrht.Cursor var username string if len(args) > 0 { username = strings.TrimLeft(args[0], ownerPrefixes) } err := pagerify(func(p pager) error { var trackers *todosrht.TrackerCursor if len(username) > 0 { user, err := todosrht.TrackersByUser(c.Client, ctx, username, cursor) if err != nil { return err } else if user == nil { return errors.New("no such user") } trackers = user.Trackers } else { var err error trackers, err = todosrht.Trackers(c.Client, ctx, cursor) if err != nil { return err } } for _, tracker := range trackers.Results { printTracker(p, &tracker) } cursor = trackers.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list [user]", Short: "List trackers", Args: cobra.MaximumNArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } return cmd } func printTracker(w io.Writer, tracker *todosrht.Tracker) { fmt.Fprintf(w, "%s (%s)\n", termfmt.Bold.String(tracker.Name), tracker.Visibility.TermString()) if tracker.Description != nil && *tracker.Description != "" { fmt.Fprintln(w, indent(strings.TrimSpace(*tracker.Description), " ")) } fmt.Fprintln(w) } func newTodoDeleteCommand() *cobra.Command { var autoConfirm bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseResourceName(args[0]) } else { var err error name, owner, instance, err = getTrackerName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("todo", cmd, instance) id := getTrackerID(c, ctx, name, owner) if !autoConfirm && !getConfirmation(fmt.Sprintf("Do you really want to delete the tracker %s", name)) { log.Println("Aborted") return } tracker, err := todosrht.DeleteTracker(c.Client, ctx, id) if err != nil { log.Fatal(err) } else if tracker == nil { log.Fatalf("failed to delete tracker %q", name) } log.Printf("Deleted tracker %s\n", tracker.Name) } cmd := &cobra.Command{ Use: "delete [tracker]", Short: "Delete a tracker", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeTracker, Run: run, } cmd.Flags().BoolVarP(&autoConfirm, "yes", "y", false, "auto confirm") return cmd } func newTodoUpdateCommand() *cobra.Command { var visibility string var description bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseResourceName(args[0]) } else { var err error name, owner, instance, err = getTrackerName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("todo", cmd, instance) id := getTrackerID(c, ctx, name, owner) var input todosrht.TrackerInput if visibility != "" { trackerVisibility, err := todosrht.ParseVisibility(visibility) if err != nil { log.Fatal(err) } input.Visibility = &trackerVisibility } if description { if !isStdinTerminal { b, err := io.ReadAll(os.Stdin) if err != nil { log.Fatalf("failed to read description: %v", err) } description := string(b) input.Description = &description } else { var ( err error user *todosrht.User username string ) if owner != "" { username = strings.TrimLeft(owner, ownerPrefixes) user, err = todosrht.TrackerDescriptionByUser(c.Client, ctx, username, name) } else { user, err = todosrht.TrackerDescription(c.Client, ctx, name) } if err != nil { log.Fatalf("failed to fetch description: %v", err) } else if user == nil { log.Fatalf("no such user %q", username) } else if user.Tracker == nil { log.Fatalf("no such tracker %q", name) } var prefill string if user.Tracker.Description != nil { prefill = *user.Tracker.Description } text, err := getInputWithEditor("hut_description*.md", prefill) if err != nil { log.Fatalf("failed to read description: %v", err) } if strings.TrimSpace(text) == "" { _, err := todosrht.ClearDescription(c.Client, ctx, id) if err != nil { log.Fatalf("failed to clear description: %v", err) } } else { input.Description = &text } } } tracker, err := todosrht.UpdateTracker(c.Client, ctx, id, input) if err != nil { log.Fatal(err) } log.Printf("Updated tracker %q\n", tracker.Name) } cmd := &cobra.Command{ Use: "update [tracker]", Short: "Update a tracker", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeTracker, Run: run, } cmd.Flags().StringVarP(&visibility, "visibility", "v", "", "tracker visibility") cmd.RegisterFlagCompletionFunc("visibility", completeVisibility) cmd.Flags().BoolVar(&description, "description", false, "edit description") return cmd } func newTodoSubscribeCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseResourceName(args[0]) } else { var err error name, owner, instance, err = getTrackerName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("todo", cmd, instance) id := getTrackerID(c, ctx, name, owner) subscription, err := todosrht.TrackerSubscribe(c.Client, ctx, id) if err != nil { log.Fatal(err) } else if subscription == nil { log.Fatalf("failed to subscribe to tracker %q", name) } log.Printf("Subscribed to %s/%s/%s\n", c.BaseURL, subscription.Tracker.Owner.CanonicalName, subscription.Tracker.Name) } cmd := &cobra.Command{ Use: "subscribe [tracker]", Short: "Subscribe to a tracker", Args: cobra.MaximumNArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } return cmd } func newTodoUnsubscribeCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseResourceName(args[0]) } else { var err error name, owner, instance, err = getTrackerName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("todo", cmd, instance) id := getTrackerID(c, ctx, name, owner) subscription, err := todosrht.TrackerUnsubscribe(c.Client, ctx, id) if err != nil { log.Fatal(err) } else if subscription == nil { log.Fatalf("you were not subscribed to %s/%s/%s", c.BaseURL, owner, name) } log.Printf("Unsubscribed from %s/%s/%s\n", c.BaseURL, subscription.Tracker.Owner.CanonicalName, subscription.Tracker.Name) } cmd := &cobra.Command{ Use: "unsubscribe [tracker]", Short: "Unubscribe from a tracker", Args: cobra.MaximumNArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } return cmd } const todoCreatePrefill = ` ` func newTodoCreateCommand() *cobra.Command { var visibility string var stdin bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("todo", cmd) todoVisibility, err := todosrht.ParseVisibility(visibility) if err != nil { log.Fatal(err) } var description *string if stdin { b, err := io.ReadAll(os.Stdin) if err != nil { log.Fatalf("failed to read tracker description: %v", err) } desc := string(b) description = &desc } else { text, err := getInputWithEditor("hut_tracker*.md", todoCreatePrefill) if err != nil { log.Fatalf("failed to read description: %v", err) } text = dropComment(text, todoCreatePrefill) description = &text } tracker, err := todosrht.CreateTracker(c.Client, ctx, args[0], description, todoVisibility) if err != nil { log.Fatal(err) } else if tracker == nil { log.Fatal("failed to create tracker") } log.Printf("Created tracker %q\n", tracker.Name) } cmd := &cobra.Command{ Use: "create ", Short: "Create a tracker", Args: cobra.ExactArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().StringVarP(&visibility, "visibility", "v", "public", "tracker visibility") cmd.RegisterFlagCompletionFunc("visibility", completeVisibility) cmd.Flags().BoolVar(&stdin, "stdin", !isStdinTerminal, "read tracker from stdin") return cmd } func newTodoTicketCommand() *cobra.Command { cmd := &cobra.Command{ Use: "ticket", Short: "Manage tickets", } cmd.AddCommand(newTodoTicketListCommand()) cmd.AddCommand(newTodoTicketCommentCommand()) cmd.AddCommand(newTodoTicketStatusCommand()) cmd.AddCommand(newTodoTicketSubscribeCommand()) cmd.AddCommand(newTodoTicketUnsubscribeCommand()) cmd.AddCommand(newTodoTicketAssignCommand()) cmd.AddCommand(newTodoTicketUnassignCommand()) cmd.AddCommand(newTodoTicketDeleteCommand()) cmd.AddCommand(newTodoTicketShowCommand()) cmd.AddCommand(newTodoTicketWebhookCommand()) cmd.AddCommand(newTodoTicketCreateCommand()) cmd.AddCommand(newTodoTicketEditCommand()) cmd.AddCommand(newTodoTicketLabelCommand()) cmd.AddCommand(newTodoTicketUnlabelCommand()) return cmd } func newTodoTicketListCommand() *cobra.Command { var status string run := func(cmd *cobra.Command, args []string) { if status != "" { _, err := todosrht.ParseTicketStatus(status) if err != nil { log.Fatal(err) } } ctx := cmd.Context() name, owner, instance, err := getTrackerName(ctx, cmd) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) var ( cursor *todosrht.Cursor user *todosrht.User username string ) if owner != "" { username = strings.TrimLeft(owner, ownerPrefixes) } err = pagerify(func(p pager) error { if username != "" { user, err = todosrht.TicketsByUser(c.Client, ctx, username, name, cursor) } else { user, err = todosrht.Tickets(c.Client, ctx, name, cursor) } if err != nil { return err } else if user == nil { return fmt.Errorf("no such user %q", username) } else if user.Tracker == nil { return fmt.Errorf("no such tracker %q", name) } for _, ticket := range user.Tracker.Tickets.Results { // TODO: filter with API if status != "" && !strings.EqualFold(status, string(ticket.Status)) { continue } printTicket(p, &ticket) } cursor = user.Tracker.Tickets.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list", Short: "List tickets", Args: cobra.ExactArgs(0), Run: run, } cmd.Flags().StringVarP(&status, "status", "s", "", "ticket status") cmd.RegisterFlagCompletionFunc("status", completeTicketStatus) return cmd } func printTicket(w io.Writer, ticket *todosrht.Ticket) { var labels string s := termfmt.DarkYellow.Sprintf("#%d %s ", ticket.Id, ticket.Status.TermString()) if ticket.Status == todosrht.TicketStatusResolved && ticket.Resolution != todosrht.TicketResolutionClosed { s += termfmt.Green.Sprintf("%s ", strings.ToLower(string(ticket.Resolution))) } if len(ticket.Labels) > 0 { labels = " " for i, label := range ticket.Labels { labels += label.TermString() if i != len(ticket.Labels)-1 { labels += " " } } } s += fmt.Sprintf("%s%s (%s %s)", ticket.Subject, labels, ticket.Submitter.CanonicalName, humanize.Time(ticket.Created.Time)) fmt.Fprintln(w, s) } func newTodoTicketCommentCommand() *cobra.Command { var stdin bool var status, resolution string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() ticketID, name, owner, instance, err := parseTicketResource(ctx, cmd, args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) trackerID := getTrackerID(c, ctx, name, owner) var input todosrht.SubmitCommentInput if resolution != "" { ticketResolution, err := todosrht.ParseTicketResolution(resolution) if err != nil { log.Fatal(err) } input.Resolution = &ticketResolution if status == "" { status = "resolved" } } if status != "" { ticketStatus, err := todosrht.ParseTicketStatus(status) if err != nil { log.Fatal(err) } input.Status = &ticketStatus } if input.Status != nil { if *input.Status != todosrht.TicketStatusResolved && input.Resolution != nil { log.Fatalf("resolution %q specified, but ticket not marked as resolved", resolution) } if *input.Status == todosrht.TicketStatusResolved && input.Resolution == nil { log.Fatalf("resolution is required when status is RESOLVED") } } if stdin { fmt.Printf("Comment %s:\n", termfmt.Dim.String("(Markdown supported)")) text, err := io.ReadAll(os.Stdin) if err != nil { log.Fatalf("failed to read comment: %v", err) } input.Text = string(text) } else { text, err := getInputWithEditor("hut_comment*.md", "") if err != nil { log.Fatalf("failed to read comment: %v", err) } input.Text = text } if strings.TrimSpace(input.Text) == "" { log.Println("Aborted writing empty comment") return } event, err := todosrht.SubmitComment(c.Client, ctx, trackerID, ticketID, input) if err != nil { log.Fatal(err) } else if event == nil { log.Fatalf("failed to comment on ticket with ID %d", ticketID) } log.Printf("Commented on %s\n", event.Ticket.Subject) } cmd := &cobra.Command{ Use: "comment ", Short: "Comment on a ticket", Args: cobra.ExactArgs(1), ValidArgsFunction: completeTicketID, Run: run, } cmd.Flags().BoolVar(&stdin, "stdin", !isStdinTerminal, "read comment from stdin") cmd.Flags().StringVarP(&status, "status", "s", "", "ticket status") cmd.RegisterFlagCompletionFunc("status", completeTicketStatus) cmd.Flags().StringVarP(&resolution, "resolution", "r", "", "ticket resolution") cmd.RegisterFlagCompletionFunc("resolution", completeTicketResolution) return cmd } func newTodoTicketStatusCommand() *cobra.Command { var status, resolution string run := func(cmd *cobra.Command, args []string) { var input todosrht.UpdateStatusInput ctx := cmd.Context() if resolution != "" { ticketResolution, err := todosrht.ParseTicketResolution(resolution) if err != nil { log.Fatal(err) } input.Resolution = &ticketResolution if status == "" { status = "resolved" } } ticketStatus, err := todosrht.ParseTicketStatus(status) if err != nil { log.Fatal(err) } input.Status = ticketStatus if ticketStatus != todosrht.TicketStatusResolved && input.Resolution != nil { log.Fatalf("resolution %q specified, but ticket not marked as resolved", resolution) } if ticketStatus == todosrht.TicketStatusResolved && input.Resolution == nil { res := todosrht.TicketResolutionClosed input.Resolution = &res } ticketID, name, owner, instance, err := parseTicketResource(ctx, cmd, args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) trackerID := getTrackerID(c, ctx, name, owner) event, err := todosrht.UpdateTicketStatus(c.Client, ctx, trackerID, ticketID, input) if err != nil { log.Fatal(err) } log.Printf("Updated status of %s\n", event.Ticket.Subject) } cmd := &cobra.Command{ Use: "update-status ", Short: "Update ticket status", Args: cobra.ExactArgs(1), ValidArgsFunction: completeTicketID, Run: run, } cmd.Flags().StringVarP(&status, "status", "s", "", "ticket status") cmd.RegisterFlagCompletionFunc("status", completeTicketStatus) cmd.Flags().StringVarP(&resolution, "resolution", "r", "", "ticket resolution") cmd.RegisterFlagCompletionFunc("resolution", completeTicketResolution) return cmd } func newTodoTicketSubscribeCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() ticketID, name, owner, instance, err := parseTicketResource(ctx, cmd, args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) trackerID := getTrackerID(c, ctx, name, owner) subscription, err := todosrht.TicketSubscribe(c.Client, ctx, trackerID, ticketID) if err != nil { log.Fatal(err) } else if subscription == nil { log.Fatalf("failed to subscribe to ticket %d", ticketID) } log.Printf("Subscribed to %s/%s/%s/%d\n", c.BaseURL, owner, name, subscription.Ticket.Id) } cmd := &cobra.Command{ Use: "subscribe ", Short: "Subscribe to a ticket", Args: cobra.ExactArgs(1), ValidArgsFunction: completeTicketID, Run: run, } return cmd } func newTodoTicketUnsubscribeCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() ticketID, name, owner, instance, err := parseTicketResource(ctx, cmd, args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) trackerID := getTrackerID(c, ctx, name, owner) subscription, err := todosrht.TicketUnsubscribe(c.Client, ctx, trackerID, ticketID) if err != nil { log.Fatal(err) } else if subscription == nil { log.Fatalf("you were not subscribed to ticket with ID %d", ticketID) } log.Printf("Unsubscribed from %s/%s/%s/%d\n", c.BaseURL, owner, name, subscription.Ticket.Id) } cmd := &cobra.Command{ Use: "unsubscribe ", Short: "Unsubscribe from a ticket", Args: cobra.ExactArgs(1), ValidArgsFunction: completeTicketID, Run: run, } return cmd } func newTodoTicketAssignCommand() *cobra.Command { var userName string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() ticketID, name, owner, instance, err := parseTicketResource(ctx, cmd, args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) trackerID := getTrackerID(c, ctx, name, owner) user, err := todosrht.UserIDByName(c.Client, ctx, userName) if err != nil { log.Fatal(err) } else if user == nil { log.Fatalf("no such user %q", userName) } event, err := todosrht.AssignUser(c.Client, ctx, trackerID, ticketID, user.Id) if err != nil { log.Fatal(err) } else if event == nil { log.Fatal("failed to assign user") } log.Printf("Assigned %q to %q\n", userName, event.Ticket.Subject) } cmd := &cobra.Command{ Use: "assign ", Short: "Assign a user to a ticket", Args: cobra.ExactArgs(1), ValidArgsFunction: completeTicketID, Run: run, } cmd.Flags().StringVarP(&userName, "user", "u", "", "username") cmd.MarkFlagRequired("user") cmd.RegisterFlagCompletionFunc("user", completeTicketAssign) return cmd } func newTodoTicketUnassignCommand() *cobra.Command { var userName string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() ticketID, name, owner, instance, err := parseTicketResource(ctx, cmd, args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) trackerID := getTrackerID(c, ctx, name, owner) user, err := todosrht.UserIDByName(c.Client, ctx, userName) if err != nil { log.Fatal(err) } else if user == nil { log.Fatalf("no such user %q", userName) } event, err := todosrht.UnassignUser(c.Client, ctx, trackerID, ticketID, user.Id) if err != nil { log.Fatal(err) } else if event == nil { log.Fatal("failed to unassign user") } log.Printf("Unassigned %q from %q\n", userName, event.Ticket.Subject) } cmd := &cobra.Command{ Use: "unassign ", Short: "Unassign a user from a ticket", Args: cobra.ExactArgs(1), ValidArgsFunction: completeTicketID, Run: run, } cmd.Flags().StringVarP(&userName, "user", "u", "", "username") cmd.MarkFlagRequired("user") cmd.RegisterFlagCompletionFunc("user", completeTicketUnassign) return cmd } func newTodoTicketDeleteCommand() *cobra.Command { var autoConfirm bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() ticketID, name, owner, instance, err := parseTicketResource(ctx, cmd, args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) trackerID := getTrackerID(c, ctx, name, owner) if !autoConfirm && !getConfirmation(fmt.Sprintf("Do you really want to delete the ticket with ID %d", ticketID)) { log.Println("Aborted") return } ticket, err := todosrht.DeleteTicket(c.Client, ctx, trackerID, ticketID) if err != nil { log.Fatal(err) } else if ticket == nil { log.Fatalf("failed to delete ticket %d", ticketID) } log.Printf("Deleted ticket %q\n", ticket.Subject) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete a ticket", Args: cobra.ExactArgs(1), ValidArgsFunction: completeTicketID, Run: run, } cmd.Flags().BoolVarP(&autoConfirm, "yes", "y", false, "auto confirm") return cmd } func newTodoTicketShowCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() ticketID, name, owner, instance, err := parseTicketResource(ctx, cmd, args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) var ( user *todosrht.User username string ) if owner != "" { username = strings.TrimLeft(owner, ownerPrefixes) user, err = todosrht.TicketByUser(c.Client, ctx, username, name, ticketID) } else { user, err = todosrht.TicketByName(c.Client, ctx, name, ticketID) } if err != nil { log.Fatal(err) } else if user == nil { log.Fatalf("no such user %q", username) } else if user.Tracker == nil { log.Fatalf("no such tracker %q", name) } ticket := user.Tracker.Ticket fmt.Printf("%s\n", termfmt.Bold.String(ticket.Subject)) fmt.Printf("%s/%s/%s/%d\n\n", c.BaseURL, owner, name, ticketID) fmt.Printf("Status: %s\n", termfmt.Green.Sprintf("%s %s", ticket.Status, ticket.Resolution)) fmt.Printf("Submitter: %s\n", ticket.Submitter.CanonicalName) assigned := "Assigned to: " if len(ticket.Assignees) == 0 { assigned += "No-one" } else { for i, assignee := range ticket.Assignees { assigned += assignee.CanonicalName if i != len(ticket.Assignees)-1 { assigned += ", " } } } fmt.Println(assigned) fmt.Printf("Submitted: %s\n", humanize.Time(ticket.Created.Time)) fmt.Printf("Updated: %s\n", humanize.Time(ticket.Updated.Time)) labels := "Labels: " if len(ticket.Labels) == 0 { labels += "No labels applied." } else { for i, label := range ticket.Labels { labels += label.TermString() if i != len(ticket.Labels)-1 { labels += " " } } } fmt.Println(labels) fmt.Println() if ticket.Body != nil { fmt.Println(*ticket.Body) } for i := len(ticket.Events.Results) - 1; i >= 0; i-- { event := ticket.Events.Results[i] for _, change := range event.Changes { comment, ok := change.Value.(*todosrht.Comment) if !ok { continue } author := termfmt.Bold.String(comment.Author.CanonicalName) created := termfmt.Dim.String("(" + humanize.Time(event.Created.Time) + ")") fmt.Println() fmt.Printf("%v %v\n", author, created) fmt.Print(indent(comment.Text, " ")) fmt.Println() } } } cmd := &cobra.Command{ Use: "show ", Short: "Show a ticket", Args: cobra.ExactArgs(1), ValidArgsFunction: completeTicketID, Run: run, } return cmd } func newTodoTicketWebhookCommand() *cobra.Command { cmd := &cobra.Command{ Use: "webhook", Short: "Manage ticket webhooks", } cmd.AddCommand(newTodoTicketWebhookCreateCommand()) cmd.AddCommand(newTodoTicketWebhookListCommand()) cmd.AddCommand(newTodoTicketWebhookDeleteCommand()) return cmd } func newTodoTicketWebhookCreateCommand() *cobra.Command { var events []string var stdin bool var url string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() ticketID, name, owner, instance, err := parseTicketResource(ctx, cmd, args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) trackerID := getTrackerID(c, ctx, name, owner) var config todosrht.TicketWebhookInput config.Url = url whEvents, err := todosrht.ParseTicketWebhookEvents(events) if err != nil { log.Fatal(err) } config.Events = whEvents config.Query = readWebhookQuery(stdin) webhook, err := todosrht.CreateTicketWebhook(c.Client, ctx, trackerID, ticketID, config) if err != nil { log.Fatal(err) } log.Printf("Created ticket webhook with ID %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "create ", Short: "Create a ticket webhook", Args: cobra.ExactArgs(1), ValidArgsFunction: completeTicketID, Run: run, } cmd.Flags().StringSliceVarP(&events, "events", "e", nil, "webhook events") cmd.RegisterFlagCompletionFunc("events", completeTicketWebhookEvents) cmd.MarkFlagRequired("events") cmd.Flags().BoolVar(&stdin, "stdin", !isStdinTerminal, "read webhook query from stdin") cmd.Flags().StringVarP(&url, "url", "u", "", "payload url") cmd.RegisterFlagCompletionFunc("url", cobra.NoFileCompletions) cmd.MarkFlagRequired("url") return cmd } func newTodoTicketWebhookListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() ticketID, name, owner, instance, err := parseTicketResource(ctx, cmd, args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) var ( cursor *todosrht.Cursor user *todosrht.User username string ) if owner != "" { username = strings.TrimLeft(owner, ownerPrefixes) } err = pagerify(func(p pager) error { if username != "" { user, err = todosrht.TicketWebhooksByUser(c.Client, ctx, username, name, ticketID, cursor) } else { user, err = todosrht.TicketWebhooks(c.Client, ctx, name, ticketID, cursor) } if err != nil { return err } else if user == nil { return fmt.Errorf("no such user %q", username) } else if user.Tracker == nil { return fmt.Errorf("no such tracker %q", name) } for _, webhook := range user.Tracker.Ticket.Webhooks.Results { fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url) } cursor = user.Tracker.Ticket.Webhooks.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list ", Short: "List ticket webhooks", Args: cobra.ExactArgs(1), ValidArgsFunction: completeTicketID, Run: run, } return cmd } func newTodoTicketWebhookDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("todo", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } webhook, err := todosrht.DeleteTicketWebhook(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Deleted webhook %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete a ticket webhook", Args: cobra.ExactArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } return cmd } func newTodoLabelCommand() *cobra.Command { cmd := &cobra.Command{ Use: "label", Short: "Manage labels", } cmd.AddCommand(newTodoLabelListCommand()) cmd.AddCommand(newTodoLabelDeleteCommand()) cmd.AddCommand(newTodoLabelCreateCommand()) cmd.AddCommand(newTodoLabelUpdateCommand()) return cmd } func newTodoLabelListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() name, owner, instance, err := getTrackerName(ctx, cmd) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) var ( cursor *todosrht.Cursor user *todosrht.User username string ) if owner != "" { username = strings.TrimLeft(owner, ownerPrefixes) } err = pagerify(func(p pager) error { if username != "" { user, err = todosrht.LabelsByUser(c.Client, ctx, username, name, cursor) } else { user, err = todosrht.Labels(c.Client, ctx, name, cursor) } if err != nil { return err } else if user == nil { return fmt.Errorf("no such user %q", username) } else if user.Tracker == nil { return fmt.Errorf("no such tracker %q", name) } for _, label := range user.Tracker.Labels.Results { fmt.Fprintln(p, label.TermString()) } cursor = user.Tracker.Labels.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list", Short: "List labels", Args: cobra.ExactArgs(0), Run: run, } return cmd } func newTodoLabelDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() trackerName, owner, instance, err := getTrackerName(ctx, cmd) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) id, err := getLabelID(c, ctx, trackerName, args[0], owner) if err != nil { log.Fatalf("failed to get label ID: %v", err) } label, err := todosrht.DeleteLabel(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Deleted label %s\n", label.Name) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete a label", Args: cobra.ExactArgs(1), ValidArgsFunction: completeLabel, Run: run, } return cmd } func newTodoLabelUpdateCommand() *cobra.Command { var bg, fg, name string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() trackerName, owner, instance, err := getTrackerName(ctx, cmd) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) id, err := getLabelID(c, ctx, trackerName, args[0], owner) if err != nil { log.Fatalf("failed to get label ID: %v", err) } var input todosrht.UpdateLabelInput if fg != "" { input.ForegroundColor = &fg } if bg != "" { input.ForegroundColor = &bg } if name != "" { input.Name = &name } label, err := todosrht.UpdateLabel(c.Client, ctx, id, input) if err != nil { log.Fatal(err) } log.Printf("Updated label %s\n", label.Name) } cmd := &cobra.Command{ Use: "update ", Short: "Update a label", Args: cobra.ExactArgs(1), ValidArgsFunction: completeLabel, Run: run, } cmd.Flags().StringVarP(&fg, "foreground", "f", "", "foreground color") cmd.RegisterFlagCompletionFunc("foreground", completeLabelColor) cmd.Flags().StringVarP(&bg, "background", "b", "", "background color") cmd.RegisterFlagCompletionFunc("background", completeLabelColor) cmd.Flags().StringVarP(&name, "name", "n", "", "label name") cmd.RegisterFlagCompletionFunc("name", cobra.NoFileCompletions) return cmd } func newTodoLabelCreateCommand() *cobra.Command { var fg, bg string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() name, owner, instance, err := getTrackerName(ctx, cmd) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) id := getTrackerID(c, ctx, name, owner) if fg == "" { fg = calcLabelForeground(bg) } label, err := todosrht.CreateLabel(c.Client, ctx, id, args[0], fg, bg) if err != nil { log.Fatal(err) } else if label == nil { log.Fatal("failed to create label") } log.Printf("Created label %s\n", label.TermString()) } cmd := &cobra.Command{ Use: "create ", Short: "Create a label", Args: cobra.ExactArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().StringVarP(&fg, "foreground", "f", "", "foreground color") cmd.RegisterFlagCompletionFunc("foreground", completeLabelColor) cmd.Flags().StringVarP(&bg, "background", "b", "", "background color") cmd.MarkFlagRequired("background") cmd.RegisterFlagCompletionFunc("background", completeLabelColor) return cmd } func newTodoACLCommand() *cobra.Command { cmd := &cobra.Command{ Use: "acl", Short: "Manage access-control lists", } cmd.AddCommand(newTodoACLListCommand()) cmd.AddCommand(newTodoACLDeleteCommand()) return cmd } func newTodoACLListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseResourceName(args[0]) } else { var err error name, owner, instance, err = getTrackerName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("todo", cmd, instance) var ( cursor *todosrht.Cursor user *todosrht.User username string err error ) if owner != "" { username = strings.TrimLeft(owner, ownerPrefixes) } err = pagerify(func(p pager) error { if username != "" { user, err = todosrht.AclByUser(c.Client, ctx, username, name, cursor) } else { user, err = todosrht.AclByTrackerName(c.Client, ctx, name, cursor) } if err != nil { return err } else if user == nil { return fmt.Errorf("no such user %q", username) } else if user.Tracker == nil { return fmt.Errorf("no such tracker %q", name) } if cursor == nil { // only print once fmt.Fprintln(p, termfmt.Bold.Sprint("Default permissions")) fmt.Fprintln(p, user.Tracker.DefaultACL.TermString()) if len(user.Tracker.Acls.Results) > 0 { fmt.Fprintln(p, termfmt.Bold.Sprint("\nUser permissions")) } } for _, acl := range user.Tracker.Acls.Results { printACLEntry(p, &acl) } cursor = user.Tracker.Acls.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list", Short: "List ACL entries", Args: cobra.MaximumNArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } return cmd } func printACLEntry(w io.Writer, acl *todosrht.TrackerACL) { s := fmt.Sprintf("%s browse %s submit %s comment %s edit %s triage", todosrht.PermissionIcon(acl.Browse), todosrht.PermissionIcon(acl.Submit), todosrht.PermissionIcon(acl.Comment), todosrht.PermissionIcon(acl.Edit), todosrht.PermissionIcon(acl.Triage)) created := termfmt.Dim.String(humanize.Time(acl.Created.Time)) fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", termfmt.DarkYellow.Sprintf("#%d", acl.Id), acl.Entity.CanonicalName, s, created) } func newTodoACLDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("todo", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } acl, err := todosrht.DeleteACL(c.Client, ctx, id) if err != nil { log.Fatal(err) } else if acl == nil { log.Fatalf("failed to delete ACL entry with ID %d", id) } log.Printf("Deleted ACL entry for %q in tracker %q\n", acl.Entity.CanonicalName, acl.Tracker.Name) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete an ACL entry", Args: cobra.ExactArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } return cmd } func newTodoWebhookCommand() *cobra.Command { cmd := &cobra.Command{ Use: "webhook", Short: "Manage tracker webhooks", } cmd.AddCommand(newTodoWebhookCreateCommand()) cmd.AddCommand(newTodoWebhookListCommand()) cmd.AddCommand(newTodoWebhookDeleteCommand()) return cmd } func newTodoWebhookCreateCommand() *cobra.Command { var events []string var stdin bool var url string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseResourceName(args[0]) } else { var err error name, owner, instance, err = getTrackerName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("todo", cmd, instance) id := getTrackerID(c, ctx, name, owner) var config todosrht.TrackerWebhookInput config.Url = url whEvents, err := todosrht.ParseTrackerWebhookEvents(events) if err != nil { log.Fatal(err) } config.Events = whEvents config.Query = readWebhookQuery(stdin) webhook, err := todosrht.CreateTrackerWebhook(c.Client, ctx, id, config) if err != nil { log.Fatal(err) } log.Printf("Created tracker webhook with ID %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "create [tracker]", Short: "Create a tracker webhook", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeTracker, Run: run, } cmd.Flags().StringSliceVarP(&events, "events", "e", nil, "webhook events") cmd.RegisterFlagCompletionFunc("events", completeTrackerWebhookEvents) cmd.MarkFlagRequired("events") cmd.Flags().BoolVar(&stdin, "stdin", !isStdinTerminal, "read webhook query from stdin") cmd.Flags().StringVarP(&url, "url", "u", "", "payload url") cmd.RegisterFlagCompletionFunc("url", cobra.NoFileCompletions) cmd.MarkFlagRequired("url") return cmd } func newTodoWebhookListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() var name, owner, instance string if len(args) > 0 { name, owner, instance = parseResourceName(args[0]) } else { var err error name, owner, instance, err = getTrackerName(ctx, cmd) if err != nil { log.Fatal(err) } } c := createClientWithInstance("todo", cmd, instance) var ( cursor *todosrht.Cursor user *todosrht.User username string err error ) if owner != "" { username = strings.TrimLeft(owner, ownerPrefixes) } err = pagerify(func(p pager) error { if username != "" { user, err = todosrht.TrackerWebhooksByUser(c.Client, ctx, username, name, cursor) } else { user, err = todosrht.TrackerWebhooks(c.Client, ctx, name, cursor) } if err != nil { return err } else if user == nil { return fmt.Errorf("no such user %q", username) } else if user.Tracker == nil { return fmt.Errorf("no such tracker %q", name) } for _, webhook := range user.Tracker.Webhooks.Results { fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url) } cursor = user.Tracker.Webhooks.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list [tracker]", Short: "List tracker webhooks", Args: cobra.MaximumNArgs(1), ValidArgsFunction: completeTracker, Run: run, } return cmd } func newTodoWebhookDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("todo", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } webhook, err := todosrht.DeleteTrackerWebhook(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Deleted webhook %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete a tracker webhook", Args: cobra.ExactArgs(1), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } return cmd } func newTodoUserWebhookCommand() *cobra.Command { cmd := &cobra.Command{ Use: "user-webhook", Short: "Manage user webhooks", } cmd.AddCommand(newTodoUserWebhookCreateCommand()) cmd.AddCommand(newTodoUserWebhookListCommand()) cmd.AddCommand(newTodoUserWebhookDeleteCommand()) return cmd } func newTodoUserWebhookCreateCommand() *cobra.Command { var events []string var stdin bool var url string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("todo", cmd) var config todosrht.UserWebhookInput config.Url = url whEvents, err := todosrht.ParseUserEvents(events) if err != nil { log.Fatal(err) } config.Events = whEvents config.Query = readWebhookQuery(stdin) webhook, err := todosrht.CreateUserWebhook(c.Client, ctx, config) if err != nil { log.Fatal(err) } log.Printf("Created user webhook with ID %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "create", Short: "Create a user webhook", Args: cobra.ExactArgs(0), ValidArgsFunction: cobra.NoFileCompletions, Run: run, } cmd.Flags().StringSliceVarP(&events, "events", "e", nil, "webhook events") cmd.RegisterFlagCompletionFunc("events", completeTodoUserWebhookEvents) cmd.MarkFlagRequired("events") cmd.Flags().BoolVar(&stdin, "stdin", !isStdinTerminal, "read webhook query from stdin") cmd.Flags().StringVarP(&url, "url", "u", "", "payload url") cmd.RegisterFlagCompletionFunc("url", cobra.NoFileCompletions) cmd.MarkFlagRequired("url") return cmd } func newTodoUserWebhookListCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("todo", cmd) var cursor *todosrht.Cursor err := pagerify(func(p pager) error { webhooks, err := todosrht.UserWebhooks(c.Client, ctx, cursor) if err != nil { return err } for _, webhook := range webhooks.Results { fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url) } cursor = webhooks.Cursor if cursor == nil { return pagerDone } return nil }) if err != nil { log.Fatal(err) } } cmd := &cobra.Command{ Use: "list", Short: "List user webhooks", Args: cobra.ExactArgs(0), Run: run, } return cmd } func newTodoUserWebhookDeleteCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() c := createClient("todo", cmd) id, err := parseInt32(args[0]) if err != nil { log.Fatal(err) } webhook, err := todosrht.DeleteUserWebhook(c.Client, ctx, id) if err != nil { log.Fatal(err) } log.Printf("Deleted webhook %d\n", webhook.Id) } cmd := &cobra.Command{ Use: "delete ", Short: "Delete a user webhook", Args: cobra.ExactArgs(1), ValidArgsFunction: completeTodoUserWebhookID, Run: run, } return cmd } const todoTicketCreatePrefill = ` ` func newTodoTicketCreateCommand() *cobra.Command { var stdin bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() name, owner, instance, err := getTrackerName(ctx, cmd) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) trackerID := getTrackerID(c, ctx, name, owner) var input todosrht.SubmitTicketInput if stdin { br := bufio.NewReader(os.Stdin) fmt.Printf("Subject: ") var err error input.Subject, err = br.ReadString('\n') if err != nil { log.Fatalf("failed to read subject: %v", err) } input.Subject = strings.TrimSpace(input.Subject) if input.Subject == "" { fmt.Println("Aborting due to empty subject.") os.Exit(1) } fmt.Printf("Description %s:\n", termfmt.Dim.String("(Markdown supported)")) bodyBytes, err := io.ReadAll(br) if err != nil { log.Fatalf("failed to read description: %v", err) } if body := strings.TrimSpace(string(bodyBytes)); body != "" { input.Body = &body } } else { text, err := getInputWithEditor("hut_ticket*.md", todoTicketCreatePrefill) if err != nil { log.Fatalf("failed to read ticket subject and description: %v", err) } text = dropComment(text, todoTicketCreatePrefill) parts := strings.SplitN(text, "\n", 2) input.Subject = strings.TrimSpace(parts[0]) if len(parts) > 1 { body := strings.TrimSpace(parts[1]) input.Body = &body } } if input.Subject == "" { log.Println("Aborting due to empty subject.") os.Exit(1) } ticket, err := todosrht.SubmitTicket(c.Client, ctx, trackerID, input) if err != nil { log.Fatal(err) } else if ticket == nil { log.Fatal("failed to create ticket") } log.Printf("Created new ticket %v\n", termfmt.DarkYellow.Sprintf("#%v", ticket.Id)) } cmd := &cobra.Command{ Use: "create", Short: "Create a new ticket", Args: cobra.ExactArgs(0), Run: run, } cmd.Flags().BoolVar(&stdin, "stdin", !isStdinTerminal, "read ticket from stdin") return cmd } func newTodoTicketEditCommand() *cobra.Command { run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() ticketID, name, owner, instance, err := parseTicketResource(ctx, cmd, args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) var ( user *todosrht.User username string ) if owner != "" { username = strings.TrimLeft(owner, ownerPrefixes) user, err = todosrht.TicketBodyByUser(c.Client, ctx, username, name, ticketID) } else { user, err = todosrht.TicketBodyByName(c.Client, ctx, name, ticketID) } if err != nil { log.Fatal(err) } else if user == nil { log.Fatalf("no such user %q", username) } else if user.Tracker == nil { log.Fatalf("no such tracker %q", name) } tracker := user.Tracker ticket := tracker.Ticket prefill := ticket.Subject + "\n\n" if ticket.Body != nil { prefill += *ticket.Body } text, err := getInputWithEditor("hut_ticket*.md", prefill) if err != nil { log.Fatalf("failed to read ticket subject and description: %v", err) } parts := strings.SplitN(text, "\n", 2) subject := strings.TrimSpace(parts[0]) var body string if len(parts) > 1 { body = strings.TrimSpace(parts[1]) } if subject == "" { log.Println("Aborting due to empty subject.") os.Exit(1) } input := todosrht.UpdateTicketInput{ Subject: &subject, Body: &body, } ticket, err = todosrht.UpdateTicket(c.Client, ctx, tracker.Id, ticket.Id, input) if err != nil { log.Fatal(err) } log.Printf("Updated ticket %v\n", termfmt.DarkYellow.Sprintf("#%v", ticket.Id)) } cmd := &cobra.Command{ Use: "edit ", Short: "Edit a ticket", Args: cobra.ExactArgs(1), ValidArgsFunction: completeTicketID, Run: run, } return cmd } func newTodoTicketLabelCommand() *cobra.Command { var labelName string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() ticketID, trackerName, owner, instance, err := parseTicketResource(ctx, cmd, args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) trackerID := getTrackerID(c, ctx, trackerName, owner) labelID, err := getLabelID(c, ctx, trackerName, labelName, owner) if err != nil { log.Fatalf("failed to get label ID: %v", err) } event, err := todosrht.LabelTicket(c.Client, ctx, trackerID, ticketID, labelID) if err != nil { log.Fatal(err) } log.Printf("Added label to %q\n", event.Ticket.Subject) } cmd := &cobra.Command{ Use: "label ", Short: "Add a label to a ticket", Args: cobra.ExactArgs(1), ValidArgsFunction: completeTicketID, Run: run, } cmd.Flags().StringVarP(&labelName, "label", "l", "", "label name") cmd.MarkFlagRequired("label") cmd.RegisterFlagCompletionFunc("label", completeTicketLabel) return cmd } func newTodoTicketUnlabelCommand() *cobra.Command { var labelName string run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() ticketID, trackerName, owner, instance, err := parseTicketResource(ctx, cmd, args[0]) if err != nil { log.Fatal(err) } c := createClientWithInstance("todo", cmd, instance) trackerID := getTrackerID(c, ctx, trackerName, owner) labelID, err := getLabelID(c, ctx, trackerName, labelName, owner) if err != nil { log.Fatalf("failed to get label ID: %v", err) } event, err := todosrht.UnlabelTicket(c.Client, ctx, trackerID, ticketID, labelID) if err != nil { log.Fatal(err) } log.Printf("Removed label from %q\n", event.Ticket.Subject) } cmd := &cobra.Command{ Use: "unlabel ", Short: "Remove a label from a ticket", Args: cobra.ExactArgs(1), ValidArgsFunction: completeTicketID, Run: run, } cmd.Flags().StringVarP(&labelName, "label", "l", "", "label name") cmd.MarkFlagRequired("label") cmd.RegisterFlagCompletionFunc("label", completeTicketUnlabel) return cmd } func getTrackerID(c *Client, ctx context.Context, name, owner string) int32 { var ( user *todosrht.User username string err error ) if owner == "" { user, err = todosrht.TrackerIDByName(c.Client, ctx, name) } else { username = strings.TrimLeft(owner, ownerPrefixes) user, err = todosrht.TrackerIDByUser(c.Client, ctx, username, name) } if err != nil { log.Fatalf("failed to get tracker ID: %v", err) } else if user == nil { log.Fatalf("user %q does not exist", username) } else if user.Tracker == nil { log.Fatalf("tracker %q does not exist", name) } return user.Tracker.Id } func getTrackerName(ctx context.Context, cmd *cobra.Command) (name, owner, instance string, err error) { s, err := cmd.Flags().GetString("tracker") if err != nil { return "", "", "", err } else if s != "" { name, owner, instance = parseResourceName(s) return name, owner, instance, nil } cfg, err := loadProjectConfig() if err != nil { return "", "", "", err } if cfg != nil && cfg.Tracker != "" { name, owner, instance = parseResourceName(cfg.Tracker) return name, owner, instance, nil } // TODO: Use hub.sr.ht API to determine trackers name, owner, instance, err = guessGitRepoName(ctx, cmd) if err != nil { return "", "", "", err } return name, owner, instance, nil } func parseTicketResource(ctx context.Context, cmd *cobra.Command, ticket string) (ticketID int32, name, owner, instance string, err error) { if strings.Contains(ticket, "/") { var resource string resource, owner, instance = parseResourceName(ticket) split := strings.Split(resource, "/") if len(split) != 2 { return 0, "", "", "", errors.New("failed to parse tracker name and/or ID") } name = split[0] var err error ticketID, err = parseInt32(split[1]) if err != nil { return 0, "", "", "", err } } else { var err error ticketID, err = parseInt32(ticket) if err != nil { return 0, "", "", "", err } name, owner, instance, err = getTrackerName(ctx, cmd) if err != nil { return 0, "", "", "", err } } return ticketID, name, owner, instance, nil } func calcLabelForeground(bg string) string { const white = "#FFFFFF" const black = "#000000" bgLuminance := calcLuminance(bg) contrastWhite := calcContrastRatio(bgLuminance, calcLuminance(white)) contrastBlack := calcContrastRatio(bgLuminance, calcLuminance(black)) if contrastBlack > contrastWhite { return black } return white } func calcLuminance(hex string) float64 { // https://www.w3.org/TR/WCAG/#dfn-relative-luminance rgb := termfmt.HexToRGB(hex) rsRGB := float64(rgb.Red) / 255 gsRGB := float64(rgb.Green) / 255 bsRGB := float64(rgb.Blue) / 255 var r, g, b float64 if rsRGB <= 0.03928 { r = rsRGB / 12.92 } else { r = math.Pow((rsRGB+0.055)/1.055, 2.4) } if gsRGB <= 0.03928 { g = gsRGB / 12.92 } else { g = math.Pow((gsRGB+0.055)/1.055, 2.4) } if bsRGB <= 0.03928 { b = bsRGB / 12.92 } else { b = math.Pow((bsRGB+0.055)/1.055, 2.4) } return 0.2126*r + 0.7152*g + 0.0722*b } func calcContrastRatio(l1, l2 float64) float64 { // https://www.w3.org/TR/WCAG/#dfn-contrast-ratio if l1 > l2 { return (l1 + 0.05) / (l2 + 0.05) } return (l2 + 0.05) / (l1 + 0.05) } func getLabelID(c *Client, ctx context.Context, trackerName, labelName, owner string) (int32, error) { var ( user *todosrht.User username string err error ) if owner == "" { user, err = todosrht.LabelIDByName(c.Client, ctx, trackerName, labelName) } else { username = strings.TrimLeft(owner, ownerPrefixes) user, err = todosrht.LabelIDByUser(c.Client, ctx, username, trackerName, labelName) } if err != nil { return 0, err } else if user == nil { return 0, fmt.Errorf("user %q does not exist", username) } else if user.Tracker == nil { return 0, fmt.Errorf("tracker %q does not exist", trackerName) } else if user.Tracker.Label == nil { return 0, fmt.Errorf("label %q does not exist", labelName) } return user.Tracker.Label.Id, nil } func completeTicketID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var tickets []string ctx := cmd.Context() name, owner, instance, err := getTrackerName(ctx, cmd) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } c := createClientWithInstance("todo", cmd, instance) var user *todosrht.User includeSubscription := false if cmd.Name() == "subscribe" || cmd.Name() == "unsubscribe" { includeSubscription = true } if owner != "" { username := strings.TrimLeft(owner, ownerPrefixes) user, err = todosrht.CompleteTicketIdByUser(c.Client, ctx, username, name, includeSubscription) } else { user, err = todosrht.CompleteTicketId(c.Client, ctx, name, includeSubscription) } if err != nil || user == nil || user.Tracker == nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, ticket := range user.Tracker.Tickets.Results { if cmd.Name() == "subscribe" && ticket.Subscription != nil { continue } else if cmd.Name() == "unsubscribe" && ticket.Subscription == nil { continue } s := fmt.Sprintf("%d\t%s", ticket.Id, ticket.Subject) tickets = append(tickets, s) } return tickets, cobra.ShellCompDirectiveNoFileComp } var completeTicketStatus = cobra.FixedCompletions([]string{ "reported", "confirmed", "in_progress", "pending", "resolved", }, cobra.ShellCompDirectiveNoFileComp) var completeTicketResolution = cobra.FixedCompletions([]string{ "unresolved", "closed", "fixed", "implemented", "wont_fix", "by_design", "invalid", "duplicate", "not_our_bug", }, cobra.ShellCompDirectiveNoFileComp) func completeLabelColor(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var colors []string colorMap := map[string]string{"black": "#000000", "white": "#FFFFFF", "blue": "#3584E4", "green": "#33D17A", "yellow": "#F6D32D", "orange": "#FF7800", "red": "#E01B24", "purple": "#9141AC", "brown": "#986A44"} for k, v := range colorMap { colors = append(colors, fmt.Sprintf("%s\t%s", v, k)) } return colors, cobra.ShellCompDirectiveNoFileComp } func completeTicketUnassign(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) == 0 { return nil, cobra.ShellCompDirectiveNoFileComp } ctx := cmd.Context() var assignees []string ticketID, name, owner, instance, err := parseTicketResource(ctx, cmd, args[0]) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } c := createClientWithInstance("todo", cmd, instance) var user *todosrht.User if owner != "" { username := strings.TrimLeft(owner, ownerPrefixes) user, err = todosrht.AssigneesByUser(c.Client, ctx, username, name, ticketID) } else { user, err = todosrht.Assignees(c.Client, ctx, name, ticketID) } if err != nil || user == nil || user.Tracker == nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, user := range user.Tracker.Ticket.Assignees { userName := strings.TrimLeft(user.CanonicalName, ownerPrefixes) assignees = append(assignees, userName) } return assignees, cobra.ShellCompDirectiveNoFileComp } func completeTicketAssign(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) == 0 { return nil, cobra.ShellCompDirectiveNoFileComp } ctx := cmd.Context() ticketID, name, owner, instance, err := parseTicketResource(ctx, cmd, args[0]) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } c := createClientWithInstance("todo", cmd, instance) var ( me *todosrht.User user *todosrht.User ) candidates := make(map[string]struct{}) if owner != "" { username := strings.TrimLeft(owner, ownerPrefixes) me, user, err = todosrht.CompleteTicketAssignByUser(c.Client, ctx, username, name, ticketID) candidates[me.CanonicalName] = struct{}{} } else { user, err = todosrht.CompleteTicketAssign(c.Client, ctx, name, ticketID) candidates[user.CanonicalName] = struct{}{} } if err != nil || user == nil || user.Tracker == nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, ticket := range user.Tracker.Tickets.Results { for _, user := range ticket.Assignees { candidates[user.CanonicalName] = struct{}{} } } assignedUsers := make(map[string]struct{}) for _, user := range user.Tracker.Ticket.Assignees { assignedUsers[user.CanonicalName] = struct{}{} } var potentialAssignees []string for user := range candidates { // user already assigned if _, ok := assignedUsers[user]; ok { continue } userName := strings.TrimLeft(user, ownerPrefixes) potentialAssignees = append(potentialAssignees, userName) } return potentialAssignees, cobra.ShellCompDirectiveNoFileComp } func completeTracker(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("todo", cmd) var trackerList []string trackers, err := todosrht.TrackerNames(c.Client, ctx) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, tracker := range trackers.Results { trackerList = append(trackerList, tracker.Name) } return trackerList, cobra.ShellCompDirectiveNoFileComp } func completeTicketWebhookEvents(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var eventList []string events := [3]string{"event_created", "ticket_update", "ticket_deleted"} set := strings.ToLower(cmd.Flag("events").Value.String()) for _, event := range events { if !strings.Contains(set, event) { eventList = append(eventList, event) } } return eventList, cobra.ShellCompDirectiveNoFileComp } func completeTodoUserWebhookEvents(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var eventList []string events := [4]string{"tracker_created", "tracker_update", "tracker_deleted", "ticket_created"} set := strings.ToLower(cmd.Flag("events").Value.String()) for _, event := range events { if !strings.Contains(set, event) { eventList = append(eventList, event) } } return eventList, cobra.ShellCompDirectiveNoFileComp } func completeTodoUserWebhookID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() c := createClient("todo", cmd) var webhookList []string webhooks, err := todosrht.UserWebhooks(c.Client, ctx, nil) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, webhook := range webhooks.Results { s := fmt.Sprintf("%d\t%s", webhook.Id, webhook.Url) webhookList = append(webhookList, s) } return webhookList, cobra.ShellCompDirectiveNoFileComp } func completeTrackerWebhookEvents(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var eventList []string events := [9]string{"tracker_update", "tracker_deleted", "label_created", "label_update", "label_deleted", "ticket_created", "ticket_update", "ticket_deleted", "event_created"} set := strings.ToLower(cmd.Flag("events").Value.String()) for _, event := range events { if !strings.Contains(set, event) { eventList = append(eventList, event) } } return eventList, cobra.ShellCompDirectiveNoFileComp } func completeLabel(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() var labelList []string name, owner, instance, err := getTrackerName(ctx, cmd) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } c := createClientWithInstance("todo", cmd, instance) var user *todosrht.User if owner != "" { username := strings.TrimLeft(owner, ownerPrefixes) user, err = todosrht.CompleteLabelByUser(c.Client, ctx, username, name) } else { user, err = todosrht.CompleteLabel(c.Client, ctx, name) } if err != nil || user == nil || user.Tracker == nil { return nil, cobra.ShellCompDirectiveNoFileComp } for _, label := range user.Tracker.Labels.Results { labelList = append(labelList, label.Name) } return labelList, cobra.ShellCompDirectiveNoFileComp } func completeTicketLabel(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // display all labels if no ticket is specified if len(args) == 0 { return completeLabel(cmd, args, toComplete) } ctx := cmd.Context() ticketID, name, owner, instance, err := parseTicketResource(ctx, cmd, args[0]) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } c := createClientWithInstance("todo", cmd, instance) var ( user *todosrht.User ) if owner != "" { username := strings.TrimLeft(owner, ownerPrefixes) user, err = todosrht.CompleteTicketLabelByUser(c.Client, ctx, username, name, ticketID) } else { user, err = todosrht.CompleteTicketLabel(c.Client, ctx, name, ticketID) } if err != nil || user == nil || user.Tracker == nil { return nil, cobra.ShellCompDirectiveNoFileComp } var ticketLabels []string for _, label := range user.Tracker.Ticket.Labels { ticketLabels = append(ticketLabels, label.Name) } var labelList []string for _, label := range user.Tracker.Labels.Results { if !sliceContains(ticketLabels, label.Name) { labelList = append(labelList, label.Name) } } return labelList, cobra.ShellCompDirectiveNoFileComp } func completeTicketUnlabel(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // display all labels if no ticket is specified if len(args) == 0 { return completeLabel(cmd, args, toComplete) } ctx := cmd.Context() ticketID, name, owner, instance, err := parseTicketResource(ctx, cmd, args[0]) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } c := createClientWithInstance("todo", cmd, instance) var ( user *todosrht.User ) if owner != "" { username := strings.TrimLeft(owner, ownerPrefixes) user, err = todosrht.CompleteTicketUnlabelByUser(c.Client, ctx, username, name, ticketID) } else { user, err = todosrht.CompleteTicketUnlabel(c.Client, ctx, name, ticketID) } if err != nil || user == nil || user.Tracker == nil { return nil, cobra.ShellCompDirectiveNoFileComp } var labelList []string for _, label := range user.Tracker.Ticket.Labels { labelList = append(labelList, label.Name) } return labelList, cobra.ShellCompDirectiveNoFileComp }