A terminal client for Tangled.
tui go tangled cli
6

Configure Feed

Select the types of activity you want to include in your feed.

init

Aly Raffauf (Jul 7, 2026, 9:52 AM EDT) bbdeaa56

+2266
+2
.envrc
··· 1 + # shellcheck shell=bash 2 + use flake
+1
.gitignore
··· 1 + tg
+594
LICENSE.md
··· 1 + # GNU General Public License 2 + 3 + _Version 3, 29 June 2007_ 4 + _Copyright © 2007 Free Software Foundation, Inc. &lt;<http://fsf.org/>&gt;_ 5 + 6 + Everyone is permitted to copy and distribute verbatim copies of this license 7 + document, but changing it is not allowed. 8 + 9 + ## Preamble 10 + 11 + The GNU General Public License is a free, copyleft license for software and other 12 + kinds of works. 13 + 14 + The licenses for most software and other practical works are designed to take away 15 + your freedom to share and change the works. By contrast, the GNU General Public 16 + License is intended to guarantee your freedom to share and change all versions of a 17 + program--to make sure it remains free software for all its users. We, the Free 18 + Software Foundation, use the GNU General Public License for most of our software; it 19 + applies also to any other work released this way by its authors. You can apply it to 20 + your programs, too. 21 + 22 + When we speak of free software, we are referring to freedom, not price. Our General 23 + Public Licenses are designed to make sure that you have the freedom to distribute 24 + copies of free software (and charge for them if you wish), that you receive source 25 + code or can get it if you want it, that you can change the software or use pieces of 26 + it in new free programs, and that you know you can do these things. 27 + 28 + To protect your rights, we need to prevent others from denying you these rights or 29 + asking you to surrender the rights. Therefore, you have certain responsibilities if 30 + you distribute copies of the software, or if you modify it: responsibilities to 31 + respect the freedom of others. 32 + 33 + For example, if you distribute copies of such a program, whether gratis or for a fee, 34 + you must pass on to the recipients the same freedoms that you received. You must make 35 + sure that they, too, receive or can get the source code. And you must show them these 36 + terms so they know their rights. 37 + 38 + Developers that use the GNU GPL protect your rights with two steps: **(1)** assert 39 + copyright on the software, and **(2)** offer you this License giving you legal permission 40 + to copy, distribute and/or modify it. 41 + 42 + For the developers' and authors' protection, the GPL clearly explains that there is 43 + no warranty for this free software. For both users' and authors' sake, the GPL 44 + requires that modified versions be marked as changed, so that their problems will not 45 + be attributed erroneously to authors of previous versions. 46 + 47 + Some devices are designed to deny users access to install or run modified versions of 48 + the software inside them, although the manufacturer can do so. This is fundamentally 49 + incompatible with the aim of protecting users' freedom to change the software. The 50 + systematic pattern of such abuse occurs in the area of products for individuals to 51 + use, which is precisely where it is most unacceptable. Therefore, we have designed 52 + this version of the GPL to prohibit the practice for those products. If such problems 53 + arise substantially in other domains, we stand ready to extend this provision to 54 + those domains in future versions of the GPL, as needed to protect the freedom of 55 + users. 56 + 57 + Finally, every program is threatened constantly by software patents. States should 58 + not allow patents to restrict development and use of software on general-purpose 59 + computers, but in those that do, we wish to avoid the special danger that patents 60 + applied to a free program could make it effectively proprietary. To prevent this, the 61 + GPL assures that patents cannot be used to render the program non-free. 62 + 63 + The precise terms and conditions for copying, distribution and modification follow. 64 + 65 + ## TERMS AND CONDITIONS 66 + 67 + ### 0. Definitions 68 + 69 + “This License” refers to version 3 of the GNU General Public License. 70 + 71 + “Copyright” also means copyright-like laws that apply to other kinds of 72 + works, such as semiconductor masks. 73 + 74 + “The Program” refers to any copyrightable work licensed under this 75 + License. Each licensee is addressed as “you”. “Licensees” and 76 + “recipients” may be individuals or organizations. 77 + 78 + To “modify” a work means to copy from or adapt all or part of the work in 79 + a fashion requiring copyright permission, other than the making of an exact copy. The 80 + resulting work is called a “modified version” of the earlier work or a 81 + work “based on” the earlier work. 82 + 83 + A “covered work” means either the unmodified Program or a work based on 84 + the Program. 85 + 86 + To “propagate” a work means to do anything with it that, without 87 + permission, would make you directly or secondarily liable for infringement under 88 + applicable copyright law, except executing it on a computer or modifying a private 89 + copy. Propagation includes copying, distribution (with or without modification), 90 + making available to the public, and in some countries other activities as well. 91 + 92 + To “convey” a work means any kind of propagation that enables other 93 + parties to make or receive copies. Mere interaction with a user through a computer 94 + network, with no transfer of a copy, is not conveying. 95 + 96 + An interactive user interface displays “Appropriate Legal Notices” to the 97 + extent that it includes a convenient and prominently visible feature that **(1)** 98 + displays an appropriate copyright notice, and **(2)** tells the user that there is no 99 + warranty for the work (except to the extent that warranties are provided), that 100 + licensees may convey the work under this License, and how to view a copy of this 101 + License. If the interface presents a list of user commands or options, such as a 102 + menu, a prominent item in the list meets this criterion. 103 + 104 + ### 1. Source Code 105 + 106 + The “source code” for a work means the preferred form of the work for 107 + making modifications to it. “Object code” means any non-source form of a 108 + work. 109 + 110 + A “Standard Interface” means an interface that either is an official 111 + standard defined by a recognized standards body, or, in the case of interfaces 112 + specified for a particular programming language, one that is widely used among 113 + developers working in that language. 114 + 115 + The “System Libraries” of an executable work include anything, other than 116 + the work as a whole, that **(a)** is included in the normal form of packaging a Major 117 + Component, but which is not part of that Major Component, and **(b)** serves only to 118 + enable use of the work with that Major Component, or to implement a Standard 119 + Interface for which an implementation is available to the public in source code form. 120 + A “Major Component”, in this context, means a major essential component 121 + (kernel, window system, and so on) of the specific operating system (if any) on which 122 + the executable work runs, or a compiler used to produce the work, or an object code 123 + interpreter used to run it. 124 + 125 + The “Corresponding Source” for a work in object code form means all the 126 + source code needed to generate, install, and (for an executable work) run the object 127 + code and to modify the work, including scripts to control those activities. However, 128 + it does not include the work's System Libraries, or general-purpose tools or 129 + generally available free programs which are used unmodified in performing those 130 + activities but which are not part of the work. For example, Corresponding Source 131 + includes interface definition files associated with source files for the work, and 132 + the source code for shared libraries and dynamically linked subprograms that the work 133 + is specifically designed to require, such as by intimate data communication or 134 + control flow between those subprograms and other parts of the work. 135 + 136 + The Corresponding Source need not include anything that users can regenerate 137 + automatically from other parts of the Corresponding Source. 138 + 139 + The Corresponding Source for a work in source code form is that same work. 140 + 141 + ### 2. Basic Permissions 142 + 143 + All rights granted under this License are granted for the term of copyright on the 144 + Program, and are irrevocable provided the stated conditions are met. This License 145 + explicitly affirms your unlimited permission to run the unmodified Program. The 146 + output from running a covered work is covered by this License only if the output, 147 + given its content, constitutes a covered work. This License acknowledges your rights 148 + of fair use or other equivalent, as provided by copyright law. 149 + 150 + You may make, run and propagate covered works that you do not convey, without 151 + conditions so long as your license otherwise remains in force. You may convey covered 152 + works to others for the sole purpose of having them make modifications exclusively 153 + for you, or provide you with facilities for running those works, provided that you 154 + comply with the terms of this License in conveying all material for which you do not 155 + control copyright. Those thus making or running the covered works for you must do so 156 + exclusively on your behalf, under your direction and control, on terms that prohibit 157 + them from making any copies of your copyrighted material outside their relationship 158 + with you. 159 + 160 + Conveying under any other circumstances is permitted solely under the conditions 161 + stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 162 + 163 + ### 3. Protecting Users' Legal Rights From Anti-Circumvention Law 164 + 165 + No covered work shall be deemed part of an effective technological measure under any 166 + applicable law fulfilling obligations under article 11 of the WIPO copyright treaty 167 + adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention 168 + of such measures. 169 + 170 + When you convey a covered work, you waive any legal power to forbid circumvention of 171 + technological measures to the extent such circumvention is effected by exercising 172 + rights under this License with respect to the covered work, and you disclaim any 173 + intention to limit operation or modification of the work as a means of enforcing, 174 + against the work's users, your or third parties' legal rights to forbid circumvention 175 + of technological measures. 176 + 177 + ### 4. Conveying Verbatim Copies 178 + 179 + You may convey verbatim copies of the Program's source code as you receive it, in any 180 + medium, provided that you conspicuously and appropriately publish on each copy an 181 + appropriate copyright notice; keep intact all notices stating that this License and 182 + any non-permissive terms added in accord with section 7 apply to the code; keep 183 + intact all notices of the absence of any warranty; and give all recipients a copy of 184 + this License along with the Program. 185 + 186 + You may charge any price or no price for each copy that you convey, and you may offer 187 + support or warranty protection for a fee. 188 + 189 + ### 5. Conveying Modified Source Versions 190 + 191 + You may convey a work based on the Program, or the modifications to produce it from 192 + the Program, in the form of source code under the terms of section 4, provided that 193 + you also meet all of these conditions: 194 + 195 + - **a)** The work must carry prominent notices stating that you modified it, and giving a 196 + relevant date. 197 + - **b)** The work must carry prominent notices stating that it is released under this 198 + License and any conditions added under section 7. This requirement modifies the 199 + requirement in section 4 to “keep intact all notices”. 200 + - **c)** You must license the entire work, as a whole, under this License to anyone who 201 + comes into possession of a copy. This License will therefore apply, along with any 202 + applicable section 7 additional terms, to the whole of the work, and all its parts, 203 + regardless of how they are packaged. This License gives no permission to license the 204 + work in any other way, but it does not invalidate such permission if you have 205 + separately received it. 206 + - **d)** If the work has interactive user interfaces, each must display Appropriate Legal 207 + Notices; however, if the Program has interactive interfaces that do not display 208 + Appropriate Legal Notices, your work need not make them do so. 209 + 210 + A compilation of a covered work with other separate and independent works, which are 211 + not by their nature extensions of the covered work, and which are not combined with 212 + it such as to form a larger program, in or on a volume of a storage or distribution 213 + medium, is called an “aggregate” if the compilation and its resulting 214 + copyright are not used to limit the access or legal rights of the compilation's users 215 + beyond what the individual works permit. Inclusion of a covered work in an aggregate 216 + does not cause this License to apply to the other parts of the aggregate. 217 + 218 + ### 6. Conveying Non-Source Forms 219 + 220 + You may convey a covered work in object code form under the terms of sections 4 and 221 + 5, provided that you also convey the machine-readable Corresponding Source under the 222 + terms of this License, in one of these ways: 223 + 224 + - **a)** Convey the object code in, or embodied in, a physical product (including a 225 + physical distribution medium), accompanied by the Corresponding Source fixed on a 226 + durable physical medium customarily used for software interchange. 227 + - **b)** Convey the object code in, or embodied in, a physical product (including a 228 + physical distribution medium), accompanied by a written offer, valid for at least 229 + three years and valid for as long as you offer spare parts or customer support for 230 + that product model, to give anyone who possesses the object code either **(1)** a copy of 231 + the Corresponding Source for all the software in the product that is covered by this 232 + License, on a durable physical medium customarily used for software interchange, for 233 + a price no more than your reasonable cost of physically performing this conveying of 234 + source, or **(2)** access to copy the Corresponding Source from a network server at no 235 + charge. 236 + - **c)** Convey individual copies of the object code with a copy of the written offer to 237 + provide the Corresponding Source. This alternative is allowed only occasionally and 238 + noncommercially, and only if you received the object code with such an offer, in 239 + accord with subsection 6b. 240 + - **d)** Convey the object code by offering access from a designated place (gratis or for 241 + a charge), and offer equivalent access to the Corresponding Source in the same way 242 + through the same place at no further charge. You need not require recipients to copy 243 + the Corresponding Source along with the object code. If the place to copy the object 244 + code is a network server, the Corresponding Source may be on a different server 245 + (operated by you or a third party) that supports equivalent copying facilities, 246 + provided you maintain clear directions next to the object code saying where to find 247 + the Corresponding Source. Regardless of what server hosts the Corresponding Source, 248 + you remain obligated to ensure that it is available for as long as needed to satisfy 249 + these requirements. 250 + - **e)** Convey the object code using peer-to-peer transmission, provided you inform 251 + other peers where the object code and Corresponding Source of the work are being 252 + offered to the general public at no charge under subsection 6d. 253 + 254 + A separable portion of the object code, whose source code is excluded from the 255 + Corresponding Source as a System Library, need not be included in conveying the 256 + object code work. 257 + 258 + A “User Product” is either **(1)** a “consumer product”, which 259 + means any tangible personal property which is normally used for personal, family, or 260 + household purposes, or **(2)** anything designed or sold for incorporation into a 261 + dwelling. In determining whether a product is a consumer product, doubtful cases 262 + shall be resolved in favor of coverage. For a particular product received by a 263 + particular user, “normally used” refers to a typical or common use of 264 + that class of product, regardless of the status of the particular user or of the way 265 + in which the particular user actually uses, or expects or is expected to use, the 266 + product. A product is a consumer product regardless of whether the product has 267 + substantial commercial, industrial or non-consumer uses, unless such uses represent 268 + the only significant mode of use of the product. 269 + 270 + “Installation Information” for a User Product means any methods, 271 + procedures, authorization keys, or other information required to install and execute 272 + modified versions of a covered work in that User Product from a modified version of 273 + its Corresponding Source. The information must suffice to ensure that the continued 274 + functioning of the modified object code is in no case prevented or interfered with 275 + solely because modification has been made. 276 + 277 + If you convey an object code work under this section in, or with, or specifically for 278 + use in, a User Product, and the conveying occurs as part of a transaction in which 279 + the right of possession and use of the User Product is transferred to the recipient 280 + in perpetuity or for a fixed term (regardless of how the transaction is 281 + characterized), the Corresponding Source conveyed under this section must be 282 + accompanied by the Installation Information. But this requirement does not apply if 283 + neither you nor any third party retains the ability to install modified object code 284 + on the User Product (for example, the work has been installed in ROM). 285 + 286 + The requirement to provide Installation Information does not include a requirement to 287 + continue to provide support service, warranty, or updates for a work that has been 288 + modified or installed by the recipient, or for the User Product in which it has been 289 + modified or installed. Access to a network may be denied when the modification itself 290 + materially and adversely affects the operation of the network or violates the rules 291 + and protocols for communication across the network. 292 + 293 + Corresponding Source conveyed, and Installation Information provided, in accord with 294 + this section must be in a format that is publicly documented (and with an 295 + implementation available to the public in source code form), and must require no 296 + special password or key for unpacking, reading or copying. 297 + 298 + ### 7. Additional Terms 299 + 300 + “Additional permissions” are terms that supplement the terms of this 301 + License by making exceptions from one or more of its conditions. Additional 302 + permissions that are applicable to the entire Program shall be treated as though they 303 + were included in this License, to the extent that they are valid under applicable 304 + law. If additional permissions apply only to part of the Program, that part may be 305 + used separately under those permissions, but the entire Program remains governed by 306 + this License without regard to the additional permissions. 307 + 308 + When you convey a copy of a covered work, you may at your option remove any 309 + additional permissions from that copy, or from any part of it. (Additional 310 + permissions may be written to require their own removal in certain cases when you 311 + modify the work.) You may place additional permissions on material, added by you to a 312 + covered work, for which you have or can give appropriate copyright permission. 313 + 314 + Notwithstanding any other provision of this License, for material you add to a 315 + covered work, you may (if authorized by the copyright holders of that material) 316 + supplement the terms of this License with terms: 317 + 318 + - **a)** Disclaiming warranty or limiting liability differently from the terms of 319 + sections 15 and 16 of this License; or 320 + - **b)** Requiring preservation of specified reasonable legal notices or author 321 + attributions in that material or in the Appropriate Legal Notices displayed by works 322 + containing it; or 323 + - **c)** Prohibiting misrepresentation of the origin of that material, or requiring that 324 + modified versions of such material be marked in reasonable ways as different from the 325 + original version; or 326 + - **d)** Limiting the use for publicity purposes of names of licensors or authors of the 327 + material; or 328 + - **e)** Declining to grant rights under trademark law for use of some trade names, 329 + trademarks, or service marks; or 330 + - **f)** Requiring indemnification of licensors and authors of that material by anyone 331 + who conveys the material (or modified versions of it) with contractual assumptions of 332 + liability to the recipient, for any liability that these contractual assumptions 333 + directly impose on those licensors and authors. 334 + 335 + All other non-permissive additional terms are considered “further 336 + restrictions” within the meaning of section 10. If the Program as you received 337 + it, or any part of it, contains a notice stating that it is governed by this License 338 + along with a term that is a further restriction, you may remove that term. If a 339 + license document contains a further restriction but permits relicensing or conveying 340 + under this License, you may add to a covered work material governed by the terms of 341 + that license document, provided that the further restriction does not survive such 342 + relicensing or conveying. 343 + 344 + If you add terms to a covered work in accord with this section, you must place, in 345 + the relevant source files, a statement of the additional terms that apply to those 346 + files, or a notice indicating where to find the applicable terms. 347 + 348 + Additional terms, permissive or non-permissive, may be stated in the form of a 349 + separately written license, or stated as exceptions; the above requirements apply 350 + either way. 351 + 352 + ### 8. Termination 353 + 354 + You may not propagate or modify a covered work except as expressly provided under 355 + this License. Any attempt otherwise to propagate or modify it is void, and will 356 + automatically terminate your rights under this License (including any patent licenses 357 + granted under the third paragraph of section 11). 358 + 359 + However, if you cease all violation of this License, then your license from a 360 + particular copyright holder is reinstated **(a)** provisionally, unless and until the 361 + copyright holder explicitly and finally terminates your license, and **(b)** permanently, 362 + if the copyright holder fails to notify you of the violation by some reasonable means 363 + prior to 60 days after the cessation. 364 + 365 + Moreover, your license from a particular copyright holder is reinstated permanently 366 + if the copyright holder notifies you of the violation by some reasonable means, this 367 + is the first time you have received notice of violation of this License (for any 368 + work) from that copyright holder, and you cure the violation prior to 30 days after 369 + your receipt of the notice. 370 + 371 + Termination of your rights under this section does not terminate the licenses of 372 + parties who have received copies or rights from you under this License. If your 373 + rights have been terminated and not permanently reinstated, you do not qualify to 374 + receive new licenses for the same material under section 10. 375 + 376 + ### 9. Acceptance Not Required for Having Copies 377 + 378 + You are not required to accept this License in order to receive or run a copy of the 379 + Program. Ancillary propagation of a covered work occurring solely as a consequence of 380 + using peer-to-peer transmission to receive a copy likewise does not require 381 + acceptance. However, nothing other than this License grants you permission to 382 + propagate or modify any covered work. These actions infringe copyright if you do not 383 + accept this License. Therefore, by modifying or propagating a covered work, you 384 + indicate your acceptance of this License to do so. 385 + 386 + ### 10. Automatic Licensing of Downstream Recipients 387 + 388 + Each time you convey a covered work, the recipient automatically receives a license 389 + from the original licensors, to run, modify and propagate that work, subject to this 390 + License. You are not responsible for enforcing compliance by third parties with this 391 + License. 392 + 393 + An “entity transaction” is a transaction transferring control of an 394 + organization, or substantially all assets of one, or subdividing an organization, or 395 + merging organizations. If propagation of a covered work results from an entity 396 + transaction, each party to that transaction who receives a copy of the work also 397 + receives whatever licenses to the work the party's predecessor in interest had or 398 + could give under the previous paragraph, plus a right to possession of the 399 + Corresponding Source of the work from the predecessor in interest, if the predecessor 400 + has it or can get it with reasonable efforts. 401 + 402 + You may not impose any further restrictions on the exercise of the rights granted or 403 + affirmed under this License. For example, you may not impose a license fee, royalty, 404 + or other charge for exercise of rights granted under this License, and you may not 405 + initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging 406 + that any patent claim is infringed by making, using, selling, offering for sale, or 407 + importing the Program or any portion of it. 408 + 409 + ### 11. Patents 410 + 411 + A “contributor” is a copyright holder who authorizes use under this 412 + License of the Program or a work on which the Program is based. The work thus 413 + licensed is called the contributor's “contributor version”. 414 + 415 + A contributor's “essential patent claims” are all patent claims owned or 416 + controlled by the contributor, whether already acquired or hereafter acquired, that 417 + would be infringed by some manner, permitted by this License, of making, using, or 418 + selling its contributor version, but do not include claims that would be infringed 419 + only as a consequence of further modification of the contributor version. For 420 + purposes of this definition, “control” includes the right to grant patent 421 + sublicenses in a manner consistent with the requirements of this License. 422 + 423 + Each contributor grants you a non-exclusive, worldwide, royalty-free patent license 424 + under the contributor's essential patent claims, to make, use, sell, offer for sale, 425 + import and otherwise run, modify and propagate the contents of its contributor 426 + version. 427 + 428 + In the following three paragraphs, a “patent license” is any express 429 + agreement or commitment, however denominated, not to enforce a patent (such as an 430 + express permission to practice a patent or covenant not to sue for patent 431 + infringement). To “grant” such a patent license to a party means to make 432 + such an agreement or commitment not to enforce a patent against the party. 433 + 434 + If you convey a covered work, knowingly relying on a patent license, and the 435 + Corresponding Source of the work is not available for anyone to copy, free of charge 436 + and under the terms of this License, through a publicly available network server or 437 + other readily accessible means, then you must either **(1)** cause the Corresponding 438 + Source to be so available, or **(2)** arrange to deprive yourself of the benefit of the 439 + patent license for this particular work, or **(3)** arrange, in a manner consistent with 440 + the requirements of this License, to extend the patent license to downstream 441 + recipients. “Knowingly relying” means you have actual knowledge that, but 442 + for the patent license, your conveying the covered work in a country, or your 443 + recipient's use of the covered work in a country, would infringe one or more 444 + identifiable patents in that country that you have reason to believe are valid. 445 + 446 + If, pursuant to or in connection with a single transaction or arrangement, you 447 + convey, or propagate by procuring conveyance of, a covered work, and grant a patent 448 + license to some of the parties receiving the covered work authorizing them to use, 449 + propagate, modify or convey a specific copy of the covered work, then the patent 450 + license you grant is automatically extended to all recipients of the covered work and 451 + works based on it. 452 + 453 + A patent license is “discriminatory” if it does not include within the 454 + scope of its coverage, prohibits the exercise of, or is conditioned on the 455 + non-exercise of one or more of the rights that are specifically granted under this 456 + License. You may not convey a covered work if you are a party to an arrangement with 457 + a third party that is in the business of distributing software, under which you make 458 + payment to the third party based on the extent of your activity of conveying the 459 + work, and under which the third party grants, to any of the parties who would receive 460 + the covered work from you, a discriminatory patent license **(a)** in connection with 461 + copies of the covered work conveyed by you (or copies made from those copies), or **(b)** 462 + primarily for and in connection with specific products or compilations that contain 463 + the covered work, unless you entered into that arrangement, or that patent license 464 + was granted, prior to 28 March 2007. 465 + 466 + Nothing in this License shall be construed as excluding or limiting any implied 467 + license or other defenses to infringement that may otherwise be available to you 468 + under applicable patent law. 469 + 470 + ### 12. No Surrender of Others' Freedom 471 + 472 + If conditions are imposed on you (whether by court order, agreement or otherwise) 473 + that contradict the conditions of this License, they do not excuse you from the 474 + conditions of this License. If you cannot convey a covered work so as to satisfy 475 + simultaneously your obligations under this License and any other pertinent 476 + obligations, then as a consequence you may not convey it at all. For example, if you 477 + agree to terms that obligate you to collect a royalty for further conveying from 478 + those to whom you convey the Program, the only way you could satisfy both those terms 479 + and this License would be to refrain entirely from conveying the Program. 480 + 481 + ### 13. Use with the GNU Affero General Public License 482 + 483 + Notwithstanding any other provision of this License, you have permission to link or 484 + combine any covered work with a work licensed under version 3 of the GNU Affero 485 + General Public License into a single combined work, and to convey the resulting work. 486 + The terms of this License will continue to apply to the part which is the covered 487 + work, but the special requirements of the GNU Affero General Public License, section 488 + 13, concerning interaction through a network will apply to the combination as such. 489 + 490 + ### 14. Revised Versions of this License 491 + 492 + The Free Software Foundation may publish revised and/or new versions of the GNU 493 + General Public License from time to time. Such new versions will be similar in spirit 494 + to the present version, but may differ in detail to address new problems or concerns. 495 + 496 + Each version is given a distinguishing version number. If the Program specifies that 497 + a certain numbered version of the GNU General Public License “or any later 498 + version” applies to it, you have the option of following the terms and 499 + conditions either of that numbered version or of any later version published by the 500 + Free Software Foundation. If the Program does not specify a version number of the GNU 501 + General Public License, you may choose any version ever published by the Free 502 + Software Foundation. 503 + 504 + If the Program specifies that a proxy can decide which future versions of the GNU 505 + General Public License can be used, that proxy's public statement of acceptance of a 506 + version permanently authorizes you to choose that version for the Program. 507 + 508 + Later license versions may give you additional or different permissions. However, no 509 + additional obligations are imposed on any author or copyright holder as a result of 510 + your choosing to follow a later version. 511 + 512 + ### 15. Disclaimer of Warranty 513 + 514 + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 515 + EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 516 + PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER 517 + EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 518 + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE 519 + QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 520 + DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 521 + 522 + ### 16. Limitation of Liability 523 + 524 + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY 525 + COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS 526 + PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, 527 + INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 528 + PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE 529 + OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE 530 + WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 531 + POSSIBILITY OF SUCH DAMAGES. 532 + 533 + ### 17. Interpretation of Sections 15 and 16 534 + 535 + If the disclaimer of warranty and limitation of liability provided above cannot be 536 + given local legal effect according to their terms, reviewing courts shall apply local 537 + law that most closely approximates an absolute waiver of all civil liability in 538 + connection with the Program, unless a warranty or assumption of liability accompanies 539 + a copy of the Program in return for a fee. 540 + 541 + _END OF TERMS AND CONDITIONS_ 542 + 543 + ## How to Apply These Terms to Your New Programs 544 + 545 + If you develop a new program, and you want it to be of the greatest possible use to 546 + the public, the best way to achieve this is to make it free software which everyone 547 + can redistribute and change under these terms. 548 + 549 + To do so, attach the following notices to the program. It is safest to attach them 550 + to the start of each source file to most effectively state the exclusion of warranty; 551 + and each file should have at least the “copyright” line and a pointer to 552 + where the full notice is found. 553 + 554 + <one line to give the program's name and a brief idea of what it does.> 555 + Copyright (C) <year> <name of author> 556 + 557 + This program is free software: you can redistribute it and/or modify 558 + it under the terms of the GNU General Public License as published by 559 + the Free Software Foundation, either version 3 of the License, or 560 + (at your option) any later version. 561 + 562 + This program is distributed in the hope that it will be useful, 563 + but WITHOUT ANY WARRANTY; without even the implied warranty of 564 + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 565 + GNU General Public License for more details. 566 + 567 + You should have received a copy of the GNU General Public License 568 + along with this program. If not, see <http://www.gnu.org/licenses/>. 569 + 570 + Also add information on how to contact you by electronic and paper mail. 571 + 572 + If the program does terminal interaction, make it output a short notice like this 573 + when it starts in an interactive mode: 574 + 575 + <program> Copyright (C) <year> <name of author> 576 + This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'. 577 + This is free software, and you are welcome to redistribute it 578 + under certain conditions; type 'show c' for details. 579 + 580 + The hypothetical commands `show w` and `show c` should show the appropriate parts of 581 + the General Public License. Of course, your program's commands might be different; 582 + for a GUI interface, you would use an “about box”. 583 + 584 + You should also get your employer (if you work as a programmer) or school, if any, to 585 + sign a “copyright disclaimer” for the program, if necessary. For more 586 + information on this, and how to apply and follow the GNU GPL, see 587 + &lt;<http://www.gnu.org/licenses/>&gt;. 588 + 589 + The GNU General Public License does not permit incorporating your program into 590 + proprietary programs. If your program is a subroutine library, you may consider it 591 + more useful to permit linking proprietary applications with the library. If this is 592 + what you want to do, use the GNU Lesser General Public License instead of this 593 + License. But first, please read 594 + &lt;<http://www.gnu.org/philosophy/why-not-lgpl.html>&gt;.
+73
README.md
··· 1 + # tg 2 + 3 + `tg` is a command-line client for [Tangled](https://tangled.org), the git forge built on atproto. It is an analogue to the GitHub CLI (`gh`), (for now) built against the read-only [Bobbin](https://docs.tangled.org/bobbin) XRPC API. 4 + 5 + ## Installation 6 + 7 + ### With Nix 8 + 9 + ```bash 10 + nix profile add github:alyraffauf/tg 11 + ``` 12 + 13 + ### From source 14 + 15 + ```bash 16 + go install github.com/alyraffauf/tg/cmd/tg@latest 17 + ``` 18 + 19 + ## Usage 20 + 21 + `tg` auto-detects the repository from the `origin` remote when run inside a cloned Tangled repo. For now, only ssh origins are supported. You can also pass a fully-qualified `handle/repo` argument. 22 + 23 + ### Repositories 24 + 25 + ```bash 26 + # List repositories for a user 27 + tg repo list microcosm.blue 28 + 29 + # Clone a repository 30 + tg repo clone microcosm.blue/microcosm-rs 31 + 32 + # Clone into a custom directory 33 + tg repo clone microcosm.blue/microcosm-rs my-fork 34 + ``` 35 + 36 + ### Issues 37 + 38 + ```bash 39 + # List issues for the current repository 40 + tg issue list 41 + 42 + # List issues for an explicit repository 43 + tg issue list microcosm.blue/microcosm-rs 44 + ``` 45 + 46 + ### Pull Requests 47 + 48 + ```bash 49 + # List pull requests 50 + tg pr list 51 + 52 + # Check out a pull request as detached HEAD 53 + tg pr checkout 3mporttfqez22 54 + ``` 55 + 56 + ## Architecture 57 + 58 + - `cmd/tg/` — CLI entry point 59 + - `internal/cli/` — Cobra command tree (`repo`, `issue`, `pr`) 60 + - `internal/gitutil/` — Git operations (clone, fetch, patch apply) 61 + - `tangled/` — Typed client for the Bobbin API (`api.tangled.org`) 62 + - `atproto/` — Identity resolution (handle ↔ DID, PDS discovery) 63 + 64 + ## Dependencies 65 + 66 + - Go 1.26+ 67 + - `git` and `ssh` (for clone and PR checkout) 68 + - [`github.com/bluesky-social/indigo`](https://github.com/bluesky-social/indigo) — atproto SDK 69 + - [`github.com/spf13/cobra`](https://github.com/spf13/cobra) — CLI framework 70 + 71 + ## License 72 + 73 + See [LICENSE.md].
+65
atproto/resolve.go
··· 1 + package atproto 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "time" 7 + 8 + "github.com/bluesky-social/indigo/atproto/identity" 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + ) 11 + 12 + // Resolver wraps an identity.Directory to provide typed handle and 13 + // DID resolution with a sane default timeout. 14 + type Resolver struct { 15 + Directory identity.Directory 16 + } 17 + 18 + func (r *Resolver) ResolveHandle(ctx context.Context, rawHandle string) (*identity.Identity, error) { 19 + handle, err := syntax.ParseHandle(rawHandle) 20 + if err != nil { 21 + return nil, fmt.Errorf("parse handle %q: %w", rawHandle, err) 22 + } 23 + 24 + resolveCtx, cancel := context.WithTimeout(ctx, 3*time.Second) 25 + defer cancel() 26 + 27 + ident, err := r.Directory.LookupHandle(resolveCtx, handle) 28 + if err != nil { 29 + return nil, fmt.Errorf("lookup handle %q: %w", rawHandle, err) 30 + } 31 + 32 + return ident, nil 33 + } 34 + 35 + func (r *Resolver) ResolveDID(ctx context.Context, didStr string) (*identity.Identity, error) { 36 + did, err := syntax.ParseDID(didStr) 37 + if err != nil { 38 + return nil, fmt.Errorf("parse DID %q: %w", didStr, err) 39 + } 40 + 41 + resolveCtx, cancel := context.WithTimeout(ctx, 3*time.Second) 42 + defer cancel() 43 + 44 + ident, err := r.Directory.LookupDID(resolveCtx, did) 45 + if err != nil { 46 + return nil, fmt.Errorf("lookup DID %q: %w", didStr, err) 47 + } 48 + 49 + return ident, nil 50 + } 51 + 52 + func (r *Resolver) ResolvePDS(ctx context.Context, didStr string) (string, error) { 53 + ident, err := r.ResolveDID(ctx, didStr) 54 + if err != nil { 55 + return "", err 56 + } 57 + 58 + for _, svc := range ident.Services { 59 + if svc.Type == "AtprotoPersonalDataServer" { 60 + return svc.URL, nil 61 + } 62 + } 63 + 64 + return "", fmt.Errorf("no AtprotoPersonalDataServer service found for %q", didStr) 65 + }
+13
cmd/tg/main.go
··· 1 + package main 2 + 3 + import ( 4 + "os" 5 + 6 + "github.com/alyraffauf/tg/internal/cli" 7 + ) 8 + 9 + func main() { 10 + if err := cli.Execute(); err != nil { 11 + os.Exit(1) 12 + } 13 + }
+118
flake.lock
··· 1 + { 2 + "nodes": { 3 + "flake-compat": { 4 + "flake": false, 5 + "locked": { 6 + "lastModified": 1767039857, 7 + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", 8 + "owner": "NixOS", 9 + "repo": "flake-compat", 10 + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", 11 + "type": "github" 12 + }, 13 + "original": { 14 + "owner": "NixOS", 15 + "repo": "flake-compat", 16 + "type": "github" 17 + } 18 + }, 19 + "flake-parts": { 20 + "inputs": { 21 + "nixpkgs-lib": "nixpkgs-lib" 22 + }, 23 + "locked": { 24 + "lastModified": 1782949081, 25 + "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=", 26 + "owner": "hercules-ci", 27 + "repo": "flake-parts", 28 + "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e", 29 + "type": "github" 30 + }, 31 + "original": { 32 + "owner": "hercules-ci", 33 + "repo": "flake-parts", 34 + "type": "github" 35 + } 36 + }, 37 + "git-hooks-nix": { 38 + "inputs": { 39 + "flake-compat": "flake-compat", 40 + "nixpkgs": [ 41 + "nixpkgs" 42 + ] 43 + }, 44 + "locked": { 45 + "lastModified": 1783008725, 46 + "narHash": "sha256-jGiy6+sxjNWXSjp25uoJuNfyH9zBK1PEDY0lVoL4ibQ=", 47 + "owner": "cachix", 48 + "repo": "git-hooks.nix", 49 + "rev": "bca82caa46d5ec0f5d422c61fb1e30bc51313cbe", 50 + "type": "github" 51 + }, 52 + "original": { 53 + "owner": "cachix", 54 + "repo": "git-hooks.nix", 55 + "type": "github" 56 + } 57 + }, 58 + "nixpkgs": { 59 + "locked": { 60 + "lastModified": 1783148766, 61 + "narHash": "sha256-uslt2pqShTIXDdAHRHv2QkYLsVdY8Oqwz0EA48/RSM8=", 62 + "rev": "a50de1b7d8a586adc18d2395c19de7d6058e6030", 63 + "revCount": 1008484, 64 + "type": "tarball", 65 + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2605.1008484%2Brev-a50de1b7d8a586adc18d2395c19de7d6058e6030/019f2e42-a67d-7ba0-bc6c-598449e6ade9/source.tar.gz" 66 + }, 67 + "original": { 68 + "type": "tarball", 69 + "url": "https://flakehub.com/f/NixOS/nixpkgs/0" 70 + } 71 + }, 72 + "nixpkgs-lib": { 73 + "locked": { 74 + "lastModified": 1782614948, 75 + "narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=", 76 + "owner": "nix-community", 77 + "repo": "nixpkgs.lib", 78 + "rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c", 79 + "type": "github" 80 + }, 81 + "original": { 82 + "owner": "nix-community", 83 + "repo": "nixpkgs.lib", 84 + "type": "github" 85 + } 86 + }, 87 + "root": { 88 + "inputs": { 89 + "flake-parts": "flake-parts", 90 + "git-hooks-nix": "git-hooks-nix", 91 + "nixpkgs": "nixpkgs", 92 + "treefmt-nix": "treefmt-nix" 93 + } 94 + }, 95 + "treefmt-nix": { 96 + "inputs": { 97 + "nixpkgs": [ 98 + "nixpkgs" 99 + ] 100 + }, 101 + "locked": { 102 + "lastModified": 1780220602, 103 + "narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=", 104 + "owner": "numtide", 105 + "repo": "treefmt-nix", 106 + "rev": "db947814a175b7ca6ded66e21383d938df01c227", 107 + "type": "github" 108 + }, 109 + "original": { 110 + "owner": "numtide", 111 + "repo": "treefmt-nix", 112 + "type": "github" 113 + } 114 + } 115 + }, 116 + "root": "root", 117 + "version": 7 118 + }
+33
flake.nix
··· 1 + { 2 + description = "Aly's flake template with flake-parts."; 3 + 4 + inputs = { 5 + nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0"; 6 + flake-parts.url = "github:hercules-ci/flake-parts"; 7 + 8 + git-hooks-nix = { 9 + url = "github:cachix/git-hooks.nix"; 10 + inputs.nixpkgs.follows = "nixpkgs"; 11 + }; 12 + 13 + treefmt-nix = { 14 + url = "github:numtide/treefmt-nix"; 15 + inputs.nixpkgs.follows = "nixpkgs"; 16 + }; 17 + }; 18 + 19 + outputs = inputs @ {flake-parts, ...}: 20 + flake-parts.lib.mkFlake {inherit inputs;} { 21 + systems = [ 22 + "aarch64-linux" 23 + "x86_64-linux" 24 + "aarch64-darwin" 25 + ]; 26 + 27 + imports = [ 28 + ./flake 29 + inputs.git-hooks-nix.flakeModule 30 + inputs.treefmt-nix.flakeModule 31 + ]; 32 + }; 33 + }
+7
flake/default.nix
··· 1 + { 2 + imports = [ 3 + ./devShell.nix 4 + ./packages.nix 5 + ./treefmt.nix 6 + ]; 7 + }
+20
flake/devShell.nix
··· 1 + _: { 2 + perSystem = { 3 + config, 4 + lib, 5 + pkgs, 6 + ... 7 + }: { 8 + devShells.default = pkgs.mkShell { 9 + packages = 10 + (with pkgs; [ 11 + go 12 + ]) 13 + ++ lib.attrValues config.treefmt.build.programs; 14 + 15 + shellHook = '' 16 + echo "👋 Welcome to the tg devShell!" 17 + ''; 18 + }; 19 + }; 20 + }
+7
flake/packages.nix
··· 1 + _: { 2 + perSystem = {pkgs, ...}: { 3 + packages = { 4 + tg = pkgs.callPackage ../nix/tg.nix {}; 5 + }; 6 + }; 7 + }
+15
flake/treefmt.nix
··· 1 + _: { 2 + perSystem = _: { 3 + treefmt.config = { 4 + programs = { 5 + alejandra.enable = true; 6 + deadnix.enable = true; 7 + gofmt.enable = true; 8 + prettier.enable = true; 9 + shellcheck.enable = true; 10 + shfmt.enable = true; 11 + statix.enable = true; 12 + }; 13 + }; 14 + }; 15 + }
+71
go.mod
··· 1 + module github.com/alyraffauf/tg 2 + 3 + go 1.26.4 4 + 5 + require ( 6 + github.com/bluesky-social/indigo v0.0.0-20260629160527-dfe5578fd537 7 + github.com/spf13/cobra v1.10.2 8 + ) 9 + 10 + require ( 11 + github.com/beorn7/perks v1.0.1 // indirect 12 + github.com/cespare/xxhash/v2 v2.2.0 // indirect 13 + github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect 14 + github.com/felixge/httpsnoop v1.0.4 // indirect 15 + github.com/go-logr/logr v1.4.1 // indirect 16 + github.com/go-logr/stdr v1.2.2 // indirect 17 + github.com/gogo/protobuf v1.3.2 // indirect 18 + github.com/google/uuid v1.4.0 // indirect 19 + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 20 + github.com/hashicorp/go-retryablehttp v0.7.5 // indirect 21 + github.com/hashicorp/golang-lru v1.0.2 // indirect 22 + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 23 + github.com/inconshreveable/mousetrap v1.1.0 // indirect 24 + github.com/ipfs/bbloom v0.0.4 // indirect 25 + github.com/ipfs/go-block-format v0.2.0 // indirect 26 + github.com/ipfs/go-cid v0.4.1 // indirect 27 + github.com/ipfs/go-datastore v0.6.0 // indirect 28 + github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect 29 + github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect 30 + github.com/ipfs/go-ipfs-util v0.0.3 // indirect 31 + github.com/ipfs/go-ipld-cbor v0.1.0 // indirect 32 + github.com/ipfs/go-ipld-format v0.6.0 // indirect 33 + github.com/ipfs/go-log v1.0.5 // indirect 34 + github.com/ipfs/go-log/v2 v2.5.1 // indirect 35 + github.com/ipfs/go-metrics-interface v0.0.1 // indirect 36 + github.com/jbenet/goprocess v0.1.4 // indirect 37 + github.com/klauspost/cpuid/v2 v2.2.7 // indirect 38 + github.com/mattn/go-isatty v0.0.20 // indirect 39 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 40 + github.com/minio/sha256-simd v1.0.1 // indirect 41 + github.com/mr-tron/base58 v1.2.0 // indirect 42 + github.com/multiformats/go-base32 v0.1.0 // indirect 43 + github.com/multiformats/go-base36 v0.2.0 // indirect 44 + github.com/multiformats/go-multibase v0.2.0 // indirect 45 + github.com/multiformats/go-multihash v0.2.3 // indirect 46 + github.com/multiformats/go-varint v0.0.7 // indirect 47 + github.com/opentracing/opentracing-go v1.2.0 // indirect 48 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 49 + github.com/prometheus/client_golang v1.17.0 // indirect 50 + github.com/prometheus/client_model v0.5.0 // indirect 51 + github.com/prometheus/common v0.45.0 // indirect 52 + github.com/prometheus/procfs v0.12.0 // indirect 53 + github.com/spaolacci/murmur3 v1.1.0 // indirect 54 + github.com/spf13/pflag v1.0.9 // indirect 55 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect 56 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 57 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 58 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect 59 + go.opentelemetry.io/otel v1.21.0 // indirect 60 + go.opentelemetry.io/otel/metric v1.21.0 // indirect 61 + go.opentelemetry.io/otel/trace v1.21.0 // indirect 62 + go.uber.org/atomic v1.11.0 // indirect 63 + go.uber.org/multierr v1.11.0 // indirect 64 + go.uber.org/zap v1.26.0 // indirect 65 + golang.org/x/crypto v0.21.0 // indirect 66 + golang.org/x/sys v0.22.0 // indirect 67 + golang.org/x/time v0.3.0 // indirect 68 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 69 + google.golang.org/protobuf v1.33.0 // indirect 70 + lukechampine.com/blake3 v1.2.1 // indirect 71 + )
+250
go.sum
··· 1 + github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 + github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 3 + github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 + github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 + github.com/bluesky-social/indigo v0.0.0-20260629160527-dfe5578fd537 h1:rHaND0argSxgbmE2ix/YuF11xXTtiP3oGUz4fS2Diqo= 6 + github.com/bluesky-social/indigo v0.0.0-20260629160527-dfe5578fd537/go.mod h1:JqQkz8lrOI6YZivP38GHmtVOTtzsNToITKj1gMpU5Jo= 7 + github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 8 + github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 10 + github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 11 + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 + github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg= 15 + github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw= 16 + github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 17 + github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 18 + github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 19 + github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 20 + github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 21 + github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 22 + github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 23 + github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 24 + github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 25 + github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 26 + github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 27 + github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 28 + github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 29 + github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= 30 + github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 31 + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 32 + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 33 + github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 34 + github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 35 + github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= 36 + github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 37 + github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= 38 + github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= 39 + github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 40 + github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 41 + github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 42 + github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 43 + github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 44 + github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 45 + github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 46 + github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 47 + github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= 48 + github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= 49 + github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 50 + github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 51 + github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= 52 + github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= 53 + github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= 54 + github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= 55 + github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= 56 + github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= 57 + github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw= 58 + github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo= 59 + github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= 60 + github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 61 + github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs= 62 + github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk= 63 + github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U= 64 + github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg= 65 + github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= 66 + github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= 67 + github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= 68 + github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= 69 + github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= 70 + github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= 71 + github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= 72 + github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= 73 + github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 74 + github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= 75 + github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 76 + github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 77 + github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 78 + github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 79 + github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 80 + github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 81 + github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 82 + github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 83 + github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 84 + github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 85 + github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 86 + github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 87 + github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 88 + github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 89 + github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 90 + github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 91 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= 92 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 93 + github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 94 + github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 95 + github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 96 + github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 97 + github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 98 + github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 99 + github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 100 + github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 101 + github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 102 + github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 103 + github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 104 + github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 105 + github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 106 + github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 107 + github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 108 + github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 109 + github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 110 + github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 111 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 112 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 113 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 114 + github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= 115 + github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= 116 + github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 117 + github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 118 + github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= 119 + github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= 120 + github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 121 + github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 122 + github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 123 + github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 124 + github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 125 + github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 126 + github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 127 + github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 128 + github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 129 + github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= 130 + github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= 131 + github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= 132 + github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 133 + github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 134 + github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= 135 + github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= 136 + github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= 137 + github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 138 + github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 139 + github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 140 + github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 141 + github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 142 + github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 143 + github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 144 + github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 145 + github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 146 + github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= 147 + github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 148 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 149 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 150 + github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 151 + github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 152 + github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 153 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 154 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 155 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= 156 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 157 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= 158 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= 159 + go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= 160 + go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= 161 + go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= 162 + go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= 163 + go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= 164 + go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= 165 + go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 166 + go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 167 + go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 168 + go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 169 + go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 170 + go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= 171 + go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= 172 + go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 173 + go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 174 + go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 175 + go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 176 + go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 177 + go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 178 + go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 179 + go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 180 + go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 181 + go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 182 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 183 + golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 184 + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 185 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 186 + golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 187 + golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 188 + golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 189 + golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 190 + golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 191 + golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 192 + golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 193 + golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 194 + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 195 + golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 196 + golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 197 + golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 198 + golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 199 + golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 200 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 201 + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 202 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 203 + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 204 + golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 205 + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 206 + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 207 + golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 208 + golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 209 + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 210 + golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 211 + golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 212 + golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 213 + golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 214 + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 215 + golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 216 + golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 217 + golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 218 + golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 219 + golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 220 + golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 221 + golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 222 + golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 223 + golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 224 + golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 225 + golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 226 + golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 227 + golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 228 + golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 229 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 230 + golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 231 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 232 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 233 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 234 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 235 + google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 236 + google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 237 + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 238 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 239 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 240 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 241 + gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 242 + gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 243 + gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 244 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 245 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 246 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 247 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 248 + honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 249 + lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= 250 + lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
+8
internal/cli/issue.go
··· 1 + package cli 2 + 3 + import "github.com/spf13/cobra" 4 + 5 + var issueCmd = &cobra.Command{ 6 + Use: "issue", 7 + Short: "Manage issues on a Tangled repository", 8 + }
+123
internal/cli/issue_list.go
··· 1 + package cli 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "strings" 8 + 9 + "github.com/alyraffauf/tg/internal/gitutil" 10 + "github.com/alyraffauf/tg/tangled" 11 + "github.com/spf13/cobra" 12 + ) 13 + 14 + var issueListCmd = &cobra.Command{ 15 + Use: "list [handle/repo]", 16 + Short: "List issues for a Tangled repository", 17 + Long: `List issues for a Tangled repository. 18 + 19 + If no argument is given, the command detects the repository from the 20 + "origin" remote URL of the git repository in the current directory.`, 21 + Args: cobra.MaximumNArgs(1), 22 + RunE: func(cmd *cobra.Command, args []string) error { 23 + ctx := cmd.Context() 24 + 25 + handle, repo, err := resolveTarget(ctx, args) 26 + if err != nil { 27 + return err 28 + } 29 + 30 + repoDid, err := findRepoDid(ctx, handle, repo) 31 + if err != nil { 32 + return err 33 + } 34 + 35 + issues, err := client.ListIssues(ctx, repoDid, tangled.IssueListOpts{ 36 + Limit: defaultListLimit, 37 + }) 38 + if err != nil { 39 + return fmt.Errorf("list issues for %q: %w", repo, err) 40 + } 41 + 42 + rows := buildIssueRows(ctx, issues.Items) 43 + renderRows(rows, "No issues found.") 44 + return nil 45 + }, 46 + } 47 + 48 + func parseHandleRepo(arg string) (string, string, error) { 49 + parts := strings.SplitN(arg, "/", 2) 50 + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { 51 + return "", "", fmt.Errorf("expected handle/repo, got %q", arg) 52 + } 53 + return parts[0], parts[1], nil 54 + } 55 + 56 + // resolveTarget returns the handle and repo name from an explicit 57 + // "handle/repo" argument or by detecting the git remote in the CWD. 58 + func resolveTarget(ctx context.Context, args []string) (string, string, error) { 59 + if len(args) == 1 { 60 + return parseHandleRepo(args[0]) 61 + } 62 + 63 + rc, err := gitutil.DetectRepoFromCWD(ctx) 64 + if err != nil { 65 + return "", "", fmt.Errorf("detect repo from current directory: %w", err) 66 + } 67 + return rc.Handle, rc.Repo, nil 68 + } 69 + 70 + // findRepoDid resolves a handle to its owner DID, lists their repos, 71 + // and returns the repoDid of the matching repo. Matches by the Name 72 + // field, falling back to the rkey in the at:// URI when Name is empty. 73 + func findRepoDid(ctx context.Context, handle, repo string) (string, error) { 74 + ident, err := resolver.ResolveHandle(ctx, handle) 75 + if err != nil { 76 + return "", fmt.Errorf("resolve handle %q: %w", handle, err) 77 + } 78 + 79 + repos, err := client.ListRepos(ctx, ident.DID.String()) 80 + if err != nil { 81 + return "", fmt.Errorf("list repos for %q: %w", handle, err) 82 + } 83 + 84 + for _, item := range repos.Items { 85 + if item.Value.Name == repo || strings.HasSuffix(item.URI, "/"+repo) { 86 + return item.Value.RepoDid, nil 87 + } 88 + } 89 + 90 + return "", fmt.Errorf("repo %q not found for handle %q", repo, handle) 91 + } 92 + 93 + // buildIssueRows resolves each issue author's DID to a handle, falling 94 + // back to the raw DID on resolution failure. 95 + func buildIssueRows(ctx context.Context, items []tangled.IssueListItem) []listRow { 96 + rows := make([]listRow, 0, len(items)) 97 + 98 + for _, item := range items { 99 + var record tangled.IssueRecord 100 + if err := json.Unmarshal(item.Value, &record); err != nil { 101 + continue 102 + } 103 + 104 + updated := item.StateUpdatedAt 105 + if updated == "" { 106 + updated = record.CreatedAt 107 + } 108 + 109 + title := record.Title 110 + if title == "" { 111 + title = "(no title)" 112 + } 113 + 114 + rows = append(rows, listRow{ 115 + title: title, 116 + state: item.State, 117 + author: resolveAuthor(ctx, extractDID(item.URI)), 118 + updated: shortDate(updated), 119 + }) 120 + } 121 + 122 + return rows 123 + }
+8
internal/cli/pr.go
··· 1 + package cli 2 + 3 + import "github.com/spf13/cobra" 4 + 5 + var prCmd = &cobra.Command{ 6 + Use: "pr", 7 + Short: "Manage pull requests on a Tangled repository", 8 + }
+101
internal/cli/pr_checkout.go
··· 1 + package cli 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "os" 7 + "strings" 8 + 9 + "github.com/alyraffauf/tg/internal/gitutil" 10 + "github.com/alyraffauf/tg/tangled" 11 + "github.com/spf13/cobra" 12 + ) 13 + 14 + var prCheckoutCmd = &cobra.Command{ 15 + Use: "checkout <pr-rkey>", 16 + Short: "Check out a pull request as a detached HEAD", 17 + Long: `Check out a pull request by fetching its target branch and applying 18 + the PR's gzipped patch blob on top, leaving you in a detached HEAD. 19 + 20 + Must be run from inside a cloned Tangled repository.`, 21 + Args: cobra.ExactArgs(1), 22 + RunE: func(cmd *cobra.Command, args []string) error { 23 + ctx := cmd.Context() 24 + prRKey := args[0] 25 + 26 + // No explicit handle/repo: must auto-detect from CWD. 27 + handle, repo, err := resolveTarget(ctx, nil) 28 + if err != nil { 29 + return err 30 + } 31 + 32 + repoDid, err := findRepoDid(ctx, handle, repo) 33 + if err != nil { 34 + return err 35 + } 36 + 37 + pulls, err := client.ListPulls(ctx, repoDid, tangled.PullListOpts{ 38 + Limit: defaultListLimit, 39 + }) 40 + if err != nil { 41 + return fmt.Errorf("list pulls for %q: %w", repo, err) 42 + } 43 + 44 + pr, authorDID, err := findPullByRKey(pulls.Items, prRKey) 45 + if err != nil { 46 + return err 47 + } 48 + if len(pr.Rounds) == 0 { 49 + return fmt.Errorf("pull request %q has no rounds", prRKey) 50 + } 51 + 52 + // The last round is the latest patch revision. 53 + cid := pr.Rounds[len(pr.Rounds)-1].PatchBlob.Ref.String() 54 + if cid == "" { 55 + return fmt.Errorf("pull request %q has no patch blob", prRKey) 56 + } 57 + 58 + pdsHost, err := resolver.ResolvePDS(ctx, authorDID) 59 + if err != nil { 60 + return fmt.Errorf("resolve PDS for author %q: %w", authorDID, err) 61 + } 62 + 63 + repoDir, err := os.Getwd() 64 + if err != nil { 65 + return fmt.Errorf("get current directory: %w", err) 66 + } 67 + 68 + if err := gitutil.CheckoutPull(ctx, gitutil.CheckoutPullParams{ 69 + RepoDir: repoDir, 70 + PDSHost: pdsHost, 71 + AuthorDID: authorDID, 72 + CID: cid, 73 + TargetHandle: handle, 74 + TargetRepo: repo, 75 + TargetBranch: pr.Target.Branch, 76 + }); err != nil { 77 + return fmt.Errorf("checkout pull %q: %w", prRKey, err) 78 + } 79 + 80 + fmt.Printf("Checked out PR %s as detached HEAD in %s\n", prRKey, repoDir) 81 + return nil 82 + }, 83 + } 84 + 85 + // findPullByRKey searches a pull list for the item matching rkey, 86 + // decodes its record, and returns the record plus the author DID 87 + // extracted from the item's URI. 88 + func findPullByRKey(items []tangled.PullListItem, rkey string) (*tangled.PullRecord, string, error) { 89 + for _, item := range items { 90 + if !strings.HasSuffix(item.URI, "/"+rkey) { 91 + continue 92 + } 93 + 94 + var pr tangled.PullRecord 95 + if err := json.Unmarshal(item.Value, &pr); err != nil { 96 + return nil, "", fmt.Errorf("decode pull record %q: %w", rkey, err) 97 + } 98 + return &pr, extractDID(item.URI), nil 99 + } 100 + return nil, "", fmt.Errorf("pull request %q not found", rkey) 101 + }
+76
internal/cli/pr_list.go
··· 1 + package cli 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + 8 + "github.com/alyraffauf/tg/tangled" 9 + "github.com/spf13/cobra" 10 + ) 11 + 12 + var prListCmd = &cobra.Command{ 13 + Use: "list [handle/repo]", 14 + Short: "List pull requests for a Tangled repository", 15 + Long: `List pull requests for a Tangled repository. 16 + 17 + If no argument is given, the command detects the repository from the 18 + "origin" remote URL of the git repository in the current directory.`, 19 + Args: cobra.MaximumNArgs(1), 20 + RunE: func(cmd *cobra.Command, args []string) error { 21 + ctx := cmd.Context() 22 + 23 + handle, repo, err := resolveTarget(ctx, args) 24 + if err != nil { 25 + return err 26 + } 27 + 28 + repoDid, err := findRepoDid(ctx, handle, repo) 29 + if err != nil { 30 + return err 31 + } 32 + 33 + pulls, err := client.ListPulls(ctx, repoDid, tangled.PullListOpts{ 34 + Limit: defaultListLimit, 35 + }) 36 + if err != nil { 37 + return fmt.Errorf("list PRs for %q: %w", repo, err) 38 + } 39 + 40 + rows := buildPullRows(ctx, pulls.Items) 41 + renderRows(rows, "No pull requests found.") 42 + return nil 43 + }, 44 + } 45 + 46 + // buildPullRows resolves each PR author's DID to a handle, falling back 47 + // to the raw DID on resolution failure. 48 + func buildPullRows(ctx context.Context, items []tangled.PullListItem) []listRow { 49 + rows := make([]listRow, 0, len(items)) 50 + 51 + for _, item := range items { 52 + var record tangled.PullRecord 53 + if err := json.Unmarshal(item.Value, &record); err != nil { 54 + continue 55 + } 56 + 57 + updated := item.StateUpdatedAt 58 + if updated == "" { 59 + updated = record.CreatedAt 60 + } 61 + 62 + title := record.Title 63 + if title == "" { 64 + title = "(no title)" 65 + } 66 + 67 + rows = append(rows, listRow{ 68 + title: title, 69 + state: item.State, 70 + author: resolveAuthor(ctx, extractDID(item.URI)), 71 + updated: shortDate(updated), 72 + }) 73 + } 74 + 75 + return rows 76 + }
+8
internal/cli/repo.go
··· 1 + package cli 2 + 3 + import "github.com/spf13/cobra" 4 + 5 + var repoCmd = &cobra.Command{ 6 + Use: "repo", 7 + Short: "Manage repositories on Tangled", 8 + }
+36
internal/cli/repo_clone.go
··· 1 + package cli 2 + 3 + import ( 4 + "fmt" 5 + 6 + "github.com/alyraffauf/tg/internal/gitutil" 7 + "github.com/spf13/cobra" 8 + ) 9 + 10 + var repoCloneCmd = &cobra.Command{ 11 + Use: "clone <handle/repo> [directory]", 12 + Short: "Clone a Tangled repository", 13 + Long: `Clone a Tangled repository via SSH into a local directory. 14 + 15 + The default destination is the repository name.`, 16 + Args: cobra.RangeArgs(1, 2), 17 + RunE: func(cmd *cobra.Command, args []string) error { 18 + ctx := cmd.Context() 19 + 20 + handle, repo, err := parseHandleRepo(args[0]) 21 + if err != nil { 22 + return err 23 + } 24 + 25 + dest := repo 26 + if len(args) == 2 { 27 + dest = args[1] 28 + } 29 + 30 + fmt.Printf("Cloning %s/%s into %s...\n", handle, repo, dest) 31 + if err := gitutil.CloneRepo(ctx, handle, repo, dest); err != nil { 32 + return fmt.Errorf("clone %q: %w", args[0], err) 33 + } 34 + return nil 35 + }, 36 + }
+103
internal/cli/repo_list.go
··· 1 + package cli 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "os" 7 + "strings" 8 + "text/tabwriter" 9 + 10 + "github.com/alyraffauf/tg/internal/gitutil" 11 + "github.com/alyraffauf/tg/tangled" 12 + "github.com/spf13/cobra" 13 + ) 14 + 15 + var repoListCmd = &cobra.Command{ 16 + Use: "list [handle]", 17 + Short: "List repositories owned by a Tangled user", 18 + Long: `List repositories owned by a Tangled user. 19 + 20 + If no argument is given, the command detects the user from the "origin" 21 + remote URL of the git repository in the current directory.`, 22 + Args: cobra.MaximumNArgs(1), 23 + RunE: func(cmd *cobra.Command, args []string) error { 24 + ctx := cmd.Context() 25 + 26 + handle, err := resolveHandleArg(ctx, args) 27 + if err != nil { 28 + return err 29 + } 30 + 31 + ident, err := resolver.ResolveHandle(ctx, handle) 32 + if err != nil { 33 + return fmt.Errorf("resolve handle %q: %w", handle, err) 34 + } 35 + 36 + repos, err := client.ListRepos(ctx, ident.DID.String()) 37 + if err != nil { 38 + return fmt.Errorf("list repos for %q: %w", handle, err) 39 + } 40 + 41 + renderRepos(buildRepoRows(repos.Items)) 42 + return nil 43 + }, 44 + } 45 + 46 + // resolveHandleArg returns the handle from an explicit argument, or 47 + // falls back to the handle of the CWD's git origin remote. 48 + func resolveHandleArg(ctx context.Context, args []string) (string, error) { 49 + if len(args) == 1 { 50 + return args[0], nil 51 + } 52 + 53 + rc, err := gitutil.DetectRepoFromCWD(ctx) 54 + if err != nil { 55 + return "", fmt.Errorf("detect repo from current directory: %w", err) 56 + } 57 + return rc.Handle, nil 58 + } 59 + 60 + type repoRow struct { 61 + name string 62 + knot string 63 + description string 64 + created string 65 + } 66 + 67 + func buildRepoRows(items []tangled.Repo) []repoRow { 68 + rows := make([]repoRow, 0, len(items)) 69 + 70 + for _, item := range items { 71 + name := item.Value.Name 72 + if name == "" { 73 + // Fall back to the rkey from the at:// URI. 74 + if idx := strings.LastIndex(item.URI, "/"); idx != -1 { 75 + name = item.URI[idx+1:] 76 + } 77 + } 78 + 79 + rows = append(rows, repoRow{ 80 + name: name, 81 + knot: item.Value.Knot, 82 + description: item.Value.Description, 83 + created: shortDate(item.Value.CreatedAt), 84 + }) 85 + } 86 + 87 + return rows 88 + } 89 + 90 + func renderRepos(rows []repoRow) { 91 + if len(rows) == 0 { 92 + fmt.Println("No repositories found.") 93 + return 94 + } 95 + 96 + tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.TabIndent) 97 + fmt.Fprintln(tw, "NAME\tKNOT\tDESCRIPTION\tCREATED") 98 + 99 + for _, row := range rows { 100 + fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", row.name, row.knot, row.description, row.created) 101 + } 102 + tw.Flush() 103 + }
+41
internal/cli/root.go
··· 1 + package cli 2 + 3 + import ( 4 + "log/slog" 5 + 6 + "github.com/alyraffauf/tg/atproto" 7 + "github.com/alyraffauf/tg/tangled" 8 + "github.com/bluesky-social/indigo/atproto/identity" 9 + "github.com/bluesky-social/indigo/xrpc" 10 + "github.com/spf13/cobra" 11 + ) 12 + 13 + var ( 14 + resolver = &atproto.Resolver{Directory: identity.DefaultDirectory()} 15 + client = &tangled.Tangled{ 16 + Client: &xrpc.Client{Host: "https://api.tangled.org"}, 17 + Logger: slog.Default(), 18 + } 19 + ) 20 + 21 + var rootCmd = &cobra.Command{ 22 + Use: "tg", 23 + Short: "A CLI for Tangled", 24 + } 25 + 26 + func Execute() error { 27 + return rootCmd.Execute() 28 + } 29 + 30 + func init() { 31 + rootCmd.AddCommand(issueCmd) 32 + issueCmd.AddCommand(issueListCmd) 33 + 34 + rootCmd.AddCommand(prCmd) 35 + prCmd.AddCommand(prListCmd) 36 + prCmd.AddCommand(prCheckoutCmd) 37 + 38 + rootCmd.AddCommand(repoCmd) 39 + repoCmd.AddCommand(repoListCmd) 40 + repoCmd.AddCommand(repoCloneCmd) 41 + }
+59
internal/cli/rows.go
··· 1 + package cli 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "os" 7 + "strings" 8 + "text/tabwriter" 9 + ) 10 + 11 + const defaultListLimit int64 = 100 12 + 13 + // listRow is display-ready data for one issue or PR. 14 + type listRow struct { 15 + title string 16 + state string 17 + author string 18 + updated string 19 + } 20 + 21 + // resolveAuthor returns the handle for didStr, falling back to the 22 + // raw DID string on resolution failure. 23 + func resolveAuthor(ctx context.Context, didStr string) string { 24 + if ident, err := resolver.ResolveDID(ctx, didStr); err == nil { 25 + return ident.Handle.String() 26 + } 27 + return didStr 28 + } 29 + 30 + // shortDate trims an ISO 8601 timestamp to its YYYY-MM-DD prefix. 31 + func shortDate(timestamp string) string { 32 + if len(timestamp) > 10 { 33 + return timestamp[:10] 34 + } 35 + return timestamp 36 + } 37 + 38 + // renderRows writes a table of rows to stdout. emptyMessage is shown 39 + // when rows has no entries. 40 + func renderRows(rows []listRow, emptyMessage string) { 41 + if len(rows) == 0 { 42 + fmt.Println(emptyMessage) 43 + return 44 + } 45 + 46 + tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.TabIndent) 47 + fmt.Fprintln(tw, "TITLE\tSTATE\tAUTHOR\tUPDATED") 48 + 49 + for _, row := range rows { 50 + fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", row.title, row.state, row.author, row.updated) 51 + } 52 + tw.Flush() 53 + } 54 + 55 + func extractDID(uri string) string { 56 + uri = strings.TrimPrefix(uri, "at://") 57 + did, _, _ := strings.Cut(uri, "/") 58 + return did 59 + }
+76
internal/gitutil/checkout_pull.go
··· 1 + package gitutil 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "io" 7 + "net/http" 8 + "os" 9 + "os/exec" 10 + ) 11 + 12 + // CheckoutPullParams groups the inputs to CheckoutPull. 13 + type CheckoutPullParams struct { 14 + RepoDir string // local git repo to operate in 15 + PDSHost string // author's PDS base URL 16 + AuthorDID string // DID of the PR author 17 + CID string // CID of the patch blob 18 + TargetHandle string // target repo owner handle 19 + TargetRepo string // target repo name 20 + TargetBranch string // target branch to fetch and detach onto 21 + } 22 + 23 + // CheckoutPull fetches the target branch as detached HEAD and applies 24 + // the PR's gzipped patch blob on top, all inside params.RepoDir. 25 + func CheckoutPull(ctx context.Context, params CheckoutPullParams) error { 26 + fetchURL := fmt.Sprintf("git@tangled.org:%s/%s", params.TargetHandle, params.TargetRepo) 27 + if err := runIn(params.RepoDir, ctx, "git", "fetch", fetchURL, params.TargetBranch); err != nil { 28 + return fmt.Errorf("fetch target branch: %w", err) 29 + } 30 + if err := runIn(params.RepoDir, ctx, "git", "checkout", "--detach", "FETCH_HEAD"); err != nil { 31 + return fmt.Errorf("checkout detached HEAD: %w", err) 32 + } 33 + 34 + blobURL := fmt.Sprintf("%s/xrpc/com.atproto.sync.getBlob?did=%s&cid=%s", params.PDSHost, params.AuthorDID, params.CID) 35 + req, err := http.NewRequestWithContext(ctx, "GET", blobURL, nil) 36 + if err != nil { 37 + return fmt.Errorf("build blob request: %w", err) 38 + } 39 + resp, err := http.DefaultClient.Do(req) 40 + if err != nil { 41 + return fmt.Errorf("download patch blob: %w", err) 42 + } 43 + defer resp.Body.Close() 44 + 45 + return applyGzippedPatch(ctx, params.RepoDir, resp.Body) 46 + } 47 + 48 + // applyGzippedPatch decompresses a gzipped patch and applies it via 49 + // `git am` inside repoDir. 50 + func applyGzippedPatch(ctx context.Context, repoDir string, body io.Reader) error { 51 + gunzip := exec.CommandContext(ctx, "gunzip") 52 + gunzip.Stdin = body 53 + 54 + gitAm := exec.CommandContext(ctx, "git", "am") 55 + gitAm.Dir = repoDir 56 + gitAm.Stdout = os.Stdout 57 + gitAm.Stderr = os.Stderr 58 + 59 + pipe, err := gunzip.StdoutPipe() 60 + if err != nil { 61 + return fmt.Errorf("create pipe: %w", err) 62 + } 63 + gitAm.Stdin = pipe 64 + 65 + if err := gunzip.Start(); err != nil { 66 + return fmt.Errorf("start gunzip: %w", err) 67 + } 68 + if err := gitAm.Run(); err != nil { 69 + return fmt.Errorf("git am: %w", err) 70 + } 71 + if err := gunzip.Wait(); err != nil { 72 + return fmt.Errorf("gunzip: %w", err) 73 + } 74 + 75 + return nil 76 + }
+11
internal/gitutil/clone_repo.go
··· 1 + package gitutil 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + ) 7 + 8 + func CloneRepo(ctx context.Context, handle, repo, repoDir string) error { 9 + url := fmt.Sprintf("git@tangled.org:%s/%s", handle, repo) 10 + return run(ctx, "git", "clone", url, repoDir) 11 + }
+46
internal/gitutil/repo_context.go
··· 1 + package gitutil 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "os/exec" 7 + "strings" 8 + ) 9 + 10 + // RepoContext holds the handle and repo name parsed from a git remote URL. 11 + type RepoContext struct { 12 + Handle string 13 + Repo string 14 + } 15 + 16 + // DetectRepoFromCWD reads the "origin" remote URL in the current directory 17 + // It supports the ssh format git@tangled.org:handle/repo. 18 + func DetectRepoFromCWD(ctx context.Context) (*RepoContext, error) { 19 + cmd := exec.CommandContext(ctx, "git", "remote", "get-url", "origin") 20 + output, err := cmd.Output() 21 + if err != nil { 22 + return nil, fmt.Errorf("get remote origin URL: %w", err) 23 + } 24 + 25 + url := strings.TrimSpace(string(output)) 26 + 27 + prefix := "git@tangled.org:" 28 + if !strings.HasPrefix(url, prefix) { 29 + return nil, fmt.Errorf( 30 + "remote URL %q does not look like a Tangled repo (expected %s<handle>/<repo>)", 31 + url, prefix, 32 + ) 33 + } 34 + 35 + path := strings.TrimPrefix(url, prefix) 36 + parts := strings.SplitN(path, "/", 2) 37 + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { 38 + return nil, fmt.Errorf("could not parse handle/repo from %q", url) 39 + } 40 + 41 + repoName := strings.TrimSuffix(parts[1], ".git") 42 + return &RepoContext{ 43 + Handle: parts[0], 44 + Repo: repoName, 45 + }, nil 46 + }
+24
internal/gitutil/runner.go
··· 1 + package gitutil 2 + 3 + import ( 4 + "context" 5 + "os" 6 + "os/exec" 7 + ) 8 + 9 + // run executes a command in the foreground, connected to the terminal. 10 + func run(ctx context.Context, name string, args ...string) error { 11 + cmd := exec.CommandContext(ctx, name, args...) 12 + cmd.Stdout = os.Stdout 13 + cmd.Stderr = os.Stderr 14 + return cmd.Run() 15 + } 16 + 17 + // runIn is like run but sets the working directory to dir. 18 + func runIn(dir string, ctx context.Context, name string, args ...string) error { 19 + cmd := exec.CommandContext(ctx, name, args...) 20 + cmd.Dir = dir 21 + cmd.Stdout = os.Stdout 22 + cmd.Stderr = os.Stderr 23 + return cmd.Run() 24 + }
+25
nix/tg.nix
··· 1 + { 2 + buildGoModule, 3 + lib, 4 + }: 5 + buildGoModule { 6 + pname = "tg"; 7 + version = "dev"; 8 + src = ../.; 9 + vendorHash = "sha256-wv8rvPaQH4iY7KpnWrI59dmUVhxvNki8EIhklY4rMPU="; 10 + subPackages = ["cmd/tg"]; 11 + env.CGO_ENABLED = "0"; 12 + 13 + ldflags = [ 14 + "-s" 15 + "-w" 16 + ]; 17 + 18 + meta = with lib; { 19 + description = "Terminal client for Tangled"; 20 + homepage = "https://github.com/alyraffauf/tg"; 21 + license = licenses.gpl3Plus; 22 + platforms = platforms.unix; 23 + mainProgram = "tg"; 24 + }; 25 + }
+1
result
··· 1 + /nix/store/nm7df1nqr6ymlngpwda83xh4bwh7l46f-tg-dev
+47
tangled/get_repo.go
··· 1 + package tangled 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + 7 + "github.com/bluesky-social/indigo/xrpc" 8 + ) 9 + 10 + // RepoRecord is the value of a sh.tangled.repo lexicon record. 11 + type RepoRecord struct { 12 + Type string `json:"$type"` 13 + Name string `json:"name,omitempty"` 14 + Description string `json:"description,omitempty"` 15 + Knot string `json:"knot"` 16 + CreatedAt string `json:"createdAt"` 17 + Owner string `json:"owner,omitempty"` 18 + AddedAt string `json:"addedAt,omitempty"` 19 + RepoDid string `json:"repoDid,omitempty"` 20 + Spindle string `json:"spindle,omitempty"` 21 + Website string `json:"website,omitempty"` 22 + Labels []string `json:"labels,omitempty"` 23 + } 24 + 25 + type Repo struct { 26 + URI string `json:"uri"` 27 + CID string `json:"cid"` 28 + Value RepoRecord `json:"value"` 29 + } 30 + 31 + func (t *Tangled) GetRepo(ctx context.Context, repoURI string) (*Repo, error) { 32 + var repo Repo 33 + err := t.Client.Do( 34 + ctx, 35 + xrpc.Query, 36 + "", 37 + "sh.tangled.repo.getRepo", 38 + map[string]any{"repo": repoURI}, 39 + nil, 40 + &repo, 41 + ) 42 + if err != nil { 43 + return nil, fmt.Errorf("get tangled repo %q: %w", repoURI, err) 44 + } 45 + 46 + return &repo, nil 47 + }
+67
tangled/list_issues.go
··· 1 + package tangled 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + 8 + "github.com/bluesky-social/indigo/xrpc" 9 + ) 10 + 11 + type IssueRecord struct { 12 + Type string `json:"$type"` 13 + Repo string `json:"repo"` 14 + Title string `json:"title"` 15 + Body string `json:"body,omitempty"` 16 + CreatedAt string `json:"createdAt"` 17 + Mentions []string `json:"mentions,omitempty"` 18 + References []string `json:"references,omitempty"` 19 + } 20 + 21 + type IssueListItem struct { 22 + URI string `json:"uri"` 23 + CID string `json:"cid,omitempty"` 24 + Value json.RawMessage `json:"value"` 25 + State string `json:"state"` 26 + StateUpdatedAt string `json:"stateUpdatedAt,omitempty"` 27 + CommentCount int64 `json:"commentCount"` 28 + } 29 + 30 + type IssueList struct { 31 + Items []IssueListItem `json:"items"` 32 + Cursor *string `json:"cursor"` 33 + } 34 + 35 + type IssueListOpts struct { 36 + Author string // only issues by this DID 37 + State string // "open" or "closed" 38 + Limit int64 // 1-1000, default 50 39 + Order string // "asc" or "desc" 40 + } 41 + 42 + func (t *Tangled) ListIssues(ctx context.Context, repoDid string, opts IssueListOpts) (*IssueList, error) { 43 + params := map[string]any{ 44 + "subject": repoDid, 45 + } 46 + if opts.Author != "" { 47 + params["author"] = opts.Author 48 + } 49 + if opts.State != "" { 50 + params["state"] = opts.State 51 + } 52 + if opts.Limit > 0 { 53 + params["limit"] = opts.Limit 54 + } else { 55 + params["limit"] = 50 56 + } 57 + if opts.Order != "" { 58 + params["order"] = opts.Order 59 + } 60 + 61 + var out IssueList 62 + err := t.Client.Do(ctx, xrpc.Query, "", "sh.tangled.repo.listIssues", params, nil, &out) 63 + if err != nil { 64 + return nil, fmt.Errorf("list issues for %q: %w", repoDid, err) 65 + } 66 + return &out, nil 67 + }
+94
tangled/list_pulls.go
··· 1 + package tangled 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + 8 + "github.com/bluesky-social/indigo/atproto/atdata" 9 + "github.com/bluesky-social/indigo/xrpc" 10 + ) 11 + 12 + type PullRecord struct { 13 + Type string `json:"$type"` 14 + Title string `json:"title"` 15 + Body string `json:"body,omitempty"` 16 + CreatedAt string `json:"createdAt"` 17 + Mentions []string `json:"mentions,omitempty"` 18 + References []string `json:"references,omitempty"` 19 + DependentOn string `json:"dependentOn,omitempty"` 20 + Target PullTarget `json:"target"` 21 + Source PullSource `json:"source"` 22 + Rounds []PullRound `json:"rounds"` 23 + } 24 + 25 + type PullTarget struct { 26 + Repo string `json:"repo"` 27 + Branch string `json:"branch"` 28 + } 29 + 30 + type PullSource struct { 31 + Repo string `json:"repo,omitempty"` 32 + Branch string `json:"branch"` 33 + } 34 + 35 + type PullRound struct { 36 + CreatedAt string `json:"createdAt"` 37 + PatchBlob PatchBlob `json:"patchBlob"` 38 + } 39 + 40 + // PatchBlob is a gzipped git patch referenced by CID. 41 + type PatchBlob struct { 42 + Type string `json:"$type"` 43 + Ref atdata.CIDLink `json:"ref"` 44 + MimeType string `json:"mimeType"` 45 + Size int64 `json:"size"` 46 + } 47 + 48 + type PullListItem struct { 49 + URI string `json:"uri"` 50 + CID string `json:"cid,omitempty"` 51 + Value json.RawMessage `json:"value"` 52 + State string `json:"state"` 53 + StateUpdatedAt string `json:"stateUpdatedAt,omitempty"` 54 + CommentCount int64 `json:"commentCount"` 55 + } 56 + 57 + type PullList struct { 58 + Items []PullListItem `json:"items"` 59 + Cursor *string `json:"cursor"` 60 + } 61 + 62 + type PullListOpts struct { 63 + Author string // only pulls by this DID 64 + State string // "open" or "closed" 65 + Limit int64 // 1-1000, default 50 66 + Order string // "asc" or "desc" 67 + } 68 + 69 + func (t *Tangled) ListPulls(ctx context.Context, repoDid string, opts PullListOpts) (*PullList, error) { 70 + params := map[string]any{ 71 + "subject": repoDid, 72 + } 73 + if opts.Author != "" { 74 + params["author"] = opts.Author 75 + } 76 + if opts.State != "" { 77 + params["state"] = opts.State 78 + } 79 + if opts.Limit > 0 { 80 + params["limit"] = opts.Limit 81 + } else { 82 + params["limit"] = 50 83 + } 84 + if opts.Order != "" { 85 + params["order"] = opts.Order 86 + } 87 + 88 + var out PullList 89 + err := t.Client.Do(ctx, xrpc.Query, "", "sh.tangled.repo.listPulls", params, nil, &out) 90 + if err != nil { 91 + return nil, fmt.Errorf("list PRs for %q: %w", repoDid, err) 92 + } 93 + return &out, nil 94 + }
+30
tangled/list_repos.go
··· 1 + package tangled 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + 7 + "github.com/bluesky-social/indigo/xrpc" 8 + ) 9 + 10 + type RepoList struct { 11 + Items []Repo `json:"items"` 12 + Cursor *string `json:"cursor"` 13 + } 14 + 15 + func (t *Tangled) ListRepos(ctx context.Context, ownerDid string) (*RepoList, error) { 16 + var repos RepoList 17 + err := t.Client.Do(ctx, 18 + xrpc.Query, "", "sh.tangled.repo.listRepos", map[string]any{ 19 + "subject": ownerDid, 20 + "limit": 100, 21 + }, 22 + nil, 23 + &repos, 24 + ) 25 + if err != nil { 26 + return nil, fmt.Errorf("list tangled repos for %q: %w", ownerDid, err) 27 + } 28 + 29 + return &repos, nil 30 + }
+13
tangled/main.go
··· 1 + package tangled 2 + 3 + import ( 4 + "log/slog" 5 + 6 + "github.com/bluesky-social/indigo/xrpc" 7 + ) 8 + 9 + // Tangled is a client for the read-only bobbin XRPC API at api.tangled.org. 10 + type Tangled struct { 11 + Client *xrpc.Client 12 + Logger *slog.Logger 13 + }
tg

This is a binary file and will not be displayed.