pax_global_header 0000666 0000000 0000000 00000000064 14527667463 0014536 g ustar 00root root 0000000 0000000 52 comment=f5a96a40208d2ea1e8e3badff5312fb975d28e82
hut-0.4.0/ 0000775 0000000 0000000 00000000000 14527667463 0012337 5 ustar 00root root 0000000 0000000 hut-0.4.0/.build.yml 0000664 0000000 0000000 00000000520 14527667463 0014234 0 ustar 00root root 0000000 0000000 image: alpine/latest
packages:
- go
- scdoc
- bmake
sources:
- https://git.sr.ht/~emersion/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.4.0/.gitignore 0000664 0000000 0000000 00000000055 14527667463 0014327 0 ustar 00root root 0000000 0000000 /hut
/doc/hut.1
/hut.bash
/hut.fish
/hut.zsh
hut-0.4.0/LICENSE 0000664 0000000 0000000 00000103333 14527667463 0013347 0 ustar 00root root 0000000 0000000 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.4.0/Makefile 0000664 0000000 0000000 00000002577 14527667463 0014012 0 ustar 00root root 0000000 0000000 .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.4.0/README.md 0000664 0000000 0000000 00000001552 14527667463 0013621 0 ustar 00root root 0000000 0000000 # [hut]
[](https://builds.sr.ht/~emersion/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: [#emersion on Libera Chat].
## License
AGPLv3 only, see [LICENSE].
Copyright (C) 2021 Simon Ser
[hut]: https://sr.ht/~emersion/hut/
[sr.ht]: https://sr.ht/~sircmpwn/sourcehut/
[mailing list]: https://lists.sr.ht/~emersion/hut-dev
[issue tracker]: https://todo.sr.ht/~emersion/hut
[#emersion on Libera Chat]: ircs://irc.libera.chat/#emersion
[LICENSE]: LICENSE
hut-0.4.0/builds.go 0000664 0000000 0000000 00000062633 14527667463 0014162 0 ustar 00root root 0000000 0000000 package 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/~emersion/hut/srht/buildssrht"
"git.sr.ht/~emersion/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)
}
pagerify(func(p pager) bool {
var jobs *buildssrht.JobCursor
if len(username) > 0 {
user, err := buildssrht.JobsByUser(c.Client, ctx, username, cursor)
if err != nil {
log.Fatal(err)
} else if user == nil {
log.Fatal("no such user")
}
jobs = user.Jobs
} else {
var err error
jobs, err = buildssrht.Jobs(c.Client, ctx, cursor)
if err != nil {
log.Fatal(err)
}
}
for _, job := range jobs.Results {
printJob(p, &job)
}
cursor = jobs.Cursor
return cursor == nil
})
}
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", false, "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
pagerify(func(p pager) bool {
webhooks, err := buildssrht.UserWebhooks(c.Client, ctx, cursor)
if err != nil {
log.Fatal(err)
}
for _, webhook := range webhooks.Results {
fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url)
}
cursor = webhooks.Cursor
return cursor == nil
})
}
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
pagerify(func(p pager) bool {
secrets, err := buildssrht.Secrets(c.Client, ctx, cursor)
if err != nil {
log.Fatal(err)
}
for i, secret := range secrets.Results {
if i != 0 {
fmt.Fprintln(p)
}
printSecret(p, &secret)
}
cursor = secrets.Cursor
return cursor == nil
})
}
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 !termfmt.IsTerminal() {
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.4.0/client.go 0000664 0000000 0000000 00000004773 14527667463 0014157 0 ustar 00root root 0000000 0000000 package 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
Hostname string
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
}
baseURL := inst.Origins[service]
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)
}
return createClientWithToken(baseURL, token)
}
func createClientWithToken(baseURL, token string) *Client {
gqlEndpoint := baseURL + "/query"
httpClient := &http.Client{
Transport: &httpTransport{accessToken: token},
Timeout: 30 * time.Second,
}
return &Client{
Client: gqlclient.New(gqlEndpoint, httpClient),
BaseURL: baseURL,
HTTP: httpClient,
}
}
type httpTransport struct {
accessToken string
}
func (tr *httpTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", "hut")
req.Header.Set("Authorization", "Bearer "+tr.accessToken)
return http.DefaultTransport.RoundTrip(req)
}
hut-0.4.0/config.go 0000664 0000000 0000000 00000013754 14527667463 0014145 0 ustar 00root root 0000000 0000000 package main
import (
"bufio"
"context"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"git.sr.ht/~emersion/go-scfg"
"github.com/spf13/cobra"
"git.sr.ht/~emersion/hut/srht/metasrht"
"git.sr.ht/~emersion/hut/termfmt"
)
type Config struct {
Instances []*InstanceConfig
}
type InstanceConfig struct {
Name string
AccessToken string
AccessTokenCmd []string
Origins map[string]string
}
func (instance InstanceConfig) match(name string) bool {
if instancesEqual(name, instance.Name) {
return true
}
for _, origin := range instance.Origins {
if stripProtocol(origin) == name {
return true
}
}
return false
}
func instancesEqual(a, b string) bool {
return a == b || strings.HasSuffix(a, "."+b) || strings.HasSuffix(b, "."+a)
}
func loadConfigFile(filename string) (*Config, error) {
rootBlock, err := scfg.Load(filename)
if err != nil {
return nil, err
}
cfg := new(Config)
instanceNames := make(map[string]struct{})
for _, instanceDir := range rootBlock.GetAll("instance") {
instance := &InstanceConfig{
Origins: make(map[string]string),
}
if err := instanceDir.ParseParams(&instance.Name); err != nil {
return nil, err
}
if _, ok := instanceNames[instance.Name]; ok {
return nil, fmt.Errorf("duplicate instance name %q", instance.Name)
}
instanceNames[instance.Name] = struct{}{}
if dir := instanceDir.Children.Get("access-token"); dir != nil {
if err := dir.ParseParams(&instance.AccessToken); err != nil {
return nil, err
}
}
if dir := instanceDir.Children.Get("access-token-cmd"); dir != nil {
if len(dir.Params) == 0 {
return nil, fmt.Errorf("instance %q: missing command name in access-token-cmd directive", instance.Name)
}
instance.AccessTokenCmd = dir.Params
}
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)
}
for _, service := range []string{"builds", "git", "hg", "lists", "meta", "pages", "paste", "todo"} {
serviceDir := instanceDir.Children.Get(service)
if serviceDir == nil {
continue
}
originDir := serviceDir.Children.Get("origin")
if originDir == nil {
continue
}
var origin string
if err := originDir.ParseParams(&origin); err != nil {
return nil, err
}
instance.Origins[service] = origin
}
cfg.Instances = append(cfg.Instances, instance)
}
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)
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.4.0/contrib/ 0000775 0000000 0000000 00000000000 14527667463 0013777 5 ustar 00root root 0000000 0000000 hut-0.4.0/contrib/update_schemas.sh 0000775 0000000 0000000 00000001056 14527667463 0017325 0 ustar 00root root 0000000 0000000 #!/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.4.0/doc/ 0000775 0000000 0000000 00000000000 14527667463 0013104 5 ustar 00root root 0000000 0000000 hut-0.4.0/doc/hut.1.scd 0000664 0000000 0000000 00000043306 14527667463 0014544 0 ustar 00root root 0000000 0000000 hut(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.
# 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.
*--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"). When this command is run from a terminal, the query
will be read from _$EDITOR_, otherwise it defaults to stdin.
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*
Export 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. When this command is run from a terminal, the query will
be read from _$EDITOR_, otherwise it defaults to stdin.
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.
*create* [options...]
Create a repository.
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.
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. When this command is run from a terminal, the query will
be read from _$EDITOR_, otherwise it defaults to stdin.
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
*create* [options...]
Create a repository.
Options are:
*-d*, *--description*
Description of the repository.
*-v*, *--visibility*
Visibility to use (public, unlisted, private). Defaults to public.
*delete* [options...]
Delete a repository.
Options are:
*-y*, *--yes*
Confirm deletion without prompt.
*list* [owner]
List repositories.
*user-webhook create* [options...]
Create a user webhook. When this command is run from a terminal, the query will
be read from _$EDITOR_, otherwise it defaults to stdin.
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.
*user-webhook create* [options...]
Create a user webhook. When this command is run from a terminal, the query will
be read from _$EDITOR_, otherwise it defaults to stdin.
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.
*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
*user-webhook create* [options...]
Create a user webhook. When this command is run from a terminal, the query will
be read from _$EDITOR_, otherwise it defaults to stdin.
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.
*--not-found*
Path to serve for page not found responses. This flag is deprecated, use
*--site-config* instead to set the path.
*-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. When this command is run from a terminal, the query will
be read from _$EDITOR_, otherwise it defaults to stdin.
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. When this command is run from a terminal, the query will
be read from _$EDITOR_, otherwise it defaults to stdin.
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* [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.
*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 with _$EDITOR_.
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 with _$EDITOR_.
Options are:
*--stdin*
Read ticket from _stdin_.
*ticket delete* [options...]
Delete a ticket.
Options are:
*-y*, *--yes*
Confirm deletion without prompt.
*ticket label* [options...]
Add a label to a ticket.
Options are:
*-l*, *--label*
Name of the label (required).
*ticket list*
List tickets.
*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.
*user-webhook create* [options...]
Create a user webhook. When this command is run from a terminal, the query will
be read from _$EDITOR_, otherwise it defaults to stdin.
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"
}
}
```
# AUTHORS
Maintained by Simon Ser , who is assisted by other
open-source contributors. For more information about hut development, see
.
hut-0.4.0/export.go 0000664 0000000 0000000 00000006645 14527667463 0014222 0 ustar 00root root 0000000 0000000 package main
import (
"encoding/json"
"log"
"os"
"path"
"time"
"github.com/spf13/cobra"
"git.sr.ht/~emersion/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
// TODO: Allow exporting a subset of all services (maybe meta should
// provide a list of services configured for that instance?)
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...")
for _, ex := range exporters {
log.Println(ex.Name)
base := path.Join(args[0], ex.Name)
if err := os.MkdirAll(base, 0o755); err != nil {
log.Fatalf("Failed to create export directory: %s", err.Error())
}
stamp := path.Join(base, "service.json")
if _, err := os.Stat(stamp); err == nil {
log.Printf("Skipping %s (already exported)", ex.Name)
continue
}
if err := ex.Export(ctx, base); err != nil {
log.Printf("Error exporting %s: %s", ex.Name, err.Error())
continue
}
info := ExportInfo{
Instance: ex.BaseURL,
Service: ex.Name,
Date: time.Now().UTC(),
}
if err := writeExportStamp(stamp, &info); err != nil {
log.Printf("Error writing stamp for %s: %s", ex.Name, err.Error())
}
}
log.Println("Export complete.")
}
return &cobra.Command{
Use: "export ",
Short: "Exports your account data",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveFilterDirs
},
Run: run,
}
}
func writeExportStamp(path string, info *ExportInfo) error {
file, err := os.Create(path)
if err != nil {
log.Fatalf("Failed to create export info: %s", err.Error())
}
defer file.Close()
err = json.NewEncoder(file).Encode(info)
if err != nil {
log.Fatalf("Failed to marshal export info: %s", err.Error())
}
return nil
}
hut-0.4.0/export/ 0000775 0000000 0000000 00000000000 14527667463 0013660 5 ustar 00root root 0000000 0000000 hut-0.4.0/export/builds.go 0000664 0000000 0000000 00000006732 14527667463 0015501 0 ustar 00root root 0000000 0000000 package export
import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path"
"strconv"
"time"
"git.sr.ht/~emersion/gqlclient"
"git.sr.ht/~emersion/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) 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
}
hut-0.4.0/export/git.go 0000664 0000000 0000000 00000004024 14527667463 0014772 0 ustar 00root root 0000000 0000000 package export
import (
"context"
"fmt"
"log"
"net/url"
"os"
"os/exec"
"path"
"git.sr.ht/~emersion/gqlclient"
"git.sr.ht/~emersion/hut/srht/gitsrht"
)
type GitExporter struct {
client *gqlclient.Client
baseURL string
}
func NewGitExporter(client *gqlclient.Client, baseURL string) *GitExporter {
return &GitExporter{client, 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"`
}
func (ex *GitExporter) Export(ctx context.Context, dir string) error {
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)
}
var cursor *gitsrht.Cursor
for {
repos, err := gitsrht.Repositories(ex.client, ctx, cursor)
if err != nil {
return err
}
// TODO: Should we fetch & store ACLs?
for _, repo := range repos.Results {
repoPath := path.Join(dir, repo.Name)
infoPath := path.Join(repoPath, infoFilename)
clonePath := path.Join(repoPath, "repository.git")
cloneURL := fmt.Sprintf("%s@%s:%s/%s", sshUser, baseURL.Host, repo.Owner.CanonicalName, repo.Name)
if _, err := os.Stat(clonePath); err == nil {
log.Printf("\tSkipping %s (already exists)", repo.Name)
continue
}
if err := os.MkdirAll(repoPath, 0o755); err != nil {
return err
}
log.Printf("\tCloning %s", repo.Name)
cmd := exec.Command("git", "clone", "--mirror", cloneURL, clonePath)
if err := cmd.Run(); err != nil {
return err
}
repoInfo := GitRepoInfo{
Info: Info{
Service: "git.sr.ht",
Name: repo.Name,
},
Description: repo.Description,
Visibility: repo.Visibility,
}
if err := writeJSON(infoPath, &repoInfo); err != nil {
return err
}
}
cursor = repos.Cursor
if cursor == nil {
break
}
}
return nil
}
hut-0.4.0/export/hg.go 0000664 0000000 0000000 00000003645 14527667463 0014615 0 ustar 00root root 0000000 0000000 package export
import (
"context"
"fmt"
"log"
"net/url"
"os"
"os/exec"
"path"
"git.sr.ht/~emersion/gqlclient"
"git.sr.ht/~emersion/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"`
}
func (ex *HgExporter) Export(ctx context.Context, dir string) error {
baseURL, err := url.Parse(ex.baseURL)
if err != nil {
panic(err)
}
var cursor *hgsrht.Cursor
for {
repos, err := hgsrht.Repositories(ex.client, ctx, cursor)
if err != nil {
return err
}
// TODO: Should we fetch & store ACLs?
for _, repo := range repos.Results {
repoPath := path.Join(dir, repo.Name)
infoPath := path.Join(repoPath, infoFilename)
clonePath := path.Join(repoPath, 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)
continue
}
if err := os.MkdirAll(repoPath, 0o755); err != nil {
return err
}
log.Printf("\tCloning %s", repo.Name)
cmd := exec.Command("hg", "clone", "-U", cloneURL, clonePath)
err := cmd.Run()
if err != nil {
return err
}
repoInfo := HgRepoInfo{
Info: Info{
Service: "hg.sr.ht",
Name: repo.Name,
},
Description: repo.Description,
Visibility: repo.Visibility,
}
if err := writeJSON(infoPath, &repoInfo); err != nil {
return err
}
}
cursor = repos.Cursor
if cursor == nil {
break
}
}
return nil
}
hut-0.4.0/export/iface.go 0000664 0000000 0000000 00000001110 14527667463 0015247 0 ustar 00root root 0000000 0000000 package export
import (
"context"
"encoding/json"
"os"
)
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
}
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()
}
hut-0.4.0/export/lists.go 0000664 0000000 0000000 00000005455 14527667463 0015356 0 ustar 00root root 0000000 0000000 package export
import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path"
"time"
"git.sr.ht/~emersion/gqlclient"
"git.sr.ht/~emersion/hut/srht/listssrht"
)
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"`
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) 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, "archive.mbox"))
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,
PermitMime: list.PermitMime,
RejectMime: list.RejectMime,
}
if err := writeJSON(infoPath, &listInfo); err != nil {
return err
}
return nil
}
hut-0.4.0/export/meta.go 0000664 0000000 0000000 00000002600 14527667463 0015133 0 ustar 00root root 0000000 0000000 package export
import (
"context"
"fmt"
"os"
"path"
"git.sr.ht/~emersion/gqlclient"
"git.sr.ht/~emersion/hut/srht/metasrht"
)
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, "ssh.keys"))
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, "keys.pgp"))
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
}
}
return nil
}
hut-0.4.0/export/paste.go 0000664 0000000 0000000 00000005103 14527667463 0015322 0 ustar 00root root 0000000 0000000 package export
import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path"
"time"
"git.sr.ht/~emersion/gqlclient"
"git.sr.ht/~emersion/hut/srht/pastesrht"
)
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 {
if err := ex.exportPaste(ctx, &paste, dir); 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) exportPaste(ctx context.Context, paste *pastesrht.Paste, dir string) error {
base := path.Join(dir, paste.Id)
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, "files")
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 {
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
}
hut-0.4.0/export/todo.go 0000664 0000000 0000000 00000004540 14527667463 0015157 0 ustar 00root root 0000000 0000000 package export
import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path"
"time"
"git.sr.ht/~emersion/gqlclient"
"git.sr.ht/~emersion/hut/srht/todosrht"
)
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) 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, "tracker.json.gz")
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
}
hut-0.4.0/git.go 0000664 0000000 0000000 00000066430 14527667463 0013462 0 ustar 00root root 0000000 0000000 package main
import (
"context"
"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/~emersion/hut/srht/gitsrht"
"git.sr.ht/~emersion/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(newGitACLCommand())
cmd.AddCommand(newGitShowCommand())
cmd.AddCommand(newGitUserWebhookCommand())
cmd.AddCommand(newGitUpdateCommand())
cmd.PersistentFlags().StringP("repo", "r", "", "name of repository")
cmd.RegisterFlagCompletionFunc("repo", completeRepo)
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)
if clone {
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)
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)
}
}
}
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])
}
pagerify(func(p pager) bool {
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 {
log.Fatal(err)
} else if user == nil {
log.Fatal("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 {
log.Fatal(err)
}
}
for _, repo := range repos.Results {
printGitRepo(p, &repo)
}
cursor = repos.Cursor
return cursor == nil
})
}
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 = getRepoName(ctx, cmd)
if err != nil {
log.Fatal(err)
}
}
c := createClientWithInstance("git", cmd, instance)
id := getRepoID(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: completeRepo,
Run: run,
}
cmd.Flags().BoolVarP(&autoConfirm, "yes", "y", false, "auto confirm")
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 := getRepoName(ctx, cmd)
if err != nil {
log.Fatal(err)
}
c := createClientWithInstance("git", cmd, instance)
c.HTTP.Timeout = fileTransferTimeout
repoID := getRepoID(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 := getRepoName(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 = getRepoName(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)
}
pagerify(func(p pager) bool {
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 {
log.Fatal(err)
} else if user == nil {
log.Fatal("no such user")
} else if user.Repository == nil {
log.Fatalf("no such repository %q", name)
}
for _, acl := range user.Repository.Acls.Results {
printGitACLEntry(p, &acl)
}
cursor = user.Repository.Acls.Cursor
return cursor == nil
})
}
cmd := &cobra.Command{
Use: "list",
Short: "List ACL entries",
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: cobra.NoFileCompletions,
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 := getRepoName(ctx, cmd)
if err != nil {
log.Fatal(err)
}
c := createClientWithInstance("git", cmd, instance)
id := getRepoID(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", completeAccessMode)
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 = getRepoName(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: completeRepo,
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", false, "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
pagerify(func(p pager) bool {
webhooks, err := gitsrht.UserWebhooks(c.Client, ctx, cursor)
if err != nil {
log.Fatal(err)
}
for _, webhook := range webhooks.Results {
fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url)
}
cursor = webhooks.Cursor
return cursor == nil
})
}
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 = getRepoName(ctx, cmd)
if err != nil {
log.Fatal(err)
}
}
c := createClientWithInstance("git", cmd, instance)
id := getRepoID(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") {
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: completeRepo,
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 getRepoName(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 getRepoID(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 completeRepo(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
ctx := cmd.Context()
c := createClient("git", cmd)
var repoList []string
repos, err := gitsrht.RepoNames(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
}
var completeAccessMode = cobra.FixedCompletions([]string{"RO", "RW"}, cobra.ShellCompDirectiveNoFileComp)
func completeArtifacts(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
ctx := cmd.Context()
repoName, owner, instance, err := getRepoName(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 := getRepoName(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.4.0/go.mod 0000664 0000000 0000000 00000001413 14527667463 0013444 0 ustar 00root root 0000000 0000000 module git.sr.ht/~emersion/hut
go 1.17
require (
git.sr.ht/~emersion/go-scfg v0.0.0-20211215104734-c2c7a15d6c99
git.sr.ht/~emersion/gqlclient v0.0.0-20230419170751-23b9305960d9
github.com/dustin/go-humanize v1.0.1
github.com/juju/ansiterm v1.0.0
github.com/spf13/cobra v1.7.0
golang.org/x/term v0.7.0
)
require (
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/dave/jennifer v1.6.1 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // 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.1 // indirect
golang.org/x/sys v0.7.0 // indirect
)
hut-0.4.0/go.sum 0000664 0000000 0000000 00000014614 14527667463 0013500 0 ustar 00root root 0000000 0000000 git.sr.ht/~emersion/go-scfg v0.0.0-20211215104734-c2c7a15d6c99 h1:1s8n5uisqkR+BzPgaum6xxIjKmzGrTykJdh+Y3f5Xao=
git.sr.ht/~emersion/go-scfg v0.0.0-20211215104734-c2c7a15d6c99/go.mod h1:t+Ww6SR24yYnXzEWiNlOY0AFo5E9B73X++10lrSpp4U=
git.sr.ht/~emersion/gqlclient v0.0.0-20230419170751-23b9305960d9 h1:WOVvc7oq9Yktm3HV5Q4N88GqBowoP2yrhXSwLJDsYgo=
git.sr.ht/~emersion/gqlclient v0.0.0-20230419170751-23b9305960d9/go.mod h1:RYVSvQ9lRVRfj+UUwVd4tygTYCm2/mj0zJjxVaHIjEY=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
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.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/dave/jennifer v1.6.1 h1:T4T/67t6RAA5AIV6+NP8Uk/BIsXgDoqEowgycdQQLuk=
github.com/dave/jennifer v1.6.1/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.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
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.1 h1:ZGu+bquAY23jsxDRcYpWjttRZrUz07LbiY77gUOHcr4=
github.com/vektah/gqlparser/v2 v2.5.1/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRFZrvOnp3hmCs=
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.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
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.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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.4.0/graphql.go 0000664 0000000 0000000 00000005636 14527667463 0014336 0 ustar 00root root 0000000 0000000 package main
import (
"encoding/json"
"fmt"
"io"
"log"
"mime"
"os"
"path/filepath"
"strings"
"git.sr.ht/~emersion/gqlclient"
"git.sr.ht/~emersion/hut/termfmt"
"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)
// Disable $EDITOR support when not in interactive terminal
if !termfmt.IsTerminal() {
stdin = true
}
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", false, "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.4.0/hg.go 0000664 0000000 0000000 00000017612 14527667463 0013273 0 ustar 00root root 0000000 0000000 package main
import (
"context"
"fmt"
"io"
"log"
"strings"
"git.sr.ht/~emersion/hut/srht/hgsrht"
"git.sr.ht/~emersion/hut/termfmt"
"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(newHgUserWebhookCommand())
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)
}
pagerify(func(p pager) bool {
var repos *hgsrht.RepositoryCursor
if len(username) > 0 {
user, err := hgsrht.RepositoriesByUser(c.Client, ctx, username, cursor)
if err != nil {
log.Fatal(err)
} else if user == nil {
log.Fatal("no such user")
}
repos = user.Repositories
} else {
var err error
repos, err = hgsrht.Repositories(c.Client, ctx, cursor)
if err != nil {
log.Fatal(err)
}
}
for _, repo := range repos.Results {
printHgRepo(p, repo)
}
cursor = repos.Cursor
return cursor == nil
})
}
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
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)
}
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)
return cmd
}
func newHgDeleteCommand() *cobra.Command {
var autoConfirm bool
run := func(cmd *cobra.Command, args []string) {
ctx := cmd.Context()
name, owner, instance := parseResourceName(args[0])
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 ",
Short: "Delete a repository",
Args: cobra.ExactArgs(1),
ValidArgsFunction: cobra.NoFileCompletions,
Run: run,
}
cmd.Flags().BoolVarP(&autoConfirm, "yes", "y", false, "auto confirm")
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", false, "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
pagerify(func(p pager) bool {
webhooks, err := hgsrht.UserWebhooks(c.Client, ctx, cursor)
if err != nil {
log.Fatal(err)
}
for _, webhook := range webhooks.Results {
fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url)
}
cursor = webhooks.Cursor
return cursor == nil
})
}
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 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
}
hut-0.4.0/lists.go 0000664 0000000 0000000 00000101434 14527667463 0014027 0 ustar 00root root 0000000 0000000 package 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/~emersion/hut/srht/listssrht"
"git.sr.ht/~emersion/hut/termfmt"
)
func newListsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "lists",
Short: "Use the lists API",
}
cmd.AddCommand(newListsDeleteCommand())
cmd.AddCommand(newListsListCommand())
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)
}
pagerify(func(p pager) bool {
var lists *listssrht.MailingListCursor
if len(username) > 0 {
user, err := listssrht.MailingListsByUser(c.Client, ctx, username, cursor)
if err != nil {
log.Fatal(err)
} else if user == nil {
log.Fatal("no such user")
}
lists = user.Lists
} else {
var err error
user, err := listssrht.MailingLists(c.Client, ctx, cursor)
if err != nil {
log.Fatal(err)
}
lists = user.Lists
}
for _, list := range lists.Results {
printList(p, &list)
}
cursor = lists.Cursor
return cursor == nil
})
}
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 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", false, "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)
c.HTTP.Timeout = fileTransferTimeout
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)
}
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)
}
}
pagerify(func(p pager) bool {
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 {
log.Fatal(err)
} else if user == nil {
log.Fatalf("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 {
log.Fatal(err)
} else if user == nil {
log.Fatalf("no such user %q", username)
} else if user.List == nil {
log.Fatalf("no such list %q", name)
}
patches = user.List.Patches
}
for _, patchset := range patches.Results {
printPatchset(p, byUser, &patchset)
}
cursor = patches.Cursor
return cursor == nil
})
}
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)
}
pagerify(func(p pager) bool {
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 {
log.Fatal(err)
} else if user == nil {
log.Fatalf("no such user %q", username)
} else if user.List == nil {
log.Fatalf("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
return cursor == nil
})
}
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", false, "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
pagerify(func(p pager) bool {
webhooks, err := listssrht.UserWebhooks(c.Client, ctx, cursor)
if err != nil {
log.Fatal(err)
}
for _, webhook := range webhooks.Results {
fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url)
}
cursor = webhooks.Cursor
return cursor == nil
})
}
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", false, "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)
}
pagerify(func(p pager) bool {
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 {
log.Fatal(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)
}
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
return cursor == nil
})
}
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
pagerify(func(p pager) bool {
subscriptions, err := listssrht.Subscriptions(c.Client, ctx, cursor)
if err != nil {
log.Fatal(err)
}
for _, sub := range subscriptions.Results {
printMailingListSubscription(p, &sub)
}
cursor = subscriptions.Cursor
return cursor == nil
})
}
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
}
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.4.0/main.go 0000664 0000000 0000000 00000011373 14527667463 0013617 0 ustar 00root root 0000000 0000000 package main
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"strconv"
"strings"
"time"
"unicode"
"git.sr.ht/~emersion/hut/termfmt"
"github.com/spf13/cobra"
)
// 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
func main() {
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.AddCommand(newBuildsCommand())
cmd.AddCommand(newExportCommand())
cmd.AddCommand(newGitCommand())
cmd.AddCommand(newGraphqlCommand())
cmd.AddCommand(newHgCommand())
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)
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")
}
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
}
cmd := exec.Command(editor, file.Name())
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
// Disable $EDITOR support when not in interactive terminal
if !termfmt.IsTerminal() {
stdin = true
}
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
}
hut-0.4.0/meta.go 0000664 0000000 0000000 00000036667 14527667463 0013636 0 ustar 00root root 0000000 0000000 package main
import (
"context"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/dustin/go-humanize"
"github.com/spf13/cobra"
"git.sr.ht/~emersion/hut/srht/metasrht"
"git.sr.ht/~emersion/hut/termfmt"
)
func newMetaCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "meta",
Short: "Use the meta API",
}
cmd.AddCommand(newMetaShowCommand())
cmd.AddCommand(newMetaAuditLogCommand())
cmd.AddCommand(newMetaSSHKeyCommand())
cmd.AddCommand(newMetaPGPKeyCommand())
cmd.AddCommand(newMetaUserWebhookCommand())
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
pagerify(func(p pager) bool {
logs, err := metasrht.AuditLog(c.Client, ctx, cursor)
if err != nil {
log.Fatal(err)
}
for _, log := range logs.Results {
printAuditLog(p, &log)
}
cursor = logs.Cursor
return cursor == nil
})
}
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 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)
}
pagerify(func(p pager) bool {
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 {
log.Fatal(err)
} else if user == nil {
log.Fatalf("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
return cursor == nil
})
}
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(ctx)
}
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(ctx context.Context) ([]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)
}
pagerify(func(p pager) bool {
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 {
log.Fatal(err)
} else if user == nil {
log.Fatalf("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
return cursor == nil
})
}
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", false, "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
pagerify(func(p pager) bool {
webhooks, err := metasrht.UserWebhooks(c.Client, ctx, cursor)
if err != nil {
log.Fatal(err)
}
for _, webhook := range webhooks.Results {
fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url)
}
cursor = webhooks.Cursor
return cursor == nil
})
}
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 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.4.0/pager.go 0000664 0000000 0000000 00000003034 14527667463 0013764 0 ustar 00root root 0000000 0000000 package main
import (
"io"
"log"
"os"
"os/exec"
"git.sr.ht/~emersion/hut/termfmt"
)
type pager interface {
io.WriteCloser
Running() bool
}
func newPager() pager {
if !termfmt.IsTerminal() {
return &singleWritePager{os.Stdout, true}
}
name, ok := os.LookupEnv("PAGER")
if !ok {
name = "less"
}
cmd := exec.Command(name)
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) bool
func pagerify(fn pagerifyFn) {
pager := newPager()
defer pager.Close()
for pager.Running() {
shouldStop := fn(pager)
if shouldStop {
break
}
}
}
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.4.0/pages.go 0000664 0000000 0000000 00000025210 14527667463 0013765 0 ustar 00root root 0000000 0000000 package 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/~emersion/hut/srht/pagessrht"
"git.sr.ht/~emersion/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, notFound, 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
}
if notFound != "" {
siteConfig.NotFound = ¬Found
}
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(¬Found, "not-found", "", "path to serve for page not found responses")
cmd.Flags().MarkDeprecated("not-found", "use site-config instead")
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)
}
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
pagerify(func(p pager) bool {
sites, err := pagessrht.Sites(c.Client, ctx, cursor)
if err != nil {
log.Fatalf("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
return cursor == nil
})
}
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", false, "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
pagerify(func(p pager) bool {
webhooks, err := pagessrht.UserWebhooks(c.Client, ctx, cursor)
if err != nil {
log.Fatal(err)
}
for _, webhook := range webhooks.Results {
fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url)
}
cursor = webhooks.Cursor
return cursor == nil
})
}
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.4.0/parsing_test.go 0000664 0000000 0000000 00000002737 14527667463 0015401 0 ustar 00root root 0000000 0000000 package 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.4.0/paste.go 0000664 0000000 0000000 00000025547 14527667463 0014017 0 ustar 00root root 0000000 0000000 package 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/~emersion/hut/srht/pastesrht"
"git.sr.ht/~emersion/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
pagerify(func(p pager) bool {
pastes, err := pastesrht.Pastes(c.Client, ctx, cursor)
if err != nil {
log.Fatal(err)
}
for _, paste := range pastes.Results {
printPaste(p, &paste)
fmt.Fprintln(p)
}
cursor = pastes.Cursor
return cursor == nil
})
}
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", false, "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
pagerify(func(p pager) bool {
webhooks, err := pastesrht.UserWebhooks(c.Client, ctx, cursor)
if err != nil {
log.Fatal(err)
}
for _, webhook := range webhooks.Results {
fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url)
}
cursor = webhooks.Cursor
return cursor == nil
})
}
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.4.0/srht/ 0000775 0000000 0000000 00000000000 14527667463 0013317 5 ustar 00root root 0000000 0000000 hut-0.4.0/srht/README.md 0000664 0000000 0000000 00000000257 14527667463 0014602 0 ustar 00root root 0000000 0000000 Code 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.4.0/srht/buildssrht/ 0000775 0000000 0000000 00000000000 14527667463 0015502 5 ustar 00root root 0000000 0000000 hut-0.4.0/srht/buildssrht/gql.go 0000664 0000000 0000000 00000055461 14527667463 0016627 0 ustar 00root root 0000000 0000000 // 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 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\tid\n\t\t\tstatus\n\t\t\tnote\n\t\t\ttags\n\t\t\tvisibility\n\t\t\tlog {\n\t\t\t\tfullURL\n\t\t\t}\n\t\t\ttasks {\n\t\t\t\tname\n\t\t\t\tstatus\n\t\t\t\tlog {\n\t\t\t\t\tfullURL\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 {
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.4.0/srht/buildssrht/operations.graphql 0000664 0000000 0000000 00000006351 14527667463 0021252 0 ustar 00root root 0000000 0000000 mutation 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 exportJobs($cursor: Cursor) {
jobs(cursor: $cursor) {
results {
id
status
note
tags
visibility
log {
fullURL
}
tasks {
name
status
log {
fullURL
}
}
}
cursor
}
}
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.4.0/srht/buildssrht/schema.graphqls 0000664 0000000 0000000 00000030274 14527667463 0020513 0 ustar 00root root 0000000 0000000 # 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 faciliate 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.4.0/srht/buildssrht/strings.go 0000664 0000000 0000000 00000004662 14527667463 0017532 0 ustar 00root root 0000000 0000000 package buildssrht
import (
"fmt"
"strings"
"git.sr.ht/~emersion/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.4.0/srht/generate.go 0000664 0000000 0000000 00000002414 14527667463 0015441 0 ustar 00root root 0000000 0000000 //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.4.0/srht/gitsrht/ 0000775 0000000 0000000 00000000000 14527667463 0015003 5 ustar 00root root 0000000 0000000 hut-0.4.0/srht/gitsrht/gql.go 0000664 0000000 0000000 00000074563 14527667463 0016134 0 ustar 00root root 0000000 0000000 // 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 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 RepoNames(client *gqlclient.Client, ctx context.Context) (repositories *RepositoryCursor, err error) {
op := gqlclient.NewOperation("query repoNames {\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\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
}
hut-0.4.0/srht/gitsrht/methods.go 0000664 0000000 0000000 00000000773 14527667463 0017004 0 ustar 00root root 0000000 0000000 package 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.4.0/srht/gitsrht/operations.graphql 0000664 0000000 0000000 00000011506 14527667463 0020551 0 ustar 00root root 0000000 0000000 query 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 sshSettings {
version {
settings {
sshUser
}
}
}
query repoNames {
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
) {
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
}
}
hut-0.4.0/srht/gitsrht/schema.graphqls 0000664 0000000 0000000 00000035573 14527667463 0020023 0 ustar 00root root 0000000 0000000 # 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 @access(scope: REPOSITORIES, kind: RO)
REPO_UPDATE @access(scope: REPOSITORIES, kind: RO)
REPO_DELETED @access(scope: REPOSITORIES, kind: RO)
}
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.4.0/srht/gitsrht/strings.go 0000664 0000000 0000000 00000002657 14527667463 0017035 0 ustar 00root root 0000000 0000000 package gitsrht
import (
"fmt"
"strings"
"git.sr.ht/~emersion/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.4.0/srht/hgsrht/ 0000775 0000000 0000000 00000000000 14527667463 0014616 5 ustar 00root root 0000000 0000000 hut-0.4.0/srht/hgsrht/gql.go 0000664 0000000 0000000 00000041161 14527667463 0015733 0 ustar 00root root 0000000 0000000 // 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 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 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\tname\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 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
}
hut-0.4.0/srht/hgsrht/operations.graphql 0000664 0000000 0000000 00000002633 14527667463 0020365 0 ustar 00root root 0000000 0000000 query 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 userWebhooks($cursor: Cursor) {
userWebhooks(cursor: $cursor) {
results {
id
url
}
cursor
}
}
mutation createRepository(
$name: String!
$visibility: Visibility!
$description: String!
) {
createRepository(
name: $name
visibility: $visibility
description: $description
) {
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
}
}
hut-0.4.0/srht/hgsrht/schema.graphqls 0000664 0000000 0000000 00000030061 14527667463 0017621 0 ustar 00root root 0000000 0000000 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")
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 @access(scope: REPOSITORIES, kind: RO)
REPO_UPDATE @access(scope: REPOSITORIES, kind: RO)
REPO_DELETED @access(scope: REPOSITORIES, kind: RO)
}
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.4.0/srht/hgsrht/strings.go 0000664 0000000 0000000 00000002314 14527667463 0016636 0 ustar 00root root 0000000 0000000 package hgsrht
import (
"fmt"
"strings"
"git.sr.ht/~emersion/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
}
hut-0.4.0/srht/listssrht/ 0000775 0000000 0000000 00000000000 14527667463 0015356 5 ustar 00root root 0000000 0000000 hut-0.4.0/srht/listssrht/gql.go 0000664 0000000 0000000 00000116722 14527667463 0016501 0 ustar 00root root 0000000 0000000 // 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,omitempty"`
Emails *EmailCursor `json:"emails,omitempty"`
Threads *ThreadCursor `json:"threads,omitempty"`
Patches *PatchsetCursor `json:"patches,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 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 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\tname\n\t\t\t\tdescription\n\t\t\t\tpermitMime\n\t\t\t\trejectMime\n\t\t\t\tarchive\n\t\t\t}\n\t\t\tcursor\n\t\t}\n\t}\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 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\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 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.4.0/srht/listssrht/operations.graphql 0000664 0000000 0000000 00000015032 14527667463 0021122 0 ustar 00root root 0000000 0000000 mutation deleteMailingList($id: Int!) {
deleteMailingList(id: $id) {
name
}
}
query mailingLists($cursor: Cursor) {
me {
lists(cursor: $cursor) {
...lists
}
}
}
query exportMailingLists($cursor: Cursor) {
me {
lists(cursor: $cursor) {
results {
name
description
permitMime
rejectMime
archive
}
cursor
}
}
}
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
}
}
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
) {
name
}
}
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.4.0/srht/listssrht/schema.graphqls 0000664 0000000 0000000 00000050154 14527667463 0020366 0 ustar 00root root 0000000 0000000 # 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 @access(scope: LISTS, kind: RO)
LIST_UPDATED @access(scope: LISTS, kind: RO)
LIST_DELETED @access(scope: LISTS, kind: RO)
EMAIL_RECEIVED @access(scope: EMAILS, kind: RO)
PATCHSET_RECEIVED @access(scope: PATCHES, kind: RO)
}
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!
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.4.0/srht/listssrht/strings.go 0000664 0000000 0000000 00000006716 14527667463 0017410 0 ustar 00root root 0000000 0000000 package listssrht
import (
"fmt"
"strings"
"git.sr.ht/~emersion/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.4.0/srht/metasrht/ 0000775 0000000 0000000 00000000000 14527667463 0015146 5 ustar 00root root 0000000 0000000 hut-0.4.0/srht/metasrht/gql.go 0000664 0000000 0000000 00000051235 14527667463 0016266 0 ustar 00root root 0000000 0000000 // 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"`
}
type OAuthGrantRegistration struct {
Grant *OAuthGrant `json:"grant"`
Grants string `json:"grants"`
Secret string `json:"secret"`
}
type OAuthPersonalToken struct {
Id int32 `json:"id"`
Issued gqlclient.Time `json:"issued"`
Expires gqlclient.Time `json:"expires"`
Comment *string `json:"comment,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"`
}
// 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 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
}
hut-0.4.0/srht/metasrht/operations.graphql 0000664 0000000 0000000 00000005062 14527667463 0020714 0 ustar 00root root 0000000 0000000 query 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
}
}
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
}
}
hut-0.4.0/srht/metasrht/schema.graphqls 0000664 0000000 0000000 00000035775 14527667463 0020172 0 ustar 00root root 0000000 0000000 # 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 | ENUM_VALUE
# 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
}
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
}
type OAuthGrantRegistration {
grant: OAuthGrant!
grants: String!
secret: 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
}
type OAuthPersonalTokenRegistration {
token: OAuthPersonalToken!
secret: String!
}
enum WebhookEvent {
"Used for user profile webhooks"
PROFILE_UPDATE @access(scope: PROFILE, kind: RO)
PGP_KEY_ADDED @access(scope: PGP_KEYS, kind: RO)
PGP_KEY_REMOVED @access(scope: PGP_KEYS, kind: RO)
SSH_KEY_ADDED @access(scope: SSH_KEYS, kind: RO)
SSH_KEY_REMOVED @access(scope: SSH_KEYS, kind: RO)
}
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
}
"""
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 updated.
"""
updateSSHKey(id: Int!): SSHKey! @access(scope: SSH_KEYS, kind: RO)
"""
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!,
clientSecret: String!, redirectUri: 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
"""
Sends a notification email to the given user.
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). It will be encrypted according to the user's
privacy settings and signed with the site key.
"""
sendEmailNotification(username: String!, message: String!): Boolean! @anoninternal
"""
Sends a notification email to an external address.
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). It will be signed with the site key.
"""
sendEmailExternal(address: String!, message: String!): Boolean! @anoninternal
"""
Deletes the authenticated user's account.
"""
deleteUser(reserve: Boolean!): Int! @internal
}
hut-0.4.0/srht/metasrht/strings.go 0000664 0000000 0000000 00000001320 14527667463 0017162 0 ustar 00root root 0000000 0000000 package 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.4.0/srht/pagessrht/ 0000775 0000000 0000000 00000000000 14527667463 0015317 5 ustar 00root root 0000000 0000000 hut-0.4.0/srht/pagessrht/gql.go 0000664 0000000 0000000 00000026355 14527667463 0016444 0 ustar 00root root 0000000 0000000 // 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.4.0/srht/pagessrht/operations.graphql 0000664 0000000 0000000 00000001647 14527667463 0021072 0 ustar 00root root 0000000 0000000 mutation 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.4.0/srht/pagessrht/schema.graphqls 0000664 0000000 0000000 00000020147 14527667463 0020326 0 ustar 00root root 0000000 0000000 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
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 @access(scope: SITES, kind: RO)
SITE_UNPUBLISHED @access(scope: SITES, kind: RO)
}
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!
}
hut-0.4.0/srht/pagessrht/strings.go 0000664 0000000 0000000 00000001305 14527667463 0017336 0 ustar 00root root 0000000 0000000 package 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.4.0/srht/pastesrht/ 0000775 0000000 0000000 00000000000 14527667463 0015334 5 ustar 00root root 0000000 0000000 hut-0.4.0/srht/pastesrht/gql.go 0000664 0000000 0000000 00000030712 14527667463 0016451 0 ustar 00root root 0000000 0000000 // 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\tid\n\t\t\tcreated\n\t\t\tvisibility\n\t\t\tfiles {\n\t\t\t\tfilename\n\t\t\t\tcontents\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 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.4.0/srht/pastesrht/operations.graphql 0000664 0000000 0000000 00000003140 14527667463 0021075 0 ustar 00root root 0000000 0000000 mutation 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 {
id
created
visibility
files {
filename
contents
}
}
cursor
}
}
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.4.0/srht/pastesrht/schema.graphqls 0000664 0000000 0000000 00000017722 14527667463 0020350 0 ustar 00root root 0000000 0000000 # 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
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_VALUE
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 @access(scope: PASTES, kind: RO)
PASTE_UPDATED @access(scope: PASTES, kind: RO)
PASTE_DELETED @access(scope: PASTES, kind: RO)
}
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!
}
hut-0.4.0/srht/pastesrht/strings.go 0000664 0000000 0000000 00000002323 14527667463 0017354 0 ustar 00root root 0000000 0000000 package pastesrht
import (
"fmt"
"strings"
"git.sr.ht/~emersion/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.4.0/srht/todosrht/ 0000775 0000000 0000000 00000000000 14527667463 0015165 5 ustar 00root root 0000000 0000000 hut-0.4.0/srht/todosrht/gql.go 0000664 0000000 0000000 00000165755 14527667463 0016322 0 ustar 00root root 0000000 0000000 // 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"`
}
// 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 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\tname\n\t\t\tdescription\n\t\t\tvisibility\n\t\t\texport\n\t\t}\n\t\tcursor\n\t}\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 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 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 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 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
}
hut-0.4.0/srht/todosrht/operations.graphql 0000664 0000000 0000000 00000030402 14527667463 0020727 0 ustar 00root root 0000000 0000000 query 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 exportTrackers($cursor: Cursor) {
trackers(cursor: $cursor) {
results {
name
description
visibility
export
}
cursor
}
}
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 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
}
}
}
}
}
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 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 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
}
}
}
hut-0.4.0/srht/todosrht/schema.graphqls 0000664 0000000 0000000 00000063672 14527667463 0020206 0 ustar 00root root 0000000 0000000 # 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 @access(scope: TRACKERS, kind: RO)
TRACKER_UPDATE @access(scope: TRACKERS, kind: RO)
TRACKER_DELETED @access(scope: TRACKERS, kind: RO)
TICKET_CREATED @access(scope: TICKETS, kind: RO)
TICKET_UPDATE @access(scope: TICKETS, kind: RO)
TICKET_DELETED @access(scope: TICKETS, kind: RO)
LABEL_CREATED @access(scope: TRACKERS, kind: RO)
LABEL_UPDATE @access(scope: TRACKERS, kind: RO)
LABEL_DELETED @access(scope: TRACKERS, kind: RO)
EVENT_CREATED @access(scope: EVENTS, kind: RO)
}
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!]
}
"""
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!,
import: 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.4.0/srht/todosrht/strings.go 0000664 0000000 0000000 00000011471 14527667463 0017211 0 ustar 00root root 0000000 0000000 package todosrht
import (
"fmt"
"strings"
"git.sr.ht/~emersion/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.4.0/termfmt/ 0000775 0000000 0000000 00000000000 14527667463 0014015 5 ustar 00root root 0000000 0000000 hut-0.4.0/termfmt/formatting.go 0000664 0000000 0000000 00000004055 14527667463 0016522 0 ustar 00root root 0000000 0000000 package termfmt
import (
"fmt"
"log"
"os"
"strconv"
"strings"
"golang.org/x/term"
)
var isTerminal = term.IsTerminal(int(os.Stdout.Fd()))
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 IsTerminal() bool {
return isTerminal
}
func Bell() {
if isTerminal {
fmt.Print("\a")
}
}
hut-0.4.0/todo.go 0000664 0000000 0000000 00000154321 14527667463 0013641 0 ustar 00root root 0000000 0000000 package main
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"log"
"math"
"os"
"strings"
"git.sr.ht/~emersion/hut/srht/todosrht"
"git.sr.ht/~emersion/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(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)
}
pagerify(func(p pager) bool {
var trackers *todosrht.TrackerCursor
if len(username) > 0 {
user, err := todosrht.TrackersByUser(c.Client, ctx, username, cursor)
if err != nil {
log.Fatal(err)
} else if user == nil {
log.Fatal("no such user")
}
trackers = user.Trackers
} else {
var err error
trackers, err = todosrht.Trackers(c.Client, ctx, cursor)
if err != nil {
log.Fatal(err)
}
}
for _, tracker := range trackers.Results {
printTracker(p, &tracker)
}
cursor = trackers.Cursor
return cursor == nil
})
}
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()
name, owner, instance := parseResourceName(args[0])
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 ",
Short: "Delete a tracker",
Args: cobra.ExactArgs(1),
ValidArgsFunction: completeTracker,
Run: run,
}
cmd.Flags().BoolVarP(&autoConfirm, "yes", "y", false, "auto confirm")
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", false, "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(newTodoTicketLabelCommand())
cmd.AddCommand(newTodoTicketUnlabelCommand())
return cmd
}
func newTodoTicketListCommand() *cobra.Command {
// TODO: Filter by ticket status
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)
}
pagerify(func(p pager) bool {
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 {
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)
}
for _, ticket := range user.Tracker.Tickets.Results {
printTicket(p, &ticket)
}
cursor = user.Tracker.Tickets.Cursor
return cursor == nil
})
}
cmd := &cobra.Command{
Use: "list",
Short: "List tickets",
Args: cobra.ExactArgs(0),
Run: run,
}
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", false, "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)
} else if event == nil {
log.Fatalf("failed to update status of ticket with ID %d", ticketID)
}
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\n", termfmt.Bold.String(ticket.Subject))
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", false, "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)
}
pagerify(func(p pager) bool {
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 {
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)
}
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
return cursor == nil
})
}
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())
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)
}
pagerify(func(p pager) bool {
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 {
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)
}
for _, label := range user.Tracker.Labels.Results {
fmt.Fprintln(p, label.TermString())
}
cursor = user.Tracker.Labels.Cursor
return cursor == nil
})
}
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 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)
}
pagerify(func(p pager) bool {
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 {
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)
}
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
return cursor == nil
})
}
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", false, "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)
}
pagerify(func(p pager) bool {
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 {
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)
}
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
return cursor == nil
})
}
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", false, "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
pagerify(func(p pager) bool {
webhooks, err := todosrht.UserWebhooks(c.Client, ctx, cursor)
if err != nil {
log.Fatal(err)
}
for _, webhook := range webhooks.Results {
fmt.Fprintf(p, "%s %s\n", termfmt.DarkYellow.Sprintf("#%d", webhook.Id), webhook.Url)
}
cursor = webhooks.Cursor
return cursor == nil
})
}
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", false, "read ticket from stdin")
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")
// TODO: complete unassigned labels
cmd.RegisterFlagCompletionFunc("label", cobra.NoFileCompletions)
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")
// TODO: complete assigned labels
cmd.RegisterFlagCompletionFunc("label", cobra.NoFileCompletions)
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
}
// 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
}