├── .gitignore ├── .travis.yml ├── CITATION.cff ├── LICENSE.txt ├── Makefile ├── README.md ├── doc ├── .gitignore ├── Makefile ├── check-completeness.sh └── youtube-script.sh ├── gi-completion.sh ├── gi.sh ├── git-issue.1 ├── git-issue.sh ├── lib └── git-issue │ └── import-export.sh ├── scripts └── replacerefs.sh ├── simple-make.sh ├── sync-docs.sh └── test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .issues 2 | set-dds-env.sh 3 | token 4 | git-issue.pdf 5 | git-issue.ps 6 | git-issue 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | addons: 2 | apt: 3 | packages: 4 | curl 5 | jq 6 | homebrew: 7 | packages: 8 | - shellcheck 9 | - jq 10 | update: true 11 | 12 | language: bash 13 | 14 | script: make test 15 | 16 | os: 17 | - linux 18 | - osx 19 | 20 | osx_image: xcode11 21 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # This CITATION.cff file was generated with cffinit. 2 | # Visit https://bit.ly/cffinit to generate yours today! 3 | 4 | cff-version: 1.2.0 5 | title: git-issue 6 | message: >- 7 | If you use this software, please cite it using the 8 | metadata from this file. 9 | type: software 10 | authors: 11 | - given-names: Diomidis 12 | family-names: Spinellis 13 | email: dds@aueb.gr 14 | affiliation: Athens University of Economics and Business 15 | orcid: 'https://orcid.org/0000-0003-4231-1897' 16 | repository-code: 'https://github.com/dspinellis/git-issue' 17 | keywords: 18 | - Git 19 | - issue tracking 20 | - CLI 21 | license: GPL-3.0 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | 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 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX ?= /usr/local 2 | BINPREFIX ?= "$(PREFIX)/bin" 3 | LIBPREFIX ?= "$(PREFIX)/lib" 4 | MANPREFIX ?= "$(PREFIX)/share/man/man1" 5 | SYSCONFDIR ?= "$(PREFIX)/etc" 6 | 7 | default: install 8 | 9 | install: 10 | @mkdir -p "$(DESTDIR)$(MANPREFIX)" 11 | @mkdir -p "$(DESTDIR)$(BINPREFIX)" 12 | @mkdir -p "$(DESTDIR)$(LIBPREFIX)/git-issue" 13 | sed "s|/usr/local|$(PREFIX)|g" git-issue.sh > git-issue 14 | install git-issue "$(DESTDIR)$(BINPREFIX)/git-issue" 15 | install lib/git-issue/import-export.sh "$(DESTDIR)$(LIBPREFIX)/git-issue/import-export.sh" 16 | install -m 644 git-issue.1 "$(DESTDIR)$(MANPREFIX)/" 17 | @mkdir -p "$(DESTDIR)$(SYSCONFDIR)/bash_completion.d" 18 | install -m 644 gi-completion.sh "$(DESTDIR)$(SYSCONFDIR)/bash_completion.d/git-issue" 19 | 20 | # Synchronize man page and usage with the contents of the README file 21 | sync-docs: 22 | ./sync-docs.sh 23 | 24 | test: 25 | if shellcheck --version >/dev/null 2>&1 ; then \ 26 | shellcheck -x *.sh lib/git-issue/*.sh ; \ 27 | else \ 28 | echo 'Skipping shellcheck; consider installing it' ; \ 29 | fi 30 | ./test.sh 31 | 32 | uninstall: 33 | rm -f "$(DESTDIR)$(BINPREFIX)/git-issue" 34 | rm -f "$(DESTDIR)$(MANPREFIX)/git-issue." 35 | rm -f "$(DESTDIR)$(SYSCONFDIR)/bash_completion.d/git-issue" 36 | 37 | clean: 38 | rm -f git-issue 39 | 40 | .PHONY: default clean install uninstall sync-docs test 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/eellak/gsoc2019-git-issue.svg?branch=gsoc-2019)](https://travis-ci.org/eellak/gsoc2019-git-issue) 2 | # git-issue 3 | 4 | This is a minimalist decentralized issue management system based on Git, 5 | offering (optional) biderectional integration with GitHub and GitLab issue management. 6 | It has the following advantages over other systems. 7 | 8 | * **No backend, no dependencies:** 9 | You can install and use _git issue_ with a single shell command. 10 | There's no need for a server or a database back-end, and the corresponding 11 | problems and requirements for their administration. 12 | * **Decentralized asynchronous management:** 13 | Anyone can add, comment, and edit issues without requiring online access 14 | to a centralized server. 15 | There's no need for online connectivity; you can pull and push issues 16 | when you're online. 17 | * **Transparent text file format:** 18 | Issues are stored as simple text files, which you can view, edit, share, and 19 | backup with any tool you like. 20 | There's no risk of losing access to your issues because a server has 21 | failed. 22 | * **Git-based:** 23 | Issues are changed and shared through Git. 24 | This provides _git issue_ with a robust, efficient, portable, 25 | and widely available infrastructure. 26 | It allows you to reuse your Git credentials and infrastructure, allows 27 | the efficient merging of work, and also provides a solid audit trail 28 | regarding any changes. 29 | You can even use Git and command-line tools directly to make sophisticated 30 | changes to your issue database. 31 | 32 | ## Installation 33 | 34 | ### Administrator installation 35 | Clone the repo and run `make install` with appropriate privileges. 36 | 37 | ### Personal installation 38 | Clone the repo and register a git alias to the `git-issue.sh` script: 39 | 40 | ``` 41 | git config --global alias.issue '!'"${REPO_PATH}/git-issue.sh" 42 | ``` 43 | 44 | If you are using a bash shell, you can also register the autocompletion 45 | by adding the following to your .bashrc. 46 | 47 | ``` 48 | source ${REPO_PATH}/gi-completion.sh 49 | ``` 50 | 51 | ### Backward compatibility with the gi command 52 | For backward compatibility you can also use the original _gi_ command, 53 | by copying `gi.sh` to someplace in your path. 54 | In this case you must register the git alias to use the auto completion feature. 55 | If you have administrative access you can install it with 56 | `sudo install gi.sh /usr/local/bin/gi`. 57 | For your personal use, 58 | assuming that the directory `~/bin` exists and is in your path, 59 | you can install it with `install gi.sh ~/bin/gi`. 60 | You can even put `gi` in your project's current directory and run it from there. 61 | 62 | ### Portability and testing 63 | The `git-issue.sh` script has been tested on: 64 | * Archlinux (20220101) 65 | * Debian GNU/Linux 66 | * FreeBSD 67 | * macOS 68 | * Cygwin. 69 | If you're running *git issue* on another system, 70 | run the `test.sh` script to verify 71 | its operation, and (please) update this file. 72 | 73 | ### Requirements 74 | `git-issue` requires the *jq* and *curl* utilities. 75 | OS X users might also need GNU date, obtained by installing `homebrew` package coreutils. 76 | For running the tests *shellcheck* is also required. 77 | 78 | ## Use 79 | You use _git issue_ with the following sub-commands. 80 | 81 | ### Start an issue repository 82 | * `git issue clone`: Clone the specified remote repository. 83 | * `git issue init`: Create a new issues repository in the current directory. 84 | The `-e` option uses an existing Git project repository. 85 | 86 | ### Work with an issue 87 | * `git issue new`: Create a new open issue (with optional `-s` summary and -c "provider user repo" for github/gitlab export). 88 | * `git issue show`: Show specified issue (and its comments with `-c`). 89 | * `git issue comment`: Add an issue comment. 90 | * `git issue edit`: Edit the specified issue's (or comment's with -c) description 91 | * `git issue tag`: Add (or remove with `-r`) a tag. 92 | * `git issue milestone`: Specify (or remove with `-r`) the issue's milestone. 93 | * `git issue weight`: Specify (or remove with `-r`) the issue's weight. 94 | The weight is a positive integer that serves as a measure of importance. 95 | * `git issue duedate`: Specify (or remove with `-r`) the issue's due date. 96 | The command accepts all formats supported by the `date` utility. 97 | * `git issue timeestimate`: Specify (or remove with `-r`) a time estimate for this issue. 98 | Time estimates can be given in a format accepted by `date`, 99 | however bear in mind that it represents a time interval, not a date. 100 | * `git issue timespent`: Specify (or remove with `-r`) the time spent working on an issue so far. 101 | Follows the same format outlined above. 102 | If the `-a` option is given, the time interval will be added together with the existing one. 103 | * `git issue assign`: Assign (or remove `-r`) an issue to a person. 104 | The person is specified with his/her email address. 105 | The form `@name` or `name@` can be used as a shortcut, provided it 106 | uniquely identifies an existing assignee or committer. 107 | Note that if you plan to export the issue to a GitHub/GitLab repository, the assignee may be rejected if 108 | it doesn't correspond to a valid username, or if you don't have the necessary permissions. 109 | * `git issue attach`: Attach (or remove with `-r`) a file to an issue. 110 | * `git issue watcher`: Add (or remove with `-r`) an issue watcher. 111 | * `git issue close`: Remove the `open` tag, add the closed tag 112 | ### Show multiple issues 113 | * `git issue list`: List open issues (or all with `-a`). 114 | An optional argument can show issues matching a tag or milestone. 115 | * `git issue list -l formatstring`: This will list issues in the specified format, given as an argument to `-l`. 116 | The following escape sequences can be used: 117 | 118 | * `%n` : newline 119 | * `%i` : issue ID 120 | * `%c` : creation date 121 | * `%d` : due date 122 | * `%e` : time estimate 123 | * `%s` : time spent 124 | * `%w` : weight 125 | * `%M` : Milestone 126 | * `%A` : Assignee(s) 127 | * `%T` : Tags 128 | * `%D` : Description(first line) 129 | 130 | If the format string is one of: (`oneline`, `short` or `full`) it will interpreted as the corresponding preset. 131 | 132 | Optionally, one of the above given with `-o` will order based on this field(reverse order with `-r`). 133 | 134 | ### Work with multiple issues 135 | * `git issue filter-apply command`: Run `command` in every issue directory. The following environment variables will be set: 136 | * `GI_SHA` : Sha of the current issue 137 | * `GI_IMPORTS` : The imports directories for current issue(one on each line) 138 | * `GI_AUTHOR` : Author of current issue 139 | * `GI_DATE` : Creation date of current issue 140 | 141 | The command can read, add/remove or edit any of the issue's attributes. 142 | Some potentially useful scripts to be used with this command are in the scripts/ directory. 143 | Remember to inspect the results (e.g. `gi git diff`) and commit them with `gi git commit -a`. 144 | 145 | ### Synchronize with remote repositories 146 | * `git issue push`: Update remote Git repository with local changes. 147 | * `git issue pull`: Update local Git repository with remote changes. 148 | * `git issue import`: Import/update GitHub/GitLab issues from the specified project. 149 | If the import involves more than a dozen of issues or if the repository 150 | is private, set the environment variable `GH_CURL_AUTH` (GitHub) or `GL_CURL_AUTH` (GitLab) to the authentication token. 151 | For example, run the following command: `export GH_CURL_AUTH="Authorization: token badf00ddead9bfee8f3c19afc3c97c6db55fcfde"` 152 | You can create the authorization token through 153 | [GitHub settings](https://github.com/settings/tokens/new), with the `repo` and `delete_repo`(only for running the tests) permissions. 154 | For GitLab: `export GL_CURL_AUTH="PRIVATE-TOKEN: JvHLsdnDmD7rjUXzT-Ea"`. The `api` permission is required. 155 | Use the [GitLab settings](https://gitlab.com/profile/personal_access_tokens) to create the token. 156 | In case the repository is part of a GitLab group, specify repository as groupname/reponame. 157 | * `git issue create`: Create the issue in the provided GitHub repository. 158 | With the `-e` option any escape sequences for the attributes present in the description, will be replaced as above. 159 | This can be used to e.g. export an unsupported attribute to GitHub as text. 160 | * `git issue export`: Export modified issues for the specified project. 161 | Only the issues that have been imported and modified (or created by `git issue create`) by `git-issue` will be exported. 162 | With the `-e` option any escape sequences for the attributes present in the description, will be replaced as above. 163 | This can be used to e.g. export an unsupported attribute to GitHub as text. 164 | * `git issue exportall`: Export all open issues in the database (`-a` to include closed ones) to GitHub/GitLab. Useful for cloning whole repositories. 165 | 166 | ### Help and debug 167 | * `git issue help`: Display help information about git issue. 168 | * `git issue log`: Output a log of changes made 169 | * `git issue git`: Run the specified Git command on the issues repository. 170 | * `git issue dump`: Dump the whole database in json format to stdout. 171 | 172 | Issues and comments are specified through the SHA hash associated with the 173 | parent of the commit that opened them, which is specifically crafted for 174 | that element and can be used to derive its date and author. 175 | 176 | ## Internals 177 | All data are stored under `.issues`, which should be placed under `.gitignore`, 178 | if it will coexist with another Git-based project. 179 | The directory contains the following elements. 180 | * A `.git` directory contains the Git data associated with the issues. 181 | * A `config` file with configuration data. 182 | * An `imports` directory contains details about imported issues. 183 | * The `sha` file under `import////` contains the 184 | _git-issue_ SHA corresponding to an imported GitHub _number_ issue. 185 | Likewise for GitLab. 186 | * The `sha` file under `import/////comments/` 187 | contains the _git-issue_ comment SHA corresponding to an imported GitHub/GitLab 188 | _number_ comment. 189 | * The file `import////checkpoint` contains the SHA 190 | of the last imported or updated issue. This can be used for merging 191 | future updates. 192 | * An `issues` directory contains the individual issues. 193 | * Each issue is stored in a directory named `issues/xx/xxxxxxx...`, 194 | where the x's are the SHA of the issue's initial commit. 195 | * Each issue can have the following elements in its directory. 196 | * A `description` file with a one-line summary and a description of the issue. 197 | * A `duedate` file with the due date stored in ISO-8601 format. 198 | * A `weight` file with the weight stored as a positive integer. 199 | * A `timespent` and `timeestimate` file with the time estimate and time spent respectively, stored in seconds. 200 | * A `comments` directory where comments are stored, each with the SHA of 201 | a commit containing the text `gi comment mark` 202 | _issue SHA_. 203 | * An `attachments` directory where the issue's attachments are stored. 204 | * A `tags` file containing the issue's tags, one in each line. 205 | * A `milestone` file containing the issue's milestone name. 206 | * A `watchers` file containing the emails of persons to be notified when the issue changes (one per line). 207 | * An `assignee` file containing the email for the person assigned to the issue. 208 | * A `templates` directory with message templates. 209 | 210 | ## Contributing 211 | Contributions are welcomed through pull requests. 212 | Before working on a new feature please look at open issues, and if no 213 | corresponding issue is open, create one to claim priority over the task. 214 | Contributions should pass tests and should be accompanied with a 215 | corresponding test case and documentation update. 216 | Note that to avoid duplicating information, the subcommands, the used files, 217 | and usage examples, are automatically inserted into the script and its 218 | documentation from the `README.md` file using the `sync-docs.sh` command. 219 | 220 | ## Video 221 | The video of a presentation of *git issue* at [FOSDEM 2020](https://fosdem.org/2020/schedule/event/git_issue_management/) is available [for streaming](https://video.fosdem.org/2020/H.2215/git_issue_management.webm) or [download](https://video.fosdem.org/2020/H.2215/git_issue_management.mp4). 222 | 223 | ## Example session 224 | You can also view a video of the following session on [YouTube](https://youtu.be/mLOZGolLmW4). 225 | 226 | ### Initialize issue repository 227 | 228 | ``` 229 | $ git issue init 230 | Initialized empty Issues repository in /home/dds/src/gi/.issues 231 | $ git issue new -s 'New issue entered from the command line' 232 | Added issue e6a95c9 233 | ``` 234 | 235 | ### Create a new issue (opens editor window) 236 | 237 | ``` 238 | $ git issue new 239 | Added issue 7dfa5b7 240 | ``` 241 | 242 | ### List open issues 243 | 244 | ``` 245 | $ git issue list 246 | 7dfa5b7 An issue entered from the editor 247 | e6a95c9 New issue entered from the command line 248 | ``` 249 | 250 | ### Add an issue comment (opens editor window) 251 | 252 | ``` 253 | $ git issue comment e6a95c9 254 | Added comment 8c0d5b3 255 | ``` 256 | 257 | ### Add a due date for the issue 258 | 259 | ``` 260 | $ git issue duedate "next Tuesday" e6a95c9 261 | Added duedate 2019-08-13T00:00:00+03:00 262 | ``` 263 | 264 | ### Keep track of time spent on the issue 265 | 266 | ``` 267 | $ git issue timespent "2hours" e6a95c9 268 | Added timespent 7200 269 | ``` 270 | 271 | ### Log additional time spent working on it 272 | 273 | ``` 274 | $ git issue timespent -a "4 hours" e6a95c9 275 | Added timespent 21600 276 | ``` 277 | 278 | ### Add tag to an issue 279 | 280 | ``` 281 | $ git issue tag e6a9 urgent 282 | Added tag urgent 283 | ``` 284 | 285 | ### Add two more tags 286 | 287 | ``` 288 | $ git issue tag e6a9 gui crash 289 | Added tag gui 290 | Added tag crash 291 | ``` 292 | 293 | ### Remove a tag 294 | 295 | ``` 296 | $ git issue tag -r e6a9 urgent 297 | Removed tag urgent 298 | ``` 299 | 300 | ### Assign issue 301 | 302 | ``` 303 | $ git issue assign e6a9 joe@example.com 304 | Assigned to joe@example.com 305 | ``` 306 | 307 | ### Add issue watcher 308 | 309 | ``` 310 | $ git issue watcher e6a9 jane@example.com 311 | Added watcher jane@example.com 312 | ``` 313 | 314 | ### List issues tagged as gui 315 | 316 | ``` 317 | $ git issue list gui 318 | e6a95c9 New issue entered from the command line 319 | ``` 320 | 321 | ### Push issues repository to a server 322 | 323 | ``` 324 | $ git issue git remote add origin git@github.com:dspinellis/gi-example.git 325 | $ git issue git push -u origin master 326 | Counting objects: 60, done. 327 | Compressing objects: 100% (50/50), done. 328 | Writing objects: 100% (60/60), 5.35 KiB | 0 bytes/s, done. 329 | Total 60 (delta 8), reused 0 (delta 0) 330 | To git@github.com:dspinellis/gi-example.git 331 | * [new branch] master -> master 332 | Branch master set up to track remote branch master from origin. 333 | ``` 334 | 335 | ### Clone issues repository from server 336 | 337 | ``` 338 | $ git issue clone git@github.com:dspinellis/gi-example.git my-issues 339 | Cloning into '.issues'... 340 | remote: Counting objects: 60, done. 341 | remote: Compressing objects: 100% (42/42), done. 342 | remote: Total 60 (delta 8), reused 60 (delta 8), pack-reused 0 343 | Receiving objects: 100% (60/60), 5.35 KiB | 0 bytes/s, done. 344 | Resolving deltas: 100% (8/8), done. 345 | Checking connectivity... done. 346 | Cloned git@github.com:dspinellis/gi-example.git into my-issues 347 | ``` 348 | 349 | ### List open issues 350 | 351 | ``` 352 | $ git issue list 353 | 7dfa5b7 An issue entered from the editor 354 | e6a95c9 New issue entered from the command line 355 | ``` 356 | 357 | ### Create new issue 358 | 359 | ``` 360 | $ git issue new -s 'Issue added on another host' 361 | Added issue abc9adc 362 | ``` 363 | 364 | ### Push changes to server 365 | 366 | ``` 367 | $ git issue push 368 | Counting objects: 7, done. 369 | Compressing objects: 100% (6/6), done. 370 | Writing objects: 100% (7/7), 767 bytes | 0 bytes/s, done. 371 | Total 7 (delta 0), reused 0 (delta 0) 372 | To git@github.com:dspinellis/gi-example.git 373 | d6be890..740f9a0 master -> master 374 | ``` 375 | 376 | ### Show issue added on the other host 377 | 378 | ``` 379 | $ git issue show 7dfa5b7 380 | issue 7dfa5b7f4591ecaa8323716f229b84ad40f5275b 381 | Author: Diomidis Spinellis 382 | Date: Fri, 29 Jan 2016 01:03:24 +0200 383 | Tags: open 384 | 385 | An issue entered from the editor 386 | 387 | Here is a longer description. 388 | ``` 389 | 390 | ### Show issue and comments 391 | 392 | ``` 393 | $ git issue show -c e6a95c9 394 | issue e6a95c91b31ded8fc229a41cc4bd7d281ce6e0f1 395 | Author: Diomidis Spinellis 396 | Date: Fri, 29 Jan 2016 01:03:20 +0200 397 | Tags: open urgent gui crash 398 | Watchers: jane@example.com 399 | Assigned-to: joe@example.com 400 | 401 | New issue entered from the command line 402 | 403 | comment 8c0d5b3d77bf93b937cb11038b129f927d49e34a 404 | Author: Diomidis Spinellis 405 | Date: Fri, 29 Jan 2016 01:03:57 +0200 406 | 407 | First comment regarding the issue. 408 | ``` 409 | 410 | ### Pull in remote changes (on the original host) 411 | 412 | ``` 413 | $ git issue pull 414 | remote: Counting objects: 7, done. 415 | remote: Compressing objects: 100% (6/6), done. 416 | remote: Total 7 (delta 0), reused 7 (delta 0), pack-reused 0 417 | Unpacking objects: 100% (7/7), done. 418 | From github.com:dspinellis/gi-example 419 | d6be890..740f9a0 master -> origin/master 420 | Updating d6be890..740f9a0 421 | Fast-forward 422 | issues/ab/c9adc61025a3cb73b0c67470b65cefc133a8d0/description | 1 + 423 | issues/ab/c9adc61025a3cb73b0c67470b65cefc133a8d0/tags | 1 + 424 | 2 files changed, 2 insertions(+) 425 | create mode 100644 issues/ab/c9adc61025a3cb73b0c67470b65cefc133a8d0/description 426 | create mode 100644 issues/ab/c9adc61025a3cb73b0c67470b65cefc133a8d0/tags 427 | ``` 428 | 429 | ### List open issues 430 | 431 | ``` 432 | $ git issue list 433 | 7dfa5b7 An issue entered from the editor 434 | abc9adc Issue added on another host 435 | e6a95c9 New issue entered from the command line 436 | ``` 437 | 438 | ### Import issues from GitHub 439 | 440 | ``` 441 | $ git issue import github dspinellis git-issue-test-issues # Import GitHub issues 442 | Imported/updated issue #3 as 0a27c66 443 | Imported/updated issue #2 as feb2a2c 444 | Imported/updated issue #2 comment 416631296 as f7de92c 445 | Imported/updated issue #2 comment 416631349 as 03acf84 446 | Imported/updated issue #2 comment 417048301 as 0cd48ed 447 | Imported/updated issue #2 comment 417049466 as 325a581 448 | Imported/updated issue #1 as bbe144d 449 | $ git issue list 450 | feb2a2c An open issue on GitHub with a description and comments 451 | 0a27c66 An open issue on GitHub with assignees and tags 452 | $ git issue show 0a27c66 453 | issue 0a27c6633f492e42bb2a24e6ae458482a4690a55 454 | Author: dspinellis 455 | Date: Thu, 30 Aug 2018 20:59:59 +0000 456 | GitHub issue: #3 at vyrondrosos/git-issue-test-issues 457 | Tags: bug 458 | duplicate 459 | enhancement 460 | good first issue 461 | open 462 | Assigned-to: dspinellis 463 | louridas 464 | 465 | An open issue on GitHub with assignees and tags 466 | 467 | Description 468 | 469 | Edit History: 470 | * Thu, 30 Aug 2018 20:59:59 +0000 by dspinellis 471 | * 472 | ``` 473 | 474 | ### Export all issues to GitHub 475 | 476 | ``` 477 | $ git issue exportall github dspinellis git-issue-test-issues 478 | Creating issue 9179d38... 479 | Couldn't add assignee dspinellis. Skipping... 480 | Couldn't add assignee louridas. Skipping... 481 | Creating issue 3651dd3... 482 | Creating new Milestone ver3... 483 | Creating comment d72c68d0177b500a91ea37548e6594f84457fd5b... 484 | Creating comment 6966d4d718c80cf8635e9276d6f391de70c22f93... 485 | Creating comment 85293a6904d0fbd6238fbb2e1c36fc65af9ffc60... 486 | Creating comment aea83723c0414ff135afcfb5165d64f8a7ad687c... 487 | ``` 488 | 489 | ### Make changes 490 | 491 | ``` 492 | $ git issue edit 9179d38 493 | Opening editor... 494 | Edited issue 9179d38 495 | $ git issue edit -c d72c6 496 | Opening editor... 497 | Edited comment d72c68d 498 | ``` 499 | 500 | ### Export modified issues back to GitHub 501 | 502 | ``` 503 | $ git issue export github dspinellis git-issue-test-issues # Needs a token with the relevant permissions 504 | Issue b83d92872dc16440402516a5f4ce1b8cc6436344 not modified, skipping... 505 | Comment a93764f32179e93493ceb0a7060efce1e980aff1 not modified, skipping... 506 | Exporting issue 9179d381135273220301f175c03b101b3e9c703d as #15 507 | Issue 3651dd38e4e1d9dbce66649710324235c773fe78 not modified, skipping... 508 | Updating comment d72c68d0177b500a91ea37548e6594f84457fd5b... 509 | Comment 6966d4d718c80cf8635e9276d6f391de70c22f93 not modified, skipping... 510 | Comment 85293a6904d0fbd6238fbb2e1c36fc65af9ffc60 not modified, skipping... 511 | Comment aea83723c0414ff135afcfb5165d64f8a7ad687c not modified, skipping... 512 | ``` 513 | 514 | ### Sub-command auto-completion 515 | 516 | ``` 517 | $ git issue [Tab] 518 | assign clone comment git init log pull show watcher 519 | attach close edit help list new push tag 520 | ``` 521 | 522 | ### Issue SHA auto-completion 523 | 524 | ``` 525 | $ git issue show [Tab] 526 | 7dfa5b7 - An issue entered from the editor 527 | e6a95c9 - New issue entered from the command line 528 | ``` 529 | 530 | ## Related work 531 | * [bug](https://github.com/driusan/bug), inspired by Bugs Everywhere, written in Go, supports git and hg 532 | * [Bugs Everywhere](http://www.bugseverywhere.org/), also written in Python, supports many version control backends and offers a web interface. 533 | * [deft](https://github.com/npryce/deft) developed in 2011 is based on 534 | the same idea. 535 | It requires Python and offers a GUI. 536 | * [Fossil](http://fossil-scm.org/) is a distributed version control software that also supports issue tracking and a wiki. It runs as a single executable. 537 | * [git-appraise](https://github.com/google/git-appraise) is a distributed 538 | code review system for Git repos based again on Git. 539 | * [git-bug](https://github.com/MichaelMure/git-bug), again written in Go, is a distributed bug tracker embedded in git. 540 | * [GitHub cli](https://cli.github.com/manual/gh_issue), offers a subcommand for managing GitHub's issues. 541 | * [Perceval](https://github.com/chaoss/grimoirelab-perceval) can download issues from a variety of systems, including GitHub and GitLab. 542 | * [SD (Simple Defects)](https://syncwith.us/sd/), an (unmaintained) distributed bug tracking system based on a distributed database. It can import/export from/to foreign ticketing systems. 543 | 544 | More historical references can be found in [this old LWN article on distributed bug tracking](https://lwn.net/Articles/281849/). 545 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | git-issue.png 2 | git-issue.sc.mov 3 | git-issue.sc.time 4 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | git-issue.sc.mov: youtube-script.sh /cygdrive/r 2 | $(MOOC)/textanimate/makemovie.sh -s youtube-script.sh git-issue 3 | 4 | # Create RAM disk; see http://www.ltr-data.se/opencode.html/#ImDisk 5 | /cygdrive/r: 6 | imdisk -a -t vm -m R: -s 2G -p "/fs:ntfs /q /y" 7 | 8 | -------------------------------------------------------------------------------- /doc/check-completeness.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Check which commands are missing from the script 4 | # 5 | 6 | # Get all available sub-commands 7 | git issue help | 8 | # Convert to complete command 9 | sed -n 's/^ \([^ ]*\).*/git issue \1/p' | 10 | while read cmd ; do 11 | if ! grep -q "^\\$ $cmd" youtube-script.sh ; then 12 | echo $cmd needs example 13 | fi 14 | done 15 | -------------------------------------------------------------------------------- /doc/youtube-script.sh: -------------------------------------------------------------------------------- 1 | $ git clone git@github.com:dspinellis/git-issue.git 2 | Cloning into 'git-issue'... 3 | remote: Enumerating objects: 1057, done. 4 | remote: Counting objects: 100% (1057/1057), done. 5 | remote: Compressing objects: 100% (260/260), done. 6 | remote: Total 1683 (delta 559), reused 1029 (delta 538), pack-reused 626 7 | Receiving objects: 100% (1683/1683), 494.52 KiB | 1.74 MiB/s, done. 8 | Resolving deltas: 100% (947/947), done. 9 | $ cd git-issue 10 | 11 | $ sudo make install # Install 12 | mkdir -p "/usr/local/share/man/man1" 13 | mkdir -p "/usr/local/bin" 14 | mkdir -p "/usr/local/lib"/git-issue 15 | install git-issue.sh "/usr/local/bin"/git-issue 16 | install lib/git-issue/import-export.sh "/usr/local/lib"/git-issue/import-export.sh 17 | install -m 644 git-issue.1 "/usr/local/share/man/man1"/ 18 | mkdir -p /usr/local/etc/bash_completion.d 19 | install -m 644 gi-completion.sh /usr/local/etc/bash_completion.d/git-issue 20 | 21 | $ make install PREFIX=$HOME # Install for current user 22 | install git-issue.sh "/home/dds/bin"/git-issue 23 | install lib/git-issue/import-export.sh "/home/dds/lib"/git-issue/import-export.sh 24 | install -m 644 git-issue.1 "/home/dds/share/man/man1"/ 25 | install -m 644 gi-completion.sh /home/dds/etc/bash_completion.d/git-issue 26 | 27 | $ git issue init # Initialize issue repository 28 | Initialized empty Issues repository in /home/dds/src/git-issue/.issues 29 | 30 | $ git issue new -s 'New issue entered from the command line' 31 | Added issue e6a95c9 32 | 33 | $ git issue new # Create a new issue (opens editor window) 34 | Added issue 7dfa5b7 35 | 36 | $ git issue list # List open issues 37 | 7dfa5b7 An issue entered from the editor 38 | e6a95c9 New issue entered from the command line 39 | 40 | $ git issue comment e6a95c9 # Add an issue comment (opens editor window) 41 | Added comment 8c0d5b3 42 | 43 | $ git issue tag e6a9 urgent # Add tag to an issue 44 | Added tag urgent 45 | 46 | $ git issue tag e6a9 gui crash # Add two more tags 47 | Added tag gui 48 | Added tag crash 49 | 50 | $ git issue tag -r e6a9 urgent # Remove a tag 51 | Removed tag urgent 52 | 53 | $ git issue assign e6a9 joe@example.com # Assign issue 54 | Assigned to joe@example.com 55 | 56 | $ git issue watcher e6a9 jane@example.com # Add issue watcher 57 | Added watcher jane@example.com 58 | 59 | $ git issue list gui # List issues tagged as gui 60 | e6a95c9 New issue entered from the command line 61 | 62 | $ # Push issues repository to a server 63 | $ git issue git remote add origin git@github.com:dspinellis/gi-example.git 64 | 65 | $ git issue git push -u origin master 66 | Counting objects: 60, done. 67 | Compressing objects: 100% (50/50), done. 68 | Writing objects: 100% (60/60), 5.35 KiB | 0 bytes/s, done. 69 | Total 60 (delta 8), reused 0 (delta 0) 70 | To git@github.com:dspinellis/gi-example.git 71 | * [new branch] master -> master 72 | Branch master set up to track remote branch master from origin. 73 | 74 | $ # Clone issues repository from server 75 | $ git issue clone git@github.com:dspinellis/gi-example.git my-issues 76 | Cloning into '.issues'... 77 | remote: Counting objects: 60, done. 78 | remote: Compressing objects: 100% (42/42), done. 79 | remote: Total 60 (delta 8), reused 60 (delta 8), pack-reused 0 80 | Receiving objects: 100% (60/60), 5.35 KiB | 0 bytes/s, done. 81 | Resolving deltas: 100% (8/8), done. 82 | Checking connectivity... done. 83 | Cloned git@github.com:dspinellis/gi-example.git into my-issues 84 | 85 | $ git issue list # List open issues 86 | 7dfa5b7 An issue entered from the editor 87 | e6a95c9 New issue entered from the command line 88 | 89 | $ git issue new -s 'Issue added on another host' # Create new issue 90 | Added issue abc9adc 91 | 92 | $ git issue push # Push changes to server 93 | Counting objects: 7, done. 94 | Compressing objects: 100% (6/6), done. 95 | Writing objects: 100% (7/7), 767 bytes | 0 bytes/s, done. 96 | Total 7 (delta 0), reused 0 (delta 0) 97 | To git@github.com:dspinellis/gi-example.git 98 | d6be890..740f9a0 master -> master 99 | 100 | $ git issue show 7dfa5b7 # Show issue added on the other host 101 | issue 7dfa5b7f4591ecaa8323716f229b84ad40f5275b 102 | Author: Diomidis Spinellis 103 | Date: Fri, 29 Jan 2016 01:03:24 +0200 104 | Tags: open 105 | 106 | An issue entered from the editor 107 | 108 | Here is a longer description. 109 | 110 | $ git issue show -c e6a95c9 # Show issue and coments 111 | issue e6a95c91b31ded8fc229a41cc4bd7d281ce6e0f1 112 | Author: Diomidis Spinellis 113 | Date: Fri, 29 Jan 2016 01:03:20 +0200 114 | Tags: open urgent gui crash 115 | Watchers: jane@example.com 116 | Assigned-to: joe@example.com 117 | 118 | New issue entered from the command line 119 | 120 | comment 8c0d5b3d77bf93b937cb11038b129f927d49e34a 121 | Author: Diomidis Spinellis 122 | Date: Fri, 29 Jan 2016 01:03:57 +0200 123 | 124 | First comment regarding the issue. 125 | 126 | 127 | $ # On the original host 128 | $ git issue pull # Pull in remote changes 129 | remote: Counting objects: 7, done. 130 | remote: Compressing objects: 100% (6/6), done. 131 | remote: Total 7 (delta 0), reused 7 (delta 0), pack-reused 0 132 | Unpacking objects: 100% (7/7), done. 133 | From github.com:dspinellis/gi-example 134 | d6be890..740f9a0 master -> origin/master 135 | Updating d6be890..740f9a0 136 | Fast-forward 137 | issues/ab/c9adc61025a3cb73b0c67470b65cefc133a8d0/description | 1 + 138 | issues/ab/c9adc61025a3cb73b0c67470b65cefc133a8d0/tags | 1 + 139 | 2 files changed, 2 insertions(+) 140 | create mode 100644 issues/ab/c9adc61025a3cb73b0c67470b65cefc133a8d0/description 141 | create mode 100644 issues/ab/c9adc61025a3cb73b0c67470b65cefc133a8d0/tags 142 | 143 | $ git issue list # List open issues 144 | 7dfa5b7 An issue entered from the editor 145 | abc9adc Issue added on another host 146 | e6a95c9 New issue entered from the command line 147 | 148 | 149 | $ # GitHub import functionality 150 | $ mkdir github-project 151 | $ cd github-project/ 152 | $ git issue init 153 | Initialized empty issues repository in /home/dds/github-project/.issues 154 | 155 | $ git issue import github dspinellis git-issue-test-issues # Import GitHub issues 156 | Imported/updated issue #3 as 4a0b58a 157 | Imported/updated issue #2 as 1e87224 158 | Imported/updated issue #2 comment 416631296 as 11bf4e3 159 | Imported/updated issue #2 comment 416631349 as 002e327 160 | Imported/updated issue #2 comment 417048301 as 20aeee8 161 | Imported/updated issue #2 comment 417049466 as a8a12ac 162 | Imported/updated issue #1 as 3ea0e3e 163 | 164 | $ git issue list 165 | 1e87224 An open issue on GitHub with a description and comments 166 | 4a0b58a An open issue on GitHub with assignees and tags 167 | 168 | $ git issue show 4a0b58a 169 | issue 4a0b58a4b7eb7e4e0a3e451746ccd687d9f45048 170 | Author: dspinellis 171 | Date: Thu, 30 Aug 2018 20:59:59 +0000 172 | GitHub issue: #3 at dspinellis/git-issue-test-issues 173 | Tags: bug 174 | duplicate 175 | enhancement 176 | good first issue 177 | open 178 | Assigned-to: dspinellis 179 | louridas 180 | 181 | An open issue on GitHub with assignees and tags 182 | 183 | Description 184 | 185 | Edit History: 186 | * Thu, 30 Aug 2018 20:59:59 +0000 by dspinellis 187 | * 188 | 189 | $ git issue milestone 4a0b58a R-3.5 # Add milestone 190 | Added milestone R-3.5 191 | 192 | $ git issue duedate 4a0b58a 2038-01-18 # Set a nice due date 193 | Added duedate 2038-01-18T00:00:00+02:00 194 | 195 | $ git issue close 1e87224 # Close another issue 196 | Added tag closed 197 | Removed tag open 198 | 199 | $ git issue export github dspinellis git-issue-test-issues # Export modified issues 200 | Issue 3ea0e3ef91619eedfdfd25f93135a9dd99e3435b not modified, skipping... 201 | Exporting issue 1e872249eebe4f984d188700115484d75ab28cd8 as #2 202 | Comment 11bf4e3c5a950142eff5579c23f805ee67c19a93 not modified, skipping... 203 | Comment 002e327f35f76111ce2693969de3cfff2f35d6c7 not modified, skipping... 204 | Comment 20aeee8898c28bc6793fb6114baf2e41397c0d8e not modified, skipping... 205 | Comment a8a12aca79e8a14c09f91e5faaa8ce79a1b9f180 not modified, skipping... 206 | Exporting issue 4a0b58a4b7eb7e4e0a3e451746ccd687d9f45048 as #3 207 | Creating new Milestone R-3.5... 208 | 209 | $  210 | -------------------------------------------------------------------------------- /gi-completion.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2207 3 | # 4 | # Shellcheck ignore list: 5 | # - SC2207: Prefer mapfile or read -a to split command output (or quote to avoid splitting). 6 | # Rationale: Required for compgen idiomatic use 7 | # 8 | # (C) Copyright 2018, 2019 Diomidis Spinellis 9 | # 10 | # This file is part of gi, the Git-based issue management system. 11 | # 12 | # gi is free software: you can redistribute it and/or modify 13 | # it under the terms of the GNU General Public License as published by 14 | # the Free Software Foundation, either version 3 of the License, or 15 | # (at your option) any later version. 16 | # 17 | # gi is distributed in the hope that it will be useful, 18 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | # GNU General Public License for more details. 21 | # 22 | # You should have received a copy of the GNU General Public License 23 | # along with gi. If not, see . 24 | # 25 | 26 | # Autocompletes the gi subcommand sequence. 27 | # Arguments: the current word 28 | _gi_autocomplete_subcommand() 29 | { 30 | local IFS=$'\n' command_regex='^\s{3}([a-z]+)\s.*' 31 | 32 | # parse help information for sub commands 33 | while read -r line; do 34 | # match only the command lines a.k.a. "gi : command help" 35 | if [[ $line =~ $command_regex ]]; then 36 | COMPREPLY+=($(compgen -W "${BASH_REMATCH[1]}" -- "$1")) 37 | fi 38 | done <<< "$(git issue help 2>/dev/null)" 39 | } 40 | 41 | # Autocompletes the gi subcommands' argument sequence. 42 | # Arguments: the subcommand, the current word 43 | _gi_autocomplete_subcommand_argument() 44 | { 45 | case $1 in 46 | show | comment | tag | assign | attach | watcher | weight | create) 47 | # list all issues 48 | list_args="-a" 49 | ;; 50 | edit | close | milestone | duedate | timeestimate | timespent) 51 | # list only open issues 52 | list_args="" 53 | ;; 54 | *) 55 | # stop completion for the rest of the sub-commands 56 | return 57 | esac 58 | 59 | local IFS=$'\n' desc sha cmd gi_list 60 | 61 | while read -r line; do 62 | # shellcheck disable=SC2001 63 | # SC2001: See if you can use ${variable//search/replace} instead. 64 | # Rationale: Can't, because it doesn't handle \n 65 | desc=($(echo "$line" | sed 's/ /\n/')) 66 | cmd=$(compgen -W "${desc[0]}" -- "$2") 67 | 68 | if [ -n "$cmd" ]; then 69 | # Store the matching issues along with their description 70 | gi_list+=($(printf '%*s' "-$COLUMNS" "$cmd - ${desc[1]}")) 71 | fi 72 | done <<< "$(git issue list "$list_args" 2>/dev/null)" 73 | 74 | if [[ ${#gi_list[@]} == 1 ]]; then 75 | # If only one match, autocomplete the sha without the description 76 | sha="${gi_list[0]/%\ */}" 77 | COMPREPLY+=($(compgen -W "$sha")) 78 | else 79 | # Display the whole sha list along with the descriptions 80 | COMPREPLY+=("${gi_list[@]}") 81 | fi 82 | } 83 | 84 | # Handles auto-completion of the gi executable. 85 | _gi_autocomplete() 86 | { 87 | local word="${COMP_WORDS[COMP_CWORD]}" 88 | local basecmd=${COMP_WORDS[0]} 89 | local baseidx="-1" 90 | if [ "$basecmd" = "gi" ]; then 91 | baseidx="1" 92 | else 93 | basecmd="${COMP_WORDS[1]}" 94 | if [ "$basecmd" = "issue" ]; then 95 | baseidx="2" 96 | else 97 | baseidx="-1" 98 | fi 99 | fi 100 | 101 | if [ "$COMP_CWORD" -ge "$baseidx" ]; then 102 | if [ "$COMP_CWORD" -eq "$baseidx" ]; then 103 | _gi_autocomplete_subcommand "$word" 104 | else 105 | local subcmd="${COMP_WORDS[$baseidx]}" 106 | local prev_word="${COMP_WORDS[COMP_CWORD-1]}" 107 | # completion is only implemented directly after a subcommand or after a 108 | # subcommand's flag, that is at maximum two positions further 109 | local max_pos=$(( "$baseidx" + "2" )) 110 | 111 | # stop completion if we already passed the hash argument 112 | [ "$COMP_CWORD" -gt "$max_pos" ] && return 113 | [ "$COMP_CWORD" -eq "$max_pos" ] && [[ $prev_word != -* ]] && return 114 | 115 | _gi_autocomplete_subcommand_argument "$subcmd" "$word" 116 | fi 117 | else 118 | __git_wrap__gitk_main 119 | fi 120 | } 121 | 122 | _git_issue() { 123 | _gi_autocomplete 124 | } 125 | 126 | complete -F _gi_autocomplete gi 127 | -------------------------------------------------------------------------------- /gi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # (C) Copyright 2016-2018 Diomidis Spinellis 4 | # 5 | # This file is part of git-issue, the Git-based issue management system. 6 | # 7 | # git-issue is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # git-issue is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with git-issue. If not, see . 19 | # 20 | exec git issue "$@" 21 | -------------------------------------------------------------------------------- /git-issue.1: -------------------------------------------------------------------------------- 1 | .TH GIT-ISSUE 1 "31 January 2020" 2 | .\" 3 | .\" (C) Copyright 2016-2020 Diomidis Spinellis 4 | .\" 5 | .\" This file is part of git-issue, the Git-based issue management system. 6 | .\" 7 | .\" git-issue is free software: you can redistribute it and/or modify 8 | .\" it under the terms of the GNU General Public License as published by 9 | .\" the Free Software Foundation, either version 3 of the License, or 10 | .\" (at your option) any later version. 11 | .\" 12 | .\" git-issue is distributed in the hope that it will be useful, 13 | .\" but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | .\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | .\" GNU General Public License for more details. 16 | .\" 17 | .\" You should have received a copy of the GNU General Public License 18 | .\" along with git-issue. If not, see . 19 | .\" 20 | .SH NAME 21 | git-issue \- distributed issue management system based on Git 22 | .SH SYNOPSIS 23 | \fBgit-issue\fP [] 24 | .SH DESCRIPTION 25 | \fIGit-issue\fP manages issues, bugs, tickets, feature proposals 26 | using Git. 27 | Issues are stored as simple text files, which you can be 28 | viewed, edited, shared, and backed up with all text-based tools. 29 | By being based on Git, anyone can add, comment, and 30 | edit issues without requiring online access to a centralized server. 31 | The use of Git gives \fIgit issue\fP a robust, efficient, portable, 32 | and widely available infrastructure. 33 | It allows the reuse of Git credentials and repositories, 34 | to support the efficient merging of work, while also providing 35 | a solid audit trail regarding any changes. 36 | 37 | .SH GIT ISSUE COMMANDS 38 | .\" Auto-generated content from README.md; do not edit this section 39 | You use \fIgit issue\fP with the following sub-commands. 40 | 41 | .RE 42 | .PP 43 | \fBgit issue clone\fP 44 | .RS 4 45 | Clone the specified remote repository. 46 | .RE 47 | .PP 48 | \fBgit issue init\fP 49 | .RS 4 50 | Create a new issues repository in the current directory. 51 | The \fC-e\fP option uses an existing Git project repository. 52 | 53 | .RE 54 | .PP 55 | \fBgit issue new\fP 56 | .RS 4 57 | Create a new open issue (with optional \fC-s\fP summary and -c "provider user repo" for github/gitlab export). 58 | .RE 59 | .PP 60 | \fBgit issue show\fP 61 | .RS 4 62 | Show specified issue (and its comments with \fC-c\fP). 63 | .RE 64 | .PP 65 | \fBgit issue comment\fP 66 | .RS 4 67 | Add an issue comment. 68 | .RE 69 | .PP 70 | \fBgit issue edit\fP 71 | .RS 4 72 | Edit the specified issue's (or comment's with -c) description 73 | .RE 74 | .PP 75 | \fBgit issue tag\fP 76 | .RS 4 77 | Add (or remove with \fC-r\fP) a tag. 78 | .RE 79 | .PP 80 | \fBgit issue milestone\fP 81 | .RS 4 82 | Specify (or remove with \fC-r\fP) the issue's milestone. 83 | .RE 84 | .PP 85 | \fBgit issue weight\fP 86 | .RS 4 87 | Specify (or remove with \fC-r\fP) the issue's weight. 88 | The weight is a positive integer that serves as a measure of importance. 89 | .RE 90 | .PP 91 | \fBgit issue duedate\fP 92 | .RS 4 93 | Specify (or remove with \fC-r\fP) the issue's due date. 94 | The command accepts all formats supported by the \fCdate\fP utility. 95 | .RE 96 | .PP 97 | \fBgit issue timeestimate\fP 98 | .RS 4 99 | Specify (or remove with \fC-r\fP) a time estimate for this issue. 100 | Time estimates can be given in a format accepted by \fCdate\fP, 101 | however bear in mind that it represents a time interval, not a date. 102 | .RE 103 | .PP 104 | \fBgit issue timespent\fP 105 | .RS 4 106 | Specify (or remove with \fC-r\fP) the time spent working on an issue so far. 107 | Follows the same format outlined above. 108 | If the \fC-a\fP option is given, the time interval will be added together with the existing one. 109 | .RE 110 | .PP 111 | \fBgit issue assign\fP 112 | .RS 4 113 | Assign (or remove \fC-r\fP) an issue to a person. 114 | The person is specified with his/her email address. 115 | The form \fC@name\fP or \fCname@\fP can be used as a shortcut, provided it 116 | uniquely identifies an existing assignee or committer. 117 | Note that if you plan to export the issue to a GitHub/GitLab repository, the assignee may be rejected if 118 | it doesn't correspond to a valid username, or if you don't have the necessary permissions. 119 | .RE 120 | .PP 121 | \fBgit issue attach\fP 122 | .RS 4 123 | Attach (or remove with \fC-r\fP) a file to an issue. 124 | .RE 125 | .PP 126 | \fBgit issue watcher\fP 127 | .RS 4 128 | Add (or remove with \fC-r\fP) an issue watcher. 129 | .RE 130 | .PP 131 | \fBgit issue close\fP 132 | .RS 4 133 | Remove the \fCopen\fP tag, add the closed tag 134 | .RE 135 | .PP 136 | \fBgit issue list\fP 137 | .RS 4 138 | List open issues (or all with \fC-a\fP). 139 | An optional argument can show issues matching a tag or milestone. 140 | .RE 141 | .PP 142 | \fBgit issue list -l formatstring\fP 143 | .RS 4 144 | This will list issues in the specified format, given as an argument to \fC-l\fP. 145 | The following escape sequences can be used: 146 | 147 | .IP "" 8 148 | \fC%n\fP : newline 149 | .IP "" 8 150 | \fC%i\fP : issue ID 151 | .IP "" 8 152 | \fC%c\fP : creation date 153 | .IP "" 8 154 | \fC%d\fP : due date 155 | .IP "" 8 156 | \fC%e\fP : time estimate 157 | .IP "" 8 158 | \fC%s\fP : time spent 159 | .IP "" 8 160 | \fC%w\fP : weight 161 | .IP "" 8 162 | \fC%M\fP : Milestone 163 | .IP "" 8 164 | \fC%A\fP : Assignee(s) 165 | .IP "" 8 166 | \fC%T\fP : Tags 167 | .IP "" 8 168 | \fC%D\fP : Description(first line) 169 | 170 | If the format string is one of: (\fConeline\fP, \fCshort\fP or `full`) it will interpreted as the corresponding preset. 171 | 172 | Optionally, one of the above given with \fC-o\fP will order based on this field(reverse order with \fC-r\fP). 173 | 174 | .RE 175 | .PP 176 | \fBgit issue filter-apply command\fP 177 | .RS 4 178 | Run \fCcommand\fP in every issue directory. The following environment variables will be set: 179 | .IP "" 8 180 | \fCGI_SHA\fP : Sha of the current issue 181 | .IP "" 8 182 | \fCGI_IMPORTS\fP : The imports directories for current issue(one on each line) 183 | .IP "" 8 184 | \fCGI_AUTHOR\fP : Author of current issue 185 | .IP "" 8 186 | \fCGI_DATE\fP : Creation date of current issue 187 | 188 | The command can read, add/remove or edit any of the issue's attributes. 189 | Some potentially useful scripts to be used with this command are in the scripts/ directory. 190 | Remember to inspect the results (e.g. \fCgi git diff\fP) and commit them with \fCgi git commit -a\fP. 191 | 192 | .RE 193 | .PP 194 | \fBgit issue push\fP 195 | .RS 4 196 | Update remote Git repository with local changes. 197 | .RE 198 | .PP 199 | \fBgit issue pull\fP 200 | .RS 4 201 | Update local Git repository with remote changes. 202 | .RE 203 | .PP 204 | \fBgit issue import\fP 205 | .RS 4 206 | Import/update GitHub/GitLab issues from the specified project. 207 | If the import involves more than a dozen of issues or if the repository 208 | is private, set the environment variable \fCGH_CURL_AUTH\fP (GitHub) or \fCGL_CURL_AUTH\fP (GitLab) to the authentication token. 209 | For example, run the following command: \fCexport GH_CURL_AUTH="Authorization: token badf00ddead9bfee8f3c19afc3c97c6db55fcfde"\fP 210 | You can create the authorization token through 211 | GitHub settings , with the \fCrepo\fP and \fCdelete_repo\fP(only for running the tests) permissions. 212 | For GitLab: \fCexport GL_CURL_AUTH="PRIVATE-TOKEN: JvHLsdnDmD7rjUXzT-Ea"\fP. The \fCapi\fP permission is required. 213 | Use the GitLab settings to create the token. 214 | In case the repository is part of a GitLab group, specify repository as groupname/reponame. 215 | .RE 216 | .PP 217 | \fBgit issue create\fP 218 | .RS 4 219 | Create the issue in the provided GitHub repository. 220 | With the \fC-e\fP option any escape sequences for the attributes present in the description, will be replaced as above. 221 | This can be used to e.g. export an unsupported attribute to GitHub as text. 222 | .RE 223 | .PP 224 | \fBgit issue export\fP 225 | .RS 4 226 | Export modified issues for the specified project. 227 | Only the issues that have been imported and modified (or created by \fCgit issue create\fP) by \fCgit-issue\fP will be exported. 228 | With the \fC-e\fP option any escape sequences for the attributes present in the description, will be replaced as above. 229 | This can be used to e.g. export an unsupported attribute to GitHub as text. 230 | .RE 231 | .PP 232 | \fBgit issue exportall\fP 233 | .RS 4 234 | Export all open issues in the database (\fC-a\fP to include closed ones) to GitHub/GitLab. Useful for cloning whole repositories. 235 | 236 | .RE 237 | .PP 238 | \fBgit issue help\fP 239 | .RS 4 240 | Display help information about git issue. 241 | .RE 242 | .PP 243 | \fBgit issue log\fP 244 | .RS 4 245 | Output a log of changes made 246 | .RE 247 | .PP 248 | \fBgit issue git\fP 249 | .RS 4 250 | Run the specified Git command on the issues repository. 251 | .RE 252 | .PP 253 | \fBgit issue dump\fP 254 | .RS 4 255 | Dump the whole database in json format to stdout. 256 | 257 | Issues and comments are specified through the SHA hash associated with the 258 | parent of the commit that opened them, which is specifically crafted for 259 | that element and can be used to derive its date and author. 260 | 261 | .SH ENVIRONMENT 262 | The \fCVISUAL\fP environment variable is used for determining the user's 263 | editor. 264 | The \fCPAGER\fP environment variable is used for determining the program 265 | to use to display long lists of results. 266 | 267 | .SH FILES 268 | .\" Auto-generated content from README.md; do not edit this section 269 | All data are stored under \fC.issues\fP, which should be placed under \fC.gitignore\fP, 270 | if it will coexist with another Git-based project. 271 | The directory contains the following elements. 272 | .IP "" 4 273 | A \fC.git\fP directory contains the Git data associated with the issues. 274 | .IP "" 4 275 | A \fCconfig\fP file with configuration data. 276 | .IP "" 4 277 | An \fCimports\fP directory contains details about imported issues. 278 | .IP "" 8 279 | The \fCsha\fP file under \fCimport////\fP contains the 280 | \fIgit-issue\fP SHA corresponding to an imported GitHub \fInumber\fP issue. 281 | Likewise for GitLab. 282 | .IP "" 8 283 | The \fCsha\fP file under \fCimport/////comments/\fP 284 | contains the \fIgit-issue\fP comment SHA corresponding to an imported GitHub/GitLab 285 | \fInumber\fP comment. 286 | .IP "" 8 287 | The file \fCimport////checkpoint\fP contains the SHA 288 | of the last imported or updated issue. This can be used for merging 289 | future updates. 290 | .IP "" 4 291 | An \fCissues\fP directory contains the individual issues. 292 | .IP "" 4 293 | Each issue is stored in a directory named \fCissues/xx/xxxxxxx...\fP, 294 | where the x's are the SHA of the issue's initial commit. 295 | .IP "" 4 296 | Each issue can have the following elements in its directory. 297 | .IP "" 8 298 | A \fCdescription\fP file with a one-line summary and a description of the issue. 299 | .IP "" 8 300 | A \fCduedate\fP file with the due date stored in ISO-8601 format. 301 | .IP "" 8 302 | A \fCweight\fP file with the weight stored as a positive integer. 303 | .IP "" 8 304 | A \fCtimespent\fP and \fCtimeestimate\fP file with the time estimate and time spent respectively, stored in seconds. 305 | .IP "" 8 306 | A \fCcomments\fP directory where comments are stored, each with the SHA of 307 | a commit containing the text \fCgi comment mark\fP 308 | \fIissue SHA\fP. 309 | .IP "" 8 310 | An \fCattachments\fP directory where the issue's attachments are stored. 311 | .IP "" 8 312 | A \fCtags\fP file containing the issue's tags, one in each line. 313 | .IP "" 8 314 | A \fCmilestone\fP file containing the issue's milestone name. 315 | .IP "" 8 316 | A \fCwatchers\fP file containing the emails of persons to be notified when the issue changes (one per line). 317 | .IP "" 8 318 | An \fCassignee\fP file containing the email for the person assigned to the issue. 319 | .IP "" 4 320 | A \fCtemplates\fP directory with message templates. 321 | 322 | .SH EXIT STATUS 323 | The command terminates with a non zero exit code on errors. 324 | 325 | .SH EXAMPLES 326 | .\" Auto-generated content from README.md; do not edit this section 327 | You can also view a video of the following session on YouTube . 328 | 329 | .fi 330 | .ft R 331 | .PP 332 | Initialize issue repository 333 | .ft C 334 | .nf 335 | $ git issue init 336 | Initialized empty Issues repository in /home/dds/src/gi/.issues 337 | $ git issue new -s 'New issue entered from the command line' 338 | Added issue e6a95c9 339 | 340 | .fi 341 | .ft R 342 | .PP 343 | Create a new issue (opens editor window) 344 | .ft C 345 | .nf 346 | $ git issue new 347 | Added issue 7dfa5b7 348 | 349 | .fi 350 | .ft R 351 | .PP 352 | List open issues 353 | .ft C 354 | .nf 355 | $ git issue list 356 | 7dfa5b7 An issue entered from the editor 357 | e6a95c9 New issue entered from the command line 358 | 359 | .fi 360 | .ft R 361 | .PP 362 | Add an issue comment (opens editor window) 363 | .ft C 364 | .nf 365 | $ git issue comment e6a95c9 366 | Added comment 8c0d5b3 367 | 368 | .fi 369 | .ft R 370 | .PP 371 | Add a due date for the issue 372 | .ft C 373 | .nf 374 | $ git issue duedate "next Tuesday" e6a95c9 375 | Added duedate 2019-08-13T00:00:00+03:00 376 | 377 | .fi 378 | .ft R 379 | .PP 380 | Keep track of time spent on the issue 381 | .ft C 382 | .nf 383 | $ git issue timespent "2hours" e6a95c9 384 | Added timespent 7200 385 | 386 | .fi 387 | .ft R 388 | .PP 389 | Log additional time spent working on it 390 | .ft C 391 | .nf 392 | $ git issue timespent -a "4 hours" e6a95c9 393 | Added timespent 21600 394 | 395 | .fi 396 | .ft R 397 | .PP 398 | Add tag to an issue 399 | .ft C 400 | .nf 401 | $ git issue tag e6a9 urgent 402 | Added tag urgent 403 | 404 | .fi 405 | .ft R 406 | .PP 407 | Add two more tags 408 | .ft C 409 | .nf 410 | $ git issue tag e6a9 gui crash 411 | Added tag gui 412 | Added tag crash 413 | 414 | .fi 415 | .ft R 416 | .PP 417 | Remove a tag 418 | .ft C 419 | .nf 420 | $ git issue tag -r e6a9 urgent 421 | Removed tag urgent 422 | 423 | .fi 424 | .ft R 425 | .PP 426 | Assign issue 427 | .ft C 428 | .nf 429 | $ git issue assign e6a9 joe@example.com 430 | Assigned to joe@example.com 431 | 432 | .fi 433 | .ft R 434 | .PP 435 | Add issue watcher 436 | .ft C 437 | .nf 438 | $ git issue watcher e6a9 jane@example.com 439 | Added watcher jane@example.com 440 | 441 | .fi 442 | .ft R 443 | .PP 444 | List issues tagged as gui 445 | .ft C 446 | .nf 447 | $ git issue list gui 448 | e6a95c9 New issue entered from the command line 449 | 450 | .fi 451 | .ft R 452 | .PP 453 | Push issues repository to a server 454 | .ft C 455 | .nf 456 | $ git issue git remote add origin git@github.com:dspinellis/gi-example.git 457 | $ git issue git push -u origin master 458 | Counting objects: 60, done. 459 | Compressing objects: 100% (50/50), done. 460 | Writing objects: 100% (60/60), 5.35 KiB | 0 bytes/s, done. 461 | Total 60 (delta 8), reused 0 (delta 0) 462 | To git@github.com:dspinellis/gi-example.git 463 | * [new branch] master -> master 464 | Branch master set up to track remote branch master from origin. 465 | 466 | .fi 467 | .ft R 468 | .PP 469 | Clone issues repository from server 470 | .ft C 471 | .nf 472 | $ git issue clone git@github.com:dspinellis/gi-example.git my-issues 473 | Cloning into '.issues'... 474 | remote: Counting objects: 60, done. 475 | remote: Compressing objects: 100% (42/42), done. 476 | remote: Total 60 (delta 8), reused 60 (delta 8), pack-reused 0 477 | Receiving objects: 100% (60/60), 5.35 KiB | 0 bytes/s, done. 478 | Resolving deltas: 100% (8/8), done. 479 | Checking connectivity... done. 480 | Cloned git@github.com:dspinellis/gi-example.git into my-issues 481 | 482 | .fi 483 | .ft R 484 | .PP 485 | List open issues 486 | .ft C 487 | .nf 488 | $ git issue list 489 | 7dfa5b7 An issue entered from the editor 490 | e6a95c9 New issue entered from the command line 491 | 492 | .fi 493 | .ft R 494 | .PP 495 | Create new issue 496 | .ft C 497 | .nf 498 | $ git issue new -s 'Issue added on another host' 499 | Added issue abc9adc 500 | 501 | .fi 502 | .ft R 503 | .PP 504 | Push changes to server 505 | .ft C 506 | .nf 507 | $ git issue push 508 | Counting objects: 7, done. 509 | Compressing objects: 100% (6/6), done. 510 | Writing objects: 100% (7/7), 767 bytes | 0 bytes/s, done. 511 | Total 7 (delta 0), reused 0 (delta 0) 512 | To git@github.com:dspinellis/gi-example.git 513 | d6be890..740f9a0 master -> master 514 | 515 | .fi 516 | .ft R 517 | .PP 518 | Show issue added on the other host 519 | .ft C 520 | .nf 521 | $ git issue show 7dfa5b7 522 | issue 7dfa5b7f4591ecaa8323716f229b84ad40f5275b 523 | Author: Diomidis Spinellis 524 | Date: Fri, 29 Jan 2016 01:03:24 +0200 525 | Tags: open 526 | 527 | An issue entered from the editor 528 | 529 | Here is a longer description. 530 | 531 | .fi 532 | .ft R 533 | .PP 534 | Show issue and comments 535 | .ft C 536 | .nf 537 | $ git issue show -c e6a95c9 538 | issue e6a95c91b31ded8fc229a41cc4bd7d281ce6e0f1 539 | Author: Diomidis Spinellis 540 | Date: Fri, 29 Jan 2016 01:03:20 +0200 541 | Tags: open urgent gui crash 542 | Watchers: jane@example.com 543 | Assigned-to: joe@example.com 544 | 545 | New issue entered from the command line 546 | 547 | comment 8c0d5b3d77bf93b937cb11038b129f927d49e34a 548 | Author: Diomidis Spinellis 549 | Date: Fri, 29 Jan 2016 01:03:57 +0200 550 | 551 | First comment regarding the issue. 552 | 553 | .fi 554 | .ft R 555 | .PP 556 | Pull in remote changes (on the original host) 557 | .ft C 558 | .nf 559 | $ git issue pull 560 | remote: Counting objects: 7, done. 561 | remote: Compressing objects: 100% (6/6), done. 562 | remote: Total 7 (delta 0), reused 7 (delta 0), pack-reused 0 563 | Unpacking objects: 100% (7/7), done. 564 | From github.com:dspinellis/gi-example 565 | d6be890..740f9a0 master -> origin/master 566 | Updating d6be890..740f9a0 567 | Fast-forward 568 | issues/ab/c9adc61025a3cb73b0c67470b65cefc133a8d0/description | 1 + 569 | issues/ab/c9adc61025a3cb73b0c67470b65cefc133a8d0/tags | 1 + 570 | 2 files changed, 2 insertions(+) 571 | create mode 100644 issues/ab/c9adc61025a3cb73b0c67470b65cefc133a8d0/description 572 | create mode 100644 issues/ab/c9adc61025a3cb73b0c67470b65cefc133a8d0/tags 573 | 574 | .fi 575 | .ft R 576 | .PP 577 | List open issues 578 | .ft C 579 | .nf 580 | $ git issue list 581 | 7dfa5b7 An issue entered from the editor 582 | abc9adc Issue added on another host 583 | e6a95c9 New issue entered from the command line 584 | 585 | .fi 586 | .ft R 587 | .PP 588 | Import issues from GitHub 589 | .ft C 590 | .nf 591 | $ git issue import github dspinellis git-issue-test-issues # Import GitHub issues 592 | Imported/updated issue #3 as 0a27c66 593 | Imported/updated issue #2 as feb2a2c 594 | Imported/updated issue #2 comment 416631296 as f7de92c 595 | Imported/updated issue #2 comment 416631349 as 03acf84 596 | Imported/updated issue #2 comment 417048301 as 0cd48ed 597 | Imported/updated issue #2 comment 417049466 as 325a581 598 | Imported/updated issue #1 as bbe144d 599 | $ git issue list 600 | feb2a2c An open issue on GitHub with a description and comments 601 | 0a27c66 An open issue on GitHub with assignees and tags 602 | $ git issue show 0a27c66 603 | issue 0a27c6633f492e42bb2a24e6ae458482a4690a55 604 | Author: dspinellis 605 | Date: Thu, 30 Aug 2018 20:59:59 +0000 606 | GitHub issue: #3 at vyrondrosos/git-issue-test-issues 607 | Tags: bug 608 | duplicate 609 | enhancement 610 | good first issue 611 | open 612 | Assigned-to: dspinellis 613 | louridas 614 | 615 | An open issue on GitHub with assignees and tags 616 | 617 | Description 618 | 619 | Edit History: 620 | .IP "" 4 621 | Thu, 30 Aug 2018 20:59:59 +0000 by dspinellis 622 | .IP "" 4 623 | 624 | 625 | .fi 626 | .ft R 627 | .PP 628 | Export all issues to GitHub 629 | .ft C 630 | .nf 631 | $ git issue exportall github dspinellis git-issue-test-issues 632 | Creating issue 9179d38... 633 | Couldn't add assignee dspinellis. Skipping... 634 | Couldn't add assignee louridas. Skipping... 635 | Creating issue 3651dd3... 636 | Creating new Milestone ver3... 637 | Creating comment d72c68d0177b500a91ea37548e6594f84457fd5b... 638 | Creating comment 6966d4d718c80cf8635e9276d6f391de70c22f93... 639 | Creating comment 85293a6904d0fbd6238fbb2e1c36fc65af9ffc60... 640 | Creating comment aea83723c0414ff135afcfb5165d64f8a7ad687c... 641 | 642 | .fi 643 | .ft R 644 | .PP 645 | Make changes 646 | .ft C 647 | .nf 648 | $ git issue edit 9179d38 649 | Opening editor... 650 | Edited issue 9179d38 651 | $ git issue edit -c d72c6 652 | Opening editor... 653 | Edited comment d72c68d 654 | 655 | .fi 656 | .ft R 657 | .PP 658 | Export modified issues back to GitHub 659 | .ft C 660 | .nf 661 | $ git issue export github dspinellis git-issue-test-issues # Needs a token with the relevant permissions 662 | Issue b83d92872dc16440402516a5f4ce1b8cc6436344 not modified, skipping... 663 | Comment a93764f32179e93493ceb0a7060efce1e980aff1 not modified, skipping... 664 | Exporting issue 9179d381135273220301f175c03b101b3e9c703d as #15 665 | Issue 3651dd38e4e1d9dbce66649710324235c773fe78 not modified, skipping... 666 | Updating comment d72c68d0177b500a91ea37548e6594f84457fd5b... 667 | Comment 6966d4d718c80cf8635e9276d6f391de70c22f93 not modified, skipping... 668 | Comment 85293a6904d0fbd6238fbb2e1c36fc65af9ffc60 not modified, skipping... 669 | Comment aea83723c0414ff135afcfb5165d64f8a7ad687c not modified, skipping... 670 | 671 | .fi 672 | .ft R 673 | .PP 674 | Sub-command auto-completion 675 | .ft C 676 | .nf 677 | $ git issue [Tab] 678 | assign clone comment git init log pull show watcher 679 | attach close edit help list new push tag 680 | 681 | .fi 682 | .ft R 683 | .PP 684 | Issue SHA auto-completion 685 | .ft C 686 | .nf 687 | $ git issue show [Tab] 688 | 7dfa5b7 - An issue entered from the editor 689 | e6a95c9 - New issue entered from the command line 690 | 691 | .SH SEE ALSO 692 | .BR git ( 1 ), 693 | <\fIhttps://github\.com/dspinellis/git\-issue\fR> 694 | 695 | .SH AUTHOR 696 | First version written by Diomidis Spinellis <\fIdds@aueb\.gr\fP>. 697 | Vuron Drosos <\fIvyrondrosos@gmail.com\fP> implemented the GitHub/GitLab 698 | export functionality during the Google Summer of Code 2019. 699 | 700 | .SH BUGS 701 | Report bugs through 702 | <\fIhttps://github\.com/dspinellis/git\-issue/issues\fR> 703 | -------------------------------------------------------------------------------- /git-issue.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck disable=SC2039,SC1117,SC2006 3 | # TODO: SC2006 not needed on latest shellcheck version 4 | # Shellcheck ignore list: 5 | # - SC2039: In POSIX sh, 'local' is undefined. 6 | # Rationale: Local makes for better code and works on many modern shells 7 | # - SC1117: Backslash is literal. Prefer explicit escaping. 8 | # 9 | # (C) Copyright 2016-2020 Diomidis Spinellis 10 | # 11 | # This file is part of git-issue, the Git-based issue management system. 12 | # 13 | # git-issue is free software: you can redistribute it and/or modify 14 | # it under the terms of the GNU General Public License as published by 15 | # the Free Software Foundation, either version 3 of the License, or 16 | # (at your option) any later version. 17 | # 18 | # git-issue is distributed in the hope that it will be useful, 19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 | # GNU General Public License for more details. 22 | # 23 | # You should have received a copy of the GNU General Public License 24 | # along with git-issue. If not, see . 25 | # 26 | 27 | # User agent string 28 | # shellcheck disable=SC2034 29 | # SC2034 : USER_AGENT appears unused. Verify use (or export if used externally) 30 | USER_AGENT=https://github.com/dspinellis/git-issue/tree/afda065 31 | 32 | # Determine our script library path 33 | my_IFS=$IFS 34 | IFS=: 35 | 36 | # Set library path 37 | # shellcheck disable=SC2086 38 | # Rationale: Word splitting not an issue 39 | LIB_PATH="$(dirname $0)/lib:$(dirname $0)/../lib:$LD_LIBRARY_PATH:/usr/local/lib:/usr/lib" 40 | if [ "x$GIT_ISSUE_LIB_PATH" != x ] ; then 41 | LIB_PATH="$GIT_ISSUE_LIB_PATH" 42 | fi 43 | for i in ${LIB_PATH} ; do 44 | if [ -d "${i}/git-issue" ] ; then 45 | MY_LIB="${i}/git-issue" 46 | break 47 | fi 48 | done 49 | 50 | 51 | IFS=$my_IFS 52 | 53 | if command -v gdate > /dev/null ; then 54 | DATEBIN="gdate" 55 | else 56 | DATEBIN="date" 57 | fi 58 | 59 | # Exit after displaying the specified error 60 | error() 61 | { 62 | echo "$1" 1>&2 63 | exit 1 64 | } 65 | 66 | $DATEBIN --help | grep 'gnu' > /dev/null || error "Require GNU date" 67 | 68 | test "x$MY_LIB" != x || error "No git-issue directory in path $LIB_PATH" 69 | 70 | # Return a unique identifier for the specified file 71 | filesysid() 72 | { 73 | stat --printf='%d:%i' "$1" 2>/dev/null || 74 | stat -f '%d:%i' "$1" 75 | } 76 | 77 | # Move to the .issues directory 78 | cdissues() 79 | { 80 | while : ; do 81 | cd .issues 2>/dev/null && return 82 | if [ "$(filesysid .)" = "$(filesysid /)" ] ; then 83 | error 'Not an issues repository (or any of the parent directories)' 84 | fi 85 | cd .. 86 | done 87 | } 88 | 89 | # Output the path of an issue given its SHA 90 | # The scheme used for storing the issues is a two level directory 91 | # structure where the first level consists of the first two SHA 92 | # letters 93 | # 94 | # issue_path_full 95 | issue_path_full() 96 | { 97 | local sha 98 | 99 | sha="$1" 100 | echo issues/"$(expr "$sha" : '\(..\)')/$(expr "$sha" : '..\(.*\)'$)" 101 | } 102 | 103 | # Output the path of an issue given its (possibly partial) SHA 104 | # Abort with an error if the full path can not be uniquely resolved 105 | # to an existing issue 106 | # issue_path_part 107 | issue_path_part() 108 | { 109 | local sha partial path 110 | 111 | sha="$1" 112 | partial=$(issue_path_full "$sha") 113 | path=$(echo "${partial}"*) 114 | test -d "$path" || error "Unknown or ambigious issue specification $sha" 115 | echo "$path" 116 | } 117 | 118 | # Given an issue path return its SHA 119 | issue_sha() 120 | { 121 | echo "$1" | sed 's/issues\/\(..\)\/\([^/]*\).*/\1\2/' 122 | } 123 | 124 | # Shorten a full SHA 125 | short_sha() 126 | { 127 | git rev-parse --short "$1" 128 | } 129 | 130 | # Start an issue transaction 131 | trans_start() 132 | { 133 | cdissues 134 | start_sha=$(git rev-parse HEAD) 135 | } 136 | 137 | # Abort an issue transaction and exit with an error 138 | trans_abort() 139 | { 140 | if [ -d .git ] || git rev-parse --git-dir > /dev/null 2>&1 ; then 141 | git reset "$start_sha" 142 | git clean -qfd 143 | git checkout -- . 144 | rm -f issue-header issue-body comments-header comments-body create-body create-header update-body update-header 145 | rm -f milestone-body milestone-header mileres-body mileres-header timestats-header 146 | rm -f timeestimate-body timeestimate-header timespent-body timespent-header timestats-body 147 | fi 148 | echo 'Operation aborted' 1>&2 149 | exit 1 150 | } 151 | 152 | # Exit with an error if the specified prerequisite command 153 | # cannot be executed 154 | prerequisite_command() 155 | { 156 | if ! $1 -help 2>/dev/null 1>&2 ; then 157 | cat <&2 158 | The $1 command is not available through the configured path. 159 | Please install it and/or configure your PATH variable. 160 | Command aborted. 161 | EOF 162 | exit 1 163 | fi 164 | } 165 | 166 | # Commit an issue's changes 167 | # commit [] 168 | commit() 169 | { 170 | commit_summary=$1 171 | shift 172 | commit_message=$1 173 | shift 174 | GIT_AUTHOR_DATE="$GIT_EVENT_DATE" GIT_COMMITTER_DATE="$GIT_EVENT_DATE" \ 175 | git commit --allow-empty -q -m "$commit_summary 176 | 177 | $commit_message" "$@" || trans_abort 178 | } 179 | 180 | # Allow the user to edit the specified file 181 | # Remove lines starting with '#' 182 | # Succeed if at the resulting file is non-empty 183 | edit() 184 | { 185 | local file 186 | 187 | file="$1" 188 | touch "$file" 189 | cp "$file" "$file.new" 190 | echo "Opening editor..." 191 | ${VISUAL:-vi} "$file.new" || return 1 192 | grep -v '^#' "$file.new" >"$file.uncommented" 193 | mv "$file.uncommented" "$file.new" 194 | if ! test -s "$file.new" ; then 195 | echo 'Empty file' 1>&2 196 | rm -f "$file.new" 197 | return 1 198 | fi 199 | if diff -q "$file" "$file.new" >/dev/null 2>&1; then 200 | echo 'File was not changed' 1>&2 201 | rm -f "$file.new" 202 | return 1 203 | fi 204 | mv "$file.new" "$file" 205 | } 206 | 207 | # Pipe input through the user's pager 208 | pager() 209 | { 210 | ${PAGER:-more} 211 | } 212 | 213 | # init: Initialize a new issue repository {{{1 214 | usage_init() 215 | { 216 | cat <<\USAGE_new_EOF 217 | gi init usage: git issue init [-e] 218 | -e Use existing project's Git repository 219 | USAGE_new_EOF 220 | exit 2 221 | } 222 | 223 | sub_init() 224 | { 225 | local existing username useremail 226 | 227 | while getopts e flag ; do 228 | case $flag in 229 | e) 230 | existing=1 231 | ;; 232 | ?) 233 | usage_init 234 | ;; 235 | esac 236 | done 237 | shift $((OPTIND - 1)); 238 | 239 | test -d .issues && error 'An .issues directory is already present' 240 | mkdir .issues || error 'Unable to create .issues directory' 241 | username=`git config --local user.name` 242 | useremail=`git config --local user.email` 243 | cdissues 244 | if ! [ "$existing" ] ; then 245 | git init -q || error 'Unable to initialize Git directory' 246 | fi 247 | 248 | # Editing templates 249 | touch config || error 'Unable to create configuration file' 250 | mkdir templates || error 'Unable to create the templates directory' 251 | cat >templates/description <<\EOF 252 | 253 | # Start with a one-line summary of the issue. Leave a blank line and 254 | # continue with the issue's detailed description. 255 | # 256 | # Remember: 257 | # - Be precise 258 | # - Be clear: explain how to reproduce the problem, step by step, 259 | # so others can reproduce the issue 260 | # - Include only one problem per issue report 261 | # 262 | # Lines starting with '#' will be ignored, and an empty message aborts 263 | # the issue addition. 264 | EOF 265 | 266 | cat >templates/comment <<\EOF 267 | 268 | # Please write here a comment regarding the issue. 269 | # Keep the conversation constructive and polite. 270 | # Lines starting with '#' will be ignored, and an empty message aborts 271 | # the issue addition. 272 | EOF 273 | cat >README.md <<\EOF 274 | This is an distributed issue tracking repository based on Git. 275 | Visit [git-issue](https://github.com/dspinellis/git-issue) for more information. 276 | EOF 277 | git add config README.md templates/comment templates/description 278 | if [ -n "$username" ] ; then 279 | git config --local user.name "$username" 280 | fi 281 | if [ -n "$useremail" ] ; then 282 | git config --local user.email "$useremail" 283 | fi 284 | commit 'gi: Initialize issues repository' 'gi init' 285 | echo "Initialized empty issues repository in $(pwd)" 286 | } 287 | 288 | # new: Open a new issue {{{1 289 | usage_new() 290 | { 291 | cat <<\USAGE_new_EOF 292 | gi new usage: git issue new [-s summary] 293 | USAGE_new_EOF 294 | exit 2 295 | } 296 | 297 | sub_new() 298 | { 299 | local summary sha path 300 | 301 | while getopts s:c: flag ; do 302 | case $flag in 303 | s) 304 | summary="$OPTARG" 305 | ;; 306 | c) 307 | create=$OPTARG 308 | ;; 309 | ?) 310 | usage_new 311 | ;; 312 | esac 313 | done 314 | shift $((OPTIND - 1)); 315 | 316 | trans_start 317 | commit 'gi: Add issue' 'gi new mark' 318 | sha=$(git rev-parse HEAD) 319 | path=$(issue_path_full "$sha") 320 | mkdir -p "$path" || trans_abort 321 | echo open >"$path/tags" || trans_abort 322 | if [ "$summary" ] ; then 323 | echo "$summary" >"$path/description" || trans_abort 324 | else 325 | cp templates/description "$path/description" || trans_abort 326 | edit "$path/description" || trans_abort 327 | fi 328 | git add "$path/description" "$path/tags" || trans_abort 329 | commit 'gi: Add issue description' "gi new description $sha" 330 | echo "Added issue $(short_sha "$sha")" 331 | # export issue immediately 332 | if [ -n "$create" ] ; then 333 | # shellcheck disable=SC2086 334 | # Rationale: We want word splitting 335 | create_issue -n "$sha" $create 336 | fi 337 | } 338 | 339 | # show: Show the specified issue {{{1 340 | usage_show() 341 | { 342 | cat <<\USAGE_show_EOF 343 | gi show usage: git issue show [-c] 344 | -c Show comments 345 | USAGE_show_EOF 346 | exit 2 347 | } 348 | 349 | sub_show() 350 | { 351 | local isha path comments rawdate rawest rawspent OPTIND 352 | 353 | while getopts c flag ; do 354 | case $flag in 355 | c) 356 | comments=1 357 | ;; 358 | ?) 359 | usage_show 360 | ;; 361 | esac 362 | done 363 | shift $((OPTIND - 1)); 364 | 365 | test -n "$1" || usage_show 366 | 367 | cdissues 368 | path=$(issue_path_part "$1") || exit 369 | isha=$(issue_sha "$path") 370 | { 371 | # SHA, author, date 372 | echo "issue $isha" 373 | git show --no-patch --format='Author: %an <%ae> 374 | Date: %aD' "$isha" 375 | 376 | # Imports 377 | for i in $(importsget "$isha") ; do 378 | local num 379 | num=$(echo "$i" | grep -o '/[0-9]\+$' | tr -d '/') 380 | if echo "$i" | grep -q '^github' ; then 381 | echo "GitHub issue: #$num at $i" | sed -e 's:/[0-9]\+$::' -e 's:github/::' 382 | else 383 | echo "GitLab issue: #$num at $i" | sed -e 's:/[0-9]\+$::' -e 's:gitlab/::' 384 | fi 385 | done 386 | 387 | # Due Date 388 | if [ -s "$path/duedate" ] ; then 389 | printf 'Due Date: ' 390 | # Print date in rfc-3339 for consistency with git show 391 | rawdate=$(cat "$path/duedate") 392 | $DATEBIN --date="$rawdate" --rfc-3339=seconds 393 | fi 394 | 395 | # Time estimate 396 | if [ -s "$path/timeestimate" ] && [ -s "$path/timespent" ] ; then 397 | printf 'Time Spent/Time Estimated: ' 398 | rawest=$(cat "$path/timeestimate") 399 | rawspent=$(cat "$path/timespent") 400 | # shellcheck disable=SC2016 401 | # SC2016: Expressions don't expand is single quotes, use double quotes for that 402 | # Rationale: We don't want expansion 403 | eval "echo $($DATEBIN --utc --date="@$rawspent" +'$((%s/3600/24)) days %H hours %M minutes %S seconds')" | 404 | # Remove newline and trim unnecessary fields 405 | tr -d '\n' | sed -e "s/00 \(hours\|minutes\|seconds\) \?//g" -e "s/^0 days //" 406 | printf '/ ' 407 | # shellcheck disable=SC2016 408 | eval "echo $($DATEBIN --utc --date="@$rawest" +'$((%s/3600/24)) days %H hours %M minutes %S seconds')" | 409 | sed -e "s/00 \(hours\|minutes\|seconds\) \?//g" -e "s/^0 days //" 410 | elif [ -s "$path/timespent" ] ; then 411 | printf 'Time Spent: ' 412 | # Print time in human readable format 413 | rawspent=$(cat "$path/timespent") 414 | # shellcheck disable=SC2016 415 | eval "echo $($DATEBIN --utc --date="@$rawspent" +'$((%s/3600/24)) days %H hours %M minutes %S seconds')" | 416 | sed -e "s/00 \(hours\|minutes\|seconds\) \?//g" -e "s/^0 days //" 417 | elif [ -s "$path/timeestimate" ] ; then 418 | printf 'Time Estimate: ' 419 | # Print time in human readable format 420 | rawest=$(cat "$path/timeestimate") 421 | # shellcheck disable=SC2016 422 | eval "echo $($DATEBIN --utc --date="@$rawest" +'$((%s/3600/24)) days %H hours %M minutes %S seconds')" | 423 | sed -e "s/00 \(hours\|minutes\|seconds\) \?//g" -e "s/^0 days //" 424 | fi 425 | 426 | # Milestone 427 | if [ -s "$path/milestone" ] ; then 428 | printf 'Milestone: ' 429 | cat "$path/milestone" 430 | fi 431 | 432 | # Weight 433 | if [ -s "$path/weight" ] ; then 434 | printf 'Weight: ' 435 | cat "$path/weight" 436 | fi 437 | 438 | 439 | # Tags 440 | if [ -s "$path/tags" ] ; then 441 | printf 'Tags:' 442 | sed 's/^/ /' "$path/tags" 443 | fi 444 | 445 | # Watchers 446 | if [ -s "$path/watchers" ] ; then 447 | printf 'Watchers:' 448 | fmt "$path/watchers" | sed 's/^/ /' 449 | fi 450 | 451 | # Assignee 452 | if [ -r "$path/assignee" ] ; then 453 | printf 'Assigned-to:' 454 | sed 's/^/ /' "$path/assignee" 455 | fi 456 | 457 | # Description 458 | echo 459 | sed 's/^/ /' "$path/description" 460 | 461 | # Edit History 462 | echo 463 | printf '%s\n' 'Edit History:' 464 | git log --reverse --format="%aD by %an <%ae>" "$path/description" | fmt | sed 's/^/* /' 465 | 466 | # Comments 467 | test -n "$comments" || return 468 | git log --reverse --grep="^gi comment mark $isha" --format='%H' | 469 | while read -r csha ; do 470 | echo 471 | echo "comment $csha" 472 | git show --no-patch --format='Author: %an <%ae> 473 | Date: %aD 474 | ' "$csha" 475 | sed 's/^/ /' "$path/comments/$csha" 476 | done 477 | } | pager 478 | } 479 | 480 | # clone: Clone the specified remote repository {{{1 481 | usage_clone() 482 | { 483 | cat <<\USAGE_clone_EOF 484 | gi clone usage: git issue clone 485 | USAGE_clone_EOF 486 | exit 2 487 | } 488 | 489 | sub_clone() 490 | { 491 | test -n "$1" -a -n "$2" || usage_clone 492 | mkdir -p "$2" || error "Unable to create local directory" 493 | cd "$2" || error "Unable to change into local directory" 494 | git clone "$1" .issues 495 | echo "Cloned $1 into $2" 496 | } 497 | 498 | # milestone: set an issue's milestone {{{1 499 | usage_milestone() 500 | { 501 | cat <<\USAGE_tag_EOF 502 | gi milestone usage: git issue milestone 503 | git issue milestone -r 504 | -r Remove the issue's milestone 505 | USAGE_tag_EOF 506 | exit 2 507 | } 508 | 509 | sub_milestone() 510 | { 511 | local isha tag remove path milestone OPTIND 512 | 513 | while getopts r flag ; do 514 | case $flag in 515 | r) 516 | remove=1 517 | ;; 518 | ?) 519 | usage_milestone 520 | ;; 521 | esac 522 | done 523 | shift $((OPTIND - 1)); 524 | 525 | test -n "$1" -a -n "$2$remove" || usage_milestone 526 | test -n "$remove" -a -n "$2" && usage_milestone 527 | 528 | milestone="$2" 529 | 530 | cdissues 531 | path=$(issue_path_part "$1") || exit 532 | shift 533 | isha=$(issue_sha "$path") 534 | if [ "$remove" ] ; then 535 | test -r "$path/milestone" || error "No milestone set" 536 | milestone=$(cat "$path/milestone") 537 | trans_start 538 | git rm "$path/milestone" >/dev/null || trans_abort 539 | commit "gi: Remove milestone" "gi milestone remove $milestone" 540 | echo "Removed milestone $milestone" 541 | else 542 | touch "$path/milestone" || error "Unable to modify milestone file" 543 | printf '%s\n' "$milestone" >"$path/milestone" 544 | trans_start 545 | git add "$path/milestone" || trans_abort 546 | commit "gi: Add milestone" "gi milestone add $milestone" 547 | echo "Added milestone $milestone" 548 | fi 549 | } 550 | 551 | # weight: set an issue's weight {{{1 552 | usage_weight() 553 | { 554 | cat <<\USAGE_tag_EOF 555 | gi weight usage: git issue weight 556 | git issue weight -r 557 | -r Remove the issue's weight 558 | USAGE_tag_EOF 559 | exit 2 560 | } 561 | 562 | sub_weight() 563 | { 564 | local isha tag remove path weight OPTIND 565 | 566 | while getopts r flag ; do 567 | case $flag in 568 | r) 569 | remove=1 570 | ;; 571 | ?) 572 | usage_weight 573 | ;; 574 | esac 575 | done 576 | shift $((OPTIND - 1)); 577 | 578 | test -n "$1" -a -n "$2$remove" || usage_weight 579 | test -n "$remove" -a -n "$2" && usage_weight 580 | weight="$2" 581 | if ! [ "$remove" ] ; then 582 | # weight is positive integer 583 | expr "$weight" : '[0-9]*$' > /dev/null || usage_weight 584 | fi 585 | 586 | cdissues 587 | path=$(issue_path_part "$1") || exit 588 | shift 589 | isha=$(issue_sha "$path") 590 | if [ "$remove" ] ; then 591 | test -r "$path/weight" || error "No weight set" 592 | weight=$(cat "$path/weight") 593 | trans_start 594 | git rm "$path/weight" >/dev/null || trans_abort 595 | commit "gi: Remove weight" "gi weight remove $weight" 596 | echo "Removed weight $weight" 597 | else 598 | touch "$path/weight" || error "Unable to modify weight file" 599 | printf '%s\n' "$weight" >"$path/weight" 600 | trans_start 601 | git add "$path/weight" || trans_abort 602 | commit "gi: Add weight" "gi weight add $weight" 603 | echo "Added weight $weight" 604 | fi 605 | } 606 | 607 | # duedate: set an issue's weight {{{1 608 | usage_duedate() 609 | { 610 | cat <<\USAGE_tag_EOF 611 | gi duedate usage: git issue duedate
612 | git issue duedate -r 613 |
date in format accepted by `date` 614 | -r Remove the issue's duedate 615 | USAGE_tag_EOF 616 | exit 2 617 | } 618 | 619 | sub_duedate() 620 | { 621 | local isha tag remove path duedate OPTIND 622 | 623 | while getopts r flag ; do 624 | case $flag in 625 | r) 626 | remove=1 627 | ;; 628 | ?) 629 | usage_duedate 630 | ;; 631 | esac 632 | done 633 | shift $((OPTIND - 1)); 634 | 635 | test -n "$1" -a -n "$2$remove" || usage_duedate 636 | test -n "$remove" -a -n "$2" && usage_duedate 637 | # Date is stored in the ISO-8601 format 638 | duedate=$($DATEBIN --date="$2" --iso-8601=seconds) || usage_duedate 639 | if ! [ "$remove" ] ; then 640 | # Convert dates to utc for accurate comparison 641 | expr "$($DATEBIN --date="$duedate" --iso-8601=seconds --utc)" '>' "$($DATEBIN --date='now' --iso-8601=seconds --utc)" \ 642 | > /dev/null || printf "Warning: duedate is in the past\n" 643 | fi 644 | 645 | cdissues 646 | path=$(issue_path_part "$1") || exit 647 | shift 648 | isha=$(issue_sha "$path") 649 | if [ "$remove" ] ; then 650 | test -r "$path/duedate" || error "No duedate set" 651 | duedate=$(cat "$path/duedate") 652 | trans_start 653 | git rm "$path/duedate" >/dev/null || trans_abort 654 | commit "gi: Remove duedate" "gi duedate remove $duedate" 655 | echo "Removed duedate $duedate" 656 | else 657 | touch "$path/duedate" || error "Unable to modify duedate file" 658 | printf '%s\n' "$duedate" >"$path/duedate" 659 | trans_start 660 | git add "$path/duedate" || trans_abort 661 | commit "gi: Add duedate" "gi duedate add $duedate" 662 | echo "Added duedate $duedate" 663 | fi 664 | } 665 | 666 | # timespent: set time spent on an issue {{{1 667 | usage_timespent() 668 | { 669 | cat <<\USAGE_tag_EOF 670 | gi timespent usage: git issue timespent [-a] 671 | -a instead of replacing the time spent, add to it 672 | git issue timespent -r 673 | time interval in format accepted by `date` 674 | -r Remove the issue's timespent 675 | USAGE_tag_EOF 676 | exit 2 677 | } 678 | 679 | sub_timespent() 680 | { 681 | local isha tag remove path timespent add OPTIND 682 | 683 | while getopts ra flag ; do 684 | case $flag in 685 | a) 686 | add=1 687 | ;; 688 | r) 689 | remove=1 690 | ;; 691 | ?) 692 | usage_timespent 693 | ;; 694 | esac 695 | done 696 | shift $((OPTIND - 1)); 697 | 698 | test -n "$1" -a -n "$2$remove" || usage_timespent 699 | test -n "$remove" -a -n "$add" && usage_timespent 700 | test -n "$remove" -a -n "$2" && usage_timespent 701 | # Timespent is stored in seconds 702 | timespent=$($DATEBIN --date="1970-1-1 +$2" --utc +%s)|| usage_timespent 703 | if ! [ "$remove" ] ; then 704 | # Check for negative time interval 705 | if [ "$timespent" -lt 0 ] ; then 706 | usage_timespent 707 | fi 708 | fi 709 | 710 | cdissues 711 | path=$(issue_path_part "$1") || exit 712 | shift 713 | isha=$(issue_sha "$path") 714 | if [ "$remove" ] ; then 715 | test -r "$path/timespent" || error "No time spent set" 716 | timespent=$(cat "$path/timespent") 717 | trans_start 718 | git rm "$path/timespent" >/dev/null || trans_abort 719 | commit "gi: Remove timespent" "gi timespent remove $timespent" 720 | echo "Removed timespent $timespent" 721 | else 722 | if [ "$add" ] ; then 723 | test -r "$path/timespent" || error "No time spent set" 724 | rawspent=$(cat "$path/timespent") 725 | # Add the existing time spent 726 | timespent=$((rawspent + timespent)) 727 | fi 728 | touch "$path/timespent" || error "Unable to modify timespent file" 729 | printf '%s\n' "$timespent" >"$path/timespent" 730 | trans_start 731 | git add "$path/timespent" || trans_abort 732 | commit "gi: Add timespent" "gi timespent add $timespent" 733 | echo "Added timespent $timespent" 734 | fi 735 | } 736 | 737 | # timeestimate: set time spent on an issue {{{1 738 | usage_timeestimate() 739 | { 740 | cat <<\USAGE_tag_EOF 741 | gi timeestimate usage: git issue timeestimate 742 | git issue timeestimate -r 743 | time interval in format accepted by `date` 744 | -r Remove the issue's time estimate 745 | USAGE_tag_EOF 746 | exit 2 747 | } 748 | 749 | sub_timeestimate() 750 | { 751 | local isha tag remove path timeestimate rawspent OPTIND 752 | 753 | while getopts r flag ; do 754 | case $flag in 755 | r) 756 | remove=1 757 | ;; 758 | ?) 759 | usage_timeestimate 760 | ;; 761 | esac 762 | done 763 | shift $((OPTIND - 1)); 764 | 765 | test -n "$1" -a -n "$2$remove" || usage_timeestimate 766 | test -n "$remove" -a -n "$2" && usage_timeestimate 767 | # Timeestimate is stored in seconds 768 | timeestimate=$($DATEBIN --date="1970-1-1 +$2" --utc +%s)|| usage_timeestimate 769 | if ! [ "$remove" ] ; then 770 | # Check for negative time interval 771 | if [ "$timeestimate" -lt 0 ] ; then 772 | usage_timespent 773 | fi 774 | fi 775 | 776 | cdissues 777 | path=$(issue_path_part "$1") || exit 778 | shift 779 | isha=$(issue_sha "$path") 780 | if [ "$remove" ] ; then 781 | test -r "$path/timeestimate" || error "No time estimate set" 782 | timeestimate=$(cat "$path/timeestimate") 783 | trans_start 784 | git rm "$path/timeestimate" >/dev/null || trans_abort 785 | commit "gi: Remove timeestimate" "gi timeestimate remove $timeestimate" 786 | echo "Removed timeestimate $timeestimate" 787 | else 788 | touch "$path/timeestimate" || error "Unable to modify timeestimate file" 789 | printf '%s\n' "$timeestimate" >"$path/timeestimate" 790 | trans_start 791 | git add "$path/timeestimate" || trans_abort 792 | commit "gi: Add timeestimate" "gi timeestimate add $timeestimate" 793 | echo "Added timeestimate $timeestimate" 794 | fi 795 | } 796 | 797 | 798 | 799 | # assign: assign an issue to a person or remove assignment {{{1 800 | usage_assignee() 801 | { 802 | cat <<\USAGE_tag_EOF 803 | gi assign usage: git issue assign [-r] ... 804 | -r Remove the specified assignee 805 | USAGE_tag_EOF 806 | exit 2 807 | } 808 | 809 | sub_assign() 810 | { 811 | file_add_rm assignee assignee "$@" 812 | } 813 | 814 | # Generic file add/remove entry {{{1 815 | # file_add_rm [-r] entry-name filename sha entry ... 816 | file_add_rm() 817 | { 818 | local usage name file isha tag remove path OPTIND 819 | 820 | name=$1 821 | shift 822 | file=$1 823 | shift 824 | usage=usage_$name 825 | 826 | while getopts r flag ; do 827 | case $flag in 828 | r) 829 | remove=1 830 | ;; 831 | ?) 832 | $usage 833 | ;; 834 | esac 835 | done 836 | shift $((OPTIND - 1)); 837 | 838 | test -n "$1" -a -n "$2" || $usage 839 | 840 | cdissues 841 | path=$(issue_path_part "$1") || exit 842 | shift 843 | isha=$(issue_sha "$path") 844 | touch "$path/$file" || error "Unable to modify $file file" 845 | for entry in "$@" ; do 846 | if [ "$remove" ] ; then 847 | grep -Fvx "$entry" "$path/$file" >"$path/$file.new" 848 | if cmp "$path/$file" "$path/$file.new" >/dev/null 2>&1 ; then 849 | echo "No such $name entry: $entry" 1>&2 850 | rm "$path/$file.new" 851 | exit 1 852 | fi 853 | mv "$path/$file.new" "$path/$file" 854 | 855 | trans_start 856 | git add "$path/$file" || trans_abort 857 | echo "Removed $name $entry" 858 | if ! [ -s "$path/$file" ] ; then 859 | git rm -f "$path/$file" >/dev/null || trans_abort 860 | fi 861 | commit "gi: Remove $name" "gi $name remove $entry" 862 | else 863 | if grep -Fx "$entry" "$path/$file" >/dev/null ; then 864 | echo "Entry $entry already exists" 1>&2 865 | exit 1 866 | fi 867 | # Add entry in sorted order to avoid gratuitous updates when importing 868 | printf '%s\n' "$entry" | 869 | LC_ALL=C sort -m - "$path/$file" >"$path/$file.new" 870 | mv "$path/$file.new" "$path/$file" 871 | 872 | trans_start 873 | git add "$path/$file" || trans_abort 874 | commit "gi: Add $name" "gi $name add $entry" 875 | echo "Added $name $entry" 876 | fi 877 | done 878 | } 879 | 880 | # tag: Add or remove an issue tag {{{1 881 | usage_tag() 882 | { 883 | cat <<\USAGE_tag_EOF 884 | gi tag usage: git issue tag [-r] ... 885 | -r Remove the specified tag 886 | USAGE_tag_EOF 887 | exit 2 888 | } 889 | 890 | sub_tag() 891 | { 892 | file_add_rm tag tags "$@" 893 | } 894 | 895 | # watcher: Add or remove an issue watcher {{{1 896 | usage_watcher() 897 | { 898 | cat <<\USAGE_watcher_EOF 899 | gi watcher usage: git issue watcher [-r] ... 900 | -r Remove the specified watcher 901 | USAGE_watcher_EOF 902 | exit 2 903 | } 904 | 905 | sub_watcher() 906 | { 907 | file_add_rm watcher watchers "$@" 908 | } 909 | 910 | # comment: Comment on an issue {{{1 911 | usage_comment() 912 | { 913 | cat <<\USAGE_comment_EOF 914 | gi comment usage: git issue comment 915 | USAGE_comment_EOF 916 | exit 2 917 | } 918 | 919 | sub_comment() 920 | { 921 | local isha csha path 922 | 923 | test -n "$1" || usage_comment 924 | 925 | cdissues 926 | path=$(issue_path_part "$1") || exit 927 | isha=$(issue_sha "$path") 928 | mkdir -p "$path/comments" || error "Unable to create comments directory" 929 | trans_start 930 | commit 'gi: Add comment' "gi comment mark $isha" 931 | csha=$(git rev-parse HEAD) 932 | cp templates/comment "$path/comments/$csha" || trans_abort 933 | edit "$path/comments/$csha" || trans_abort 934 | git add "$path/comments/$csha" || trans_abort 935 | commit 'gi: Add comment message' "gi comment message $isha $csha" 936 | echo "Added comment $(short_sha "$csha")" 937 | } 938 | 939 | # edit: Edit an issue's description 940 | usage_edit() 941 | { 942 | cat <<\USAGE_edit_EOF 943 | gi edit usage: git issue edit [-c] 944 | USAGE_edit_EOF 945 | exit 2 946 | } 947 | 948 | sub_edit() 949 | { 950 | local isha csha path comment fullpath OPTIND 951 | 952 | while getopts c flag ; do 953 | case $flag in 954 | c) 955 | comment=1 956 | ;; 957 | ?) 958 | usage_edit 959 | ;; 960 | esac 961 | done 962 | 963 | shift $((OPTIND - 1)); 964 | test -n "$1" || usage_edit 965 | 966 | cdissues 967 | if [ -z "$comment" ] ; then 968 | # Edit Issue 969 | path=$(issue_path_part "$1") || exit 970 | isha=$(issue_sha "$path") 971 | 972 | trans_start 973 | edit "$path/description" || trans_abort 974 | git add "$path/description" || trans_abort 975 | commit 'gi: Edit issue description' "gi edit description $isha" 976 | echo "Edited issue $(short_sha "$isha")" 977 | else 978 | # Edit Comment 979 | commit=$(git show --format='%b' "$1" 2> /dev/null ) || error "Unknown or ambigious comment specification $1" 980 | # get issue sha 981 | isha=$(echo "$commit" | sed 's/gi comment mark //') 982 | echo "$isha" | grep -q '^[a-f0-9]\+$' || error "Not a comment sha." 983 | path=$(issue_path_part "$isha") 984 | # Get full comment sha 985 | csha=$(git rev-parse "$1") 986 | # shellcheck disable=SC2206 987 | # SC2128: Expanding an array without an index only gives the first element. 988 | edit "$path/comments/$csha" || trans_abort 989 | git add "$path/comments/$csha" || trans_abort 990 | commit 'gi: Edit comment' "gi edit comment $csha" 991 | echo "Edited comment $(short_sha "$csha")" 992 | fi 993 | 994 | } 995 | 996 | 997 | # shellcheck source=lib/git-issue/import-export.sh 998 | . "$MY_LIB/import-export.sh" 999 | 1000 | # list: Show issues matching a tag {{{1 1001 | usage_list() 1002 | { 1003 | cat <<\USAGE_list_EOF 1004 | gi list usage: git issue list [-a] [tag|milestone] 1005 | git issue list [-a] -l formatstring [-o sort_field] [-r] [tag|milestone] 1006 | USAGE_list_EOF 1007 | exit 2 1008 | } 1009 | 1010 | # helper function for long listing format, each call proccesses a single issue 1011 | shortshow() 1012 | { 1013 | 1014 | local path formatstring sortfield id date duedate rawdate milestone 1015 | local weight assignee tags description rawest timeestimate rawspent timespent 1016 | 1017 | path=$1 1018 | formatstring=$2 1019 | sortfield=$3 1020 | id=$4 1021 | 1022 | # Date 1023 | date=$(git show --no-patch --format='%ai' "$id") 1024 | 1025 | # Milestone 1026 | if [ -s "$path/milestone" ] ; then 1027 | # Escape sed special chars before passing them 1028 | milestone=$(fmt "$path/milestone"|sed -e 's/[\/&]/\\&/g') 1029 | fi 1030 | 1031 | # Weight 1032 | if [ -s "$path/weight" ] ; then 1033 | weight=$(fmt "$path/weight") 1034 | fi 1035 | 1036 | # Due Date 1037 | if [ -s "$path/duedate" ] ; then 1038 | rawdate=$(fmt "$path/duedate") 1039 | # Print it in rfc-3339 for consistency with git show format 1040 | duedate=$($DATEBIN --date="$rawdate" --rfc-3339=seconds) 1041 | fi 1042 | 1043 | # Time Estimate 1044 | if [ -s "$path/timeestimate" ] ; then 1045 | rawest=$(cat "$path/timeestimate") 1046 | # shellcheck disable=SC2016 1047 | # SC2016: Expressions don't expand is single quotes, use double quotes for that 1048 | # Rationale: We don't want expansion 1049 | timeestimate=$(eval "echo $($DATEBIN --utc --date="@$rawest" +'$((%s/3600/24)) days %H hours %M minutes %S seconds')"| 1050 | sed -e "s/00 \(hours\|minutes\|seconds\) \?//g" -e "s/^0 days //") 1051 | else 1052 | timeestimate='-' 1053 | fi 1054 | 1055 | # Time Spent 1056 | if [ -s "$path/timespent" ] ; then 1057 | rawspent=$(cat "$path/timespent") 1058 | # shellcheck disable=SC2016 1059 | timespent=$(eval "echo $($DATEBIN --utc --date="@$rawspent" +'$((%s/3600/24)) days %H hours %M minutes %S seconds')"| 1060 | sed -e "s/00 \(hours\|minutes\|seconds\) \?//g" -e "s/^0 days //") 1061 | else 1062 | timespent='-' 1063 | fi 1064 | 1065 | # Assignee 1066 | if [ -r "$path/assignee" ] ; then 1067 | assignee=$(tr '\n' ' ' < "$path/assignee"|sed -e 's/[\/&]/\\&/g') 1068 | fi 1069 | 1070 | # Tags 1071 | if [ -s "$path/tags" ] ; then 1072 | tags=$(tr '\n' ' ' < "$path/tags"|sed -e 's/[\/&]/\\&/g') 1073 | fi 1074 | 1075 | # Description 1076 | description=$(head -n 1 "$path/description"|sed -e 's/[\/&]/\\&/g') 1077 | 1078 | # Print the field to sort by first, and remove it after sorting 1079 | (echo "$sortfield"$'\002'"$formatstring") | 1080 | 1081 | sed -e s/%n/$'\001'/g \ 1082 | -e s/%i/"$id"/g \ 1083 | -e s/%c/"$date"/g \ 1084 | -e s/%d/"$duedate"/g \ 1085 | -e s/%e/"$timeestimate"/g \ 1086 | -e s/%s/"$timespent"/g \ 1087 | -e s/%M/"$milestone"/g \ 1088 | -e s/%w/"$weight"/g \ 1089 | -e s/%A/"$assignee"/g \ 1090 | -e s/%T/"$tags"/g \ 1091 | -e s/%D/"$description"/g | 1092 | tr '\n' '\001' 1093 | echo 1094 | 1095 | } 1096 | 1097 | 1098 | sub_list() 1099 | { 1100 | local all tag path id sortrev OPTIND 1101 | 1102 | while getopts al:o:r flag ; do 1103 | case "$flag" in 1104 | a) 1105 | all=1 1106 | ;; 1107 | l) 1108 | long=1 1109 | formatstring="$OPTARG" 1110 | ;; 1111 | o) 1112 | sortfield=$OPTARG 1113 | if [ -z "$sortfield" ] ; then 1114 | usage_list 1115 | fi 1116 | if ! expr "$sortfield" : '^%[icdeswMATD]$' > /dev/null ; then 1117 | usage_list 1118 | fi 1119 | ;; 1120 | r) 1121 | sortrev="-r" 1122 | ;; 1123 | ?) 1124 | usage_list 1125 | ;; 1126 | esac 1127 | done 1128 | 1129 | case "$formatstring" in 1130 | oneline) 1131 | formatstring='ID: %i Date: %c Tags: %T Desc: %D' 1132 | ;; 1133 | short) 1134 | formatstring='ID: %i%nDate: %c%nDue Date: %d%nTags: %T%nDescription: %D' 1135 | ;; 1136 | compact) 1137 | formatstring='ID: %i Date: %c%nDue Date: %d Weight: %w%nTags: %T Milestone: %M%nDescription: %D' 1138 | ;; 1139 | medium) 1140 | formatstring='ID: %i%nDate: %c%nDue Date: %d%nMilestone: %M%nWeight: %w%nTags: %T%nDescription: %D' 1141 | ;; 1142 | full) 1143 | formatstring='ID: %i%nDate: %c%nDue Date: %d%nTime Spent: %s%nTime Estimate: %e%nAssignees: %A%nMilestone: %M%nWeight: %w%nTags: %T%nDescription: %D' 1144 | ;; 1145 | esac 1146 | shift $((OPTIND - 1)); 1147 | tag="$1" 1148 | if [ "$tag" = "closed" ] ; then 1149 | # when explicitly searching for closed tickets, don't show only open ones 1150 | all=1 1151 | fi 1152 | cdissues 1153 | test -d issues || exit 0 1154 | find issues -type f -name tags -o -name milestone | 1155 | if [ "$tag" ] ; then 1156 | xargs grep -Flx "$tag" 1157 | else 1158 | cat 1159 | fi | 1160 | if [ "$all" ] ; then 1161 | cat 1162 | else 1163 | sed 's/\/milestone$/\/tags/' | xargs grep -Flx open 1164 | fi | 1165 | # Convert list of tag or milestone file paths into the corresponding 1166 | # directory and issue id 1167 | sed 's/^\(.*\)\/[^\/]*$/\1/;s/\(issues\/\(..\)\/\(.....\).*\)/\1 \2\3/' | 1168 | sort -u | 1169 | if [ "$long" ] ; then 1170 | 1171 | while read -r path id ; do 1172 | shortshow "$path" "$formatstring" "$sortfield" "$id" 1173 | done | 1174 | sort $sortrev | 1175 | sed 's/^.*\x02//' | 1176 | tr '\001' '\n' 1177 | else 1178 | while read -r path id ; do 1179 | printf '%s' "$id " 1180 | head -1 "$path/description" 1181 | done | 1182 | sort -k 2 1183 | fi | 1184 | tee results | 1185 | pager 1186 | 1187 | # Error checking 1188 | if ! [ -s results ] ; then 1189 | echo 'No matching issues found' 1>&2 1190 | exit 1 1191 | fi 1192 | rm -f results 1193 | } 1194 | 1195 | # log: Show log of issue changes {{{1 1196 | usage_log() 1197 | { 1198 | cat <<\USAGE_log_EOF 1199 | gi log usage: git issue log [-I issue-SHA] [git log options] 1200 | USAGE_log_EOF 1201 | exit 2 1202 | } 1203 | 1204 | sub_log() 1205 | { 1206 | while getopts I: flag ; do 1207 | case $flag in 1208 | I) 1209 | sha="$OPTARG" 1210 | ;; 1211 | ?) 1212 | usage_log 1213 | ;; 1214 | esac 1215 | done 1216 | shift $((OPTIND - 1)); 1217 | 1218 | cdissues 1219 | if [ "$sha" ] ; then 1220 | git log --grep="^gi new $sha" "$@" 1221 | else 1222 | git log "$@" 1223 | fi 1224 | 1225 | } 1226 | 1227 | usage_filter() 1228 | { 1229 | cat <<\USAGE_filter_EOF 1230 | gi filter usage: git issue filter-apply command 1231 | USAGE_filter_EOF 1232 | exit 2 1233 | } 1234 | 1235 | #filter-apply: Apply a filter (script or other command) for each issue 1236 | sub_filter() 1237 | { 1238 | 1239 | test -n "$1" || usage_filter 1240 | command -v "$1" > /dev/null || test -x "$1" || error "$1 is not a recognized command." 1241 | # Get absolute path 1242 | #shellcheck disable=SC2164 1243 | # SC2164: Use 'cd ... || exit' or 'cd ... || return' in case cd fails. 1244 | abscmd="$(cd "$(dirname "$1")"; pwd -P)/$(basename "$1")" 1245 | shift 1246 | cdissues 1247 | test -d issues || error "There are no issues to filter." 1248 | cd issues || trans_abort 1249 | for i in */* ; do 1250 | # In case the command changes directory 1251 | cdissues 1252 | #set environment variables for the command to use 1253 | GI_SHA=$(echo "$i" | tr -d '/') 1254 | GI_IMPORTS=$(importsget "$GI_SHA") 1255 | GI_AUTHOR=$(git show --no-patch --format='%an' "$GI_SHA") 1256 | GI_DATE=$(git show --no-patch --format='%aD' "$GI_SHA") 1257 | export GI_SHA 1258 | export GI_IMPORTS 1259 | export GI_AUTHOR 1260 | export GI_DATE 1261 | cd "issues/$i" || trans_abort 1262 | echo "Filtering issue $GI_SHA..." 1263 | eval "$abscmd" "$@" || error "$abscmd returned non-zero exit status code. Aborting..." 1264 | done 1265 | echo 1266 | echo 'Filter applied. To commit the results, run "gi git commit -a".' 1267 | 1268 | } 1269 | # dump: Generate a json dump of all issues 1270 | sub_dump() 1271 | { 1272 | local jstring path assignee tags title description duedate weight milestone timeestimate timespent 1273 | 1274 | shas=$(sub_list -l %i -o %c -a | sed '/^$/d' | tr '\n' ' ') 1275 | 1276 | cdissues 1277 | { 1278 | echo '{ "issues" : [' 1279 | for sha in $shas ; do 1280 | jstring="{\"sha\" : \"$sha\" }" 1281 | path=$(issue_path_part "$sha") || exit 1282 | # Get the attributes 1283 | 1284 | # Assignee 1285 | if [ -r "$path/assignee" ] ; then 1286 | assignee=$(tr '\n' ' ' < "$path/assignee") 1287 | # TODO: this directive is not needed on latest shellcheck versions 1288 | # shellcheck disable=SC2016 1289 | jstring=$(echo "$jstring" | jq --arg A "$assignee" -r '. + { assignee: $A }') 1290 | fi 1291 | 1292 | # Tags 1293 | if [ -s "$path/tags" ] ; then 1294 | # format tags as json array 1295 | tags=$(jq --slurp --raw-input 'split("\n")' "$path/tags") 1296 | tags=$(echo "$tags" | jq 'map(select(. != ""))') 1297 | if [ "$tags" != '[]' ] ; then 1298 | jstring=$(echo "$jstring" | jq -r ". + { tags: $tags }") 1299 | fi 1300 | fi 1301 | 1302 | # Description 1303 | # Title is the first line of description 1304 | description=$(cat "$path/description") 1305 | # TODO: this directive is not needed on latest shellcheck versions 1306 | # shellcheck disable=SC2016 1307 | jstring=$(echo "$jstring" | jq --arg desc "$description" -r '. + { description: $desc }') 1308 | 1309 | # Due Date 1310 | if [ -s "$path/duedate" ] ; then 1311 | duedate=$($DATEBIN --iso-8601 --date="$(fmt "$path/duedate")") 1312 | # TODO: this directive is not needed on latest shellcheck versions 1313 | # shellcheck disable=SC2016 1314 | jstring=$(echo "$jstring" | jq --arg D "$duedate" -r '. + { due_date: $D }') 1315 | fi 1316 | 1317 | # Weight 1318 | if [ -s "$path/weight" ] ; then 1319 | weight=$(fmt "$path/weight") 1320 | # TODO: this directive is not needed on latest shellcheck versions 1321 | # shellcheck disable=SC2016 1322 | jstring=$(echo "$jstring" | jq --arg W "$weight" -r '. + { weight: $W }') 1323 | fi 1324 | 1325 | # Milestone 1326 | if [ -s "$path/milestone" ] ; then 1327 | milestone=$(fmt "$path/milestone") 1328 | # TODO: this directive is not needed on latest shellcheck versions 1329 | # shellcheck disable=SC2016 1330 | jstring=$(echo "$jstring" | jq --arg A "$milestone" -r '. + { milestone: $A }') 1331 | fi 1332 | 1333 | # Time estimate/time spent 1334 | if [ -s "$path/timeestimate" ] ; then 1335 | timeestimate=$(fmt "$path/timeestimate") 1336 | # TODO: this directive is not needed on latest shellcheck versions 1337 | # shellcheck disable=SC2016 1338 | jstring=$(echo "$jstring" | jq --arg A "$timeestimate" -r '. + { timeestimate: $A }') 1339 | fi 1340 | 1341 | if [ -s "$path/timespent" ] ; then 1342 | timeestimate=$(fmt "$path/timespent") 1343 | # TODO: this directive is not needed on latest shellcheck versions 1344 | # shellcheck disable=SC2016 1345 | jstring=$(echo "$jstring" | jq --arg A "$timespent" -r '. + { timespent: $A }') 1346 | fi 1347 | 1348 | if [ -d "$path/comments" ] ; then 1349 | local csha 1350 | cstring='[]' 1351 | cshas=$(git rev-list --reverse --grep="^gi comment mark $sha" HEAD) 1352 | for csha in $cshas ; do 1353 | # TODO: this directive is not needed on latest shellcheck versions 1354 | # shellcheck disable=SC2016 1355 | cstring=$(echo "$cstring" | jq --arg C "$(cat "$path/comments/$csha")" --arg S "$csha" '.+= [{ sha : $S , body: $C }]') 1356 | done 1357 | 1358 | jstring=$(echo "$jstring" | jq -r ". + { comments: $cstring }") 1359 | fi 1360 | echo "$jstring" ',' 1361 | done 1362 | echo '{} ] }' 1363 | } | jq 1364 | } 1365 | 1366 | # tags: List all used tags and their count {{{1 1367 | sub_tags() 1368 | { 1369 | cdissues 1370 | sort issues/*/*/tags | uniq -c | pager 1371 | } 1372 | 1373 | # help: display help information {{{1 1374 | usage_help() 1375 | { 1376 | cat <<\USAGE_help_EOF 1377 | gi help usage: git issue help 1378 | USAGE_help_EOF 1379 | exit 2 1380 | } 1381 | 1382 | sub_help() 1383 | { 1384 | # 1385 | # The following list is automatically created from README.md by running 1386 | # make sync-docs 1387 | # DO NOT EDIT IT HERE; UPDATE README.md instead 1388 | # 1389 | cat <<\USAGE_EOF 1390 | usage: git issue [] 1391 | 1392 | The following commands are available: 1393 | 1394 | Start an issue repository 1395 | clone Clone the specified remote repository. 1396 | init Create a new issues repository in the current directory. 1397 | 1398 | Work with an issue 1399 | new Create a new open issue (with optional -s summary and -c "provider user repo" for github/gitlab export). 1400 | show Show specified issue (and its comments with -c). 1401 | comment Add an issue comment. 1402 | edit Edit the specified issue's (or comment's with -c) description 1403 | tag Add (or remove with -r) a tag. 1404 | milestone Specify (or remove with -r) the issue's milestone. 1405 | weight Specify (or remove with -r) the issue's weight. 1406 | duedate Specify (or remove with -r) the issue's due date. 1407 | * timeestimate: Specify (or remove with -r) a time estimate for this issue. 1408 | timespent Specify (or remove with -r) the time spent working on an issue so far. 1409 | assign Assign (or remove -r) an issue to a person. 1410 | attach Attach (or remove with -r) a file to an issue. 1411 | watcher Add (or remove with -r) an issue watcher. 1412 | close Remove the open tag, add the closed tag 1413 | 1414 | Show multiple issues 1415 | list List open issues (or all with -a). 1416 | * list -l formatstring: This will list issues in the specified format, given as an argument to -l. 1417 | 1418 | Work with multiple issues 1419 | * filter-apply command: Run command in every issue directory. The following environment variables will be set: 1420 | 1421 | Synchronize with remote repositories 1422 | push Update remote Git repository with local changes. 1423 | pull Update local Git repository with remote changes. 1424 | import Import/update GitHub/GitLab issues from the specified project. 1425 | create Create the issue in the provided GitHub repository. 1426 | export Export modified issues for the specified project. 1427 | exportall Export all open issues in the database (-a to include closed ones) to GitHub/GitLab. Useful for cloning whole repositories. 1428 | 1429 | Help and debug 1430 | help Display help information about git issue. 1431 | log Output a log of changes made 1432 | git Run the specified Git command on the issues repository. 1433 | USAGE_EOF 1434 | } 1435 | 1436 | # Subcommand selection {{{1 1437 | 1438 | subcommand="$1" 1439 | if ! [ "$subcommand" ] ; then 1440 | sub_help 1441 | exit 1 1442 | fi 1443 | 1444 | shift 1445 | case "$subcommand" in 1446 | 1447 | filter-apply) 1448 | sub_filter "$@" 1449 | ;; 1450 | dump) 1451 | sub_dump "$@" 1452 | ;; 1453 | exportall) 1454 | sub_exportall "$@" 1455 | ;; 1456 | export) 1457 | export_issues "$@" 1458 | ;; 1459 | create) 1460 | create_issue "$@" 1461 | ;; 1462 | init) # Initialize a new issue repository. 1463 | sub_init "$@" 1464 | ;; 1465 | clone) # Clone specified remote directory. 1466 | sub_clone "$@" 1467 | ;; 1468 | import) # Import issues from specified source 1469 | sub_import "$@" 1470 | ;; 1471 | new) # Create a new issue and mark it as open. 1472 | sub_new "$@" 1473 | ;; 1474 | list) # List the issues with the specified tag. 1475 | sub_list "$@" 1476 | ;; 1477 | show) # Show specified issue (and its comments with -c). 1478 | sub_show "$@" 1479 | ;; 1480 | comment) # Add an issue comment. 1481 | sub_comment "$@" 1482 | ;; 1483 | tag) # Add (or remove with -r) a tag. 1484 | sub_tag "$@" 1485 | ;; 1486 | assign) # Assign (or reassign) an issue to a person. 1487 | sub_assign "$@" 1488 | ;; 1489 | attach) # Attach (or remove with -r) a file to an issue. 1490 | echo 'Not implemented yet' 1>&2 1491 | exit 1 1492 | ;; 1493 | watcher) # Add (or remove with -r) an issue watcher. 1494 | sub_watcher "$@" 1495 | ;; 1496 | edit) # Edit the specified issue's summary or comment. 1497 | sub_edit "$@" 1498 | ;; 1499 | close) # Remove the open tag from the issue, marking it as closed. 1500 | sha="$1" 1501 | sub_tag "$sha" closed 1502 | sub_tag -r "$sha" open 1503 | ;; 1504 | help) # Display help information. 1505 | sub_help 1506 | ;; 1507 | log) # Output log of changes made. 1508 | sub_log "$@" 1509 | ;; 1510 | milestone) # Add (or remove with -r) a milestone 1511 | sub_milestone "$@" 1512 | ;; 1513 | duedate) # Add (or remove with -r) a duedate 1514 | sub_duedate "$@" 1515 | ;; 1516 | timespent) # Add (or remove with -r) the time spent 1517 | sub_timespent "$@" 1518 | ;; 1519 | timeestimate) # Add (or remove with -r) the time estimate 1520 | sub_timeestimate "$@" 1521 | ;; 1522 | weight) # Add (or remove with -r) a weight 1523 | sub_weight "$@" 1524 | ;; 1525 | push) # Update remote repository with local changes. 1526 | cdissues 1527 | git push "$@" 1528 | ;; 1529 | pull) # Update local repository with remote changes. 1530 | cdissues 1531 | git pull "$@" 1532 | ;; 1533 | git) # Run the specified Git command on the issues repository. 1534 | cdissues 1535 | git "$@" 1536 | ;; 1537 | tags) # List all tags 1538 | sub_tags 1539 | ;; 1540 | *) 1541 | # Default to help. 1542 | sub_help 1543 | exit 1 1544 | ;; 1545 | esac 1546 | -------------------------------------------------------------------------------- /lib/git-issue/import-export.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck disable=2039 3 | # SC2039: In POSIX sh, 'local' is undefined 4 | 5 | # import: import issues from GitHub/GitLab {{{1 6 | usage_import() 7 | { 8 | cat <<\USAGE_import_EOF 9 | gi import usage: git issue import [remote | provider user repo] 10 | Example: git issue import github torvalds linux 11 | USAGE_import_EOF 12 | exit 2 13 | } 14 | 15 | # export: export issues to GitHub {{{1 16 | usage_export() 17 | { 18 | cat <<\USAGE_export_EOF 19 | gi export usage: git issue export provider user repo 20 | -e Expand escape attribute sequences before exporting (see gi list -l) 21 | 22 | Example: git issue export github torvalds linux 23 | USAGE_export_EOF 24 | exit 2 25 | } 26 | 27 | convert_to_lower_case() 28 | { 29 | echo "$1" | tr '[:upper:]' '[:lower:]' 30 | } 31 | 32 | # importsget: Return all mentions of SHA given in imports 33 | 34 | importsget() 35 | { 36 | local path isha sha num 37 | 38 | cdissues 39 | path=$(issue_path_part "$1") || exit 40 | isha=$(issue_sha "$path") 41 | test -d imports || return 42 | # Get all issues(glob can't handle arbitrary nesting) 43 | find imports/ -name 'sha' | rev | sed 's:ahs/::' | rev | 44 | while read -r i ; do 45 | local sha 46 | if [ -r "$i/sha" ] ; then 47 | sha=$(cat "$i/sha") 48 | if [ "$sha" = "$isha" ] ; then 49 | echo "${i#imports/}" 50 | fi 51 | fi 52 | done 53 | 54 | } 55 | 56 | # Escape special URL characters in argument string using Percent-encoding 57 | urlescape() 58 | { 59 | echo "$1" | 60 | sed 's.%.%25.g' | 61 | sed -e 's./.%2F.g' \ 62 | -e 's.!.%21.g' \ 63 | -e 's.#.%23.g' \ 64 | -e 's.\$.%24.g' \ 65 | -e 's.&.%26.g' \ 66 | -e 's.'\''.%27.g' \ 67 | -e 's.(.%28.g' \ 68 | -e 's.).%29.g' \ 69 | -e 's.*.%2A.g' \ 70 | -e 's.+.%2B.g' \ 71 | -e 's.,.%2C.g' \ 72 | -e 's.:.%3A.g' \ 73 | -e 's.;.%3B.g' 74 | 75 | } 76 | # Get a page using the GitHub/GitLab API; abort transaction on error 77 | # Header is saved in the file $prefix-header; body in $prefix-body 78 | rest_api_get() 79 | { 80 | local url prefix provider authtoken 81 | 82 | url="$1" 83 | prefix="$2" 84 | provider="$3" 85 | 86 | # use the correct authentication token 87 | if [ "$provider" = github ] ; then 88 | authtoken="$GH_CURL_AUTH" 89 | elif [ "$provider" = gitlab ] ; then 90 | authtoken="$GL_CURL_AUTH" 91 | else 92 | trans_abort 93 | fi 94 | 95 | if ! curl -H "$authtoken" -A "$USER_AGENT" -s \ 96 | -o "$prefix-body" -D "$prefix-header" "$url" ; then 97 | echo "$provider connection failed" 1>&2 98 | trans_abort 99 | fi 100 | 101 | if ! grep -q '^\(Status: 200\|HTTP/[[:digit:]].[[:digit:]] 200 OK\|HTTP/[[:digit:]] 200\)' "$prefix-header" ; then 102 | echo "$provider API communication failure" 1>&2 103 | echo "URL: $url" 1>&2 104 | if grep -q '^\(Status: 4\|HTTP/[0-9].[0-9] 4\|HTTP/[0-9] 4\)' "$prefix-header" ; then 105 | jq -r '.message' "$prefix-body" 1>&2 106 | fi 107 | trans_abort 108 | fi 109 | } 110 | 111 | # POST, PATCH, PUT or DELETE data using the GitHub API; abort transaction on error 112 | # Header is saved in the file $prefix-header; body in $prefix-body 113 | 114 | rest_api_send() 115 | { 116 | local url prefix data mode curl_mode authtoken 117 | 118 | url="$1" 119 | prefix="$2" 120 | data="$3" 121 | mode=${4:-"POST"} 122 | provider="$5" 123 | if [ "$mode" = 'PATCH' ] ; then 124 | curl_mode='--request PATCH' 125 | elif [ "$mode" = 'PUT' ] ; then 126 | curl_mode='--request PUT' 127 | elif [ "$mode" = 'DELETE' ] ; then 128 | curl_mode='--request DELETE' 129 | elif [ "$mode" = 'POST' ] ; then 130 | curl_mode='' 131 | else 132 | error "incorrect rest_api_send() mode: $mode" 133 | fi 134 | 135 | # use the correct authentication token 136 | if [ "$provider" = github ] ; then 137 | authtoken="$GH_CURL_AUTH" 138 | elif [ "$provider" = gitlab ] ; then 139 | authtoken="$GL_CURL_AUTH" 140 | else 141 | trans_abort 142 | fi 143 | 144 | # The unquoted curl_mode won't glob, due to the way it's initialized 145 | # shellcheck disable=SC2086 146 | if ! curl --header "Content-Type: application/json" -H "$authtoken" -A "$USER_AGENT" -s \ 147 | -o "$prefix-body" -D "$prefix-header" $curl_mode --data "$data" "$url" ; then 148 | echo 'GitHub connection failed' 1>&2 149 | trans_abort 150 | fi 151 | 152 | if ! grep -q '^\(Status: 20[0-9]\|HTTP/[[:digit:]].[[:digit:]] 20[0-9] Created\|HTTP/[[:digit:]].[[:digit:]] 200 OK\|HTTP/[[:digit:]] 200\)' "$prefix-header" ; then 153 | echo "$provider API communication failure" 1>&2 154 | echo "URL: $url" 1>&2 155 | echo "Data: $data" 1>&2 156 | if grep -q '^\(Status: 4\|HTTP/[0-9].[0-9] 4\|HTTP/[0-9] 4\)' "$prefix-header" ; then 157 | jq -r '.message' "$prefix-body" 1>&2 158 | fi 159 | trans_abort 160 | fi 161 | } 162 | # create_issue: export issues to GitHub {{{1 163 | usage_create_issue() 164 | { 165 | cat <<\USAGE_create_issue_EOF 166 | gi create usage: git issue create id provider user repo 167 | -e Expand escape attribute sequences before exporting (see gi list -l) 168 | -n Keep HTTP transaction files 169 | -u num Update issue #num instead of creating a new one 170 | 171 | Example: git issue create 0123 github torvalds linux 172 | USAGE_create_issue_EOF 173 | exit 2 174 | } 175 | 176 | # Create an issue in GitHub/GitLab, based on a local one 177 | create_issue() 178 | { 179 | 180 | local isha path assignee tags title description url provider user repo nassignee 181 | local nodelete OPTIND escrepo update num import_dir attr_expand jstring i 182 | 183 | while getopts neu: flag ; do 184 | case $flag in 185 | n) 186 | nodelete=1 187 | ;; 188 | u) 189 | num=$OPTARG 190 | update=1 191 | ;; 192 | e) 193 | attr_expand=1 194 | ;; 195 | ?) 196 | usage_create_issue 197 | ;; 198 | esac 199 | done 200 | shift $((OPTIND - 1)); 201 | 202 | test -n "$1" || usage_create_issue 203 | test "$2" = github -o "$2" = gitlab || usage_create_issue 204 | test -n "$3" || usage_create_issue 205 | test -n "$4" || usage_create_issue 206 | cdissues 207 | path=$(issue_path_part "$1") || exit 208 | isha=$(issue_sha "$path") 209 | provider="$2" 210 | user="$(convert_to_lower_case "$3")" 211 | repo="$(convert_to_lower_case "$4")" 212 | 213 | if [ "$provider" = gitlab ] ; then 214 | # if the repo belongs to a group, repo will be in the format groupname/reponame 215 | # we need to escape the / for URLs 216 | escrepo=$(urlescape "$repo") 217 | fi 218 | # initialize the string 219 | jstring='{}' 220 | # Get the attributes 221 | # Assignee 222 | if [ -r "$path/assignee" ] ; then 223 | assignee=$(tr '\n' ' ' < "$path/assignee") 224 | nassignee="$assignee" 225 | if [ "$provider" = github ] ; then 226 | for a in $nassignee ; do 227 | ret=$(curl -H "$GH_CURL_AUTH" -A "$USER_AGENT" "https://api.github.com/repos/$user/$repo/assignees/$a" --stderr /dev/null ) 228 | # if assignee is valid github should return no data 229 | if [ -n "$ret" ] ; then 230 | echo "Couldn't add assignee $a. Skipping..." 231 | assignee=$(echo "$assignee" | sed "s/\\($a \\| $a\\|^$a$\\)//") 232 | fi 233 | done 234 | if [ "$assignee" != '[]' ] ; then 235 | jstring=$(printf '%s' "$jstring" | jq ". + { assignees : $(echo "$assignee" | tr -d '\n' | jq --slurp --raw-input 'split(" ")') }") 236 | fi 237 | else 238 | rest_api_get "https://gitlab.com/api/v4/users?username=$(echo "$assignee" | cut -f 1 -d ' ')" assignee gitlab 239 | if [ "$(cat assignee-body)" = '[]' ] ; then 240 | echo "Couldn't find assignee in GitLab, skipping assignment." 241 | else 242 | jstring=$(printf '%s' "$jstring" | jq -r ". + { assignee_ids: [$(jq -r '.[0].id' assignee-body)]}") 243 | fi 244 | fi 245 | fi 246 | 247 | # Tags 248 | if [ -s "$path/tags" ] ; then 249 | # format tags as json array 250 | tags=$(jq --slurp --raw-input 'split("\n")' "$path/tags") 251 | # Process state (open--opened-- or closed) 252 | if [ -n "$update" ] ; then 253 | if grep '^open$' >/dev/null < "$path/tags"; then 254 | if [ "$provider" = github ] ; then 255 | jstring=$(printf '%s' "$jstring" | jq -r '. + { state: "open" }') 256 | else 257 | jstring=$(printf '%s' "$jstring" | jq -r '. + { state_event: "reopen" }') 258 | fi 259 | else 260 | if [ "$provider" = gitlab ] ; then 261 | jstring=$(printf '%s' "$jstring" | jq -r '. + { state_event: "close" }') 262 | else 263 | jstring=$(printf '%s' "$jstring" | jq -r '. + { state: "closed" }') 264 | fi 265 | fi 266 | fi 267 | tags=$(echo "$tags" | jq 'map(select(. != "open"))') 268 | tags=$(echo "$tags" | jq 'map(select(. != "closed"))') 269 | tags=$(echo "$tags" | jq 'map(select(. != ""))') 270 | if [ "$tags" != '[]' ] ; then 271 | jstring=$(printf '%s' "$jstring" | jq -r ". + { labels: $tags }") 272 | fi 273 | fi 274 | 275 | # Description 276 | # Title is the first line of description 277 | title=$(head -n 1 "$path/description") 278 | if [ -z "$(head -n 2 "$path/description" | tail -n +2)" ] ; then 279 | description=$(tail --lines=+3 "$path/description" | head -c -1 ; echo x) 280 | else 281 | echo "Warning: Found non empty second line on issue description." 282 | #include second line if non empty 283 | description=$(tail --lines=+2 "$path/description" | head -c -1 ; echo x) 284 | fi 285 | 286 | 287 | # Handle formatting indicators 288 | if [ -n "$attr_expand" ] ; then 289 | title=$(shortshow "$path" "$title" 'i' "$isha" | sed 's/^.*\x02//' | tr '\001' '\n') 290 | description=$(shortshow "$path" "$description" 'i' "$isha" | sed 's/^.*\x02//' | tr '\001' '\n') 291 | # update description 292 | { 293 | echo "$title" 294 | echo 295 | echo "$description" 296 | } >"$path/description" 297 | git add "$path/description" || trans_abort 298 | if ! git diff --quiet HEAD ; then 299 | commit "gi: expand attributes in description of issue $isha" "gi description attribute expand $isha" 300 | fi 301 | fi 302 | 303 | # jq handles properly escaping the string if passed as variable 304 | if [ "$provider" = github ] ; then 305 | # TODO: this directive is not needed on latest shellcheck versions 306 | # shellcheck disable=SC2016 307 | jstring=$(printf '%s' "$jstring" | jq --arg desc "${description%x}" --arg tit "$title" \ 308 | -r '. + {title: $tit, body: $desc}') 309 | else 310 | # add trailing spaces if needed, or gitlab will ignore the newline 311 | description=$(echo "$description" | sed '$!s/[^ ] \?$/& /') 312 | # TODO: this directive is not needed on latest shellcheck versions 313 | # shellcheck disable=SC2016 314 | jstring=$(printf '%s' "$jstring" | jq --arg desc "${description%x}" --arg tit "$title" \ 315 | -r '. + {title: $tit, description: $desc}') 316 | fi 317 | 318 | # Due Date (not supported on github) 319 | if [ -s "$path/duedate" ] && [ "$provider" = gitlab ] ; then 320 | local duedate 321 | # gitlab date must be in YYYY-MM-DD format 322 | duedate=$($DATEBIN --iso-8601 --date="$(fmt "$path/duedate")") 323 | # TODO: this directive is not needed on latest shellcheck versions 324 | # shellcheck disable=SC2016 325 | jstring=$(printf '%s' "$jstring" | jq --arg D "$duedate" -r '. + { due_date: $D }') 326 | fi 327 | 328 | # Weight (only supported on gitlab starter+) 329 | if [ -s "$path/weight" ] && [ "$provider" = gitlab ] ; then 330 | local weight 331 | weight=$(fmt "$path/weight") 332 | # TODO: this directive is not needed on latest shellcheck versions 333 | # shellcheck disable=SC2016 334 | jstring=$(printf '%s' "$jstring" | jq --arg W "$weight" -r '. + { weight: $W }') 335 | fi 336 | 337 | # Milestone 338 | if [ -s "$path/milestone" ] ; then 339 | local mileurl jmileid milestone milenum miletitle found 340 | milestone=$(fmt "$path/milestone") 341 | # Milestones are separate entities in the GitHub and GitLab API 342 | # They need to be created before use on an issue 343 | if [ "$provider" = github ] ; then 344 | mileurl="https://api.github.com/repos/$user/$repo/milestones" 345 | jmileid='number' 346 | else 347 | mileurl="https://gitlab.com/api/v4/projects/$user%2F$escrepo/milestones" 348 | jmileid='id' 349 | fi 350 | # get milestone list 351 | rest_api_get "$mileurl" milestone "$provider" 352 | 353 | for i in $( jq 'range( .|length )' milestone-body ) ; do 354 | milenum=$(jq -r ".[$i].$jmileid" milestone-body) 355 | miletitle=$(jq -r ".[$i].title" milestone-body) 356 | if [ "$miletitle" = "$milestone" ] ; then 357 | # it already exists 358 | found=$milenum 359 | break 360 | fi 361 | done 362 | 363 | if [ -z "$found" ] ; then 364 | # we need to create it 365 | echo "Creating new Milestone $milestone..." 366 | rest_api_send "$mileurl" mileres "{ \"title\": \"$milestone\", 367 | \"state\": \"open\", \"description\":\"\"}" POST "$provider" 368 | found=$(jq ".$jmileid" mileres-body) 369 | fi 370 | if [ "$provider" = github ] ; then 371 | # TODO: this directive is not needed on latest shellcheck versions 372 | # shellcheck disable=SC2016 373 | jstring=$(printf '%s' "$jstring" | jq --arg A "$found" -r '. + { milestone: $A }') 374 | else 375 | # TODO: this directive is not needed on latest shellcheck versions 376 | # shellcheck disable=SC2016 377 | jstring=$(printf '%s' "$jstring" | jq --arg A "$found" -r '. + { milestone_id: $A }') 378 | fi 379 | fi 380 | 381 | if [ -n "$update" ] ; then 382 | if [ "$provider" = github ] ; then 383 | url="https://api.github.com/repos/$user/$repo/issues/$num" 384 | rest_api_send "$url" update "$jstring" PATCH github 385 | else 386 | url="https://gitlab.com/api/v4/projects/$user%2F$escrepo/issues/$num" 387 | rest_api_send "$url" update "$jstring" PUT gitlab 388 | fi 389 | else 390 | # Check if issue already exists 391 | for i in "imports/$provider/$user/$repo"/[0-9]* ; do 392 | local sha 393 | sha=$(cat "$i/sha" 2> /dev/null) 394 | if [ "$sha" = "$isha" ] ; then 395 | local num 396 | num=$(echo "$i" | grep -o '/[0-9].*$' | tr -d '/') 397 | error "Error: Local issue $sha is linked with $provider issue #$num.Cannot create duplicate." 398 | fi 399 | done 400 | 401 | if [ "$provider" = github ] ; then 402 | url="https://api.github.com/repos/$user/$repo/issues" 403 | else 404 | url="https://gitlab.com/api/v4/projects/$user%2F$escrepo/issues" 405 | fi 406 | rest_api_send "$url" create "$jstring" POST "$provider" 407 | if [ "$provider" = github ] ; then 408 | num=$(jq '.number' create-body) 409 | url="https://api.github.com/repos/$user/$repo/issues/$num" 410 | else 411 | num=$(jq '.iid' create-body) 412 | # update url to that of created issue 413 | url="https://gitlab.com/api/v4/projects/$user%2F$escrepo/issues/$num" 414 | fi 415 | fi 416 | import_dir="imports/$provider/$user/$repo/$num" 417 | 418 | 419 | # Time estimate/time spent 420 | local timeestimate timespent 421 | if [ -s "$path/timeestimate" ] && [ "$provider" = gitlab ] ; then 422 | timeestimate=$(fmt "$path/timeestimate") 423 | echo "Adding Time Estimate..." 424 | rest_api_send "$url/time_estimate?duration=${timeestimate}s" timeestimate "" POST gitlab 425 | fi 426 | 427 | if [ -s "$path/timespent" ] && [ "$provider" = gitlab ] ; then 428 | timespent=$(fmt "$path/timespent") 429 | if [ -n "$update" ] ; then 430 | local oldspent 431 | # get existing timestats 432 | rest_api_get "$url/time_stats" timestats gitlab 433 | oldspent=$(jq -r '.total_time_spent' timestats-body) 434 | if [ "$oldspent" -lt "$timespent" ] ; then 435 | echo "Adding Time Spent..." 436 | rest_api_send "$url/add_spent_time?duration=$((timespent - oldspent))s" timespent "" POST gitlab 437 | elif [ "$oldspent" -gt "$timespent" ] ; then 438 | # we need to reset time first 439 | echo "Local Time Spent less than remote. Resetting and adding Time Spent..." 440 | rest_api_send "$url/reset_spent_time" timespent "" POST gitlab 441 | rest_api_send "$url/add_spent_time?duration=${timespent}s" timespent "" POST gitlab 442 | fi 443 | else 444 | rest_api_send "$url/add_spent_time?duration=${timespent}s" timespent "" POST gitlab 445 | fi 446 | fi 447 | 448 | # Update issue state if we create a closed issue 449 | if grep -q '^closed$' "$path/tags" && [ -z "$update" ] ; then 450 | if [ "$provider" = github ] ; then 451 | rest_api_send "$url" update "{ \"state\": \"closed\" }" PATCH github 452 | else 453 | rest_api_send "$url" update "{ \"state_event\": \"close\" }" PUT gitlab 454 | fi 455 | fi 456 | 457 | test -d "$import_dir" || mkdir -p "$import_dir" 458 | echo "$isha" > "$import_dir/sha" 459 | git add "$import_dir" 460 | git diff --quiet HEAD || commit "gi: Add $import_dir" 'gi new mark' 461 | 462 | # delete temp files 463 | test -z $nodelete && rm -f create-body create-header update-body update-header 464 | rm -f milestone-body milestone-header mileres-body mileres-header timestats-header 465 | rm -f timeestimate-body timeestimate-header timespent-body timespent-header timestats-body 466 | } 467 | 468 | # Create a comment in GitHub/GitLab, based on a local one 469 | # create_comment 470 | create_comment() 471 | { 472 | local cbody cfound cjstring csha isha path provider user repo num import_dir url escrepo j 473 | 474 | csha="$1" 475 | provider="$2" 476 | user="$3" 477 | repo="$4" 478 | num="$5" 479 | 480 | 481 | if [ "$provider" = github ] ; then 482 | url="https://api.github.com/repos/$user/$repo/issues/$num" 483 | else 484 | escrepo=$(urlescape "$repo") 485 | url="https://gitlab.com/api/v4/projects/$user%2F$escrepo/issues/$num" 486 | fi 487 | 488 | import_dir="imports/$provider/$user/$repo/$num/" 489 | commit=$(git show --format='%b' "$csha" 2> /dev/null ) || error "Unknown or ambigious comment specification $csha" 490 | # get issue sha 491 | isha=$(echo "$commit" | sed 's/gi comment mark //') 492 | path=$(issue_path_part "$isha") 493 | 494 | cdissues 495 | cbody=$(head -c -1 < "$path/comments/$csha"; echo x) 496 | if [ "$provider" = gitlab ] ; then 497 | cbody=$(printf '%s' "$cbody" | sed '$!s/[^ ] \?$/& /') 498 | echo "${cbody%x}" | head -c -1 > "$path/comments/$csha" 499 | git add "$path/comments/$csha" 500 | if ! git diff --quiet HEAD ; then 501 | commit "Update comment formatting" "gi edit comment $csha" 502 | fi 503 | fi 504 | 505 | cfound= 506 | for j in "$import_dir"/comments/* ; do 507 | if [ "$(cat "$j" 2> /dev/null)" = "$csha" ] ; then 508 | cfound=$(echo "$j" | sed 's:.*comments/\(.*\)$:\1:') 509 | break 510 | fi 511 | done 512 | # TODO: this directive is not needed on latest shellcheck versions 513 | # shellcheck disable=SC2016 514 | cjstring=$(echo '{}' | jq --arg desc "${cbody%x}" '{body: $desc}') 515 | if [ -n "$cfound" ] ; then 516 | # the comment exists already 517 | echo "Updating comment $csha..." 518 | if [ "$provider" = github ] ; then 519 | rest_api_send "https://api.github.com/repos/$user/$repo/issues/comments/$cfound" \ 520 | commentupdate "$cjstring" PATCH github 521 | else 522 | rest_api_send "$url/notes/$cfound" commentupdate "$cjstring" PUT gitlab 523 | fi 524 | else 525 | # we need to create it 526 | echo "Creating comment $csha..." 527 | if [ "$provider" = github ] ; then 528 | rest_api_send "$url/comments" commentcreate "$cjstring" POST github 529 | else 530 | rest_api_send "$url/notes" commentcreate "$cjstring" POST gitlab 531 | fi 532 | test -d "$import_dir/comments" || mkdir -p "$import_dir/comments" 533 | echo "$csha" > "$import_dir/comments/$(jq -r '.id' commentcreate-body)" 534 | fi 535 | git add "$import_dir" 536 | # mark export 537 | commit "gi: Export comment $csha" "gi comment export $csha at $provider $user $repo" 538 | rm -f commentupdate-header commentupdate-body commentcreate-header commentcreate-body 539 | } 540 | 541 | # Import GitHub/GitLab comments for the specified issue 542 | # import_comments 543 | import_comments() 544 | { 545 | local user repo issue_number isha 546 | local i endpoint comment_id import_dir csha provider juser 547 | 548 | user="$1" 549 | shift 550 | repo="$1" 551 | shift 552 | issue_number="$1" 553 | shift 554 | isha="$1" 555 | shift 556 | provider="$1" 557 | shift 558 | 559 | if [ "$provider" = github ] ; then 560 | endpoint="https://api.github.com/repos/$user/$repo/issues/$issue_number/comments" 561 | juser='user.login' 562 | elif [ "$provider" = gitlab ] ; then 563 | # if $repo contains '/' then it's part of a group and needs to be escaped 564 | local escrepo 565 | escrepo=$(urlescape "$repo") 566 | endpoint="https://gitlab.com/api/v4/projects/$user%2F$escrepo/issues/$issue_number/notes" 567 | juser='author.username' 568 | else 569 | trans_abort 570 | fi 571 | 572 | while [ -n "$endpoint" ] ; do 573 | rest_api_get "$endpoint" comments "$provider" 574 | 575 | # For each comment in the comments-body file 576 | for i in $( jq 'range( .|length )' comments-body ) ; do 577 | # Dont import automated system comments 578 | test ! "$(jq -r ".[$i].system" comments-body)" = true || continue 579 | comment_id=$(jq -r ".[$i].id" comments-body) 580 | 581 | # See if comment already there 582 | import_dir="imports/$provider/$user/$repo/$issue_number/comments" 583 | if [ -r "$import_dir/$comment_id" ] ; then 584 | csha=$(cat "$import_dir/$comment_id") 585 | else 586 | name=$(jq -r ".[$i].$juser" comments-body) 587 | GIT_EVENT_DATE=$(jq -r ".[$i].updated_at" comments-body) \ 588 | commit 'gi: Add comment' "gi comment mark $isha" \ 589 | --author="$name <$name@users.noreply.$provider.com>" 590 | csha=$(git rev-parse HEAD) 591 | fi 592 | 593 | path=$(issue_path_full "$isha")/comments 594 | mkdir -p "$path" || trans_abort 595 | mkdir -p "$import_dir" || trans_abort 596 | 597 | 598 | # Add issue import number to allow future updates 599 | echo "$csha" >"$import_dir/$comment_id" 600 | 601 | # Create comment body 602 | jq -r ".[$i].body" comments-body >/dev/null || trans_abort 603 | jq -r ".[$i].body" comments-body | 604 | tr -d \\r >"$path/$csha" 605 | 606 | git add "$path/$csha" "$import_dir/$comment_id" || trans_abort 607 | if ! git diff --quiet HEAD ; then 608 | local name html_url 609 | name=$(jq -r ".[$i].$juser" comments-body) 610 | if [ "$provider" = github ] ; then 611 | html_url=$(jq -r ".[$i].html_url" comments-body) 612 | GIT_EVENT_DATE=$(jq -r ".[$i].updated_at" comments-body) \ 613 | commit 'gi: Import comment message' "gi comment message $isha $csha 614 | Comment URL: $html_url" \ 615 | --author="$name <$name@users.noreply.github.com>" 616 | else 617 | GIT_EVENT_DATE=$(jq -r ".[$i].updated_at" comments-body) \ 618 | commit 'gi: Import comment message' "gi comment message $isha $csha"\ 619 | --author="$name <$name@users.noreply.gitlab.com>" 620 | fi 621 | 622 | echo "Imported/updated issue #$issue_number comment $comment_id as $(short_sha "$csha")" 623 | fi 624 | done # For all comments on page 625 | 626 | # Return if no more pages 627 | if ! grep -q '^[Ll]ink:.*rel="next"' comments-header ; then 628 | break 629 | fi 630 | 631 | # Move to next point 632 | endpoint=$(rest_next_page_url comments) 633 | done 634 | } 635 | 636 | # Import GitHub or GitLab issues stored in the file issue-body as JSON data 637 | # import_issues user repo provider 638 | import_issues() 639 | { 640 | local user repo provider 641 | local i issue_number import_dir sha path name 642 | local duedate timeestimate timespent weight 643 | local jid juser jlogin jdesc 644 | 645 | 646 | user="$1" 647 | repo="$2" 648 | provider="$3" 649 | 650 | # some json field names differ 651 | if [ "$provider" = github ] ; then 652 | jid='number' 653 | juser='user.login' 654 | jlogin='login' 655 | jdesc='body' 656 | elif [ "$provider" = gitlab ] ; then 657 | jid='iid' 658 | juser='author.username' 659 | jlogin='username' 660 | jdesc='description' 661 | else 662 | trans_abort 663 | fi 664 | 665 | # For each issue in the issue-body file 666 | for i in $( jq 'range( .|length )' issue-body ) ; do 667 | issue_number=$(jq -r ".[$i].$jid" issue-body) 668 | 669 | # See if issue already there 670 | import_dir="imports/$provider/$user/$repo/$issue_number" 671 | if [ -d "$import_dir" ] ; then 672 | sha=$(cat "$import_dir/sha") 673 | else 674 | name=$(jq -r ".[$i].$juser" issue-body) 675 | GIT_EVENT_DATE=$(jq -r ".[$i].updated_at" issue-body) \ 676 | commit 'gi: Add issue' 'gi new mark' \ 677 | --author="$name <$name@users.noreply.$provider.com>" 678 | sha=$(git rev-parse HEAD) 679 | fi 680 | 681 | path=$(issue_path_full "$sha") 682 | mkdir -p "$path" || trans_abort 683 | mkdir -p "$import_dir" || trans_abort 684 | 685 | # Add issue import number to allow future updates 686 | echo "$sha" >"$import_dir/sha" 687 | 688 | # Create tags (in sorted order to avoid gratuitous updates) 689 | { 690 | # convert to our format 691 | jq -r ".[$i].state" issue-body | sed 's/opened/open/' 692 | if [ "$provider" = github ] ; then 693 | jq -r ".[$i].labels[] | .name" issue-body 694 | else 695 | jq -r ".[$i].labels[]" issue-body 696 | fi 697 | } | 698 | LC_ALL=C sort >"$path/tags" || trans_abort 699 | 700 | # Create assignees (in sorted order to avoid gratuitous updates) 701 | if [ "$(jq -r ".[$i].assignees | length" issue-body)" != 0 ] ; then 702 | jq -r ".[$i].assignees[] | .$jlogin" issue-body | 703 | LC_ALL=C sort >"$path/assignee" || trans_abort 704 | fi 705 | 706 | if [ -s "$path/assignee" ] ; then 707 | git add "$path/assignee" || trans_abort 708 | else 709 | rm -f "$path/assignee" 710 | fi 711 | 712 | # Obtain milestone 713 | if [ "$(jq -r ".[$i].milestone" issue-body)" = null ] ; then 714 | if [ -r "$path/milestone" ] ; then 715 | git rm "$path/milestone" || trans_abort 716 | fi 717 | else 718 | jq -r ".[$i].milestone.title" issue-body >"$path/milestone" || trans_abort 719 | git add "$path/milestone" || trans_abort 720 | fi 721 | 722 | if [ "$provider" = gitlab ] ; then 723 | 724 | # Due Date 725 | duedate=$(jq -r ".[$i].due_date" issue-body) 726 | if [ "$duedate" = null ] ; then 727 | if [ -r "$path/duedate" ] ; then 728 | git rm "$path/duedate" || trans_abort 729 | fi 730 | else 731 | # convert duedate to our format before saving 732 | $DATEBIN --date="$duedate" --iso-8601=seconds >"$path/duedate" || trans_abort 733 | git add "$path/duedate" || trans_abort 734 | fi 735 | 736 | # Timespent 737 | timespent=$(jq -r ".[$i].time_stats.total_time_spent" issue-body) 738 | if [ "$timespent" = '0' ] ; then 739 | if [ -r "$path/timespent" ] ; then 740 | git rm "$path/timespent" || trans_abort 741 | fi 742 | else 743 | echo "$timespent" >"$path/timespent" || trans_abort 744 | git add "$path/timespent" || trans_abort 745 | fi 746 | 747 | # Timeestimate 748 | timeestimate=$(jq -r ".[$i].time_stats.time_estimate" issue-body) 749 | if [ "$timeestimate" = '0' ] ; then 750 | if [ -r "$path/timeestimate" ] ; then 751 | git rm "$path/timeestimate" || trans_abort 752 | fi 753 | else 754 | echo "$timeestimate" >"$path/timeestimate" || trans_abort 755 | git add "$path/timeestimate" || trans_abort 756 | fi 757 | 758 | # Weight 759 | weight=$(jq -r ".[$i].weight" issue-body) 760 | if [ "$weight" = 'null' ] ; then 761 | if [ -r "$path/weight" ] ; then 762 | git rm "$path/weight" || trans_abort 763 | fi 764 | else 765 | echo "$weight" > "$path/weight" || trans_abort 766 | git add "$path/weight" || trans_abort 767 | fi 768 | fi 769 | 770 | # Create description 771 | jq -r ".[$i].title" issue-body >/dev/null || trans_abort 772 | jq -r ".[$i].$jdesc" issue-body >/dev/null || trans_abort 773 | { 774 | jq -r ".[$i].title" issue-body 775 | echo 776 | if jq -e -r ".[$i].$jdesc" issue-body > /dev/null ; then 777 | jq -r ".[$i].$jdesc" issue-body 778 | fi 779 | 780 | } | 781 | tr -d \\r >"$path/description" 782 | 783 | git add "$path/description" "$path/tags" imports || trans_abort 784 | if ! git diff --quiet HEAD ; then 785 | name=${name:-$(jq -r ".[$i].$juser" issue-body)} 786 | GIT_EVENT_DATE=$(jq -r ".[$i].updated_at" issue-body) \ 787 | commit "gi: Import issue #$issue_number from $provider/$user/$repo" \ 788 | "Issue URL: https://$provider.com/$user/$repo/issues/$issue_number" \ 789 | --author="$name <$name@users.noreply.$provider.com>" 790 | echo "Imported/updated issue #$issue_number as $(short_sha "$sha")" 791 | fi 792 | 793 | # Import issue comments 794 | import_comments "$user" "$repo" "$issue_number" "$sha" "$provider" 795 | done 796 | } 797 | 798 | export_issues() 799 | { 800 | local i import_dir sha url provider user repo flag attr_expand OPTIND sha num lastimport 801 | 802 | while getopts e flag ; do 803 | case $flag in 804 | e) 805 | # global flag to enable escape sequence 806 | attr_expand=1 807 | ;; 808 | ?) 809 | usage_export 810 | ;; 811 | esac 812 | done 813 | shift $((OPTIND - 1)); 814 | 815 | test -n "$2" -a -n "$3" || usage_export 816 | test "$1" = github -o "$1" = gitlab || usage_export 817 | provider=$1 818 | user="$(convert_to_lower_case "$2")" 819 | repo="$(convert_to_lower_case "$3")" 820 | 821 | cdissues 822 | test -d "imports/$provider/$user/$repo" || error "No local issues found for this repository." 823 | 824 | # For each issue in the respective import dir 825 | for i in "imports/$provider/$user/$repo"/[0-9]* ; do 826 | sha=$(cat "$i/sha") 827 | path=$(issue_path_part "$sha") || exit 828 | # Extract number 829 | num=$(echo "$i" | grep -o '/[0-9].*$' | tr -d '/') 830 | # Check if the issue has been modified since last import/export 831 | lastimport=$(git rev-list --grep "gi: \\(Add imports/$provider/$user/$repo/$num\\|Import issue #$num from $provider/$user/$repo\\)" HEAD | head -n 1) 832 | test -n "$lastimport" || error "Cannot find import commit." 833 | if [ -n "$(git rev-list --grep='\(gi: Import comment message\|gi: Add comment message\|gi: Edit comment\)' --invert-grep "$lastimport"..HEAD "$path")" ] ; then 834 | echo "Exporting issue $sha as #$num" 835 | create_issue -u "$num" "$sha" "$provider" "$user" "$repo" 836 | 837 | rm -f create-body create-header 838 | else 839 | echo "Issue $sha not modified, skipping..." 840 | fi 841 | 842 | # Comments 843 | if [ -d "$path/comments" ] ; then 844 | 845 | local csha cfound 846 | git rev-list --reverse --grep="^gi comment mark $sha" HEAD | 847 | while read -r csha ; do 848 | lastimport=$(git rev-list --grep "\\(gi comment message .* $csha\\|gi comment export $csha at $provider $user $repo\\)" HEAD | head -n 1) 849 | cfound= 850 | for j in "imports/$provider/$user/$repo/$num"/comments/* ; do 851 | if [ "$(cat "$j" 2> /dev/null)" = "$csha" ] ; then 852 | cfound=$(echo "$j" | sed 's:.*comments/\(.*\)$:\1:') 853 | break 854 | fi 855 | done 856 | 857 | if [ -n "$(git rev-list "$lastimport"..HEAD "$path/comments/$csha")" ] || [ -z "$cfound" ] ; then 858 | create_comment "$csha" "$provider" "$user" "$repo" "$num" 859 | else 860 | echo "Comment $csha not modified, skipping..." 861 | fi 862 | done 863 | fi 864 | 865 | done 866 | } 867 | # Return the next page API URL specified in the header with the specified prefix 868 | # Header examples (easy and tricky) 869 | # Link: ; rel="next", ; rel="last", ; rel="first" 870 | # Link: ; rel="prev", ; rel="next", ; rel="last", ; rel="first" 871 | rest_next_page_url() 872 | { 873 | sed -n ' 874 | :again 875 | # Print "next" link 876 | # This works only for the first element of the Link header 877 | s/^[Ll]ink:.<\([^>]*\)>; rel="next".*/\1/p 878 | # If substitution worked branch to end of script 879 | t 880 | # Remove first element of the Link header and retry 881 | s/^[Ll]ink: <[^>]*>; rel="[^"]*", */Link: / 882 | t again 883 | ' "$1"-header 884 | } 885 | 886 | # Returns the git's repository URL 887 | get_url() 888 | { 889 | local remote=$1 890 | 891 | if [ -z "${remote}" ] 892 | then 893 | remote="origin" 894 | fi 895 | 896 | git remote get-url ${remote} 2>&1 897 | } 898 | 899 | # Returns the git's repository provider name from the URL 900 | get_provider() 901 | { 902 | echo "$1" | sed "s|^git@||; s|^https://||; s|/[a-z]*||; s|.com.*||" 903 | } 904 | # Returns the git's repository user name from the URL 905 | get_user() 906 | { 907 | echo "$1" | sed -E "s/.*\.com[:|/]([^/]*).*/\1/" 908 | } 909 | # Returns the git's repository repo name from the URL 910 | get_repo() 911 | { 912 | echo "$1" | sed -E "s/.*\.com[:|/].*\/([^.]*).git/\1/" 913 | } 914 | 915 | # Import issues from specified source (currently github and gitlab) 916 | sub_import() 917 | { 918 | local endpoint user repo begin_sha provider url 919 | 920 | test "$1" = github -o "$1" = gitlab -a -n "$2" -a -n "$3" || 921 | url=$(get_url "$1"); if [ "${url}" = "fatal: No such remote '${1}'" ]; then printf "%s\n\n" "${url}"; usage_import; fi; provider=$(get_provider "${url}"); user=$(get_user "${url}"); repo=$(get_repo "${url}") 922 | 923 | if ! test -n "${provider}" && ! test -n "${user}" && ! test -n "${repo}" 924 | then 925 | provider="$1" 926 | 927 | # convert to lowercase to avoid duplicates 928 | user="$(convert_to_lower_case "$2")" 929 | repo="$(convert_to_lower_case "$3")" 930 | fi 931 | 932 | cdissues 933 | 934 | prerequisite_command jq 935 | prerequisite_command curl 936 | 937 | begin_sha=$(git rev-parse HEAD) 938 | 939 | mkdir -p "imports/$provider/$user/$repo" 940 | # Process issues page by page 941 | trans_start 942 | if [ "$provider" = github ] ; then 943 | endpoint="https://api.github.com/repos/$user/$repo/issues?state=all" 944 | else 945 | # if $repo contains '/' then it's part of a group and needs to be escaped 946 | local escrepo 947 | escrepo=$(urlescape "$repo") 948 | endpoint="https://gitlab.com/api/v4/projects/$user%2F$escrepo/issues" 949 | fi 950 | while true ; do 951 | rest_api_get "$endpoint" issue "$provider" 952 | import_issues "$user" "$repo" "$provider" 953 | 954 | # Return if no more pages 955 | if ! grep -q '^[Ll]ink:.*rel="next"' issue-header ; then 956 | break 957 | fi 958 | 959 | # Move to next point 960 | endpoint=$(rest_next_page_url issue) 961 | done 962 | 963 | rm -f issue-header issue-body comments-header comments-body 964 | 965 | # Mark last import SHA, so we can use this for merging 966 | if [ "$begin_sha" != "$(git rev-parse HEAD)" ] ; then 967 | local checkpoint="imports/$provider/$user/$repo/checkpoint" 968 | git rev-parse HEAD >"$checkpoint" 969 | git add "$checkpoint" 970 | commit "gi: Import issues from $provider checkpoint" \ 971 | "Issues URL: https://$provider.com/$user/$repo/issues" 972 | fi 973 | } 974 | 975 | usage_exportall() 976 | { 977 | cat <<\USAGE_exportall_EOF 978 | gi new usage: git issue exportall [-a] provider user repo 979 | USAGE_exportall_EOF 980 | exit 2 981 | } 982 | 983 | # Export all not already present issues to GitHub/GitLab repo 984 | sub_exportall() 985 | { 986 | local all provider user repo flag OPTIND shas num path i 987 | while getopts a flag ; do 988 | case "$flag" in 989 | a) 990 | all='-a' 991 | ;; 992 | ?) 993 | usage_exportall 994 | ;; 995 | esac 996 | done 997 | shift $((OPTIND - 1)); 998 | 999 | test "$1" = github -o "$1" = gitlab || usage_exportall 1000 | test -n "$2" -a -n "$3" || usage_exportall 1001 | provider="$1" 1002 | user="$(convert_to_lower_case "$2")" 1003 | repo="$(convert_to_lower_case "$3")" 1004 | 1005 | # Create list of relevant shas sorted by date 1006 | shas=$(sub_list -l %i -o %c "$all"| sed '/^$/d' | tr '\n' ' ') 1007 | 1008 | cdissues 1009 | 1010 | # Remove already exported issues 1011 | if [ -d "imports/$provider/$user/$repo" ] ; then 1012 | for i in "imports/$provider/$user/$repo/"[0-9]* ; do 1013 | shas=$(echo "$shas" | sed "s/$(head -c 7 "$i/sha")//") 1014 | done 1015 | fi 1016 | 1017 | for i in $shas ; do 1018 | echo "Creating issue $i..." 1019 | create_issue -n "$i" "$provider" "$user" "$repo" 1020 | # get created issue id 1021 | if [ "$provider" = github ] ; then 1022 | num=$(jq '.number' create-body) 1023 | else 1024 | num=$(jq '.iid' create-body) 1025 | fi 1026 | rm -f create-header create-body 1027 | 1028 | # Create comments 1029 | 1030 | path=$(issue_path_part "$i") || exit 1031 | if [ -d "$path/comments" ] ; then 1032 | local csha cfound 1033 | git rev-list --reverse --grep="^gi comment mark $i" HEAD | 1034 | while read -r csha ; do 1035 | create_comment "$csha" "$provider" "$user" "$repo" "$num" 1036 | done 1037 | fi 1038 | done 1039 | } 1040 | -------------------------------------------------------------------------------- /scripts/replacerefs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Filter an issue by replacing all references to issues in repo source to references in repo target 3 | # replacerefs sourcerepo targetrepo 4 | 5 | test -n "$2" || exit 2 6 | expr "$1" : '.*/.*' > /dev/null || exit 2 7 | expr "$2" : '.*/.*' > /dev/null || exit 2 8 | sourcerepo=$1 9 | targetrepo=$2 10 | string=$(cat description) 11 | refs=$(echo "$string" | grep -o '\([^[[:alnum:]_]\|^\)#[0-9]\+\([^][:alnum:]_]\|$\)' | grep -o '[0-9]\+' | sort | uniq) 12 | for ref in $refs ; do 13 | test -d "../../../imports/$sourcerepo/$ref" || echo "Warning: Couldn't find $sourcerepo/$ref" 1>&2 14 | newref=$(git issue show "$(cat "../../../imports/$sourcerepo/$ref/sha")" | 15 | grep -i "${targetrepo%%/*} issue: #[0-9]\+ at ${targetrepo#*/}" | grep -o '#[0-9]\+') 16 | # if not found, replace the ref with a link to the original issue 17 | if [ -z "$newref" ] ; then 18 | echo "Warning: Couldn't find $sourcerepo/$ref issue in $targetrepo" 1>&2 19 | newref="[#$ref](https://${sourcerepo%%/*}\.com/${sourcerepo#*/}/issues/$ref)" 20 | fi 21 | 22 | string=$(echo "$string" | sed "s?\([^[[:alnum:]_]\|^\)#$ref\([^][:alnum:]_]\|$\)?\1$newref\2?g") 23 | done 24 | echo "$string" > description 25 | -------------------------------------------------------------------------------- /simple-make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Convert the Makefile into a shell script and # execute it via the shell 4 | # 5 | 6 | sed -n ' 7 | # Set optional variables 8 | / ?= / { 9 | s/[()"]//g 10 | s/\([^ ]*\) ?= \(.*\)/\1="${\1:-\2}"/ 11 | p 12 | b 13 | } 14 | 15 | # Convert Makefile rules into shell functions 16 | /:$/ { 17 | s/^\([^\t].*\):$/\1() {/p 18 | n 19 | :body 20 | s/^\t@/\t/ 21 | y/()/{}/ 22 | s/\tinstall/\tcommand install/ 23 | s/^\t/ /p 24 | # Close function and terminate block processing on empty line 25 | # (Reset "t" status) 26 | t reset 27 | :reset 28 | s/^$/}/p 29 | t 30 | # Read next line and repeat 31 | n 32 | b body 33 | } 34 | 35 | # Convert the PHONY rules list into a case statement 36 | /^\.PHONY:/ { 37 | s/^\.PHONY: default // 38 | i\ 39 | case "'$1'" in 40 | s/ /|/g 41 | s/$/)/p 42 | i\ 43 | '$1'\ 44 | ;;\ 45 | *) 46 | s/^/ echo "Usage: gfw_make {/ 47 | s/)$/}"/p 48 | i\ 49 | ;;\ 50 | esac 51 | } 52 | ' Makefile | bash 53 | -------------------------------------------------------------------------------- /sync-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck disable=SC1004,SC2039 3 | # 4 | # Shellcheck ignore list: 5 | # - SC1004: This backslash+linefeed is literal. Break outside single quotes 6 | # if you just want to break the line. 7 | # Rationale: Required to continue sed(1) lines 8 | # - SC2039: In POSIX sh, 'local' is undefined. 9 | # Rationale: Local makes for better code and works on many modern shells 10 | # 11 | # (C) Copyright 2016-2019 Diomidis Spinellis 12 | # 13 | # This file is part of git-issue, the Git-based issue management system. 14 | # 15 | # git-issue is free software: you can redistribute it and/or modify 16 | # it under the terms of the GNU General Public License as published by 17 | # the Free Software Foundation, either version 3 of the License, or 18 | # (at your option) any later version. 19 | # 20 | # git-issue is distributed in the hope that it will be useful, 21 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | # GNU General Public License for more details. 24 | # 25 | # You should have received a copy of the GNU General Public License 26 | # along with git-issue. If not, see . 27 | # 28 | 29 | # Synchronize script and its documentation with the contents of the README file 30 | 31 | SCRIPT_NAME=git-issue.sh 32 | MAN_PAGE=git-issue.1 33 | 34 | # Update usage information in the script based on README.md 35 | { 36 | sed -n '1,/^The following commands are available:/p' $SCRIPT_NAME 37 | # Keep lines from `### start ` to `git issue git` 38 | # shellcheck disable=SC2016 39 | sed -E -n '/^### Start/,/^\* `git issue git`/ { 40 | # Only keep listed commands or subheaders 41 | /^\* |^### /!d 42 | # Format headers by eliminating all preceding space 43 | s/^### (.*)/\ 44 | \1/g 45 | # Remove repetitive git issue 46 | s/git issue //g 47 | # Remove code markup 48 | s/`//g 49 | # Format commands, depending on length 50 | s/^\* ([^:]{3}): / \1 /g 51 | s/^\* ([^:]{4}): / \1 /g 52 | s/^\* ([^:]{5}): / \1 /g 53 | s/^\* ([^:]{6}): / \1 /g 54 | s/^\* ([^:]{7}): / \1 /g 55 | s/^\* ([^:]{8}): / \1 /g 56 | s/^\* ([^:]{9}): / \1 /g 57 | 58 | p 59 | }' README.md | tee foo 60 | sed -n '/^USAGE_EOF/,$p' $SCRIPT_NAME 61 | } | 62 | if [ "$1" = "--no-user-agent" ] ; then 63 | cat 64 | else 65 | # Update user agent version 66 | sed '/^USER_AGENT/s/\/tree.*/\/tree\/'"$(git rev-parse --short HEAD)/" 67 | fi >newgi.sh 68 | mv newgi.sh $SCRIPT_NAME 69 | chmod +x git-issue.sh 70 | 71 | # Update the specified man section from the specified README section 72 | # pre-processing its body with the given sed command 73 | replace_section() 74 | { 75 | local man_section="$1" 76 | local md_section="$2" 77 | local command="$3" 78 | local extra_command="$4" 79 | 80 | { 81 | # Output until the specified section 82 | sed -n "1,/^\\.SH $man_section/p" $MAN_PAGE 83 | 84 | # Output specified section from README 85 | echo '.\" Auto-generated content from README.md; do not edit this section' 86 | # shellcheck disable=SC2016 87 | sed -n "/^## $md_section/,/^## / { 88 | $command"' 89 | # Remove section titles 90 | /^## /d 91 | /^###/d 92 | # Set code text with Courier (twice per line) 93 | s/`/\\fC/;s/`/\\fP/ 94 | s/`/\\fC/;s/`/\\fP/ 95 | # Set italic text (twice per line; not when switched to Courier) 96 | /\\fC/!s/_/\\fI/;/\\fC/!s/_/\\fP/ 97 | /\\fC/!s/_/\\fI/;/\\fC/!s/_/\\fP/ 98 | # Set first-level and second-level bullets 99 | s/^\* /.IP "" 4\ 100 | / 101 | s/^ \* /.IP "" 8\ 102 | / 103 | s/^ \* /.IP "" 12\ 104 | / 105 | # format urls 106 | s/\[\([^]]*\)\](\([^)]*\))/\1 <\2>/ 107 | 108 | '"$extra_command"' 109 | p 110 | }' README.md 111 | 112 | # Output the rest of the man page 113 | sed -n "1,/^\\.SH $man_section/d;{/^\\.SH /,/xyzzy/p;}" $MAN_PAGE 114 | } >man-$$.1 115 | mv man-$$.1 $MAN_PAGE 116 | } 117 | 118 | # Update subcommands, implementation, and examples in the manual page 119 | # from the README file 120 | # shellcheck disable=SC2016 121 | replace_section 'GIT ISSUE COMMANDS' 'Use' 's/^\* `\([^`]*\)`: /.RE\ 122 | .PP\ 123 | \\fB\1\\fP\ 124 | .RS 4\ 125 | /' 's/^ *//' 126 | 127 | replace_section FILES 'Internals' '' 's/^ *//' 128 | # shellcheck disable=SC2016 129 | replace_section EXAMPLES 'Example session' '/```/d;/^###/N;s/^### \(.*\)/.fi\ 130 | .ft R\ 131 | .PP\ 132 | \1.ft C\ 133 | .nf/' 134 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck disable=SC2039,SC2164,SC2086,SC2103 3 | # 4 | # Shellcheck ignore list: 5 | # - SC2039: In POSIX sh, 'local' is undefined. 6 | # Rationale: Local makes for better code and works on many modern shells 7 | # - SC2164: Use cd ... || exit in case cd fails. 8 | # Rationale: We run this after creating the directory 9 | # - SC2164: Use a ( subshell ) to avoid having to cd back. 10 | # Rationale: We run this after creating the directory 11 | # 12 | # 13 | # (C) Copyright 2016-2020 Diomidis Spinellis 14 | # 15 | # This file is part of gi, the Git-based issue management system. 16 | # 17 | # gi is free software: you can redistribute it and/or modify 18 | # it under the terms of the GNU General Public License as published by 19 | # the Free Software Foundation, either version 3 of the License, or 20 | # (at your option) any later version. 21 | # 22 | # gi is distributed in the hope that it will be useful, 23 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 24 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 | # GNU General Public License for more details. 26 | # 27 | # You should have received a copy of the GNU General Public License 28 | # along with gi. If not, see . 29 | # 30 | 31 | # Display a test's result 32 | message() 33 | { 34 | local okfail 35 | 36 | okfail=$1 37 | shift 38 | if [ "$1" ] ; then 39 | echo "$okfail $ntest - $*" 40 | else 41 | echo "$okfail $ntest - $testname" 42 | fi | 43 | sed "s/$gi_re/gi/" 44 | } 45 | 46 | ok() 47 | { 48 | message ok "$*" 49 | } 50 | 51 | fail() 52 | { 53 | printf "%d " "$ntest" >>"$TopDir/failure" 54 | message fail "$*" 55 | } 56 | 57 | # Test specified command, which should succeed 58 | try() 59 | { 60 | local exit_code 61 | 62 | ntest=$((ntest + 1)) 63 | echo "Test $ntest: $*" >>"$TopDir/error.log" 64 | "$@" >/dev/null 2>>"$TopDir/error.log" 65 | exit_code=$? 66 | cd .issues 67 | if git status | grep 'not staged' >/dev/null ; then 68 | fail staging "$*" 69 | else 70 | ok staging "$*" 71 | fi 72 | cd .. 73 | start 74 | if [ $exit_code = 0 ] ; then 75 | ok "$*" 76 | else 77 | fail "$*" 78 | fi 79 | } 80 | 81 | # Test specified command, which should fail 82 | ntry() 83 | { 84 | ntest=$((ntest + 1)) 85 | if ! "$@" >/dev/null 2>&1 ; then 86 | ok "fail $*" 87 | else 88 | fail "fail $*" 89 | fi 90 | } 91 | 92 | # grep for the specified pattern, which should be found 93 | # Does not increment ntest, because it is executed as a separate process 94 | try_grep() 95 | { 96 | test -z "$testname" && echo "Test $ntest: grep $*" >>"$TopDir/error.log" 97 | if tee input | grep "$@" >/dev/null 2>&1 ; then 98 | ok "grep $*" 99 | else 100 | fail "grep $*" 101 | echo 'Input:' >>"$TopDir/error.log" 102 | cat input >>"$TopDir/error.log" 103 | fi 104 | } 105 | 106 | # grep for the specified pattern, which should not be found 107 | # Does not increment ntest, because it is executed as a separate process 108 | try_ngrep() 109 | { 110 | test -z "$testname" && echo "Test $ntest: ! grep $*" >>"$TopDir/error.log" 111 | if ! tee input | grep "$@" >/dev/null 2>&1 ; then 112 | ok "not grep $1" 113 | else 114 | fail "not grep $1" 115 | echo 'Input:' >>"$TopDir/error.log" 116 | cat input >>"$TopDir/error.log" 117 | fi 118 | } 119 | 120 | # Start a new test with the specified description 121 | start() 122 | { 123 | ntest=$((ntest + 1)) 124 | testname="$*" 125 | test -n "$testname" && echo "Test $ntest: $*" >>"$TopDir/error.log" 126 | } 127 | 128 | # Fold header continuation lines 129 | header_continuation() 130 | { 131 | sed -n ' 132 | # Header 133 | /^[^ ]/ { 134 | # Print previous hold space 135 | x 136 | s/\n//g 137 | /^./p 138 | x 139 | # Keep in hold space 140 | h 141 | } 142 | # Continuation 143 | /^[ ]/ { 144 | # Append to hold space 145 | H 146 | } 147 | $ { 148 | # Print previous hold space 149 | x 150 | s/\n/ /g 151 | /^./p 152 | } 153 | ' 154 | } 155 | 156 | TopDir=$(mktemp -d) 157 | { 158 | jq --version || exit 1 159 | curl --version 160 | echo "Test artifacts saved in $TopDir" 161 | } 1>&2 162 | 163 | if command -v gdate ; then 164 | DATEBIN="gdate" 165 | else 166 | DATEBIN="date" 167 | fi 168 | 169 | # Setup GitHub authentication token for Travis CI for curl version >= 7.55 170 | # The GH_TOKEN environment variable with the secret token is specified in 171 | # https://travis-ci.org/dspinellis/git-issue/settings 172 | if [ -n "$GH_TOKEN" ] && curl --version | awk '/curl/{exit $2 >= "7.55" ? 0 : 1}' ; then 173 | echo "Authorization: token $GH_TOKEN" >"$HOME/.token" 174 | export GH_CURL_AUTH="Authorization: token $GH_TOKEN" 175 | echo "Set GH_CURL_AUTH to $GH_CURL_AUTH using GH_TOKEN" 176 | fi 177 | 178 | echo 'TAP version 13' 179 | ntest=0 180 | gi="$(cd "$(dirname "$0")" && pwd)"/git-issue.sh 181 | gi_re=$(echo "$gi" | sed 's/[^0-9A-Za-z]/\\&/g') 182 | 183 | start sync-docs 184 | GenFiles='git-issue.sh git-issue.1' 185 | if ! git diff --quiet HEAD ; then 186 | fail "Uncommitted files sync-docs test skipped and pending" 187 | else 188 | sh sync-docs.sh --no-user-agent 189 | Status=$(git status --porcelain -- $GenFiles) 190 | if [ -z "$Status" ]; then 191 | ok "make sync-docs left $GenFiles as committed" 192 | else 193 | fail "make sync-docs changed $GenFiles" 194 | git diff -- $GenFiles >>"$TopDir/error.log" 195 | git checkout -- $GenFiles 196 | fi 197 | fi 198 | cd "$TopDir" 199 | 200 | mkdir testdir 201 | cd testdir 202 | git init > /dev/null 2>&1; 203 | git config --local user.name "Joe" 204 | git config --local user.email "joe@example.com" 205 | echo ".issues" > .gitignore 206 | 207 | try "$gi" init 208 | try "$gi" list 209 | 210 | start ; ( cd .issues; git config --local user.name ) | try_grep "Joe" 211 | start ; ( cd .issues; git config --local user.email ) | try_grep "joe@example.com" 212 | start ; "$gi" list "$issue" | try_ngrep . 213 | 214 | # New 215 | try "$gi" new -s 'First-issue' 216 | start ; "$gi" list | try_grep 'First-issue' 217 | 218 | # New with editor 219 | export VISUAL='mv ../issue-desc ' 220 | 221 | # Empty summary/description should fail 222 | touch issue-desc 223 | ntry "$gi" new 224 | 225 | cat <issue-desc 226 | Second issue 227 | 228 | Line in description 229 | EOF 230 | try "$gi" new 231 | 232 | issue=$("$gi" list | awk '/Second issue/{print $1}') 233 | issue2=$("$gi" list | awk '/First-issue/{print $1}') 234 | 235 | # Show 236 | start ; "$gi" show "$issue" | try_grep 'Second issue' 237 | start ; "$gi" show "$issue" | try_grep 'Line in description' 238 | start ; "$gi" show "$issue" | try_grep '^Author:' 239 | start ; "$gi" show "$issue" | header_continuation | try_grep '^Tags:[ ]*open' 240 | ntry "$gi" show xyzzy 241 | 242 | # Edit 243 | 244 | # Unmodified issue should fail 245 | ntry "$gi" edit "$issue" 246 | 247 | cat <issue-desc 248 | Second issue 249 | 250 | Modified line in description 251 | EOF 252 | try "$gi" edit "$issue" 253 | start ; "$gi" show "$issue" | try_grep 'Second issue' 254 | start ; "$gi" show "$issue" | try_grep 'Modified line in description' 255 | start ; "$gi" show "$issue" | try_ngrep 'Line in description' 256 | 257 | export VISUAL= 258 | 259 | # Comment 260 | start 261 | cat <comment 262 | Comment line 263 | another line 264 | EOF 265 | export VISUAL='mv ../comment '; try "$gi" comment "$issue" 266 | export VISUAL= 267 | start ; "$gi" show -c "$issue" | try_grep 'another line' 268 | 269 | # Comment editing 270 | start 271 | cat <comment 272 | Comment first line 273 | comment second line 274 | EOF 275 | commentsha=$("$gi" show -c "$issue" | awk '/comment/{print $2}') 276 | export VISUAL='mv ../comment '; try "$gi" edit -c "$commentsha" 277 | # Try passing issue sha as comment sha 278 | export VISUAL='mv ../comment '; ntry "$gi" edit -c "$issue" 279 | export VISUAL= 280 | start ; "$gi" show -c "$issue" | try_grep 'comment second line' 281 | start ; "$gi" show -c "$issue" | try_ngrep 'another comment line' 282 | 283 | # Assign 284 | try "$gi" assign "$issue" joe@example.com 285 | ntry "$gi" assign "$issue" joe@example.com 286 | start ; "$gi" show "$issue" | header_continuation | try_grep '^Assigned-to:[ ]joe@example.com' 287 | try "$gi" assign "$issue" jane@example.com 288 | start ; "$gi" show "$issue" | header_continuation | try_grep '^Assigned-to:.*jane@example.com' 289 | start ; "$gi" show "$issue" | header_continuation | try_grep '^Assigned-to:.*joe@example.com' 290 | try "$gi" assign -r "$issue" joe@example.com 291 | start ; "$gi" show "$issue" | header_continuation | try_ngrep '^Assigned-to:.*joe@example.com' 292 | ntry "$gi" assign -r "$issue" joe@example.com 293 | try "$gi" assign -r "$issue" jane@example.com 294 | start ; "$gi" show "$issue" | header_continuation | try_ngrep '^Assigned-to:.*jane@example.com' 295 | try "$gi" assign "$issue" joe@example.com 296 | 297 | # Milestone 298 | ntry "$gi" list ver2 299 | try "$gi" milestone "$issue" ver2 300 | start ; "$gi" list ver2 | try_grep "$issue" 301 | start ; "$gi" show "$issue" | try_grep '^Milestone:[ ]ver2' 302 | try "$gi" milestone "$issue" ver2 303 | try "$gi" milestone "$issue" ver3 304 | start ; "$gi" show "$issue" | try_grep '^Milestone:[ ]ver3' 305 | start ; "$gi" show "$issue" | try_ngrep ver2 306 | ntry "$gi" milestone -r "$issue" foo 307 | try "$gi" milestone -r "$issue" 308 | start ; "$gi" show "$issue" | try_ngrep ver3 309 | 310 | # Weight 311 | ntry "$gi" weight "$issue" l33t 312 | ntry "$gi" weight -r "$issue" 313 | try "$gi" weight "$issue" 1337 314 | start ; "$gi" show "$issue" | try_grep 1337 315 | try "$gi" weight -r "$issue" 316 | start ; "$gi" show "$issue" | try_ngrep 1337 317 | 318 | # Due Date 319 | ntry "$gi" duedate "$issue" someday 320 | ntry "$gi" duedate -r "$issue" 321 | ntry "$gi" duedate -r "$issue" someday 322 | start ; "$gi" duedate "$issue" yesterday | try_grep Warning 323 | try "$gi" duedate "$issue" tomorrow 324 | start ; "$gi" show "$issue" | try_grep "$($DATEBIN --date=tomorrow --rfc-3339=date)" 325 | try "$gi" duedate -r "$issue" 326 | start ; "$gi" show "$issue" | try_ngrep 'Due Date' 327 | 328 | # Time Spent/Time Estimate 329 | ntry "$gi" timespent "$issue" alot 330 | ntry "$gi" timespent -r "$issue" 331 | ntry "$gi" timespent -r "$issue" alot 332 | ntry "$gi" timeestimate "$issue" alot 333 | ntry "$gi" timeestimate -r "$issue" 334 | ntry "$gi" timeestimate -r "$issue" 3months 335 | try "$gi" timespent "$issue" 2hours 336 | start ; "$gi" show "$issue" | try_grep '^Time Spent: 02 hours ' 337 | try "$gi" timespent -a "$issue" 3hours 338 | start ; "$gi" show "$issue" | try_grep '^Time Spent: 05 hours ' 339 | try "$gi" timeestimate "$issue" 3days 340 | try "$gi" timespent -a "$issue" 15minutes 341 | # start ; "$gi" show "$issue" | try_grep 'Time Spent/Time Estimated: 05 hours 15 minutes \?/ \?3 days' 342 | try "$gi" timespent -r "$issue" 343 | start ; "$gi" show "$issue" | try_grep 'Time Estimate: 3 days' 344 | 345 | # Watchers 346 | try "$gi" watcher "$issue" jane@example.com 347 | start ; "$gi" show "$issue" | header_continuation | try_grep '^Watchers:[ ]jane@example.com' 348 | try "$gi" watcher "$issue" alice@example.com 349 | ntry "$gi" watcher "$issue" alice@example.com 350 | start ; "$gi" show "$issue" | header_continuation | try_grep '^Watchers:.*jane@example.com' 351 | start ; "$gi" show "$issue" | header_continuation | try_grep '^Watchers:.*alice@example.com' 352 | try "$gi" watcher -r "$issue" alice@example.com 353 | start ; "$gi" show "$issue" | header_continuation | try_ngrep '^Watchers:.*alice@example.com' 354 | try "$gi" watcher "$issue" alice@example.com 355 | 356 | # Tags (most also tested through watchers) 357 | try "$gi" tag "$issue" feature 358 | start ; "$gi" show "$issue" | header_continuation | try_grep '^Tags:.*feature' 359 | ntry "$gi" tag "$issue" feature 360 | 361 | # List by tag 362 | start ; "$gi" list feature | try_grep 'Second issue' 363 | start ; "$gi" list open | try_grep 'First-issue' 364 | start ; "$gi" list feature | try_ngrep 'First-issue' 365 | try "$gi" tag -r "$issue" feature 366 | start ; "$gi" list feature 2>/dev/null | try_ngrep 'Second issue' 367 | try "$gi" tag "$issue" feature 368 | 369 | 370 | # Long list 371 | start ; "$gi" list -l oneline feature | try_grep 'Second issue' 372 | start ; "$gi" list -l oneline feature | try_ngrep 'First-issue' 373 | start ; "$gi" list -l "Tags:%T" | try_grep 'feature' 374 | try "$gi" milestone "$issue" ver2 375 | try "$gi" weight "$issue" 99 376 | start ; "$gi" list -l full | try_grep 'ver2' 377 | start ; "$gi" list -l compact | try_grep 'Weight: 99' 378 | try "$gi" milestone -r "$issue" 379 | 380 | # Long list ordering 381 | 382 | ntry "$gi" list -l short -o "%iA" 383 | start ; "$gi" list -l oneline -o "%D" | head -n 1 | try_grep 'First-issue' 384 | start ; "$gi" list -l oneline -o "%D" -r | head -n 1 | try_grep 'Second issue' 385 | start ; "$gi" list -l short -o "%T" | head -n 4 | try_grep 'feature' 386 | 387 | # Filter-apply 388 | 389 | cat <test-filter.sh 390 | #!/bin/sh 391 | echo "Line added by test-filter" >> description 392 | EOF 393 | 394 | chmod +x test-filter.sh 395 | start ; "$gi" filter-apply test-filter.sh | try_grep "Filter applied" 396 | start ; "$gi" show "$issue" | try_grep "Line added by test-filter" 397 | # Reset changes 398 | "$gi" git checkout -- . 399 | 400 | # close 401 | try "$gi" close "$issue" 402 | start ; "$gi" list | try_ngrep 'Second issue' 403 | start ; "$gi" list closed | try_grep 'Second issue' 404 | 405 | # log 406 | try "$gi" log 407 | start ; n=$("$gi" log | tee foo | grep -c gi:) 408 | try test "$n" -ge 18 409 | 410 | # clone 411 | # Required in order to allow a push to a non-bare repo 412 | "$gi" git config --add receive.denyCurrentBranch ignore 413 | cd .. 414 | rm -rf testdir2 415 | mkdir testdir2 416 | cd testdir2 417 | git clone ../testdir/.issues/ 2>/dev/null 418 | start ; "$gi" show "$issue" | header_continuation | try_grep '^Watchers:.*alice@example.com' 419 | start ; "$gi" show "$issue" | header_continuation | try_grep '^Tags:.*feature' 420 | start ; "$gi" show "$issue" | header_continuation | try_grep '^Assigned-to:.*joe@example.com' 421 | start ; "$gi" show "$issue" | try_grep 'Second issue' 422 | start ; "$gi" show "$issue" | try_grep 'Modified line in description' 423 | start ; "$gi" show "$issue" | try_grep '^Author:' 424 | start ; "$gi" show "$issue" | header_continuation | try_grep '^Tags:.*closed' 425 | 426 | # Push and pull 427 | try "$gi" tag "$issue" cloned 428 | try "$gi" push 429 | cd ../testdir 430 | "$gi" git reset --hard >/dev/null # Required, because we pushed to a non-bare repo 431 | start ; "$gi" show "$issue" | header_continuation | try_grep '^Tags:.*cloned' 432 | 433 | # Pull 434 | try "$gi" tag "$issue" modified-upstream 435 | cd ../testdir2 436 | try "$gi" pull 437 | "$gi" show "$issue" | try_grep modified-upstream 438 | cd ../testdir 439 | 440 | if [ -z "$GH_CURL_AUTH" ] ; then 441 | echo "Skipping GitHub import/export tests due to lack of GitHub authentication token." 442 | else 443 | # Import 444 | # GitHub 445 | echo "Starting GitHub import tests..." 446 | try "$gi" import github dspinellis git-issue-test-issues 447 | start ; "$gi" list | try_grep 'An open issue on GitHub with a description and comments' 448 | # Closed issues 449 | start ; "$gi" list | try_grep -v 'A closed issue on GitHub without description' 450 | start ; "$gi" list -a | try_grep 'A closed issue on GitHub without description' 451 | # Description and comments 452 | issue=$("$gi" list | awk '/An open issue on GitHub with a description and comments/ {print $1}') 453 | start ; "$gi" show "$issue" | try_grep '^ *line 1$' 454 | start ; "$gi" show "$issue" | try_grep '^ *line 2$' 455 | start ; "$gi" show "$issue" | try_grep 'Line 3 with special characters "'\''<>|\$' 456 | start ; "$gi" show -c "$issue" | try_grep '^ *comment 1 line 1$' 457 | start ; "$gi" show -c "$issue" | try_grep '^ *comment 1 line 2$' 458 | start ; "$gi" show -c "$issue" | try_grep '^ *comment 2$' 459 | start ; "$gi" show -c "$issue" | try_grep '^ *comment 4$' 460 | start ; "$gi" show "$issue" | try_grep '^GitHub issue: #[0-9]* at dspinellis/git-issue-test-issues$' 461 | # Assignees and tags 462 | issue=$("$gi" list | awk '/An open issue on GitHub with assignees and tags/ {print $1}') 463 | start ; "$gi" show "$issue" | try_grep 'good first issue' 464 | start ; "$gi" show "$issue" | header_continuation | try_grep 'Assigned-to:.*dspinellis' 465 | start ; "$gi" show "$issue" | header_continuation | try_grep 'Assigned-to:.*louridas' 466 | # Milestone 467 | try "$gi" list ver3 468 | # Import should be idempotent 469 | before=$(cd .issues ; git rev-parse --short HEAD) 470 | try "$gi" import github dspinellis git-issue-test-issues 471 | after=$(cd .issues ; git rev-parse --short HEAD) 472 | try test x"$before" = x"$after" 473 | 474 | # Export 475 | # create new repository to test issue exporting 476 | echo "Trying to create GitHub repository..." 477 | curl -H "$GH_CURL_AUTH" -s --data '{"name": "git-issue-test-export-'"$($DATEBIN -Is | tr ':+' '--')"'", "private": true}' --output ghrepo https://api.github.com/user/repos 478 | if grep "git-issue-test-export" > /dev/null < ghrepo ; then 479 | echo "Starting export tests..." 480 | ghrepo=$(jq --raw-output '.full_name' < ghrepo | tr '/' ' ') 481 | ghrepourl=$(jq --raw-output '.url' < ghrepo) 482 | ghuser=$(jq --raw-output '.owner.login' < ghrepo) 483 | # remove assignees to prevent notifications about test issues on GitHub 484 | "$gi" assign -r "$issue" dspinellis > /dev/null 2>&1 485 | "$gi" assign -r "$issue" louridas > /dev/null 2>&1 486 | try "$gi" create -n "$issue" github $ghrepo 487 | # Get the created issue 488 | try "$gi" create -u "$(jq -r '.number' .issues/create-body)" "$issue" github $ghrepo 489 | rm -f .issues/create-body .issues/create-header 490 | # modify and export 491 | try "$gi" create -n "$issue2" github $ghrepo 492 | try "$gi" new -c "github $ghrepo" -s "Issue exported directly" 493 | "$gi" assign "$issue2" "$ghuser" > /dev/null 2>&1 494 | try "$gi" export github $ghrepo 495 | start ; "$gi" export github $ghrepo | try_grep "Issue $issue.* not modified, skipping..." 496 | # Test invalid assignee 497 | "$gi" assign "$issue2" octocat > /dev/null 2>&1 498 | start ; "$gi" export github $ghrepo | try_grep "Couldn't add assignee octocat. Skipping..." 499 | "$gi" assign -r "$issue2" octocat > /dev/null 2>&1 500 | 501 | # test milestone creation 502 | "$gi" new -s "milestone issue" > /dev/null 2>&1 503 | issue3=$("$gi" list | awk '/milestone issue/{print $1}') 504 | "$gi" milestone "$issue3" ver4 > /dev/null 2>&1 505 | "$gi" duedate "$issue3" week > /dev/null 2>&1 506 | "$gi" timeestimate "$issue3" 3hours > /dev/null 2>&1 507 | try "$gi" create -e "$issue3" github $ghrepo 508 | try "$gi" exportall -a github $ghrepo 509 | 510 | # Basic round-trip tests 511 | echo "Starting GitHub round-trip test..." 512 | cd .. 513 | mkdir testdir3 514 | cd testdir3 515 | "$gi" init 516 | try "$gi" import github $ghrepo 517 | rissue=$("$gi" list | awk '/An open issue on GitHub with a description and comments/ {print $1}') 518 | rissue2=$("$gi" list | awk '/milestone issue/{print $1}') 519 | start ; "$gi" show "$rissue" | try_grep '^ *line 1' 520 | start ; "$gi" show "$rissue" | try_grep '^ *line 2' 521 | start ; "$gi" show "$rissue" | try_grep 'Line 3 with special characters "'\''<>|\$' 522 | start ; "$gi" show -c "$rissue" | try_grep '^ *comment 1 line 1' 523 | start ; "$gi" show -c "$rissue" | try_grep '^ *comment 1 line 2' 524 | start ; "$gi" show -c "$rissue" | try_grep '^ *comment 2' 525 | start ; "$gi" show -c "$rissue" | try_grep '^ *comment 4' 526 | 527 | start ; "$gi" show "$rissue2" | try_grep '^Milestone: ver4' 528 | rissue3=$("$gi" list | awk '/An open issue on GitHub with assignees and tags/ {print $1}') 529 | start ; "$gi" show "$rissue3" | try_grep 'good first issue' 530 | 531 | cd ../testdir 532 | 533 | # delete repo 534 | curl -H "$GH_CURL_AUTH" -s --request DELETE $ghrepourl | 535 | grep "{" && printf "Couldn't delete repository. 536 | You probably don't have delete permittions activated on the OAUTH token.\\nPlease delete %s manually." "$ghrepo" 537 | 538 | else 539 | echo "Couldn't create test repository. Skipping export tests." 540 | fi 541 | fi 542 | 543 | # shellcheck disable=2153 544 | if [ -z "$GL_CURL_AUTH" ] ; then 545 | echo "Skipping GitLab import/export tests due to lack of GitLab authentication token." 546 | else 547 | # Import 548 | echo "Starting GitLab import tests..." 549 | try "$gi" import gitlab vyrondrosos git-issue-test-issues 550 | start ; "$gi" list | try_grep 'An open issue on GitLab with a description and comments' 551 | # Closed issues 552 | start ; "$gi" list | try_grep -v 'A closed issue on GitLab without description' 553 | start ; "$gi" list -a | try_grep 'A closed issue on GitLab without description' 554 | # Description and comments 555 | glissue=$("$gi" list | awk '/An open issue on GitLab with a description and comments/ {print $1}') 556 | start ; "$gi" show "$glissue" | try_grep '^ *line 1$' 557 | start ; "$gi" show "$glissue" | try_grep '^ *line 2$' 558 | start ; "$gi" show "$glissue" | try_grep 'Line 3 with special characters "'\''<>|\$' 559 | start ; "$gi" show -c "$glissue" | try_grep '^ *comment 2$' 560 | start ; "$gi" show -c "$glissue" | try_grep '^ *comment 3$' 561 | start ; "$gi" show -c "$glissue" | try_grep '^ *comment 4$' 562 | start ; "$gi" show "$glissue" | try_grep '^GitLab issue: #[0-9]* at vyrondrosos/git-issue-test-issues$' 563 | # Assignees and tags 564 | glissue=$("$gi" list | awk '/An open issue on GitLab with assignees and tags/ {print $1}') 565 | start ; "$gi" show "$glissue" | try_grep 'good first issue' 566 | start ; "$gi" show "$glissue" | header_continuation | try_grep 'Assigned-to:.*'"$gluser" 567 | # Milestone 568 | try "$gi" list ver3 569 | # Import should be idempotent 570 | before=$(cd .issues ; git rev-parse --short HEAD) 571 | try "$gi" import gitlab vyrondrosos git-issue-test-issues 572 | after=$(cd .issues ; git rev-parse --short HEAD) 573 | try test x"$before" = x"$after" 574 | 575 | # Import repo belonging to group 576 | try "$gi" import gitlab git-issue-test-group git-issue-subgroup-test/git-issue-group-test 577 | start ; "$gi" list | try_grep 'Issue in group repo' 578 | glissue2=$("$gi" list | awk '/Issue in group repo/ {print $1}') 579 | start ; "$gi" show "$glissue2" | try_grep '^GitLab issue: #1 at git-issue-test-group/git-issue-subgroup-test/git-issue-group-test$' 580 | 581 | # Export 582 | # Create new repository to test issue exporting 583 | echo "Trying to create GitLab repository..." 584 | curl -H "$GL_CURL_AUTH" -s --header "Content-Type: application/json" --data '{"name": "git-issue-test-export-'"$($DATEBIN -Is | tr ':+' '--')"'", "visibility": "private"}' --output glrepo https://gitlab.com/api/v4/projects 585 | if grep "git-issue-test-export" > /dev/null < glrepo ; then 586 | echo "Starting export tests..." 587 | glrepo=$(jq --raw-output '.path_with_namespace' < glrepo | tr '/' ' ') 588 | glrepourl=$(jq --raw-output '._links.self' < glrepo) 589 | gluser=$(jq --raw-output '.owner.username' < glrepo) 590 | try "$gi" create -n "$issue" gitlab $glrepo 591 | # Get the created issue 592 | try "$gi" create -u "$(jq -r '.iid' .issues/create-body)" "$issue" gitlab $glrepo 593 | rm -f .issues/create-body .issues/create-header 594 | # modify and export 595 | try "$gi" create -n "$issue2" gitlab $glrepo 596 | try "$gi" new -c "gitlab $glrepo" -s "Issue exported directly" 597 | "$gi" assign "$issue2" "$gluser" > /dev/null 2>&1 598 | try "$gi" export gitlab $glrepo 599 | 600 | "$gi" assign "$issue2" octocat > /dev/null 2>&1 601 | try "$gi" export gitlab $glrepo 602 | "$gi" assign -r "$issue2" octocat > /dev/null 2>&1 603 | 604 | # test milestone creation 605 | "$gi" new -s "gitlab milestone issue : %M" > /dev/null 2>&1 606 | glissue3=$("$gi" list | awk '/gitlab milestone issue : %M/{print $1}') 607 | "$gi" milestone "$glissue3" ver4 > /dev/null 2>&1 608 | "$gi" duedate "$glissue3" week > /dev/null 2>&1 609 | "$gi" timeestimate "$glissue3" 3hours > /dev/null 2>&1 610 | try "$gi" create -e "$glissue3" gitlab $glrepo 611 | start ; "$gi" show "$glissue3" | try_grep 'ver4' 612 | # Try to create duplicate 613 | ntry "$gi" create -e "$glissue3" gitlab $glrepo 614 | try "$gi" exportall -a gitlab $glrepo 615 | 616 | # Basic GitLab round-trip tests 617 | echo "Starting GitHub round-trip test..." 618 | cd .. 619 | mkdir testdir4 620 | cd testdir4 621 | "$gi" init 622 | try "$gi" import gitlab $glrepo 623 | start ; "$gi" list | try_grep 'An open issue on GitLab with a description and comments' 624 | # Closed issues 625 | start ; "$gi" list | try_grep -v 'A closed issue on GitLab without description' 626 | start ; "$gi" list -a | try_grep 'A closed issue on GitLab without description' 627 | # Description and comments 628 | rglissue=$("$gi" list | awk '/An open issue on GitLab with a description and comments/ {print $1}') 629 | rglissue2=$("$gi" list | awk '/gitlab milestone issue/ {print $1}') 630 | start ; "$gi" show "$rglissue" | try_grep '^ *line 1' 631 | start ; "$gi" show "$rglissue" | try_grep '^ *line 2' 632 | start ; "$gi" show "$rglissue" | try_grep 'Line 3 with special characters "'\''<>|\$' 633 | start ; "$gi" show -c "$rglissue" | try_grep '^ *comment 2' 634 | start ; "$gi" show -c "$rglissue" | try_grep '^ *comment 3' 635 | start ; "$gi" show -c "$rglissue" | try_grep '^ *comment 4' 636 | start ; "$gi" show "$rglissue" | try_grep '^GitLab issue: #[0-9]* at vyrondrosos/git-issue' 637 | 638 | start ; "$gi" show "$rglissue2" | try_grep "^Due Date: $($DATEBIN --date=week --rfc-3339=date)" 639 | start ; "$gi" show "$rglissue2" | try_grep '^Time Estimate: 03 hours' 640 | start ; "$gi" show "$rglissue2" | try_grep '^Milestone: ver4' 641 | # Assignees and tags 642 | rglissue3=$("$gi" list | awk '/An open issue on GitLab with assignees and tags/ {print $1}') 643 | start ; "$gi" show "$rglissue3" | try_grep 'good first issue' 644 | start ; "$gi" show "$rglissue3" | header_continuation | try_grep 'Assigned-to:.*'"$gluser" 645 | 646 | cd ../testdir 647 | # delete repo 648 | curl -H "$GL_CURL_AUTH" -s --request DELETE $glrepourl | 649 | grep "Accepted" > /dev/null || printf "Couldn't delete repository. 650 | You probably don't have delete permittions activated on the OAUTH token.\\nPlease delete %s manually." "$glrepo" 651 | else 652 | echo "Couldn't create test repository. Skipping export tests." 653 | fi 654 | 655 | fi 656 | 657 | if ! [ -r "$TopDir/failure" ]; then 658 | echo "All tests passed!" 659 | exit 0 660 | else 661 | echo "Some test(s) failed: $(cat "$TopDir/failure")" 662 | if [ -n "$TRAVIS_OS_NAME" ] ; then 663 | echo 'Error output follows' 1>&2 664 | cat "$TopDir/error.log" 1>&2 665 | else 666 | echo "Error output is in $TopDir/error.log" 667 | fi 668 | exit 1 669 | fi 670 | --------------------------------------------------------------------------------