diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,17 @@
+# Pre-release versions (February 2022)
+
+## 0.0.3.4
+
+Builds with `Cabal-3.4` and `GHC 9.0`.
+
+## 0.0.3.2
+
+Builds with `Cabal-3.2` and `GHC 8.10`.
+
+## 0.0.3.0
+
+Builds with `Cabal-3.0` and `GHC 8.8`.
+
+## 0.0.2.4
+
+Builds with `Cabal-2.4` and `GHC 8.6`.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+              GNU GENERAL PUBLIC LICENSE
+                Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                     Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+              END OF TERMS AND CONDITIONS
+
+     How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,117 @@
+# `hackage-cli`
+
+A command-line tool to manage package metadata
+[revisions](https://github.com/haskell-infra/hackage-trustees/blob/master/revisions-information.md)
+on Hackage.
+
+Caveat: this is a developer tool and work-in-progress.
+Check known bugs at the [issue tracker](https://github.com/hackage-trustees/hackage-cli/issues).
+
+## Command-line reference
+
+```
+hackage-cli - CLI tool for Hackage
+
+Usage: hackage-cli [--version] [--verbose] [--hostname HOSTNAME] COMMAND
+
+Available options:
+  -h,--help                Show this help text
+  --version                output version information and exit
+  --verbose                enable verbose output
+  --hostname HOSTNAME      Hackage hostname (default: "hackage.haskell.org")
+
+Available commands:
+  pull-cabal               download .cabal files for a package
+  push-cabal               upload revised .cabal files
+  sync-cabal               upadate/sync local .cabal file with latest revision
+                           on Hackage
+  push-candidate           upload package candidate(s)
+  list-versions            list versions for a package
+  check-revision           validate revision
+  index-sha256sum          generate sha256sum-format file
+  add-bound                add bound to the library section of a package. .cabal
+                           file is edited in place
+
+Each command has a sub-`--help` text. Hackage credentials are expected to be
+stored in an `${HOME}/.netrc`-entry for the respective Hackage hostname. E.g.
+"machine hackage.haskell.org login MyUserName password TrustNo1". All
+interactions with Hackage occur TLS-encrypted via the HTTPS protocol.
+```
+
+(Section created 2015-04-26, last updated 2018-03-21.)
+
+## How to: bulk revision
+
+Suppose that all versions starting with `1.2.3` of the package `pkg-x`
+on hackage need the additional bound `< 4.5.6` on their dependency
+`pkg-y`, typically to prevent compilation attempts with `pkg-y-4.5.6`
+and up that will fail.
+
+With `hackage-cli` this can be done in a typical workflow that consists of
+
+  1. downloading the `.cabal` files,
+  2. adding bounds (with `hackage-cli`), or making other revisions (manually),
+  3. re-uploading the modified `.cabal` files.
+
+We walk through a typical workflow:
+
+1. Download the relevant `pkg-x.cabal` files to a new temporary directory:
+   ```
+   $ cd $(mktemp)
+   $ hackage-cli pull-cabal pkg-x '>= 1.2.3'
+   ```
+
+2. Adding bound `pkg-y < 4.5.6` to all of them:
+   ```
+   $ hackage-cli add-bound pkg-y '< 4.5.6' *.cabal
+   ```
+   This will write a new line
+   ```
+     build-depends: pkg-y < 4.5.6
+   ```
+   to the `library` section of each of the `.cabal` files.
+   There must be exactly one `library` section, otherwise `hackage-cli` will crash
+   or produce a garbage result.
+
+   If this bound does not further constrain the existing version range
+   for `pkg-y`, it will not be added unless `--force` is used.
+   (E.g., one of the cabal files could already have a bound `pkg-y <
+   3.0.0` or so---this file will remain unchanged.)
+
+   You can of course add more bounds using `add-bound` or make further
+   manual changes to `.cabal` files.
+
+3. Review the changes via a trial upload:
+   ```
+   $ hackage-cli push-cabal --incr-rev *.cabal
+   ```
+   (The flag `--incr-rev` will increment the `x-revision` field by `1` during the upload.
+   Existing revisions cannot be overwritten.)
+
+   During (trial) upload you will see for each `.cabal` file a summary of what will be changed.
+   Hackage might reject your revision, if it does not match the criteria
+   of what it thinks is
+   [legal](https://github.com/haskell-infra/hackage-trustees/blob/master/revisions-information.md).
+   Note that the legality check of the Hackage server is neither
+   complete nor sound, so some legal revisions might be rejected, and some illegal ones accepted.
+   You bear the responsibility for correct revisions yourself.
+
+4. Upload!
+   ```
+   $ hackage-cli push-cabal --incr-rev --publish *.cabal
+   ```
+   Adding the flag `--publish` will actually commit the revisions to Hackage.
+
+(Section created 2022-02-21.)
+
+## License
+
+- Licensed under GPL-3.
+
+- (C) 2015 Herbert Valerio Riedel.
+- (C) 2016-2019 Herbert Valerio Riedel and Oleg Grenrus.
+- (C) 2021-2022 Andreas Abel.
+
+- Further contributors: Simon Jakobi, Kevin Buhr.
+
+(Section created 2022-02-21.)
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/fixtures/SVGFonts.0.1.diff b/fixtures/SVGFonts.0.1.diff
new file mode 100644
--- /dev/null
+++ b/fixtures/SVGFonts.0.1.diff
@@ -0,0 +1,2 @@
+ERROR
+Cannot add or remove 'if' conditionals
diff --git a/fixtures/SVGFonts.0.2.diff b/fixtures/SVGFonts.0.2.diff
new file mode 100644
--- /dev/null
+++ b/fixtures/SVGFonts.0.2.diff
@@ -0,0 +1,2 @@
+ERROR
+Cannot add or remove 'if' conditionals
diff --git a/fixtures/SVGFonts.0.cabal b/fixtures/SVGFonts.0.cabal
new file mode 100644
--- /dev/null
+++ b/fixtures/SVGFonts.0.cabal
@@ -0,0 +1,88 @@
+Name:             SVGFonts
+Version:          1.6.0.3
+Synopsis:         Fonts from the SVG-Font format
+Description:      Native font support for the diagrams framework (<http://projects.haskell.org/diagrams/>). Note that this package can be used with any diagrams backend, not just the SVG backend.  The SVG-font format is easy to parse
+                  and was therefore chosen for a font library completely written in Haskell.
+                  .
+                  You can convert your own font to SVG with <http://fontforge.github.io/>,
+                  or use the included LinLibertine and Bitstream fonts.
+                  .
+                  Features:
+                  .
+                  * Complete implementation of the features that Fontforge produces (though not the complete SVG format)
+                  .
+                  * Kerning (/i.e./ the two characters in \"VA\" are closer than the characters in \"VV\")
+                  .
+                  * Unicode
+                  .
+                  * Ligatures
+                  .
+                  * An example that shows how to do text boxes with syntax highlighting using highlighting-kate:
+                    <http://hackage.haskell.org/package/highlighting-kate>
+                  .
+                  XML speed issues can be solved by trimming the svg file to only those characters that are used (or maybe binary xml one day).
+                  .
+                  Version 1.0 of this library supports texturing, though this only makes sense in a diagrams backend that does rasterization in Haskell, such as diagrams-rasterific.
+                  .
+                  Example:
+                  .
+                  >  # LANGUAGE NoMonomorphismRestriction #
+                  >
+                  > import Diagrams.Prelude
+                  > import Diagrams.Backend.Rasterific.CmdLine
+                  > import Graphics.SVGFonts
+                  >
+                  > main = defaultMain (text' "Hello World")
+                  >
+                  > text'   t = stroke (textSVG t 1) # fc purple # fillRule EvenOdd
+                  > text''  t = stroke (textSVG' (TextOpts lin INSIDE_H KERN False 1 1) t) # fillRule EvenOdd
+                  > text''' t =        (textSVG_ (TextOpts lin INSIDE_H KERN True  1 1) t) # fillRule EvenOdd
+                  .
+category:         Graphics
+License:          BSD3
+License-file:     LICENSE
+Author:           Tillmann Vogt
+Maintainer:       diagrams-discuss@googlegroups.com
+build-type:       Simple
+Cabal-Version:    >=1.18
+
+data-files:       fonts/*.svg
+extra-source-files: CHANGES.md, README.md, diagrams/*.svg
+extra-doc-files:    diagrams/*.svg
+Tested-with:      GHC == 7.6.3, GHC == 7.8.4, GHC == 7.10.2, GHC == 8.0.1, GHC == 8.2.1
+source-repository head
+  type: git
+  location: https://github.com/diagrams/SVGFonts.git
+
+Library
+    hs-source-dirs: src
+    ghc-options: -W -Wall -fno-warn-unused-do-bind
+    other-modules: Paths_SVGFonts
+    build-depends:
+        attoparsec,
+        base == 4.*,
+        bytestring >= 0.10 && < 1.0,
+        cereal,
+        cereal-vector,
+        containers >= 0.4 && < 0.6,
+        data-default-class < 0.2,
+        diagrams-core >= 1.3 && < 1.5,
+        diagrams-lib >= 1.3 && < 1.5,
+        directory >= 1.1,
+        blaze-svg >= 0.3.3,
+        blaze-markup >= 0.5,
+        parsec,
+        split,
+        text,
+        tuple,
+        vector,
+        xml
+    exposed-modules:
+        Graphics.SVGFonts
+        Graphics.SVGFonts.Text
+        Graphics.SVGFonts.ReadFont
+        Graphics.SVGFonts.Fonts
+        Graphics.SVGFonts.CharReference
+        Graphics.SVGFonts.ReadPath
+        Graphics.SVGFonts.WriteFont
+    default-language: Haskell2010
diff --git a/fixtures/SVGFonts.1.0.diff b/fixtures/SVGFonts.1.0.diff
new file mode 100644
--- /dev/null
+++ b/fixtures/SVGFonts.1.0.diff
@@ -0,0 +1,2 @@
+ERROR
+Cannot add or remove 'if' conditionals
diff --git a/fixtures/SVGFonts.1.2.diff b/fixtures/SVGFonts.1.2.diff
new file mode 100644
--- /dev/null
+++ b/fixtures/SVGFonts.1.2.diff
@@ -0,0 +1,5 @@
+OK
+Normal
+the library component's library dependency on 'diagrams-lib'
+- <1.4
++ >=0
diff --git a/fixtures/SVGFonts.1.cabal b/fixtures/SVGFonts.1.cabal
new file mode 100644
--- /dev/null
+++ b/fixtures/SVGFonts.1.cabal
@@ -0,0 +1,92 @@
+Name:             SVGFonts
+Version:          1.6.0.3
+x-revision: 1
+Synopsis:         Fonts from the SVG-Font format
+Description:      Native font support for the diagrams framework (<http://projects.haskell.org/diagrams/>). Note that this package can be used with any diagrams backend, not just the SVG backend.  The SVG-font format is easy to parse
+                  and was therefore chosen for a font library completely written in Haskell.
+                  .
+                  You can convert your own font to SVG with <http://fontforge.github.io/>,
+                  or use the included LinLibertine and Bitstream fonts.
+                  .
+                  Features:
+                  .
+                  * Complete implementation of the features that Fontforge produces (though not the complete SVG format)
+                  .
+                  * Kerning (/i.e./ the two characters in \"VA\" are closer than the characters in \"VV\")
+                  .
+                  * Unicode
+                  .
+                  * Ligatures
+                  .
+                  * An example that shows how to do text boxes with syntax highlighting using highlighting-kate:
+                    <http://hackage.haskell.org/package/highlighting-kate>
+                  .
+                  XML speed issues can be solved by trimming the svg file to only those characters that are used (or maybe binary xml one day).
+                  .
+                  Version 1.0 of this library supports texturing, though this only makes sense in a diagrams backend that does rasterization in Haskell, such as diagrams-rasterific.
+                  .
+                  Example:
+                  .
+                  >  # LANGUAGE NoMonomorphismRestriction #
+                  >
+                  > import Diagrams.Prelude
+                  > import Diagrams.Backend.Rasterific.CmdLine
+                  > import Graphics.SVGFonts
+                  >
+                  > main = defaultMain (text' "Hello World")
+                  >
+                  > text'   t = stroke (textSVG t 1) # fc purple # fillRule EvenOdd
+                  > text''  t = stroke (textSVG' (TextOpts lin INSIDE_H KERN False 1 1) t) # fillRule EvenOdd
+                  > text''' t =        (textSVG_ (TextOpts lin INSIDE_H KERN True  1 1) t) # fillRule EvenOdd
+                  .
+category:         Graphics
+License:          BSD3
+License-file:     LICENSE
+Author:           Tillmann Vogt
+Maintainer:       diagrams-discuss@googlegroups.com
+build-type:       Simple
+Cabal-Version:    >=1.18
+
+data-files:       fonts/*.svg
+extra-source-files: CHANGES.md, README.md, diagrams/*.svg
+extra-doc-files:    diagrams/*.svg
+Tested-with:      GHC == 7.6.3, GHC == 7.8.4, GHC == 7.10.2, GHC == 8.0.1, GHC == 8.2.1
+source-repository head
+  type: git
+  location: https://github.com/diagrams/SVGFonts.git
+
+Library
+    hs-source-dirs: src
+    ghc-options: -W -Wall -fno-warn-unused-do-bind
+    other-modules: Paths_SVGFonts
+    build-depends:
+        attoparsec,
+        base == 4.*,
+        bytestring >= 0.10 && < 1.0,
+        cereal,
+        cereal-vector,
+        containers >= 0.4 && < 0.6,
+        data-default-class < 0.2,
+        diagrams-core >= 1.3 && < 1.5,
+        diagrams-lib >= 1.3 && < 1.5,
+        directory >= 1.1,
+        blaze-svg >= 0.3.3,
+        blaze-markup >= 0.5,
+        parsec,
+        split,
+        text,
+        tuple,
+        vector,
+        xml
+    exposed-modules:
+        Graphics.SVGFonts
+        Graphics.SVGFonts.Text
+        Graphics.SVGFonts.ReadFont
+        Graphics.SVGFonts.Fonts
+        Graphics.SVGFonts.CharReference
+        Graphics.SVGFonts.ReadPath
+        Graphics.SVGFonts.WriteFont
+    default-language: Haskell2010
+
+    if impl(ghc == 7.8.*)
+      build-depends: diagrams-lib < 1.4
diff --git a/fixtures/SVGFonts.2.0.diff b/fixtures/SVGFonts.2.0.diff
new file mode 100644
--- /dev/null
+++ b/fixtures/SVGFonts.2.0.diff
@@ -0,0 +1,2 @@
+ERROR
+Cannot add or remove 'if' conditionals
diff --git a/fixtures/SVGFonts.2.1.diff b/fixtures/SVGFonts.2.1.diff
new file mode 100644
--- /dev/null
+++ b/fixtures/SVGFonts.2.1.diff
@@ -0,0 +1,5 @@
+OK
+Normal
+the library component's library dependency on 'diagrams-lib'
+- >=0
++ <1.4
diff --git a/fixtures/SVGFonts.2.cabal b/fixtures/SVGFonts.2.cabal
new file mode 100644
--- /dev/null
+++ b/fixtures/SVGFonts.2.cabal
@@ -0,0 +1,92 @@
+Name:             SVGFonts
+Version:          1.6.0.3
+x-revision: 2
+Synopsis:         Fonts from the SVG-Font format
+Description:      Native font support for the diagrams framework (<http://projects.haskell.org/diagrams/>). Note that this package can be used with any diagrams backend, not just the SVG backend.  The SVG-font format is easy to parse
+                  and was therefore chosen for a font library completely written in Haskell.
+                  .
+                  You can convert your own font to SVG with <http://fontforge.github.io/>,
+                  or use the included LinLibertine and Bitstream fonts.
+                  .
+                  Features:
+                  .
+                  * Complete implementation of the features that Fontforge produces (though not the complete SVG format)
+                  .
+                  * Kerning (/i.e./ the two characters in \"VA\" are closer than the characters in \"VV\")
+                  .
+                  * Unicode
+                  .
+                  * Ligatures
+                  .
+                  * An example that shows how to do text boxes with syntax highlighting using highlighting-kate:
+                    <http://hackage.haskell.org/package/highlighting-kate>
+                  .
+                  XML speed issues can be solved by trimming the svg file to only those characters that are used (or maybe binary xml one day).
+                  .
+                  Version 1.0 of this library supports texturing, though this only makes sense in a diagrams backend that does rasterization in Haskell, such as diagrams-rasterific.
+                  .
+                  Example:
+                  .
+                  >  # LANGUAGE NoMonomorphismRestriction #
+                  >
+                  > import Diagrams.Prelude
+                  > import Diagrams.Backend.Rasterific.CmdLine
+                  > import Graphics.SVGFonts
+                  >
+                  > main = defaultMain (text' "Hello World")
+                  >
+                  > text'   t = stroke (textSVG t 1) # fc purple # fillRule EvenOdd
+                  > text''  t = stroke (textSVG' (TextOpts lin INSIDE_H KERN False 1 1) t) # fillRule EvenOdd
+                  > text''' t =        (textSVG_ (TextOpts lin INSIDE_H KERN True  1 1) t) # fillRule EvenOdd
+                  .
+category:         Graphics
+License:          BSD3
+License-file:     LICENSE
+Author:           Tillmann Vogt
+Maintainer:       diagrams-discuss@googlegroups.com
+build-type:       Simple
+Cabal-Version:    >=1.18
+
+data-files:       fonts/*.svg
+extra-source-files: CHANGES.md, README.md, diagrams/*.svg
+extra-doc-files:    diagrams/*.svg
+Tested-with:      GHC == 7.6.3, GHC == 7.8.4, GHC == 7.10.2, GHC == 8.0.1, GHC == 8.2.1
+source-repository head
+  type: git
+  location: https://github.com/diagrams/SVGFonts.git
+
+Library
+    hs-source-dirs: src
+    ghc-options: -W -Wall -fno-warn-unused-do-bind
+    other-modules: Paths_SVGFonts
+    build-depends:
+        attoparsec,
+        base == 4.*,
+        bytestring >= 0.10 && < 1.0,
+        cereal,
+        cereal-vector,
+        containers >= 0.4 && < 0.6,
+        data-default-class < 0.2,
+        diagrams-core >= 1.3 && < 1.5,
+        diagrams-lib >= 1.3 && < 1.5,
+        directory >= 1.1,
+        blaze-svg >= 0.3.3,
+        blaze-markup >= 0.5,
+        parsec,
+        split,
+        text,
+        tuple,
+        vector,
+        xml
+    exposed-modules:
+        Graphics.SVGFonts
+        Graphics.SVGFonts.Text
+        Graphics.SVGFonts.ReadFont
+        Graphics.SVGFonts.Fonts
+        Graphics.SVGFonts.CharReference
+        Graphics.SVGFonts.ReadPath
+        Graphics.SVGFonts.WriteFont
+    default-language: Haskell2010
+
+    if impl(ghc == 7.8.*)
+      build-depends: diagrams-lib -any
diff --git a/fixtures/deepseq.0.1.diff b/fixtures/deepseq.0.1.diff
new file mode 100644
--- /dev/null
+++ b/fixtures/deepseq.0.1.diff
@@ -0,0 +1,2 @@
+ERROR
+Cannot add new library dependency on 'text' in library component
diff --git a/fixtures/deepseq.0.cabal b/fixtures/deepseq.0.cabal
new file mode 100644
--- /dev/null
+++ b/fixtures/deepseq.0.cabal
@@ -0,0 +1,98 @@
+cabal-version:  1.12
+name:           deepseq
+version:        1.4.4.0
+-- NOTE: Don't forget to update ./changelog.md
+
+license:        BSD3
+license-file:   LICENSE
+maintainer:     libraries@haskell.org
+bug-reports:    https://github.com/haskell/deepseq/issues
+synopsis:       Deep evaluation of data structures
+category:       Control
+description:
+    This package provides methods for fully evaluating data structures
+    (\"deep evaluation\"). Deep evaluation is often used for adding
+    strictness to a program, e.g. in order to force pending exceptions,
+    remove space leaks, or force lazy I/O to happen. It is also useful
+    in parallel programs, to ensure pending work does not migrate to the
+    wrong thread.
+    .
+    The primary use of this package is via the 'deepseq' function, a
+    \"deep\" version of 'seq'. It is implemented on top of an 'NFData'
+    typeclass (\"Normal Form Data\", data structures with no unevaluated
+    components) which defines strategies for fully evaluating different
+    data types. See module documentation in "Control.DeepSeq" for more
+    details.
+
+build-type:     Simple
+tested-with:    GHC==8.6.1,
+                GHC==8.4.3, GHC==8.4.2, GHC==8.4.1,
+                GHC==8.2.2, GHC==8.2.1,
+                GHC==8.0.2, GHC==8.0.1,
+                GHC==7.10.3, GHC==7.10.2, GHC==7.10.1,
+                GHC==7.8.4, GHC==7.8.3, GHC==7.8.2, GHC==7.8.1,
+                GHC==7.6.3, GHC==7.6.2, GHC==7.6.1,
+                GHC==7.4.2, GHC==7.4.1
+
+extra-source-files: changelog.md
+
+source-repository head
+  type:     git
+  location: https://github.com/haskell/deepseq.git
+
+library
+  default-language: Haskell2010
+  other-extensions:
+    BangPatterns
+    CPP
+    DefaultSignatures
+    FlexibleContexts
+    FlexibleInstances
+    GADTs
+    MultiParamTypeClasses
+    Safe
+    TypeOperators
+
+  -- GHC.Generics lived in `ghc-prim` for GHC 7.2 & GHC 7.4
+  if impl(ghc == 7.4.*)
+    build-depends: ghc-prim == 0.2.*
+
+  if impl(ghc>=7.6)
+    other-extensions: PolyKinds
+
+  if impl(ghc>=7.8)
+    other-extensions: EmptyCase
+
+  build-depends: base       >= 4.5 && < 4.13,
+                 array      >= 0.4 && < 0.6
+  ghc-options: -Wall
+
+  exposed-modules: Control.DeepSeq
+  other-modules:   Control.DeepSeq.BackDoor
+
+test-suite deepseq-generics-tests
+    default-language:    Haskell2010
+    type:                exitcode-stdio-1.0
+    hs-source-dirs:      . tests
+    main-is:             Main.hs
+    other-extensions:
+        CPP
+        BangPatterns
+        DefaultSignatures
+        DeriveDataTypeable
+        DeriveGeneric
+        FlexibleContexts
+        Safe
+        TupleSections
+        TypeOperators
+
+    ghc-options:         -Wall
+
+    build-depends:
+        array,
+        base,
+        ghc-prim,
+        -- end of packages with inherited version constraints
+        test-framework == 0.8.*,
+        test-framework-hunit == 0.3.*,
+        HUnit >= 1.2 && < 1.7
diff --git a/fixtures/deepseq.1.0.diff b/fixtures/deepseq.1.0.diff
new file mode 100644
--- /dev/null
+++ b/fixtures/deepseq.1.0.diff
@@ -0,0 +1,2 @@
+ERROR
+Cannot remove existing library dependency on 'text' in library  component
diff --git a/fixtures/deepseq.1.cabal b/fixtures/deepseq.1.cabal
new file mode 100644
--- /dev/null
+++ b/fixtures/deepseq.1.cabal
@@ -0,0 +1,100 @@
+cabal-version:  1.12
+name:           deepseq
+version:        1.4.4.0
+x-revision:     1
+-- NOTE: Don't forget to update ./changelog.md
+
+license:        BSD3
+license-file:   LICENSE
+maintainer:     libraries@haskell.org
+bug-reports:    https://github.com/haskell/deepseq/issues
+synopsis:       Deep evaluation of data structures
+category:       Control
+description:
+    This package provides methods for fully evaluating data structures
+    (\"deep evaluation\"). Deep evaluation is often used for adding
+    strictness to a program, e.g. in order to force pending exceptions,
+    remove space leaks, or force lazy I/O to happen. It is also useful
+    in parallel programs, to ensure pending work does not migrate to the
+    wrong thread.
+    .
+    The primary use of this package is via the 'deepseq' function, a
+    \"deep\" version of 'seq'. It is implemented on top of an 'NFData'
+    typeclass (\"Normal Form Data\", data structures with no unevaluated
+    components) which defines strategies for fully evaluating different
+    data types. See module documentation in "Control.DeepSeq" for more
+    details.
+
+build-type:     Simple
+tested-with:    GHC==8.6.1,
+                GHC==8.4.3, GHC==8.4.2, GHC==8.4.1,
+                GHC==8.2.2, GHC==8.2.1,
+                GHC==8.0.2, GHC==8.0.1,
+                GHC==7.10.3, GHC==7.10.2, GHC==7.10.1,
+                GHC==7.8.4, GHC==7.8.3, GHC==7.8.2, GHC==7.8.1,
+                GHC==7.6.3, GHC==7.6.2, GHC==7.6.1,
+                GHC==7.4.2, GHC==7.4.1
+
+extra-source-files: changelog.md
+
+source-repository head
+  type:     git
+  location: https://github.com/haskell/deepseq.git
+
+library
+  default-language: Haskell2010
+  other-extensions:
+    BangPatterns
+    CPP
+    DefaultSignatures
+    FlexibleContexts
+    FlexibleInstances
+    GADTs
+    MultiParamTypeClasses
+    Safe
+    TypeOperators
+
+  -- GHC.Generics lived in `ghc-prim` for GHC 7.2 & GHC 7.4
+  if impl(ghc == 7.4.*)
+    build-depends: ghc-prim == 0.2.*
+
+  if impl(ghc>=7.6)
+    other-extensions: PolyKinds
+
+  if impl(ghc>=7.8)
+    other-extensions: EmptyCase
+
+  build-depends: base       >= 4.5 && < 4.13,
+                 array      >= 0.4 && < 0.6,
+                 text       >= 1.2.3.0 && <1.4
+  ghc-options: -Wall
+
+  exposed-modules: Control.DeepSeq
+  other-modules:   Control.DeepSeq.BackDoor
+
+test-suite deepseq-generics-tests
+    default-language:    Haskell2010
+    type:                exitcode-stdio-1.0
+    hs-source-dirs:      . tests
+    main-is:             Main.hs
+    other-extensions:
+        CPP
+        BangPatterns
+        DefaultSignatures
+        DeriveDataTypeable
+        DeriveGeneric
+        FlexibleContexts
+        Safe
+        TupleSections
+        TypeOperators
+
+    ghc-options:         -Wall
+
+    build-depends:
+        array,
+        base,
+        ghc-prim,
+        -- end of packages with inherited version constraints
+        test-framework == 0.8.*,
+        test-framework-hunit == 0.3.*,
+        HUnit >= 1.2 && < 1.7
diff --git a/fixtures/semigroups.0.1.diff b/fixtures/semigroups.0.1.diff
new file mode 100644
--- /dev/null
+++ b/fixtures/semigroups.0.1.diff
@@ -0,0 +1,2 @@
+ERROR
+Cannot add or remove 'if' conditionals
diff --git a/fixtures/semigroups.0.2.diff b/fixtures/semigroups.0.2.diff
new file mode 100644
--- /dev/null
+++ b/fixtures/semigroups.0.2.diff
@@ -0,0 +1,5 @@
+OK
+Normal
+the library component's library dependency on 'containers'
+- >=0.3 && <0.7
++ >=0.3 && <0.8
diff --git a/fixtures/semigroups.0.cabal b/fixtures/semigroups.0.cabal
new file mode 100644
--- /dev/null
+++ b/fixtures/semigroups.0.cabal
@@ -0,0 +1,173 @@
+name:          semigroups
+category:      Algebra, Data, Data Structures, Math
+version:       0.18.5
+license:       BSD3
+cabal-version: >= 1.10
+license-file:  LICENSE
+author:        Edward A. Kmett
+maintainer:    Edward A. Kmett <ekmett@gmail.com>
+stability:     provisional
+homepage:      http://github.com/ekmett/semigroups/
+bug-reports:   http://github.com/ekmett/semigroups/issues
+copyright:     Copyright (C) 2011-2015 Edward A. Kmett
+synopsis:      Anything that associates
+description:
+    In mathematics, a semigroup is an algebraic structure consisting of a set together with an associative binary operation. A semigroup generalizes a monoid in that there might not exist an identity element. It also (originally) generalized a group (a monoid with all inverses) to a type where every element did not have to have an inverse, thus the name semigroup.
+build-type:    Simple
+extra-source-files: .travis.yml README.markdown CHANGELOG.markdown
+tested-with:   GHC == 7.0.4
+             , GHC == 7.2.2
+             , GHC == 7.4.2
+             , GHC == 7.6.3
+             , GHC == 7.8.4
+             , GHC == 7.10.3
+             , GHC == 8.0.2
+             , GHC == 8.2.2
+             , GHC == 8.4.3
+             , GHC == 8.6.1
+
+source-repository head
+  type: git
+  location: git://github.com/ekmett/semigroups.git
+
+flag hashable
+  description:
+    You can disable the use of the `hashable` package using `-f-hashable`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+    .
+    If disabled we will not supply instances of `Hashable`
+    .
+    Note: `-f-hashable` implies `-f-unordered-containers`, as we are necessarily not able to supply those instances as well.
+  default: True
+  manual: True
+
+flag binary
+  description:
+    You can disable the use of the `binary` package using `-f-binary`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag bytestring
+  description:
+    You can disable the use of the `bytestring` package using `-f-bytestring`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag bytestring-builder
+  description:
+    Decides whether to use an older version of bytestring along with bytestring-builder or just a newer version of bytestring.
+    .
+    This flag normally toggles automatically but you can use `-fbytestring-builder` or `-f-bytestring-builder` to explicitly change it.
+  default: False
+  manual: False
+
+flag containers
+  description:
+    You can disable the use of the `containers` package using `-f-containers`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag deepseq
+  description:
+    You can disable the use of the `deepseq` package using `-f-deepseq`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag tagged
+  description:
+    You can disable the use of the `tagged` package using `-f-tagged`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag text
+  description:
+    You can disable the use of the `text` package using `-f-text`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag transformers
+  description:
+    You can disable the use of the `transformers` and `transformers-compat` packages using `-f-transformers`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag unordered-containers
+  description:
+    You can disable the use of the `unordered-containers` package using `-f-unordered-containers`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+library
+  default-language: Haskell98
+  hs-source-dirs: src
+  ghc-options: -Wall
+
+  build-depends: base >= 2 && < 5
+
+  if impl(ghc >= 7.2)
+    exposed-modules:
+      Data.Semigroup.Generic
+
+  -- legacy configuration
+  if impl(ghc < 7.11.20151002)
+    -- starting with GHC 8 these modules are provided by `base`
+    hs-source-dirs: src-ghc7
+    exposed-modules:
+      Data.Semigroup
+      Data.List.NonEmpty
+
+    -- Not needed anymore since GHC 7.10
+    if impl(ghc < 7.10)
+      build-depends: nats >= 0.1 && < 2
+
+    if impl(ghc >= 7.2 && < 7.5)
+      build-depends: ghc-prim
+
+    if flag(binary)
+      build-depends: binary
+
+    if flag(bytestring)
+      if flag(bytestring-builder)
+        build-depends: bytestring         >= 0.9    && < 0.10.4,
+                       bytestring-builder >= 0.10.4 && < 1
+      else
+        build-depends: bytestring         >= 0.10.4 && < 1
+
+    if flag(containers)
+      build-depends: containers >= 0.3 && < 0.7
+
+    if flag(deepseq)
+      build-depends: deepseq >= 1.1 && < 1.5
+
+    if flag(tagged)
+      build-depends: tagged >= 0.4.4 && < 1
+
+    if flag(text)
+      build-depends: text >= 0.10 && < 2
+
+    if flag(hashable)
+      build-depends: hashable >= 1.1  && < 1.3
+
+    if flag(hashable) && flag(unordered-containers)
+      build-depends: unordered-containers >= 0.2  && < 0.3
+
+    if flag(transformers)
+      build-depends: transformers        >= 0.2 && < 0.6
+                   , transformers-compat >= 0.5 && < 1
diff --git a/fixtures/semigroups.1.0.diff b/fixtures/semigroups.1.0.diff
new file mode 100644
--- /dev/null
+++ b/fixtures/semigroups.1.0.diff
@@ -0,0 +1,2 @@
+ERROR
+Cannot add or remove 'if' conditionals
diff --git a/fixtures/semigroups.1.2.diff b/fixtures/semigroups.1.2.diff
new file mode 100644
--- /dev/null
+++ b/fixtures/semigroups.1.2.diff
@@ -0,0 +1,2 @@
+ERROR
+Cannot add or remove 'if' conditionals
diff --git a/fixtures/semigroups.1.cabal b/fixtures/semigroups.1.cabal
new file mode 100644
--- /dev/null
+++ b/fixtures/semigroups.1.cabal
@@ -0,0 +1,176 @@
+name:          semigroups
+category:      Algebra, Data, Data Structures, Math
+version:       0.18.5
+license:       BSD3
+cabal-version: >= 1.10
+license-file:  LICENSE
+author:        Edward A. Kmett
+maintainer:    Edward A. Kmett <ekmett@gmail.com>
+stability:     provisional
+homepage:      http://github.com/ekmett/semigroups/
+bug-reports:   http://github.com/ekmett/semigroups/issues
+copyright:     Copyright (C) 2011-2015 Edward A. Kmett
+synopsis:      Anything that associates
+description:
+    In mathematics, a semigroup is an algebraic structure consisting of a set together with an associative binary operation. A semigroup generalizes a monoid in that there might not exist an identity element. It also (originally) generalized a group (a monoid with all inverses) to a type where every element did not have to have an inverse, thus the name semigroup.
+build-type:    Simple
+extra-source-files: .travis.yml README.markdown CHANGELOG.markdown
+tested-with:   GHC == 7.0.4
+             , GHC == 7.2.2
+             , GHC == 7.4.2
+             , GHC == 7.6.3
+             , GHC == 7.8.4
+             , GHC == 7.10.3
+             , GHC == 8.0.2
+             , GHC == 8.2.2
+             , GHC == 8.4.3
+             , GHC == 8.6.1
+
+source-repository head
+  type: git
+  location: git://github.com/ekmett/semigroups.git
+
+flag hashable
+  description:
+    You can disable the use of the `hashable` package using `-f-hashable`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+    .
+    If disabled we will not supply instances of `Hashable`
+    .
+    Note: `-f-hashable` implies `-f-unordered-containers`, as we are necessarily not able to supply those instances as well.
+  default: True
+  manual: True
+
+flag binary
+  description:
+    You can disable the use of the `binary` package using `-f-binary`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag bytestring
+  description:
+    You can disable the use of the `bytestring` package using `-f-bytestring`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag bytestring-builder
+  description:
+    Decides whether to use an older version of bytestring along with bytestring-builder or just a newer version of bytestring.
+    .
+    This flag normally toggles automatically but you can use `-fbytestring-builder` or `-f-bytestring-builder` to explicitly change it.
+  default: False
+  manual: False
+
+flag containers
+  description:
+    You can disable the use of the `containers` package using `-f-containers`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag deepseq
+  description:
+    You can disable the use of the `deepseq` package using `-f-deepseq`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag tagged
+  description:
+    You can disable the use of the `tagged` package using `-f-tagged`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag text
+  description:
+    You can disable the use of the `text` package using `-f-text`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag transformers
+  description:
+    You can disable the use of the `transformers` and `transformers-compat` packages using `-f-transformers`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag unordered-containers
+  description:
+    You can disable the use of the `unordered-containers` package using `-f-unordered-containers`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+library
+  default-language: Haskell98
+  hs-source-dirs: src
+  ghc-options: -Wall
+
+  build-depends: base >= 2 && < 5
+
+  if impl(ghc >= 7.2)
+    exposed-modules:
+      Data.Semigroup.Generic
+
+  -- legacy configuration
+  if impl(ghc < 7.11.20151002)
+    -- starting with GHC 8 these modules are provided by `base`
+    hs-source-dirs: src-ghc7
+    exposed-modules:
+      Data.Semigroup
+      Data.List.NonEmpty
+
+    -- Not needed anymore since GHC 7.10
+    if impl(ghc < 7.10)
+      build-depends: nats >= 0.1 && < 2
+
+    if impl(ghc >= 7.2 && < 7.5)
+      build-depends: ghc-prim
+
+    if flag(binary)
+      build-depends: binary
+
+    if flag(bytestring)
+      if flag(bytestring-builder)
+        build-depends: bytestring         >= 0.9    && < 0.10.4,
+                       bytestring-builder >= 0.10.4 && < 1
+      else
+        build-depends: bytestring         >= 0.10.4 && < 1
+
+    if flag(containers)
+      build-depends: containers >= 0.3 && < 0.8
+
+    if flag(deepseq)
+      build-depends: deepseq >= 1.1 && < 1.5
+
+    if flag(tagged)
+      build-depends: tagged >= 0.4.4 && < 1
+
+    if flag(text)
+      build-depends: text >= 0.10 && < 2
+
+    if flag(hashable)
+      build-depends: hashable >= 1.1  && < 1.3
+
+    if flag(hashable) && flag(unordered-containers)
+      build-depends: unordered-containers >= 0.2  && < 0.3
+
+    if flag(transformers)
+      build-depends: transformers        >= 0.2 && < 0.6
+                   , transformers-compat >= 0.5 && < 1
+
+    if impl(ghc==8.0.1)
+      build-depends: base <0
diff --git a/fixtures/semigroups.2.0.diff b/fixtures/semigroups.2.0.diff
new file mode 100644
--- /dev/null
+++ b/fixtures/semigroups.2.0.diff
@@ -0,0 +1,5 @@
+OK
+Normal
+the library component's library dependency on 'containers'
+- >=0.3 && <0.8
++ >=0.3 && <0.7
diff --git a/fixtures/semigroups.2.1.diff b/fixtures/semigroups.2.1.diff
new file mode 100644
--- /dev/null
+++ b/fixtures/semigroups.2.1.diff
@@ -0,0 +1,2 @@
+ERROR
+Cannot add or remove 'if' conditionals
diff --git a/fixtures/semigroups.2.cabal b/fixtures/semigroups.2.cabal
new file mode 100644
--- /dev/null
+++ b/fixtures/semigroups.2.cabal
@@ -0,0 +1,173 @@
+name:          semigroups
+category:      Algebra, Data, Data Structures, Math
+version:       0.18.5
+license:       BSD3
+cabal-version: >= 1.10
+license-file:  LICENSE
+author:        Edward A. Kmett
+maintainer:    Edward A. Kmett <ekmett@gmail.com>
+stability:     provisional
+homepage:      http://github.com/ekmett/semigroups/
+bug-reports:   http://github.com/ekmett/semigroups/issues
+copyright:     Copyright (C) 2011-2015 Edward A. Kmett
+synopsis:      Anything that associates
+description:
+    In mathematics, a semigroup is an algebraic structure consisting of a set together with an associative binary operation. A semigroup generalizes a monoid in that there might not exist an identity element. It also (originally) generalized a group (a monoid with all inverses) to a type where every element did not have to have an inverse, thus the name semigroup.
+build-type:    Simple
+extra-source-files: .travis.yml README.markdown CHANGELOG.markdown
+tested-with:   GHC == 7.0.4
+             , GHC == 7.2.2
+             , GHC == 7.4.2
+             , GHC == 7.6.3
+             , GHC == 7.8.4
+             , GHC == 7.10.3
+             , GHC == 8.0.2
+             , GHC == 8.2.2
+             , GHC == 8.4.3
+             , GHC == 8.6.1
+
+source-repository head
+  type: git
+  location: git://github.com/ekmett/semigroups.git
+
+flag hashable
+  description:
+    You can disable the use of the `hashable` package using `-f-hashable`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+    .
+    If disabled we will not supply instances of `Hashable`
+    .
+    Note: `-f-hashable` implies `-f-unordered-containers`, as we are necessarily not able to supply those instances as well.
+  default: True
+  manual: True
+
+flag binary
+  description:
+    You can disable the use of the `binary` package using `-f-binary`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag bytestring
+  description:
+    You can disable the use of the `bytestring` package using `-f-bytestring`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag bytestring-builder
+  description:
+    Decides whether to use an older version of bytestring along with bytestring-builder or just a newer version of bytestring.
+    .
+    This flag normally toggles automatically but you can use `-fbytestring-builder` or `-f-bytestring-builder` to explicitly change it.
+  default: False
+  manual: False
+
+flag containers
+  description:
+    You can disable the use of the `containers` package using `-f-containers`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag deepseq
+  description:
+    You can disable the use of the `deepseq` package using `-f-deepseq`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag tagged
+  description:
+    You can disable the use of the `tagged` package using `-f-tagged`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag text
+  description:
+    You can disable the use of the `text` package using `-f-text`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag transformers
+  description:
+    You can disable the use of the `transformers` and `transformers-compat` packages using `-f-transformers`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+flag unordered-containers
+  description:
+    You can disable the use of the `unordered-containers` package using `-f-unordered-containers`.
+    .
+    Disabling this is an unsupported configuration, but it may be useful for accelerating builds in sandboxes for expert users.
+  default: True
+  manual: True
+
+library
+  default-language: Haskell98
+  hs-source-dirs: src
+  ghc-options: -Wall
+
+  build-depends: base >= 2 && < 5
+
+  if impl(ghc >= 7.2)
+    exposed-modules:
+      Data.Semigroup.Generic
+
+  -- legacy configuration
+  if impl(ghc < 7.11.20151002)
+    -- starting with GHC 8 these modules are provided by `base`
+    hs-source-dirs: src-ghc7
+    exposed-modules:
+      Data.Semigroup
+      Data.List.NonEmpty
+
+    -- Not needed anymore since GHC 7.10
+    if impl(ghc < 7.10)
+      build-depends: nats >= 0.1 && < 2
+
+    if impl(ghc >= 7.2 && < 7.5)
+      build-depends: ghc-prim
+
+    if flag(binary)
+      build-depends: binary
+
+    if flag(bytestring)
+      if flag(bytestring-builder)
+        build-depends: bytestring         >= 0.9    && < 0.10.4,
+                       bytestring-builder >= 0.10.4 && < 1
+      else
+        build-depends: bytestring         >= 0.10.4 && < 1
+
+    if flag(containers)
+      build-depends: containers >= 0.3 && < 0.8
+
+    if flag(deepseq)
+      build-depends: deepseq >= 1.1 && < 1.5
+
+    if flag(tagged)
+      build-depends: tagged >= 0.4.4 && < 1
+
+    if flag(text)
+      build-depends: text >= 0.10 && < 2
+
+    if flag(hashable)
+      build-depends: hashable >= 1.1  && < 1.3
+
+    if flag(hashable) && flag(unordered-containers)
+      build-depends: unordered-containers >= 0.2  && < 0.3
+
+    if flag(transformers)
+      build-depends: transformers        >= 0.2 && < 0.6
+                   , transformers-compat >= 0.5 && < 1
diff --git a/fixtures/tree-diff.0.1.diff b/fixtures/tree-diff.0.1.diff
new file mode 100644
--- /dev/null
+++ b/fixtures/tree-diff.0.1.diff
@@ -0,0 +1,13 @@
+OK
+Trivial
+tested-with
+- ghc ==7.8.4: ghc ==7.10.3: ghc ==8.0.2: ghc ==8.2.2: ghc ==8.4.4: ghc ==8.6.2
++ ghc ==7.8.4: ghc ==7.10.3: ghc ==8.0.2: ghc ==8.2.2: ghc ==8.4.4: ghc ==8.6.3
+Normal
+the library component's library dependency on 'ansi-terminal'
+- >=0.6.3.1 && <0.9
++ >=0.6.3.1 && <0.10
+Normal
+the test suite 'test' component's library dependency on 'tasty'
+- >=0.11.2.5 && <1.2
++ >=0.11.2.5 && <1.3
diff --git a/fixtures/tree-diff.0.cabal b/fixtures/tree-diff.0.cabal
new file mode 100644
--- /dev/null
+++ b/fixtures/tree-diff.0.cabal
@@ -0,0 +1,129 @@
+cabal-version:       >=1.10
+name:                tree-diff
+version:             0.0.2
+
+synopsis:            Diffing of (expression) trees.
+category:            Data, Testing
+description:
+  Common diff algorithm works on list structures:
+  .
+  @
+  diff :: Eq a => [a] -> [a] -> [Edit a]
+  @
+  .
+  This package works on trees.
+  .
+  @
+  treeDiff :: Eq a => Tree a -> Tree a -> Edit (EditTree a)
+  @
+  .
+  This package also provides a way to diff arbitrary ADTs,
+  using @Generics@-derivable helpers.
+  .
+  This package differs from <http://hackage.haskell.org/package/gdiff gdiff>,
+  in a two ways: @tree-diff@ doesn't have patch function,
+  and the "edit-script" is a tree itself, which is useful for pretty-printing.
+  .
+  @
+  >>> prettyEditExpr $ ediff (Foo 42 [True, False] "old") (Foo 42 [False, False, True] "new")
+  Foo
+    {fooBool = [-True, +False, False, +True],
+     fooInt = 42,
+     fooString = -"old" +"new"}
+  @
+homepage:            https://github.com/phadej/tree-diff
+bug-reports:         https://github.com/phadej/tree-diff/issues
+license:             BSD3
+license-file:        LICENSE
+author:              Oleg Grenrus <oleg.grenrus@iki.fi>
+maintainer:          Oleg.Grenrus <oleg.grenrus@iki.fi>
+copyright:           (c) 2017-2018 Oleg Grenrus
+build-type:          Simple
+extra-source-files:  README.md ChangeLog.md
+tested-with:
+  GHC==7.8.4,
+  GHC==7.10.3,
+  GHC==8.0.2,
+  GHC==8.2.2,
+  GHC==8.4.4,
+  GHc==8.6.2
+
+extra-source-files:
+  fixtures/exfoo.expr
+
+source-repository head
+  type:      git
+  location:  https://github.com/phadej/tree-diff.git
+
+library
+  exposed-modules:
+    Data.TreeDiff
+    Data.TreeDiff.List
+    Data.TreeDiff.Tree
+    Data.TreeDiff.Expr
+    Data.TreeDiff.Class
+    Data.TreeDiff.Pretty
+    Data.TreeDiff.Parser
+    Data.TreeDiff.Golden
+    Data.TreeDiff.QuickCheck
+  build-depends:
+    base                 >=4.7      && <4.13,
+    aeson                >=1.2.1.0  && <1.5,
+    ansi-wl-pprint       >=0.6.8.1  && <0.7,
+    ansi-terminal        >=0.6.3.1  && <0.9,
+    base-compat          >=0.9.3    && <0.11,
+    bytestring           >=0.10.4.0 && <0.11,
+    containers           >=0.5.5.1  && <0.7,
+    generics-sop         >=0.3.1.0  && <0.5,
+    hashable             >=1.2.6.1  && <1.3,
+    MemoTrie             >=0.6.8    && <0.7,
+    parsec               >=3.1.11   && <3.2,
+    parsers              >=0.12.7   && <0.13,
+    pretty               >=1.1.1.1  && <1.2,
+    QuickCheck           >=2.10.0.1 && <2.13,
+    scientific           >=0.3.5.2  && <0.4,
+    tagged               >=0.8.5    && <0.9,
+    text                 >=1.2.2.2  && <1.3,
+    time                 >=1.4.2    && <1.9,
+    unordered-containers >=0.2.8.0  && <0.3,
+    uuid-types           >=1.0.3    && <1.1,
+    vector               >=0.12     && <0.13
+
+  if !impl(ghc >= 8.0)
+    build-depends:
+      semigroups         >=0.18.3   && <0.19
+
+  if !impl(ghc >= 7.10)
+    build-depends:
+      void               >=0.7.2    && <0.8,
+      nats               >=1.1.1    && <1.2,
+      transformers       >=0.3.0.0  && <0.6
+
+  other-extensions:
+    ConstraintKinds
+    CPP
+    DefaultSignatures
+    FlexibleContexts
+    GADTs
+    RankNTypes
+    ScopedTypeVariables
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+
+test-suite test
+  default-language:    Haskell2010
+  type:                exitcode-stdio-1.0
+  main-is:             Tests.hs
+  hs-source-dirs:      tests
+  ghc-options:         -Wall -threaded
+  build-depends:
+    base, tree-diff,
+    base-compat,
+    QuickCheck,
+    ansi-terminal,
+    ansi-wl-pprint,
+    parsec,
+    trifecta             >=1.7.1.1  && <2.1,
+    tasty                >=0.11.2.5 && <1.2,
+    tasty-golden         >=2.3.1.1  && <2.4,
+    tasty-quickcheck     >=0.9.1    && <0.11
diff --git a/fixtures/tree-diff.1.0.diff b/fixtures/tree-diff.1.0.diff
new file mode 100644
--- /dev/null
+++ b/fixtures/tree-diff.1.0.diff
@@ -0,0 +1,13 @@
+OK
+Trivial
+tested-with
+- ghc ==7.8.4: ghc ==7.10.3: ghc ==8.0.2: ghc ==8.2.2: ghc ==8.4.4: ghc ==8.6.3
++ ghc ==7.8.4: ghc ==7.10.3: ghc ==8.0.2: ghc ==8.2.2: ghc ==8.4.4: ghc ==8.6.2
+Normal
+the library component's library dependency on 'ansi-terminal'
+- >=0.6.3.1 && <0.10
++ >=0.6.3.1 && <0.9
+Normal
+the test suite 'test' component's library dependency on 'tasty'
+- >=0.11.2.5 && <1.3
++ >=0.11.2.5 && <1.2
diff --git a/fixtures/tree-diff.1.cabal b/fixtures/tree-diff.1.cabal
new file mode 100644
--- /dev/null
+++ b/fixtures/tree-diff.1.cabal
@@ -0,0 +1,130 @@
+cabal-version:       >=1.10
+name:                tree-diff
+version:             0.0.2
+x-revision:          1
+
+synopsis:            Diffing of (expression) trees.
+category:            Data, Testing
+description:
+  Common diff algorithm works on list structures:
+  .
+  @
+  diff :: Eq a => [a] -> [a] -> [Edit a]
+  @
+  .
+  This package works on trees.
+  .
+  @
+  treeDiff :: Eq a => Tree a -> Tree a -> Edit (EditTree a)
+  @
+  .
+  This package also provides a way to diff arbitrary ADTs,
+  using @Generics@-derivable helpers.
+  .
+  This package differs from <http://hackage.haskell.org/package/gdiff gdiff>,
+  in a two ways: @tree-diff@ doesn't have patch function,
+  and the "edit-script" is a tree itself, which is useful for pretty-printing.
+  .
+  @
+  >>> prettyEditExpr $ ediff (Foo 42 [True, False] "old") (Foo 42 [False, False, True] "new")
+  Foo
+    {fooBool = [-True, +False, False, +True],
+     fooInt = 42,
+     fooString = -"old" +"new"}
+  @
+homepage:            https://github.com/phadej/tree-diff
+bug-reports:         https://github.com/phadej/tree-diff/issues
+license:             BSD3
+license-file:        LICENSE
+author:              Oleg Grenrus <oleg.grenrus@iki.fi>
+maintainer:          Oleg.Grenrus <oleg.grenrus@iki.fi>
+copyright:           (c) 2017-2018 Oleg Grenrus
+build-type:          Simple
+extra-source-files:  README.md ChangeLog.md
+tested-with:
+  GHC==7.8.4,
+  GHC==7.10.3,
+  GHC==8.0.2,
+  GHC==8.2.2,
+  GHC==8.4.4,
+  GHc==8.6.3
+
+extra-source-files:
+  fixtures/exfoo.expr
+
+source-repository head
+  type:      git
+  location:  https://github.com/phadej/tree-diff.git
+
+library
+  exposed-modules:
+    Data.TreeDiff
+    Data.TreeDiff.List
+    Data.TreeDiff.Tree
+    Data.TreeDiff.Expr
+    Data.TreeDiff.Class
+    Data.TreeDiff.Pretty
+    Data.TreeDiff.Parser
+    Data.TreeDiff.Golden
+    Data.TreeDiff.QuickCheck
+  build-depends:
+    base                 >=4.7      && <4.13,
+    aeson                >=1.2.1.0  && <1.5,
+    ansi-wl-pprint       >=0.6.8.1  && <0.7,
+    ansi-terminal        >=0.6.3.1  && <0.10,
+    base-compat          >=0.9.3    && <0.11,
+    bytestring           >=0.10.4.0 && <0.11,
+    containers           >=0.5.5.1  && <0.7,
+    generics-sop         >=0.3.1.0  && <0.5,
+    hashable             >=1.2.6.1  && <1.3,
+    MemoTrie             >=0.6.8    && <0.7,
+    parsec               >=3.1.11   && <3.2,
+    parsers              >=0.12.7   && <0.13,
+    pretty               >=1.1.1.1  && <1.2,
+    QuickCheck           >=2.10.0.1 && <2.13,
+    scientific           >=0.3.5.2  && <0.4,
+    tagged               >=0.8.5    && <0.9,
+    text                 >=1.2.2.2  && <1.3,
+    time                 >=1.4.2    && <1.9,
+    unordered-containers >=0.2.8.0  && <0.3,
+    uuid-types           >=1.0.3    && <1.1,
+    vector               >=0.12     && <0.13
+
+  if !impl(ghc >= 8.0)
+    build-depends:
+      semigroups         >=0.18.3   && <0.19
+
+  if !impl(ghc >= 7.10)
+    build-depends:
+      void               >=0.7.2    && <0.8,
+      nats               >=1.1.1    && <1.2,
+      transformers       >=0.3.0.0  && <0.6
+
+  other-extensions:
+    ConstraintKinds
+    CPP
+    DefaultSignatures
+    FlexibleContexts
+    GADTs
+    RankNTypes
+    ScopedTypeVariables
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+
+test-suite test
+  default-language:    Haskell2010
+  type:                exitcode-stdio-1.0
+  main-is:             Tests.hs
+  hs-source-dirs:      tests
+  ghc-options:         -Wall -threaded
+  build-depends:
+    base, tree-diff,
+    base-compat,
+    QuickCheck,
+    ansi-terminal,
+    ansi-wl-pprint,
+    parsec,
+    trifecta             >=1.7.1.1  && <2.1,
+    tasty                >=0.11.2.5 && <1.3,
+    tasty-golden         >=2.3.1.1  && <2.4,
+    tasty-quickcheck     >=0.9.1    && <0.11
diff --git a/hackage-cli.cabal b/hackage-cli.cabal
new file mode 100644
--- /dev/null
+++ b/hackage-cli.cabal
@@ -0,0 +1,122 @@
+cabal-version:       2.4
+name:                hackage-cli
+version:             0.0.3.6
+
+synopsis:            CLI tool for Hackage
+description:
+  With @hackage-cli@ you can manage @.cabal@ files,
+  e.g. (bulk-)upload revised variants.
+homepage:            https://github.com/hackage-trustees/hackage-cli
+bug-reports:         https://github.com/hackage-trustees/hackage-cli/issues
+license:             GPL-3.0-or-later
+license-file:        LICENSE
+author:              Herbert Valerio Riedel
+maintainer:          Andreas Abel
+category:            Development
+build-type:          Simple
+
+tested-with:
+  -- Keep in descending order.
+  GHC == 9.2.2
+  GHC == 9.0.2
+  GHC == 8.10.7
+  GHC == 8.8.4
+  GHC == 8.6.5
+  GHC == 8.4.4
+  GHC == 8.2.2
+
+extra-source-files:
+  CHANGELOG.md
+  README.md
+  fixtures/*.diff
+  fixtures/*.cabal
+
+source-repository head
+  Type:     git
+  Location: https://github.com/hackage-trustees/hackage-cli.git
+
+library cabal-revisions
+  X-SPDX-License-Identifier: BSD-3-Clause
+  default-language:    Haskell2010
+  hs-source-dirs:      lib
+  ghc-options:         -Wall -Wcompat
+
+  build-depends:
+    , base         >= 4.10.0.0 && < 4.17
+    , bytestring   >= 0.10.4.0 && < 0.12
+    , Cabal        >= 3.4      && < 3.7
+    , containers   >= 0.5.0.0  && < 0.7
+    , mtl          >= 2.2.2    && < 2.4
+    , pretty      ^>= 1.1.2
+
+  exposed-modules:
+    Distribution.Server.Util.CabalRevisions
+
+test-suite cabal-revisions-tests
+  default-language:    Haskell2010
+  hs-source-dirs:      tests
+  ghc-options:         -Wall -Wcompat
+  type:                exitcode-stdio-1.0
+  main-is:             Tests.hs
+  build-depends:
+    , base
+    , bytestring
+    , Cabal
+    , cabal-revisions
+    , tasty         >= 1.0      && < 1.5
+        -- tasty-1.0 for stack-8.2.2.yaml
+    , tasty-golden ^>= 2.3.2
+    , filepath     ^>= 1.4.0.0
+
+executable hackage-cli
+  default-language:    Haskell2010
+  other-extensions:
+    CPP
+    LambdaCase
+    OverloadedStrings
+    RecordWildCards
+    TemplateHaskell
+
+  hs-source-dirs:      src
+  main-is:             Main.hs
+
+  other-modules:
+    IndexShaSum
+    CabalEdit
+    Paths_hackage_cli
+
+  autogen-modules:
+    Paths_hackage_cli
+
+  build-depends:
+    , base
+    , bytestring
+    , Cabal
+    , cabal-revisions
+    , containers
+    , mtl
+
+  build-depends:
+    , aeson                  >= 1.2.4.0 && < 2.1
+        -- aeson-1.2.4.0 for stack-8.2.2.yaml
+    , deepseq               ^>= 1.4.0.0
+    , directory             ^>= 1.2.0.1 || ^>= 1.3.0.0
+    , filepath              ^>= 1.4.0.0
+    , http-io-streams       ^>= 0.1.0.0
+    , io-streams            ^>= 1.5.0.1
+    , microlens              >= 0.4.8.3  && < 4.13
+    , microlens-mtl          >= 0.1.11.1 && < 0.3
+    , microlens-th           >= 0.4.1.3  && < 0.5
+    , netrc                 ^>= 0.2.0.0
+    , optparse-applicative   >= 0.14    && < 0.18
+    , process-extras        ^>= 0.7.4
+    , semigroups             >= 0.18.3  && < 0.21
+    , stringsearch          ^>= 0.3.6
+    , tagsoup               ^>= 0.14
+    , tar                   ^>= 0.5
+    , text                   >= 1.2     && < 2.1
+    , time                   >= 1.5.0.1 && < 1.13
+    , unordered-containers  ^>= 0.2.7
+    , zlib                  ^>= 0.6.1
+
+  ghc-options: -Wall -Wcompat -threaded
diff --git a/lib/Distribution/Server/Util/CabalRevisions.hs b/lib/Distribution/Server/Util/CabalRevisions.hs
new file mode 100644
--- /dev/null
+++ b/lib/Distribution/Server/Util/CabalRevisions.hs
@@ -0,0 +1,771 @@
+{-# LANGUAGE CPP                        #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE FunctionalDependencies     #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE TypeFamilies               #-}
+
+-- |
+-- Module      :  Distribution.Server.Util.CabalRevisions
+-- Copyright   :  Duncan Coutts et al.
+-- SPDX-License-Identifier: BSD-3-Clause
+--
+-- Maintainer  :  libraries@haskell.org
+-- Stability   :  provisional
+-- Portability :  portable
+--
+-- Validation and helpers for Cabal revision handling
+module Distribution.Server.Util.CabalRevisions
+    ( diffCabalRevisions
+    , diffCabalRevisions'
+    , Change(..)
+    , insertRevisionField
+    ) where
+
+-- NB: This module avoids to import any hackage-server modules
+import Distribution.CabalSpecVersion (CabalSpecVersion(..), cabalSpecLatest, showCabalSpecVersion)
+import Distribution.Types.Dependency
+import Distribution.Types.ExeDependency
+import Distribution.Types.PkgconfigDependency
+import Distribution.Types.PkgconfigVersionRange
+import Distribution.Types.LegacyExeDependency
+import Distribution.Types.UnqualComponentName
+import Distribution.Types.CondTree
+import Distribution.Types.ForeignLib
+import Distribution.Package
+import Distribution.Pretty (Pretty (..), prettyShow)
+import Distribution.Version
+import Distribution.Compiler (CompilerFlavor)
+import Distribution.FieldGrammar (prettyFieldGrammar)
+import Distribution.Fields.Pretty (PrettyField (..), showFields)
+import Distribution.PackageDescription
+import Distribution.PackageDescription.Parsec (parseGenericPackageDescription, runParseResult)
+import Distribution.PackageDescription.FieldGrammar (sourceRepoFieldGrammar)
+import Distribution.PackageDescription.Check
+import Distribution.Parsec (showPWarning, showPError, PWarning (..))
+import Distribution.Utils.ShortText
+import Text.PrettyPrint as Doc
+         ((<+>), colon, text, Doc, hsep, punctuate)
+
+import Control.Applicative
+import Control.Monad
+import Control.Monad.Except  (ExceptT, runExceptT, throwError)
+import Control.Monad.Writer (MonadWriter(..), Writer, runWriter)
+import Data.Foldable (for_)
+import Data.List
+         ((\\), deleteBy, intercalate)
+import Data.ByteString.Lazy (ByteString)
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy.Char8 as LBS8
+import qualified Data.Char as Char
+import qualified Data.Semigroup as S
+import qualified Data.Monoid as M
+import qualified Data.Set as Set
+import qualified Data.Map.Strict as Map
+import Data.Proxy (Proxy(Proxy))
+
+import qualified Control.Monad.Fail as Fail
+
+-- | Entry point to cabal revision validator
+--
+-- This takes an original and a revised @.cabal@ decoded as Unicode
+-- 'String' and performs validations. Returns either a validation
+-- error or a list of detected changes.
+diffCabalRevisions :: BS.ByteString -> BS.ByteString -> Either String [Change]
+diffCabalRevisions = diffCabalRevisions' True
+
+-- | Like 'diffCabalRevisions' but only optionally check @x-revision@ field modifications.
+diffCabalRevisions'
+    :: Bool                    -- ^ check @x-revision@
+    -> BS.ByteString           -- ^ old revision
+    -> BS.ByteString           -- ^ new revision
+    -> Either String [Change]
+diffCabalRevisions' checkXRevision oldVersion newRevision = runCheck $
+    checkCabalFileRevision checkXRevision oldVersion newRevision
+
+newtype CheckM a = CheckM { unCheckM :: ExceptT String (Writer [Change]) a }
+    deriving (Functor, Applicative)
+
+runCheck :: CheckM () -> Either String [Change]
+runCheck c = case runWriter . runExceptT . unCheckM $ c of
+               (Left err, _      ) -> Left err
+               (Right (), changes)
+                 | foldMap changeSeverity changes /= Trivial -> Right changes
+                 | otherwise ->
+                   Left "Only trivial changes, don't bother making this revision."
+
+changeSeverity :: Change -> Severity
+changeSeverity (Change s _ _ _) = s
+
+instance Monad CheckM where
+  return         = Control.Applicative.pure
+  CheckM m >>= f = CheckM (m >>= unCheckM . f)
+
+#if !MIN_VERSION_base(4,13,0)
+  fail = Fail.fail
+#endif
+
+instance Fail.MonadFail CheckM where
+  fail           = CheckM . throwError
+
+-- | If we have only 'Trivial' changes, then there is no point to make
+-- a revision. In other words for changes to be accepted, there should
+-- be at least one 'Normal' change.
+data Severity
+    = Normal
+    | Trivial
+  deriving (Eq, Ord, Show, Enum, Bounded)
+
+instance S.Semigroup Severity where
+    Normal  <> _ = Normal
+    Trivial <> x = x
+
+-- | "Max" monoid.
+instance M.Monoid Severity where
+    mempty = Trivial
+    mappend = (S.<>)
+
+data Change = Change Severity String String String -- severity, what, from, to
+  deriving Show
+
+
+
+logChange :: Change -> CheckM ()
+logChange change = CheckM (tell [change])
+
+type Check a = a -> a -> CheckM ()
+
+checkCabalFileRevision :: Bool -> Check BS.ByteString
+checkCabalFileRevision checkXRevision old new = do
+    (pkg,  warns)  <- parseCabalFile old
+    (pkg', warns') <- parseCabalFile new
+
+    let pkgid    = packageId pkg
+        filename = prettyShow pkgid ++ ".cabal"
+
+    checkGenericPackageDescription checkXRevision pkg pkg'
+    checkParserWarnings filename warns warns'
+    checkPackageChecks  pkg   pkg'
+
+  where
+    parseCabalFile fileContent =
+      case runParseResult $ parseGenericPackageDescription fileContent of
+        (warnings,  Right pkg) -> return (pkg, warnings)
+        (_warnings, Left (_mver, errs)) -> do
+            for_ errs $ \err -> fail (showPError "-" err)
+            fail "no better error"
+
+    -- new PWarning isn't Eq
+    differenceBy :: (a -> a -> Bool) -> [a] -> [a] -> [a]
+    differenceBy eq = foldl (flip $ deleteBy eq)
+
+    -- things can move, pos can change
+    eqPWarning :: PWarning -> PWarning -> Bool
+    eqPWarning (PWarning t _pos s) (PWarning t' _pos' s') =
+        t == t' && s == s'
+
+    checkParserWarnings :: FilePath -> Check [PWarning]
+    checkParserWarnings filename warns warns' =
+      case differenceBy eqPWarning warns' warns of
+        []       -> return ()
+        newwarns -> fail $ "New parse warning: "
+                        ++ unlines (map (showPWarning filename) newwarns)
+
+    checkPackageChecks pkg pkg' =
+      let checks  = checkPackage pkg  Nothing
+          checks' = checkPackage pkg' Nothing
+       in case checks' \\ checks of
+            []        -> return ()
+            newchecks -> fail $ unlines (map explanation newchecks)
+
+checkGenericPackageDescription :: Bool -> Check GenericPackageDescription
+checkGenericPackageDescription checkXRevision
+    (GenericPackageDescription descrA _versionA flagsA libsA sublibsA flibsA exesA testsA benchsA)
+    (GenericPackageDescription descrB _versionB flagsB libsB sublibsB flibsB exesB testsB benchsB) = do
+
+    checkPackageDescriptions checkXRevision descrA descrB
+
+    checkList "Cannot add or remove flags" checkFlag flagsA flagsB
+
+    checkMaybe "Cannot add or remove library sections"
+      (checkCondTree checkLibrary)
+      (withComponentName' (CLibName LMainLibName) <$> libsA)
+      (withComponentName' (CLibName LMainLibName) <$> libsB)
+
+    checkListAssoc "Cannot add or remove sub-library sections"
+      (checkCondTree checkLibrary)
+      (withComponentName (CLibName . LSubLibName) <$> sublibsA)
+      (withComponentName (CLibName . LSubLibName) <$> sublibsB)
+
+    checkListAssoc "Cannot add or remove foreign-library sections"
+      (checkCondTree checkForeignLib)
+      (withComponentName CFLibName <$> flibsA)
+      (withComponentName CFLibName <$> flibsB)
+
+    checkListAssoc "Cannot add or remove executable sections"
+      (checkCondTree checkExecutable)
+      (withComponentName CExeName <$> exesA)
+      (withComponentName CExeName <$> exesB)
+
+    checkListAssoc "Cannot add or remove test-suite sections"
+      (checkCondTree checkTestSuite)
+      (withComponentName CTestName <$> testsA)
+      (withComponentName CTestName <$> testsB)
+
+    checkListAssoc "Cannot add or remove benchmark sections"
+      (checkCondTree checkBenchmark)
+      (withComponentName CBenchName <$> benchsA)
+      (withComponentName CBenchName <$> benchsB)
+  where
+    withComponentName  f (name, condTree) = (name, (f name, condTree))
+    withComponentName' f        condTree  = (f,             condTree)
+
+
+checkFlag :: Check PackageFlag
+checkFlag flagOld flagNew = do
+    -- This check is applied via 'checkList' and for simplicity we
+    -- disallow renaming/reordering flags (even though reordering
+    -- would be fine semantically)
+    checkSame "Cannot change ordering of flags"
+              (flagName flagOld) (flagName flagNew)
+
+    -- Automatic flags' defaults may be changed as they don't make new
+    -- configurations reachable by the solver that weren't before
+    --
+    -- Moreover, automatic flags may be converted into manual flags
+    -- but not the other way round.
+    --
+    -- NB: We always allow to change the flag description as it has
+    --     purely informational value
+    when (flagManual flagOld) $ do
+        checkSame "Cannot change the default of a manual flag"
+                  (flagDefault flagOld) (flagDefault flagNew)
+
+        checkSame "Cannot change a manual flag into an automatic flag"
+                  (flagManual flagOld) (flagManual flagNew)
+
+    let fname = unFlagName (flagName flagOld)
+
+    changesOk ("type of flag '" ++ fname ++ "'")
+              (\b -> if b then "manual" else "automatic")
+              (flagManual flagOld) (flagManual flagNew)
+
+    changesOk ("default of flag '" ++ fname ++ "'") prettyShow
+              (flagDefault flagOld) (flagDefault flagNew)
+
+    changesOk ("description of flag '" ++ fname ++ "'") id
+              (flagDescription flagOld) (flagDescription flagNew)
+
+checkPackageDescriptions :: Bool -> Check PackageDescription
+checkPackageDescriptions checkXRevision
+  pdA@(PackageDescription
+     { specVersion     = _specVersionA
+     , package         = packageIdA
+     , licenseRaw      = licenseRawA
+     , licenseFiles    = licenseFilesA
+     , copyright       = copyrightA
+     , maintainer      = maintainerA
+     , author          = authorA
+     , stability       = stabilityA
+     , testedWith      = testedWithA
+     , homepage        = homepageA
+     , pkgUrl          = pkgUrlA
+     , bugReports      = bugReportsA
+     , sourceRepos     = sourceReposA
+     , synopsis        = synopsisA
+     , description     = descriptionA
+     , category        = categoryA
+     , customFieldsPD  = customFieldsPDA
+     , buildTypeRaw    = buildTypeRawA
+     , setupBuildInfo  = setupBuildInfoA
+     , library         = _libraryA
+     , subLibraries    = _subLibrariesA
+     , executables     = _executablesA
+     , foreignLibs     = _foreignLibsA
+     , testSuites      = _testSuitesA
+     , benchmarks      = _benchmarksA
+     , dataFiles       = dataFilesA
+     , dataDir         = dataDirA
+     , extraSrcFiles   = extraSrcFilesA
+     , extraTmpFiles   = extraTmpFilesA
+     , extraDocFiles   = extraDocFilesA
+     })
+  pdB@(PackageDescription
+     { specVersion     = _specVersionB
+     , package         = packageIdB
+     , licenseRaw      = licenseRawB
+     , licenseFiles    = licenseFilesB
+     , copyright       = copyrightB
+     , maintainer      = maintainerB
+     , author          = authorB
+     , stability       = stabilityB
+     , testedWith      = testedWithB
+     , homepage        = homepageB
+     , pkgUrl          = pkgUrlB
+     , bugReports      = bugReportsB
+     , sourceRepos     = sourceReposB
+     , synopsis        = synopsisB
+     , description     = descriptionB
+     , category        = categoryB
+     , customFieldsPD  = customFieldsPDB
+     , buildTypeRaw    = buildTypeRawB
+     , setupBuildInfo  = setupBuildInfoB
+     , library         = _libraryB
+     , subLibraries    = _subLibrariesB
+     , executables     = _executablesB
+     , foreignLibs     = _foreignLibsB
+     , testSuites      = _testSuitesB
+     , benchmarks      = _benchmarksB
+     , dataFiles       = dataFilesB
+     , dataDir         = dataDirB
+     , extraSrcFiles   = extraSrcFilesB
+     , extraTmpFiles   = extraTmpFilesB
+     , extraDocFiles   = extraDocFilesB
+     })
+  = do
+  checkSame "Don't be silly! You can't change the package name!"
+            (packageName packageIdA) (packageName packageIdB)
+  checkSame "You can't change the package version!"
+            (packageVersion packageIdA) (packageVersion packageIdB)
+  checkSame "Cannot change the license"
+            (licenseRawA, licenseFilesA) (licenseRawB, licenseFilesB)
+  changesOk "copyright"  fromShortText copyrightA copyrightB
+  changesOk "maintainer" fromShortText maintainerA maintainerB
+  changesOk "author"     fromShortText authorA authorB
+  checkSame "The stability field is unused, don't bother changing it."
+            stabilityA stabilityB
+  changesOk' Trivial "tested-with" (show . ppTestedWith) testedWithA testedWithB
+  changesOk "homepage" fromShortText homepageA homepageB
+  checkSame "The package-url field is unused, don't bother changing it."
+            pkgUrlA pkgUrlB
+  changesOk "bug-reports" fromShortText bugReportsA bugReportsB
+  changesOkList changesOk "source-repository" (showFields (const []) . (:[]) . ppSourceRepo)
+            sourceReposA sourceReposB
+  changesOk "synopsis"    fromShortText synopsisA synopsisB
+  changesOk "description" fromShortText descriptionA descriptionB
+  changesOk "category"    fromShortText categoryA categoryB
+  checkSame "Cannot change the build-type"
+            buildTypeRawA buildTypeRawB
+  checkSame "Cannot change the data files"
+            (dataFilesA, dataDirA) (dataFilesB, dataDirB)
+  checkSame "Changing extra-tmp-files is a bit pointless at this stage"
+            extraTmpFilesA extraTmpFilesB
+  checkSame "Changing extra-source-files would not make sense!"
+            extraSrcFilesA extraSrcFilesB
+  checkSame "You can't change the extra-doc-files."
+            extraDocFilesA extraDocFilesB
+
+  checkSame "Cannot change custom/extension fields"
+            (filter (\(f,_) -> not (f `elem` ["x-revision","x-curation"])) customFieldsPDA)
+            (filter (\(f,_) -> not (f `elem` ["x-revision","x-curation"])) customFieldsPDB)
+
+  checkSpecVersionRaw pdA pdB
+  checkSetupBuildInfo setupBuildInfoA setupBuildInfoB
+
+  when checkXRevision $ checkRevision customFieldsPDA customFieldsPDB
+  checkCuration customFieldsPDA customFieldsPDB
+
+checkSpecVersionRaw :: Check PackageDescription
+checkSpecVersionRaw pdA pdB
+  | range110To120 specVersionA
+  , range110To120 specVersionB
+  = changesOk "cabal-version" showCabalSpecVersion specVersionA specVersionB
+
+  | otherwise
+  = checkSame "Cannot change the Cabal spec version"
+              specVersionA specVersionB
+  where
+    specVersionA = specVersion pdA
+    specVersionB = specVersion pdB
+
+    -- nothing interesting changed within the  Cabal >=1.10 && <1.21 range
+    -- therefore we allow to change the spec version within this interval
+    range110To120 v = CabalSpecV1_10 >= v && v <= CabalSpecV1_20
+
+checkRevision :: Check [(String, String)]
+checkRevision customFieldsA customFieldsB =
+    checkSame ("The new x-revision must be " ++ show expectedRevision)
+              newRevision expectedRevision
+  where
+    oldRevision = getRevision customFieldsA
+    newRevision = getRevision customFieldsB
+    expectedRevision = oldRevision + 1
+
+    getRevision customFields =
+      case lookup "x-revision" customFields of
+        Just s  | [(n,"")] <- reads s -> n :: Int
+        _                             -> 0
+
+checkCuration :: Check [(String, String)]
+checkCuration customFieldsA customFieldsB =
+    checkNotPresent "Revised metadata must not contain an x-curation field as revisions necessarily imply curation, and revising an uncurated package adopts it into the curated layer." oldCuration newCuration
+  where
+    oldCuration = lookup "x-curation" customFieldsA
+    newCuration = lookup "x-curation" customFieldsB
+
+
+
+checkCondTree :: (ComponentName -> Check a) -> Check (ComponentName, CondTree ConfVar [Dependency] a)
+checkCondTree checkElem (componentName, condNodeA)
+                        (_            , condNodeB) =
+    checkCondNode condNodeA condNodeB
+  where
+    checkCondNode (CondNode dataA constraintsA componentsA)
+                  (CondNode dataB constraintsB componentsB) = do
+      checkDependencies componentName constraintsA constraintsB
+      checkList "Cannot add or remove 'if' conditionals"
+                checkComponent componentsA componentsB
+      checkElem componentName dataA dataB
+
+    checkComponent (CondBranch condA ifPartA thenPartA)
+                   (CondBranch condB ifPartB thenPartB) = do
+      checkSame "Cannot change the 'if' condition expressions"
+                condA condB
+      checkCondNode ifPartA ifPartB
+      checkMaybe "Cannot add or remove the 'else' part in conditionals"
+                 checkCondNode thenPartA thenPartB
+
+checkDependencies :: forall d vr. (Pretty d, IsDependency vr d) => ComponentName -> Check [d]
+checkDependencies componentName ds1 ds2 = do
+    forM_ removed $ \dep -> do
+        fail (unwords [ "Cannot remove existing", depKind, "on"
+                      , depKeyShow dproxy (depKey dep), "in", cnameStr, " component"])
+
+    forM_ added $ \dep ->
+        if depInAddWhitelist dep
+           then logChange (Change Normal (unwords ["added the", cnameStr, "component's"
+                                                  , depKind, "on"]) "" (prettyShow dep))
+           else fail (unwords [ "Cannot add new", depKind, "on"
+                              , depKeyShow dproxy (depKey dep)
+                              , "in", cnameStr, "component"])
+
+    forM_ changed $ \(depk, (verA, verB)) -> do
+        changesOk (unwords ["the", cnameStr, "component's", depKind, "on"
+                           , depKeyShow dproxy depk])
+                   prettyShow verA verB
+  where
+    (removed, changed, added) = computeCanonDepChange ds1 ds2
+
+    dproxy :: Proxy d
+    dproxy = Proxy
+
+    cnameStr = showComponentName componentName
+
+    depKind = depTypeName dproxy ++ " dependency"
+
+class (Ord (DepKey d), Pretty vr, Eq vr) => IsDependency vr d | d -> vr where
+    type DepKey d
+
+    depTypeName    :: Proxy d -> String
+    depKey         :: d -> DepKey d
+    depKeyShow     :: Proxy d -> DepKey d -> String
+    depVerRg       :: d -> vr
+    reconstructDep :: DepKey d -> vr -> d
+
+    depInAddWhitelist :: d -> Bool
+    depInAddWhitelist _ = False
+
+    intersectVr :: Proxy d -> vr -> vr -> vr
+
+instance IsDependency VersionRange Dependency where
+    type DepKey Dependency = PackageName
+
+    depTypeName Proxy             = "library"
+    depKey (Dependency pkgname _ _) = pkgname
+    depKeyShow Proxy              = prettyShow''
+    depVerRg (Dependency _ vr _)  = vr
+    reconstructDep                = \n vr -> Dependency n vr mainLibSet
+
+    depInAddWhitelist (Dependency pn _ _) = pn `elem`
+    -- Special case: there are some pretty weird broken packages out there, see
+    --   https://github.com/haskell/hackage-server/issues/303
+    -- which need us to add a new dep on `base`
+            [ mkPackageName "base"
+
+    -- See also https://github.com/haskell/hackage-server/issues/472
+    --
+    -- this is mostly to allow to add dependencies on `base-orphans == 0`
+    -- as otherwise we have no way to express when a package is
+    -- incompatible with the recently introduced `base-orphans` package
+    -- which started adopting orphan instances; in the long-term we need a
+    -- more general approach to this, as otherwise we'll end up adding
+    -- ad-hoc exceptions like this one. See e.g.
+    --   https://github.com/haskell/cabal/issues/3061
+    --
+            , mkPackageName "base-orphans"
+            ]
+
+    intersectVr _ = intersectVersionRanges
+
+
+instance IsDependency VersionRange ExeDependency where
+    type DepKey ExeDependency = (PackageName,UnqualComponentName)
+
+    depTypeName Proxy                   = "tool"
+    depKey (ExeDependency pkgname cn _) = (pkgname,cn)
+    depKeyShow Proxy (pkgname,cn)       = concat ["'", prettyShow pkgname, ":", prettyShow cn, "'"]
+    depVerRg (ExeDependency _ _ vr)     = vr
+    reconstructDep (pkgname,cn)         = ExeDependency pkgname cn
+
+    intersectVr _ = intersectVersionRanges
+
+instance IsDependency VersionRange LegacyExeDependency where
+    type DepKey LegacyExeDependency = String
+
+    depTypeName Proxy                      = "legacy-tool"
+    depKey (LegacyExeDependency tname _)   = tname
+    depKeyShow Proxy tname                 = "'" ++ tname ++ "'"
+    depVerRg (LegacyExeDependency _ vr)    = vr
+    reconstructDep                         = LegacyExeDependency
+
+    intersectVr _ = intersectVersionRanges
+
+    depInAddWhitelist (LegacyExeDependency pn _) = pn `elem`
+    -- list of trusted tools cabal supports w/o explicit build-tools
+    -- c.f. Distribution.Simple.BuildToolDepends.desugarBuildTool
+    -- and 'knownSuffixHandlers' in "Distribution.Client.Init.Heuristics"
+            [ "alex"
+            , "c2hs"
+            , "cpphs"
+            , "greencard"
+            , "happy"
+            , "hsc2hs"
+            ]
+
+instance IsDependency PkgconfigVersionRange PkgconfigDependency where
+    type DepKey PkgconfigDependency = PkgconfigName
+
+    depTypeName Proxy                      = "pkg-config"
+    depKey (PkgconfigDependency pkgname _) = pkgname
+    depKeyShow Proxy                       = prettyShow''
+    depVerRg (PkgconfigDependency _ vr)    = vr
+    reconstructDep                         = PkgconfigDependency
+
+    intersectVr _ = PcIntersectVersionRanges
+
+
+-- The result tuple represents the 3 canonicalised dependency
+-- (removed deps (old ranges), retained deps (old & new ranges), added deps (new ranges))
+-- or expressed as set-operations: (A \ B, (A ∩ B), B \ A)
+computeCanonDepChange :: forall vr d. IsDependency vr d => [d] -> [d] -> ([d],[(DepKey d,(vr,vr))],[d])
+computeCanonDepChange depsA depsB
+    = ( mapToDeps (a `Map.difference` b)
+      , Map.toList $ Map.intersectionWith (,) a b
+      , mapToDeps (b `Map.difference` a)
+      )
+  where
+    a = depsToMapWithCanonVerRange depsA
+    b = depsToMapWithCanonVerRange depsB
+
+    depsToMapWithCanonVerRange
+        = Map.fromListWith (flip $ intersectVr (Proxy :: Proxy d)) .
+          map (\d -> (depKey d, depVerRg d))
+
+    mapToDeps
+        = map (\(pkgname, verrange) -> reconstructDep pkgname verrange) . Map.toList
+
+
+checkSetupBuildInfo :: Check (Maybe SetupBuildInfo)
+checkSetupBuildInfo Nothing  Nothing = return ()
+checkSetupBuildInfo (Just _) Nothing =
+    fail "Cannot remove a 'custom-setup' section"
+
+checkSetupBuildInfo Nothing (Just (SetupBuildInfo setupDependsA _internalA)) =
+    logChange $ Change Normal
+                       ("added a 'custom-setup' section with 'setup-depends'")
+                       "[implicit]" (intercalate ", " (map prettyShow setupDependsA))
+
+checkSetupBuildInfo (Just (SetupBuildInfo setupDependsA _internalA))
+                    (Just (SetupBuildInfo setupDependsB _internalB)) = do
+    forM_ removed $ \dep ->
+      logChange $ Change Normal ("removed 'custom-setup' dependency on") (prettyShow dep) ""
+    forM_ added $ \dep ->
+      logChange $ Change Normal ("added 'custom-setup' dependency on") "" (prettyShow dep)
+    forM_ changed $ \(pkgn, (verA, verB)) ->
+        changesOk ("the 'custom-setup' dependency on " ++ prettyShow'' pkgn)
+                  prettyShow verA verB
+  where
+    (removed, changed, added) =
+      computeCanonDepChange setupDependsA setupDependsB
+
+checkLibrary :: ComponentName -> Check Library
+checkLibrary componentName
+             (Library modulesA reexportedA requiredSigsA exposedSigsA
+                      exposedA visibilityA buildInfoA)
+             (Library modulesB reexportedB requiredSigsB exposedSigsB
+                      exposedB visibilityB buildInfoB) = do
+  checkSame "Cannot change the exposed modules" modulesA modulesB
+  checkSame "Cannot change the re-exported modules" reexportedA reexportedB
+  checkSame "Cannot change the required signatures" requiredSigsA requiredSigsB
+  checkSame "Cannot change the exposed signatures"  exposedSigsA  exposedSigsB
+  checkSame "Cannot change the package exposed status" exposedA exposedB
+  checkSame "Cannot change the package visibility" visibilityA visibilityB
+  checkBuildInfo componentName buildInfoA buildInfoB
+
+checkForeignLib :: ComponentName -> Check ForeignLib
+checkForeignLib componentName
+             (ForeignLib nameA typeA optionsA buildInfoA verA verLinuxA modDefA)
+             (ForeignLib nameB typeB optionsB buildInfoB verB verLinuxB modDefB) = do
+  checkSame "Cannot change the foreign library name" nameA nameB
+  checkSame "Cannot change the foreign library type" typeA typeB
+  checkSame "Cannot change the foreign library options" optionsA optionsB
+  checkSame "Cannot change the foreign library version" verA verB
+  checkSame "Cannot change the foreign library version for Linux" verLinuxA verLinuxB
+  checkSame "Cannot change the module definition files" modDefA modDefB
+  checkBuildInfo componentName buildInfoA buildInfoB
+
+checkExecutable :: ComponentName -> Check Executable
+checkExecutable componentName
+                (Executable _nameA pathA scopeA buildInfoA)
+                (Executable _nameB pathB scopeB buildInfoB) = do
+  checkSame "Cannot change build information" pathA pathB
+  checkSame "Cannot change executable scope" scopeA scopeB
+  checkBuildInfo componentName buildInfoA buildInfoB
+
+checkTestSuite :: ComponentName -> Check TestSuite
+checkTestSuite componentName
+               (TestSuite _nameA interfaceA buildInfoA)
+               (TestSuite _nameB interfaceB buildInfoB) = do
+  checkSame "Cannot change test-suite type" interfaceA interfaceB
+  checkBuildInfo componentName buildInfoA buildInfoB
+
+checkBenchmark :: ComponentName -> Check Benchmark
+checkBenchmark componentName
+               (Benchmark _nameA interfaceA buildInfoA)
+               (Benchmark _nameB interfaceB buildInfoB) = do
+  checkSame "Cannot change benchmark type" interfaceA interfaceB
+  checkBuildInfo componentName buildInfoA buildInfoB
+
+checkBuildInfo :: ComponentName -> Check BuildInfo
+checkBuildInfo componentName biA biB = do
+    -- @other-extension@
+    changesOkSet ("'other-extensions' in " ++ showComponentName componentName ++ " component")
+              prettyShow
+              (Set.fromList $ otherExtensions biA) (Set.fromList $ otherExtensions biB)
+
+    -- @buildable@
+    changesOk ("'buildable' in " ++ showComponentName componentName ++ " component") prettyShow
+              (buildable biA) (buildable biB)
+
+    -- @build-tool-depends@
+    checkDependencies componentName
+        (buildToolDepends biA)
+        (buildToolDepends biB)
+
+    -- @build-tools@
+    checkDependencies componentName
+        (buildTools biA)
+        (buildTools biB)
+
+    -- @pkgconfig-depends@
+    checkDependencies componentName
+        (pkgconfigDepends biA)
+        (pkgconfigDepends biB)
+
+    checkSame "Cannot change build information (just the dependency version constraints)"
+              (biA { targetBuildDepends = [], otherExtensions = [], buildTools = [], buildToolDepends = [], pkgconfigDepends = [], buildable = True })
+              (biB { targetBuildDepends = [], otherExtensions = [], buildTools = [], buildToolDepends = [], pkgconfigDepends = [], buildable = True })
+
+changesOk' :: Eq a => Severity -> String -> (a -> String) -> Check a
+changesOk' rel what render a b
+  | a == b    = return ()
+  | otherwise = logChange (Change rel what (render a) (render b))
+
+changesOk :: Eq a => String -> (a -> String) -> Check a
+changesOk = changesOk' Normal
+
+changesOkList :: (String -> (a -> String) -> Check a)
+              -> String -> (a -> String) -> Check [a]
+changesOkList changesOkElem what render = go
+  where
+    go []     []     = return ()
+    go (a:_)  []     = logChange (Change Normal ("removed " ++ what) (render a) "")
+    go []     (b:_)  = logChange (Change Normal ("added "   ++ what) "" (render b))
+    go (a:as) (b:bs) = changesOkElem what render a b >> go as bs
+
+changesOkSet :: Ord a => String -> (a -> String) -> Check (Set.Set a)
+changesOkSet what render old new = do
+    unless (Set.null removed) $
+        logChange (Change Normal ("removed " ++ what) (renderSet removed) "")
+    unless (Set.null added) $
+        logChange (Change Normal ("added " ++ what) "" (renderSet added))
+    return ()
+  where
+    added   = new Set.\\ old
+    removed = old Set.\\ new
+    renderSet = intercalate ", " . map render . Set.toList
+
+
+-- | Single-quote-wrapping 'prettyShow'
+prettyShow'' :: Pretty a => a -> String
+prettyShow'' x = "'" ++ prettyShow x ++ "'"
+
+checkSame :: Eq a => String -> Check a
+checkSame msg x y | x == y    = return ()
+                  | otherwise = fail msg
+
+checkList :: String -> Check a -> Check [a]
+checkList _   _         []     []     = return ()
+checkList msg checkElem (x:xs) (y:ys) = checkElem x y
+                                     >> checkList msg checkElem xs ys
+checkList msg _         _      _      = fail msg
+
+checkListAssoc :: Eq b => String -> Check a -> Check [(b,a)]
+checkListAssoc _   _         [] [] = return ()
+checkListAssoc msg checkElem ((kx,x):xs) ((ky,y):ys)
+                       | kx == ky  = checkElem x y
+                                  >> checkListAssoc msg checkElem xs ys
+                       | otherwise = fail msg
+checkListAssoc msg _         _  _  = fail msg
+
+checkNotPresent :: String -> Check (Maybe String)
+checkNotPresent msg _ (Just _) = fail msg
+checkNotPresent _ _ Nothing = return ()
+
+checkMaybe :: String -> Check a -> Check (Maybe a)
+checkMaybe _   _     Nothing  Nothing  = return ()
+checkMaybe _   check (Just x) (Just y) = check x y
+checkMaybe msg _     _        _        = fail msg
+
+ppTestedWith :: [(CompilerFlavor, VersionRange)] -> Doc
+ppTestedWith = hsep . punctuate colon . map (uncurry ppPair)
+  where
+    ppPair compiler vr = text (prettyShow compiler) <+> text (prettyShow vr)
+
+ppSourceRepo :: SourceRepo -> PrettyField ()
+ppSourceRepo repo = PrettySection () "source-repository" [pretty kind] $
+    prettyFieldGrammar cabalSpecLatest (sourceRepoFieldGrammar kind) repo
+  where
+    kind = repoKind repo
+
+-- TODO: Verify that we don't need to worry about UTF8
+-- | Insert or update \"x-revision:\" field
+insertRevisionField :: Int -> ByteString -> ByteString
+insertRevisionField rev
+    | rev == 1  = LBS8.unlines . insertAfterVersion . LBS8.lines
+    | otherwise = LBS8.unlines . replaceRevision    . LBS8.lines
+  where
+    replaceRevision [] = []
+    replaceRevision (ln:lns)
+      | isField (LBS8.pack "x-revision") ln
+      = LBS8.pack ("x-revision: " ++ show rev) : lns
+
+      | otherwise
+      = ln : replaceRevision lns
+
+    insertAfterVersion [] = []
+    insertAfterVersion (ln:lns)
+      | isField (LBS8.pack "version") ln
+      = ln : LBS8.pack ("x-revision: " ++ show rev) : lns
+
+      | otherwise
+      = ln : insertAfterVersion lns
+
+    isField nm ln
+      | LBS8.isPrefixOf nm (LBS8.map Char.toLower ln)
+      , let (_, t) = LBS8.span (\c -> c == ' ' || c == '\t')
+                             (LBS8.drop (LBS8.length nm) ln)
+      , Just (':',_) <- LBS8.uncons t
+                  = True
+      | otherwise = False
diff --git a/src/CabalEdit.hs b/src/CabalEdit.hs
new file mode 100644
--- /dev/null
+++ b/src/CabalEdit.hs
@@ -0,0 +1,98 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+-- |
+-- Copyright   :  Herbert Valerio Riedel
+-- SPDX-License-Identifier: GPL-3.0-or-later
+module CabalEdit
+    ( cabalEditXRev
+    , cabalSplitAtField
+    , PkgRev
+    ) where
+
+import           Data.ByteString                        (ByteString)
+import qualified Data.ByteString.Char8                  as BS8
+#if !MIN_VERSION_base(4,11,0)
+import           Data.Semigroup                         ((<>))
+#endif
+import qualified Distribution.Fields.Field              as C
+import qualified Distribution.Fields.Parser             as C
+import qualified Distribution.Parsec.Position           as C (Position(Position))
+
+type PkgRev  = Word
+
+-- | Insert or replace existing "x-revision" line
+--
+-- __NOTE__: This uses 'cabalSplitAtField' and therefore currently
+-- supports only simplified (i.e. without use of @{@ @}@ layout
+-- tokens) Cabal file grammar
+cabalEditXRev :: PkgRev -> ByteString -> ByteString
+cabalEditXRev xrev oldcab = pre1 <> mid1 <> xrevLine <> post1
+  where
+    (pre0,_,post0)    = either (error . show) id $ cabalSplitAtField "x-revision" oldcab
+    (pre1,mid1,post1) = either (error . show) id $ cabalSplitAtField "version" (pre0 <> post0)
+
+    ls = BS8.lines oldcab
+
+    xrevLine = "x-revision: " <> BS8.pack (show xrev) <> if isCRLF then "\r\n" else "\n"
+
+    -- simple heuristic
+    isCRLF = case ls of
+        []     -> False
+        ("":_) -> False
+        (l1:_) -> BS8.last l1 == '\r'
+
+
+-- | Split a cabal file into three fragments: before the (first
+-- occurence of a field), the field, and the rest after the field.
+--
+-- This should, in spirit, follow the invariant (assuming there are no parsing failures):
+--
+-- > cabalSplitAtField someField (pre <> mid <> post) == Right (pre, mid, post)
+--
+-- If field not found, the middle and trailing fragment will be empty 'ByteString's
+--
+-- This operation is quite universal; it can be used to remove fields,
+-- or insert content before a field or after a field etc
+--
+-- __NOTE__: This doesn't properly handle layout-mode (i.e. as controlled by the @{@ and @}@ tokens) yet
+cabalSplitAtField :: ByteString -- ^ fieldname
+                  -> ByteString -- ^ @.cabal@ description
+                  -> Either String (ByteString,ByteString,ByteString) -- ^ pre, mid, post
+cabalSplitAtField fname cab0 = do
+    fields <- either (Left . show) Right $ C.readFields cab0
+
+    case [ (npos, vals) | C.Field (C.Name (C.Position npos _) n) vals <- fields, n == fname ] of
+      [] -> pure (cab0, BS8.empty, BS8.empty)
+      (npos, vals):_ -> case vals of
+                          [] -> pure $! go (npos-1) 1
+
+                          _ -> let C.FieldLine (C.Position pos2 _) _ = last vals
+                               in  pure $! go (npos-1) (1+pos2-npos)
+
+  where
+    cab0lines = BS8.lines cab0
+
+    -- TODO: detect '{'s in `mid` and fixup k to include the closing '}'
+    --
+    -- NB: it's not enough to simply look at the next field's line
+    -- number, as we don't want 'mid' to include trailing comments
+    -- which conventionally belong to the next fields; i.e. in case of
+    -- something like
+    --
+    --   name: foo
+    --   version: 1.2.3
+    --   x-revision: {
+    --   42
+    --   }
+    --   -- for reasons this must remain Custom instead of Simple
+    --   build-type: Custm
+    --   synopsis: ...
+    --
+    -- We want the middle of 'cabalSplitAtField "x-revision"' to stop
+    -- right after the '}' and *not* include the comment
+    --
+    go j k = (BS8.unlines pre, BS8.unlines mid, BS8.unlines post)
+      where
+        (pre, midpost) = splitAt j cab0lines
+        (mid,post)     = splitAt k midpost
diff --git a/src/IndexShaSum.hs b/src/IndexShaSum.hs
new file mode 100644
--- /dev/null
+++ b/src/IndexShaSum.hs
@@ -0,0 +1,161 @@
+{-# LANGUAGE BangPatterns       #-}
+{-# LANGUAGE CPP                #-}
+{-# LANGUAGE OverloadedStrings  #-}
+{-# LANGUAGE RecordWildCards    #-}
+{-# LANGUAGE StandaloneDeriving #-}
+
+-- |
+-- Module      :  IndexShaSum
+-- Copyright   :  Herbert Valerio Riedel
+-- SPDX-License-Identifier: GPL-3.0-or-later
+--
+module IndexShaSum (run, IndexShaSumOptions(..)) where
+
+import qualified Codec.Archive.Tar      as Tar
+import qualified Codec.Compression.GZip as GZip
+import           Control.Monad
+import qualified Data.Aeson             as J
+import qualified Data.Aeson.Types       as J
+import           Data.ByteString        (ByteString)
+import qualified Data.ByteString        as BS
+import qualified Data.ByteString.Lazy   as BSL
+import qualified Data.ByteString.Short  as BSS
+import           Data.Maybe
+import           Data.Set               (Set)
+import qualified Data.Set               as Set
+import           Data.String
+import           Data.Text              (Text)
+import           Data.Text.Encoding     as T
+import           System.FilePath
+
+#if !MIN_VERSION_base(4,11,0)
+import           Data.Semigroup         ((<>))
+#endif
+
+#if MIN_VERSION_aeson(2,0,0)
+import qualified Data.Aeson.Key         as Key
+import qualified Data.Aeson.KeyMap      as KeyMap
+type Key = J.Key
+
+keyToText :: Key -> Text
+keyToText = Key.toText
+#else
+import qualified Data.HashMap.Strict    as KeyMap
+type Key = Text
+
+keyToText :: Key -> Text
+keyToText = id
+#endif
+
+data IndexShaSumOptions = IndexShaSumOptions
+    { optFlatStyle   :: Bool
+    , optISSIndexTar :: FilePath
+    , optBaseDir     :: Maybe FilePath
+    } deriving Show
+
+type SrcTarName   = BSS.ShortByteString -- with .tar.gz suffix
+type SrcTarSha256 = BSS.ShortByteString -- base16
+
+run :: IndexShaSumOptions -> IO ()
+run (IndexShaSumOptions {..}) = do
+    idx <- readTarEntries optISSIndexTar
+    forM_ (collect idx) (uncurry printSumLine)
+  where
+    printSumLine fn sh256 = BS.putStr line
+      where
+        line = mconcat [BSS.fromShort (fixupSum sh256), "  " , bdirpfx, BSS.fromShort fn', "\n"]
+        bdirpfx = maybe "" fromString optBaseDir
+        fn' = if optFlatStyle then fn else unFlat fn
+
+    -- | Missing checksums are denoted by a 0-checksum
+    fixupSum x
+       | BSS.null x  = BSS.toShort (BS.replicate 64 48)
+       | otherwise   = x
+
+    collect :: [Tar.Entry] -> [(SrcTarName,SrcTarSha256)]
+    collect = go mempty mempty
+
+    go :: Set SrcTarName -> Set SrcTarName -> [Tar.Entry] -> [(SrcTarName,SrcTarSha256)]
+    go !seen1 !seen2 []
+      | missingCabs <- Set.difference seen1 seen2
+      , not (Set.null missingCabs) = error "missing .cabal file(s)"
+      | otherwise -- append files with missing checksum
+      = [ (missingSum, "") | missingSum <- Set.toList (Set.difference seen2 seen1) ]
+    go !seen1 !seen2 (e:es)
+      | takeExtension fn == ".cabal"
+      , [pn,pv,_cn] <- splitDirectories fn
+      = let fn' = fromString (pn ++ "-" ++ pv ++ ".tar.gz")
+        in go seen1 (Set.insert fn' seen2) es
+      | takeFileName fn == "package.json"
+      , Tar.NormalFile bs _sz <- Tar.entryContent e
+      = let (fn',cksum) = fromMaybe undefined (decodePkgJsonFile bs)
+        in if Set.member fn' seen1
+           then go seen1 seen2 es
+           else ((fn',cksum) : go (Set.insert fn' seen1) seen2 es)
+      | otherwise = go seen1 seen2 es
+      where
+        fn = Tar.entryPath e
+
+-- | Convert to non-flat layout (i.e. @<name>/<ver>/<name>-<ver>.tar.gz@)
+unFlat :: SrcTarName -> SrcTarName
+unFlat fn0 = BSS.toShort $ mconcat [pn <> "/" <> pv <> "/" <> fn0']
+  where
+    fn0' = BSS.fromShort fn0
+
+    Just base = stripSuffixBS ".tar.gz" fn0'
+
+    (pn_, pv) = BS.spanEnd (\c -> (c >= 0x30 && c <= 0x3a) || c == 0x2e) base
+    Just (pn, 0x2d) = BS.unsnoc pn_
+
+-- | Read tarball lazily (and possibly decompress)
+readTarEntries :: FilePath -> IO [Tar.Entry]
+readTarEntries idxtar = do
+    es <- case takeExtension idxtar of
+            ".gz"  -> Tar.read . GZip.decompress <$> BSL.readFile idxtar
+            ".tar" -> Tar.read                   <$> BSL.readFile idxtar
+            ext    -> error ("unknown extension " ++ show ext)
+
+    return (Tar.foldEntries (:) [] (\err -> error ("readTarEntries " ++ show err)) es)
+
+-- | Decode and extract source-tarball filename and sha256 checksum from TUF @package.json@
+decodePkgJsonFile :: BSL.ByteString -> Maybe (SrcTarName, SrcTarSha256)
+decodePkgJsonFile bs = do
+    metainfo <- J.decode' bs
+    [(fn,s256)] <- packagejson2sha metainfo
+
+    return $! strictPair (BSS.toShort $ normaliseFn fn) (BSS.toShort s256)
+  where
+    normaliseFn fn = fromMaybe fn $ stripPrefixBS "<repo>/package/" fn
+
+    packagejson2sha :: J.Value -> Maybe [(ByteString, ByteString)]
+    packagejson2sha = J.parseMaybe go1
+      where
+        go1 :: J.Value -> J.Parser [(ByteString, ByteString)]
+        go1 = J.withObject "PackageJson" $ \o -> do
+            signed   <- o      J..: "signed"
+            targets  <- signed J..: "targets"
+            J.withObject "PackageJson.signed.targets" go2 targets
+
+        go2 :: J.Object -> J.Parser [(ByteString, ByteString)]
+        go2 m = forM (KeyMap.toList m) $ \(k,v) -> do
+            J.withObject ".targets{}" (go3 k) v
+
+        go3 :: Key -> J.Object -> J.Parser (ByteString, ByteString)
+        go3 k o = do
+            hashes <- o J..: "hashes"
+            sh256 <- hashes J..: "sha256"
+            return (T.encodeUtf8 (keyToText k), T.encodeUtf8 sh256)
+
+strictPair :: a -> b -> (a,b)
+strictPair !a !b = (a,b)
+
+stripPrefixBS :: ByteString -> ByteString -> Maybe ByteString
+stripPrefixBS pfx b
+  | BS.isPrefixOf pfx b = Just $ BS.drop (BS.length pfx) b
+  | otherwise           = Nothing
+
+
+stripSuffixBS :: ByteString -> ByteString -> Maybe ByteString
+stripSuffixBS sfx b
+  | BS.isSuffixOf sfx b = Just $ BS.take (BS.length b - BS.length sfx) b
+  | otherwise           = Nothing
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,958 @@
+{-# LANGUAGE BangPatterns      #-}
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE LambdaCase        #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TemplateHaskell   #-}
+{-# LANGUAGE ViewPatterns      #-}
+
+-- |
+-- Module      :  Main
+-- Copyright   :  Herbert Valerio Riedel, Andreas Abel
+-- SPDX-License-Identifier: GPL-3.0-or-later
+--
+module Main where
+
+import qualified Data.Aeson                           as J
+import qualified Data.Aeson.Types                     as J
+import qualified Data.ByteString.Builder              as Builder
+import           Control.DeepSeq
+import           Control.Exception
+import           Control.Monad
+import           Control.Monad.State.Strict
+import           Data.Bits
+import           Data.ByteString                        (ByteString)
+import qualified Data.ByteString                        as BS
+import qualified Data.ByteString.Char8                  as BS8
+import qualified Data.ByteString.Lazy                   as BSL
+import qualified Data.ByteString.Search                 as BSS
+import           Data.Char                              (isSpace)
+import           Data.Foldable                          (toList)
+import qualified Data.List                              as List
+import           Data.Maybe
+import           Data.Time.Clock.POSIX                  (getPOSIXTime)
+#if !MIN_VERSION_base(4,11,0)
+import           Data.Semigroup                         (Semigroup(..))
+#endif
+
+import qualified Distribution.Package                   as C
+import qualified Distribution.PackageDescription        as C
+import qualified Distribution.PackageDescription.Parsec as C
+import qualified Distribution.Parsec                    as C
+import qualified Distribution.Fields                    as C
+import qualified Distribution.Fields.Field              as C (fieldAnn)
+import qualified Distribution.Pretty                    as C
+import qualified Distribution.Verbosity                 as C
+import qualified Distribution.Version                   as C
+import qualified Distribution.Text                      as C
+
+import           Lens.Micro
+import           Lens.Micro.Mtl
+import           Lens.Micro.TH
+
+import           Network.Http.Client
+import           Network.NetRc
+import           Numeric.Natural                        (Natural)
+import           Options.Applicative                    as OA
+import           System.Directory
+import           System.Environment                     (lookupEnv)
+import           System.Exit                            (ExitCode (..), exitFailure)
+import           System.FilePath
+import           System.IO.Error                        (tryIOError, isDoesNotExistError)
+import qualified System.IO.Streams                      as Streams
+import           System.Process.ByteString              (readProcessWithExitCode)
+import           Text.HTML.TagSoup
+import           Text.Printf                            (printf)
+import qualified Paths_hackage_cli
+import qualified Data.Version as V
+
+import qualified Distribution.Types.BuildInfo.Lens                 as LC
+import qualified Distribution.Types.GenericPackageDescription.Lens as LC
+
+-- import Cabal
+
+import Distribution.Server.Util.CabalRevisions
+import IndexShaSum
+import CabalEdit
+
+type PkgName = ByteString
+type PkgVer  = ByteString
+
+showVersion :: C.Version -> String
+showVersion = C.display
+
+pkgVerToVersion :: PkgVer -> C.Version
+pkgVerToVersion = fromMaybe (error "invalid version") . C.simpleParse . BS8.unpack
+
+pkgVerInRange :: PkgVer -> C.VersionRange -> Bool
+pkgVerInRange v vr = pkgVerToVersion v `C.withinRange` vr
+
+type HIO = StateT HConn IO
+
+data HConn = HConn
+    { _hcMkConn :: IO Connection
+    , _hcConn   :: Maybe Connection
+    , _hcReqCnt :: Natural -- ^ requests submitted on current 'Connection'
+    , _hcRspCnt :: Natural -- ^ responses read from current 'Connection'
+    }
+
+makeLenses ''HConn
+
+-- | Requests that can be issued in current connection before exhausting the 50-req/conn server limit
+hcReqLeft :: SimpleGetter HConn Natural -- (Natural -> f Natural) -> HConn -> f HConn
+hcReqLeft = hcReqCnt . to f
+  where
+    f n | n > lim   = 0
+        | otherwise = lim - n
+    lim = 50
+
+setUA :: RequestBuilder ()
+setUA = setHeader "User-Agent" uaStr
+  where
+    uaStr = "hackage-cli/" <> BS8.pack (V.showVersion Paths_hackage_cli.version)
+
+hackageSendGET :: ByteString -> ByteString -> HIO ()
+hackageSendGET p a = do
+    q1 <- liftIO $ buildRequest $ do
+        http GET p
+        setUA
+        setAccept a
+
+    lft <- use hcReqLeft
+    unless (lft > 0) $
+        fail "hackageSendGET: request budget exhausted for current connection"
+
+    c <- openHConn
+    liftIO $ sendRequest c q1 emptyBody
+    hcReqCnt += 1
+
+hackagePutTgz :: ByteString -> ByteString -> HIO ByteString
+hackagePutTgz p tgz = do
+    q1 <- liftIO $ buildRequest $ do
+        http PUT p
+        setUA
+        -- setAccept "application/json" -- wishful thinking
+        setContentType "application/x-tar"
+        -- setContentEncoding "gzip"
+        setContentLength (fromIntegral $ BS.length tgz)
+
+    lft <- use hcReqLeft
+    unless (lft > 0) $
+        fail "hackagePutTgz: request budget exhausted for current connection"
+
+    c <- openHConn
+    liftIO $ sendRequest c q1 (bsBody tgz)
+    resp <- liftIO $ try (receiveResponse c concatHandler')
+    closeHConn
+    hcReqCnt += 1
+
+    case resp of
+        Right bs -> -- do
+            -- liftIO $ BS.writeFile "raw.out" bs
+            return bs
+
+        Left e@HttpClientError {} -> -- do
+            return (BS8.pack $ show e)
+
+hackageRecvResp :: HIO ByteString
+hackageRecvResp = do
+    c <- openHConn
+    reqCnt <- use hcReqCnt
+    rspCnt <- use hcRspCnt
+    unless (reqCnt > rspCnt) $
+        fail "hackageRecvResp: not response available to receive"
+
+    resp <- liftIO $ receiveResponse c concatHandler'
+    hcRspCnt += 1
+
+    return resp
+
+-- | Whether to operate in review/test mode or publish the revision for real
+data DryWetRun = DryRun -- ^ review/test mode
+               | WetRun -- ^ publish
+
+hackagePostCabal :: (ByteString,ByteString) -> (PkgName,PkgVer) -> ByteString -> DryWetRun -> HIO ByteString
+hackagePostCabal cred (pkgn,pkgv) rawcab dry = do
+    when (boundary `BS.isInfixOf` rawcab) $ fail "WTF... cabal file contains boundary-pattern"
+
+    q1 <- liftIO $ buildRequest $ do
+        http POST urlpath
+        setUA
+        uncurry setAuthorizationBasic cred
+        setAccept "application/json" -- wishful thinking
+        setContentType ("multipart/form-data; boundary="<>boundary) -- RFC2388
+        setContentLength bodyLen
+
+    c <- reOpenHConn
+
+    liftIO $ sendRequest c q1 (bsBody body)
+
+    resp <- liftIO $ try (receiveResponse c concatHandler')
+    closeHConn
+
+    case resp of
+        Right bs -> -- do
+            -- liftIO $ BS.writeFile "raw.out" bs
+            return (BS8.unlines [ h2 <> ":\n" <> renderTags ts | (h2, ts) <- scrape200 bs ])
+
+        Left e@HttpClientError {} -> -- do
+            -- Hackage currently timeouts w/ 503 guru meditation errors,
+            -- which usually means that the transaction has succeeded
+            -- liftIO $ BS.writeFile "raw.out" bs
+            return (BS8.pack $ show e)
+  where
+    urlpath = mconcat [ "/package/", pkgn, "-", pkgv, "/", pkgn, ".cabal/edit" ]
+
+    isDry DryRun = True
+    isDry WetRun = False
+
+    body = Builder.toLazyByteString $ multiPartBuilder boundary
+           [ ("cabalfile",[],[],rawcab)
+           , if isDry dry
+             then ("review", [],[],"Review changes")
+             else ("publish",[],[],"Publish new revision")
+           ]
+
+    bodyLen = fromIntegral $ BSL.length body
+
+    boundary = "4d5bb1565a084d78868ff0178bdf4f61"
+
+    -- scrape200 :: ByteString -> (Bool, h2parts)
+    scrape200 html = h2parts
+      where
+        tags = parseTags (html :: ByteString)
+
+        h2parts = [ (t,map cleanText $ takeWhile (/= TagClose "form") xs)
+                  | (TagOpen "h2" _: TagText t: TagClose "h2": xs) <- partitions (== TagOpen "h2" []) tags
+                  , t /= "Advice on adjusting version constraints" ]
+
+        cleanText (TagText t)
+          | t' == "", '\n' `BS8.elem` t = TagText "\n"
+          | otherwise               = TagText t
+          where
+            t' = fst . BS8.spanEnd (=='\n') . BS8.dropWhile (=='\n') $ t
+        cleanText x = x
+
+class ToBuilder a where
+  toBuilder :: a -> Builder.Builder
+
+instance ToBuilder ByteString where
+  toBuilder = Builder.byteString
+
+instance ToBuilder BSL.ByteString where
+  toBuilder = Builder.lazyByteString
+
+bsBody :: ToBuilder a => a -> Streams.OutputStream Builder.Builder -> IO ()
+bsBody bs = Streams.write (Just (toBuilder bs))
+
+-- | Upload a candidate to Hackage
+--
+-- This is a bit overkill, as one could easily just use @curl(1)@ for this:
+--
+-- > curl --form package=@"$PKGID".tar.gz -u "${CREDS}" https://hackage.haskell.org/packages/candidates/
+--
+hackagePushCandidate :: (ByteString,ByteString) -> (FilePath,ByteString) -> HIO ByteString
+hackagePushCandidate cred (tarname,rawtarball) = do
+    when (boundary `BS.isInfixOf` rawtarball) $ fail "WTF... tarball contains boundary-pattern"
+
+    q1 <- liftIO $ buildRequest $ do
+        http POST urlpath
+        setUA
+        uncurry setAuthorizationBasic cred
+        setAccept "application/json" -- wishful thinking
+        setContentType ("multipart/form-data; boundary="<>boundary) -- RFC2388
+        setContentLength bodyLen
+
+    c <- reOpenHConn
+
+    liftIO $ sendRequest c q1 (bsBody body)
+
+    resp <- liftIO $ try (receiveResponse c (\r is -> (,) r <$> concatHandler r is))
+    closeHConn
+
+    case resp of
+        Right (rc,bs) -> do
+            return (BS8.pack (show rc) <> bs)
+        Left (HttpClientError code bs) -> return (BS8.pack ("code=" <> show code <> "\n") <> bs)
+            -- Hackage currently timeouts w/ 503 guru meditation errors,
+            -- which usually means that the transaction has succeeded
+  where
+    urlpath = "/packages/candidates/"
+
+    body = Builder.toLazyByteString $
+           multiPartBuilder boundary [ ("package", [("filename", BS8.pack tarname)]
+                                     , ["Content-Type: application/gzip"], rawtarball)]
+    bodyLen = fromIntegral $ BSL.length body
+
+    boundary = "4d5bb1565a084d78868ff0178bdf4f61"
+
+-- | Simplified RFC2388 multipart/form-data formatter
+--
+-- TODO: make a streaming-variant
+multiPartBuilder :: ByteString -> [(ByteString,[(ByteString,ByteString)],[ByteString],ByteString)] -> Builder.Builder
+multiPartBuilder boundary mparts = mconcat $ concatMap mkPart mparts ++ trailer
+  where
+    mkPart (name, xprops, xhdrs, payload)
+        = [ dash, bs boundary, crlf
+          , bs"Content-Disposition: form-data; name=\"", bs name, bs"\""
+          ] ++
+          concat [ [bs "; ", bs k, bs"=\"", bs v, bs"\"" ] | (k,v) <- xprops ] ++ [ crlf ] ++
+          concat [ [bs h, crlf] | h <- xhdrs ] ++
+          [ crlf
+          , bs payload, crlf
+          ]
+
+    trailer = [ dash, bs boundary, dash, crlf ]
+
+    crlf = bs"\r\n"
+    dash = bs"--"
+    bs = Builder.byteString
+
+fetchVersions :: PkgName -> HIO [(PkgVer,PkgVerStatus)]
+fetchVersions pkgn = do
+    hackageSendGET ("/package/" <> pkgn <> "/preferred") "application/json"
+    resp <- hackageRecvResp
+    case decodeVersions resp of
+        Right xs -> pure xs
+        Left err -> fail err
+
+fetchCabalFile :: PkgName -> PkgVer -> HIO ByteString
+fetchCabalFile pkgn pkgv = do
+    hackageSendGET urlpath "text/plain"
+    hackageRecvResp
+  where
+    urlpath = mconcat ["/package/", pkgn, "-", pkgv, "/", pkgn, ".cabal"]
+
+fetchCabalFiles :: PkgName -> [PkgVer] -> HIO [(PkgVer,ByteString)]
+fetchCabalFiles pkgn pkgvs0 = do
+    -- HTTP pipelining
+    tmp <- go [] pkgvs0
+    return (concat . reverse $ tmp)
+  where
+    go acc [] = pure acc
+    go acc vs0 = do
+        (_,lft) <- getHConn
+        let (vs,vs') = nsplitAt lft vs0
+        when (null vs) $ fail "fetchCabalFiles: the impossible happened"
+
+        -- HTTP-pipeline requests; compensates a bit for SSL-induced latency
+        mcabs <- forM (mkPipeline 4 vs) $ \case
+            Left pkgv -> do -- request
+                let urlpath = mconcat ["/package/", pkgn, "-", pkgv, "/", pkgn, ".cabal"]
+                -- liftIO $ putStrLn $ show urlpath
+                hackageSendGET urlpath "text/plain"
+                return Nothing
+
+            Right pkgv -> do -- response
+                -- liftIO $ putStrLn ("read " ++ show pkgv)
+                resp <- hackageRecvResp
+                return $ Just (pkgv, resp)
+
+        go (catMaybes mcabs : acc) vs'
+
+-- Left means request; Right means receive
+mkPipeline :: Natural -> [a] -> [Either a a]
+mkPipeline maxQ vs
+  | not postCond = error "mkPipeline: internal error" -- paranoia
+  | otherwise    = concat [ map Left rqs1
+                          , concat [ [Left v1, Right v2] | (v1,v2) <- zip rqs2 res2 ]
+                          , map Right res3
+                          ]
+  where
+    (rqs1,rqs2) = nsplitAt n vs
+    (res2,res3) = nsplitAt m vs
+
+    postCond = sameLen rqs2 res2 && sameLen rqs1 res3
+
+    l = nlength vs
+    n = min l maxQ
+    m = l-n
+
+    sameLen [] []         = True
+    sameLen (_:xs) (_:ys) = sameLen xs ys
+    sameLen [] (_:_)      = False
+    sameLen (_:_) []      = False
+
+fetchAllCabalFiles :: PkgName -> C.VersionRange -> HIO [(PkgVer,Maybe ByteString)]
+fetchAllCabalFiles pkgn vrange = do
+    vs <- fetchVersions pkgn
+    liftIO $ putStrLn ("Found " ++ show (length vs) ++ " package versions for " ++ show pkgn ++ ", downloading now...")
+
+    let (wanted,unwanted) = List.partition (`pkgVerInRange` vrange) (map fst vs)
+
+    fetched <- map (fmap Just) <$> fetchCabalFiles pkgn wanted
+    pure (List.sortOn (pkgVerToVersion . fst) (fetched ++ [ (v,Nothing) | v <- unwanted ]))
+
+data PkgVerStatus = Normal | UnPreferred | Deprecated deriving (Eq,Show)
+instance NFData PkgVerStatus where rnf !_ = ()
+
+decodeVersions :: ByteString -> Either String [(PkgVer,PkgVerStatus)]
+decodeVersions bs = do
+    obj <- J.eitherDecode' (BSL.fromStrict bs)
+    flip J.parseEither obj $ \o -> do
+        normal <- o J..:? "normal-version"
+        unpreferred <- o J..:? "unpreferred-version"
+        deprecated <- o J..:? "deprecated-version"
+        pure $ toPairs Normal normal ++ toPairs UnPreferred unpreferred ++ toPairs Deprecated deprecated
+  where
+    toPairs :: PkgVerStatus -> Maybe [String] -> [(PkgVer,PkgVerStatus)]
+    toPairs s (Just vs) = [(BS8.pack v, s) | v <- vs]
+    toPairs _ _ = []
+
+closeHConn :: HIO ()
+closeHConn = do
+    mhc <- use hcConn
+    forM_ mhc $ \hc -> do
+        liftIO $ closeConnection hc
+        hcConn   .= Nothing
+
+        reqCnt <- use hcReqCnt
+        rspCnt <- use hcRspCnt
+        unless (reqCnt == rspCnt) $
+            liftIO $ putStrLn $ concat ["warning: req-cnt=", show reqCnt, " rsp-cnt=", show rspCnt]
+
+        hcReqCnt .= 0
+        hcRspCnt .= 0
+
+openHConn :: HIO Connection
+openHConn = use hcConn >>= \case
+    Just c -> return c
+    Nothing -> do
+        mkConn <- use hcMkConn
+        c <- liftIO mkConn
+        hcConn   .= Just c
+        hcReqCnt .= 0 -- redundant
+        hcRspCnt .= 0 -- redundant
+        return c
+
+reOpenHConn :: HIO Connection
+reOpenHConn = closeHConn >> openHConn
+
+getHConn :: HIO (Connection,Natural)
+getHConn = do
+    lft <- use hcReqLeft
+    c <- if lft > 0 then openHConn else reOpenHConn
+    (,) c <$> use hcReqLeft
+
+nlength :: [a] -> Natural
+nlength = fromIntegral . length
+
+nsplitAt :: Natural -> [a] -> ([a],[a])
+nsplitAt n = splitAt i
+  where
+    i = fromMaybe (error "nsplitAt: overflow") $ toIntegralSized n
+
+findLibrarySection :: [C.Field C.Position] -> Maybe (Int, Int)
+findLibrarySection [] = Nothing
+findLibrarySection (C.Section (C.Name (C.Position row _) "library") [] fs : _) =
+    Just (row, findIndent fs)
+  where
+    findIndent [] = 4
+    findIndent (f : _) = case C.fieldAnn f of
+        C.Position _ col -> pred col
+findLibrarySection (_ : fs) = findLibrarySection fs
+
+----------------------------------------------------------------------------
+-- CLI Interface
+
+data Options = Options
+  { optVerbose :: !Bool
+  , optHost    :: !Hostname
+  , optCommand :: !Command
+  } deriving Show
+
+data PullCOptions = PullCOptions
+  { optPlCPkgName :: !PkgName
+  , optPlCIncrRev :: !Bool
+  , optPlCForce   :: !Bool
+  , optPlCPkgVers :: Maybe C.VersionRange
+  } deriving Show
+
+data SyncCOptions = SyncCOptions
+  { optSyCFile    :: FilePath
+  , optSyCIncrRev :: !Bool
+  , optSyCForce   :: !Bool
+  } deriving Show
+
+data ListCOptions = ListCOptions
+  { optLCPkgName :: !PkgName
+  , optNoAnn     :: !Bool
+  , optRevUrls   :: !Bool
+  } deriving Show
+
+data PushCOptions = PushCOptions
+  { optPsCIncrRev :: !Bool
+  , optPsCPublish :: !Bool
+  , optPsCFiles   :: [FilePath]
+  } deriving Show
+
+data PushPCOptions = PushPCOptions
+  { optPPCFiles :: [FilePath]
+  } deriving Show
+
+data CheckROptions = CheckROptions
+  { optCRNew  :: FilePath
+  , optCROrig :: FilePath
+  } deriving Show
+
+data AddBoundOptions = AddBoundOptions
+  { optABPackageName  :: C.PackageName
+  , optABVersionRange :: C.VersionRange
+  , optForce          :: Bool              -- ^ Disable the check whether bound is subsumed by existing constraints.
+  , optABMessage      :: [String]
+  , optABFiles        :: [FilePath]
+  } deriving Show
+
+data Command
+    = ListCabal !ListCOptions
+    | PullCabal !PullCOptions
+    | PushCabal !PushCOptions
+    | SyncCabal !SyncCOptions
+    | PushCandidate !PushPCOptions
+    | CheckRevision !CheckROptions
+    | IndexShaSum   !IndexShaSumOptions
+    | AddBound !AddBoundOptions
+    deriving Show
+
+optionsParserInfo :: ParserInfo Options
+optionsParserInfo
+    = info (helper <*> verOption <*> oParser)
+           (fullDesc
+            <> header "hackage-cli - CLI tool for Hackage"
+            <> footer footerStr)
+  where
+    footerStr = unwords
+        [ "Each command has a sub-`--help` text. Hackage credentials are expected to be"
+        , "stored in an `${HOME}/.netrc`-entry (or `.netrc.gpg`) for the respective Hackage hostname."
+        , "E.g. \"machine hackage.haskell.org login MyUserName password TrustNo\"."
+        , "All interactions with Hackage occur TLS-encrypted via the HTTPS protocol."
+        ]
+
+    bstr = BS8.pack <$> str
+
+    prsc :: C.Parsec s => OA.ReadM s
+    prsc = OA.eitherReader C.eitherParsec
+
+    vrange = do
+        s <- str
+        case C.simpleParse s of
+            Nothing -> fail "invalid version range"
+            Just vr -> pure vr
+
+    listcoParser = ListCabal <$>
+        (ListCOptions <$> OA.argument bstr (metavar "PKGNAME" <> action "file")
+                      <*> switch (long "no-annotations" <> help "don't add preferred-versions annotations")
+                      <*> switch (long "rev-urls" <> help "list revision URLs"))
+
+    pullcoParser = PullCabal <$>
+        (PullCOptions <$> OA.argument bstr (metavar "PKGNAME" <> action "file")
+                      <*> switch (long "incr-rev" <> help "increment x-revision field")
+                      <*> switch (long "force" <> help "force overwriting existing files")
+                      <*> optional (OA.argument vrange (metavar "VERSION-CONSTRAINT")))
+
+    synccoParser = SyncCabal <$>
+        (SyncCOptions <$> OA.argument str (metavar "CABALFILE" <> action "file")
+                      <*> switch (long "incr-rev" <> help "increment x-revision field")
+                      <*> switch (long "force" <> help "force overwriting local file with older revision"))
+
+    pushcoParser = PushCabal <$> (PushCOptions
+                             <$> switch (long "incr-rev" <> help "increment x-revision field")
+                             <*> switch (long "publish"  <> help "publish revision (review-mode)")
+                             <*> some (OA.argument str (metavar "CABALFILES..." <> action "file")))
+
+    pushpcoParser = PushCandidate <$> (PushPCOptions <$> some (OA.argument str (metavar "TARBALLS..." <> action "file")))
+
+    checkrevParsser = CheckRevision <$> (CheckROptions <$> OA.argument str (metavar "NEWCABAL" <> action "file")
+                                                       <*> OA.argument str (metavar "OLDCABAL" <> action "file"))
+
+
+    indexssParser = IndexShaSum <$> (IndexShaSumOptions <$> switch (long "flat" <> help "flat filesystem layout (used by mirrors)")
+                                                        <*> OA.argument str (metavar "INDEX-TAR" <> action "file")
+                                                        <*> optional (OA.argument str (metavar "BASEDIR" <> action "directory")))
+
+    addboundParser = AddBound <$> (AddBoundOptions <$> OA.argument prsc (metavar "DEPENDENCY")
+                                                   <*> OA.argument prsc (metavar "VERSIONRANGE")
+                                                   <*> OA.switch (long "force" <> help "Add bound even if it is already subsumed by existing constraints.")
+                                                   <*> many (OA.option str (OA.short 'm' <> OA.long "message" <> metavar "MSG" <> help "Use given MSG as a comment. If multiple -m options are given, their values are concatenated with 'unlines'."))
+                                                   <*> some (OA.argument str (metavar "CABALFILES..." <> action "file")))
+
+    oParser
+        = Options <$> switch (long "verbose" <> help "Enable verbose output.")
+                  <*> option bstr (long "hostname"  <> metavar "HOSTNAME" <> value "hackage.haskell.org"
+                                   <> help "Hackage hostname" <> showDefault)
+                  <*> subparser (mconcat [ command "pull-cabal" (info (helper <*> pullcoParser)
+                                                   (progDesc "Download .cabal files for a package."))
+                                         , command "push-cabal" (info (helper <*> pushcoParser)
+                                                   (progDesc "Upload revised .cabal files."))
+                                         , command "sync-cabal" (info (helper <*> synccoParser)
+                                                   (progDesc "Update/sync local .cabal file with latest revision on Hackage."))
+                                         , command "push-candidate" (info (helper <*> pushpcoParser)
+                                                   (progDesc "Upload package candidate(s)."))
+                                         , command "list-versions" (info (helper <*> listcoParser)
+                                                   (progDesc "List versions for a package."))
+                                         , command "check-revision" (info (helper <*> checkrevParsser)
+                                                   (progDesc "Validate revision."))
+                                         , command "index-sha256sum" (info (helper <*> indexssParser)
+                                                   (progDesc "Generate sha256sum-format file."))
+                                         , command "add-bound" (info (helper <*> addboundParser)
+                                                   (progDesc "Add bound to the library section of a package, unless the bound is redundant. The .cabal file is edited in place."))
+                                         ])
+
+    verOption = infoOption verMsg (long "version" <> help "Output version information and exit.")
+      where
+        verMsg = "hackage-cli " <> V.showVersion Paths_hackage_cli.version
+
+----------------------------------------------------------------------------
+
+main :: IO ()
+main = do
+    opts <- execParser optionsParserInfo
+    mainWithOptions opts
+
+mainWithOptions :: Options -> IO ()
+mainWithOptions Options {..} = do
+   case optCommand of
+       PullCabal (PullCOptions {..}) -> do
+           let pkgn = optPlCPkgName
+
+           cs <- runHConn (fetchAllCabalFiles pkgn (fromMaybe C.anyVersion optPlCPkgVers))
+
+           forM_ cs $ \(v,mraw) -> case mraw of
+             Nothing -> putStrLn ("skipped excluded " ++ BS8.unpack v)
+             Just raw0 -> do
+               let fn = BS8.unpack $ pkgn <> "-" <> v <> ".cabal"
+
+               let raw | optPlCIncrRev = incrXrev raw0
+                       | otherwise     = raw0
+
+               doesFileExist fn >>= \case
+                   False -> do
+                       BS.writeFile fn raw
+                       putStrLn ("saved " ++ fn ++ " (" ++ show (BS.length raw) ++ " bytes)")
+                   True ->
+                       if optPlCForce
+                       then do
+                         BS.writeFile fn raw
+                         putStrLn ("overwritten " ++ fn ++ " (" ++ show (BS.length raw) ++ " bytes)")
+                       else
+                         putStrLn ("WARNING: skipped existing " ++ fn ++ " (use --force to overwrite)")
+
+           return ()
+
+       SyncCabal (SyncCOptions {..}) -> do
+           (pkgn,pkgv,xrev) <- pkgDescToPkgIdXrev <$> C.readGenericPackageDescription C.deafening optSyCFile
+           cab0 <- BS.readFile optSyCFile
+
+           BS8.putStrLn $ mconcat [ "local :  "
+                                  , pkgn, "-", pkgv, "-r", BS8.pack (show xrev)
+                                  , "   ('", BS8.pack optSyCFile, "')"
+                                  ]
+
+           cab' <- runHConn (fetchCabalFile pkgn pkgv)
+
+           let (pkgn',pkgv',xrev') = pkgDescToPkgIdXrev $ parseGenericPackageDescription' cab'
+
+           BS8.putStrLn $ mconcat [ "remote:  "
+                                  , pkgn', "-", pkgv', "-r", BS8.pack (show xrev')
+                                  ]
+
+           let cab'' = cabalEditXRev (xrev'+1) cab'
+               bakfn = optSyCFile <> "~"
+
+           case () of
+             _ | optSyCIncrRev, cab'' == cab0 -> do
+                     putStrLn "INFO: local and (incremented) latest remote .cabal revision are already identical! Nothing to do."
+
+             _ | not optSyCForce, xrev' < xrev -> do
+                     putStrLn "ERROR: Local file has higher revision number than Hackage - aborting! (use --force to allow this)"
+                     exitFailure
+
+             _ | optSyCIncrRev, cab'' /= cab0 -> do
+                     when (cab' == cab0) $ do
+                         putStrLn "NOTE: local and (non-incremented) latest remote .cabal revision are identical."
+
+                     BS.writeFile bakfn cab0
+                     putStrLn ("INFO: saved backup of original local file to " <> bakfn)
+
+                     BS.writeFile optSyCFile cab''
+                     BS8.putStrLn $ mconcat [ "local :  "
+                                            , pkgn, "-", pkgv, "-r", BS8.pack (show $ xrev'+1)
+                                            , "   ('", BS8.pack optSyCFile, "')"
+                                            ]
+
+             _ | cab' == cab0 -> do
+                     putStrLn "INFO: local and latest remote .cabal revision are already identical! Nothing to do."
+
+             _ -> do
+                     BS.writeFile bakfn cab0
+                     putStrLn ("INFO: saved backup of original local file to " <> bakfn)
+
+                     BS.writeFile optSyCFile cab'
+                     BS8.putStrLn $ mconcat [ "local :  "
+                                            , pkgn, "-", pkgv, "-r", BS8.pack (show $ xrev')
+                                            , "   ('", BS8.pack optSyCFile, "')"
+                                            ]
+
+
+       ListCabal (ListCOptions {..}) -> do
+           let pkgn = optLCPkgName
+
+           vs <- runHConn (fetchVersions pkgn)
+
+           unless optNoAnn $
+             putStrLn $ concat [ "Found ", show (length vs), " package versions for "
+                               , show pkgn, " ([U]npreferred, [D]eprecated):"
+                               ]
+
+           if optRevUrls then do
+             forM_ vs $ \(v,_) -> do
+                 let pid = pkgn <> "-" <> v
+                 BS8.putStrLn $ mconcat [ " - https://hackage.haskell.org/package/", pid, "/revisions/" ]
+           else do
+             forM_ vs $ \(v,unp) -> do
+                 let status = case unp of
+                         _ | optNoAnn -> ""
+                         Normal      -> "    "
+                         Deprecated  -> "[D] "
+                         UnPreferred -> "[U] "
+                 BS8.putStrLn $ status <> pkgn <> "-" <> v
+           return ()
+
+       PushCabal (PushCOptions {..}) -> do
+           (username,password) <- maybe (fail "missing Hackage credentials") return =<< getHackageCreds
+           putStrLn $ "Using Hackage credentials for username " ++ show username
+
+           forM_ optPsCFiles $ \fn -> do
+               (pkgn,pkgv,xrev) <- pkgDescToPkgIdXrev <$> C.readGenericPackageDescription C.deafening fn
+               putStrLn $ concat [ "Pushing ", show fn
+                                 , " (", BS8.unpack pkgn, "-", BS8.unpack pkgv, "~", show xrev, ")"
+                                 , if not optPsCPublish then " [review-mode]" else "", " ..."
+                                 ]
+
+               let editCab | optPsCIncrRev = cabalEditXRev (xrev+1)
+                           | otherwise    = id
+
+               rawcab <- editCab <$> BS.readFile fn
+               (dt,tmp) <- timeIt $ runHConn (hackagePostCabal (username,password) (pkgn,pkgv) rawcab
+                                                               (if optPsCPublish then WetRun else DryRun))
+
+               printf "Hackage response was (after %.3f secs):\n" dt
+               putStrLn (replicate 80 '=')
+               BS8.putStrLn (tidyHtml tmp)
+               putStrLn (replicate 80 '=')
+
+       PushCandidate (PushPCOptions {..}) -> do
+           (username,password) <- maybe (fail "missing Hackage credentials") return =<< getHackageCreds
+           putStrLn $ "Using Hackage credentials for username " ++ show username
+
+           forM_ optPPCFiles $ \fn -> do
+               putStrLn $ "reading " ++ show fn ++ " ..."
+               rawtar <- BS.readFile fn
+               putStrLn $ "uplading to Hackage..."
+               tmp <- runHConn (hackagePushCandidate (username,password) (takeFileName fn, rawtar))
+
+               putStrLn "Hackage response was:"
+               putStrLn (replicate 80 '=')
+               BS8.putStrLn tmp
+               putStrLn (replicate 80 '=')
+
+
+       CheckRevision (CheckROptions {..}) -> do
+           old <- BS.readFile optCROrig
+           new <- BS.readFile optCRNew
+
+           case diffCabalRevisions old new of
+               Left err -> do
+                   putStrLn "change not allowed:"
+                   putStrLn err
+                   exitFailure
+
+               Right [] -> do
+                   putStrLn "no-op change detected"
+                   exitFailure
+
+               Right changes -> do
+                   putStrLn "change allowed:"
+                   forM_ changes $ \(Change _ what old' new') -> do
+                       putStrLn $ "what: " ++ what
+                       putStrLn $ " old: " ++ old'
+                       putStrLn $ " new: " ++ new'
+
+           return ()
+
+       IndexShaSum opts -> IndexShaSum.run opts
+
+
+       AddBound AddBoundOptions {..} -> forM_ optABFiles $ \fp -> do
+           old <- BS.readFile fp
+
+           -- idea is simple:
+           -- - .cabal is line oriented file
+           -- - find "library" section start
+           -- - bonus: look of an indentation used from the next field/section there
+           -- - insert data into a bytestring "manually"
+           fs <- either (exitFailureWith . show) return $ C.readFields old
+           (lin, indent) <- maybe
+               (exitFailureWith $ "Cannot find library section in " ++ fp)
+               return
+               (findLibrarySection fs)
+
+           let msgLines  = map ("-- " ++) optABMessage
+               bdLine    = "build-depends: " ++ C.prettyShow optABPackageName ++ " " ++ C.prettyShow optABVersionRange
+               midLines  = [ BS8.pack $ replicate indent ' ' ++ l
+                           | l <- msgLines ++ [bdLine]
+                           ] ++ [""] -- also add an empty line separator
+               (preLines, postLines) = splitAt lin $ BS8.lines old
+               new = BS8.unlines (preLines ++ midLines ++ postLines)
+
+           -- interpretation of version ranges
+           let oldGpd = parseGenericPackageDescription' old
+               newGpd = parseGenericPackageDescription' new
+
+               oldRange = extractRange oldGpd optABPackageName
+               newRange = extractRange newGpd optABPackageName
+
+               oldRange' = C.intersectVersionRanges oldRange optABVersionRange
+
+               -- Canonical forms (semantics)
+               oldSem  = C.toVersionIntervals oldRange   -- existing range
+               oldSem' = C.toVersionIntervals oldRange'  -- range after adding the bound (theory)
+               newSem  = C.toVersionIntervals newRange   -- range after adding the bound (practice)
+
+           -- Necessity check: does the addition of the bound change the semantics?
+           -- if not, it can be skipped.
+
+           if not optForce && oldSem' == oldSem then do
+
+             putStrLn $ concat [ "Skipping ", fp, ": bound already subsumed by existing constraints (use --force to add nevertheless)." ]
+
+           else do
+             -- sanity check: did the addition have the intended outcome?
+             unless (newSem == oldSem') $
+                exitFailureWith $ unwords
+                    [ "Edit failed, version ranges don't match: "
+                    , C.prettyShow oldRange
+                    , "&&"
+                    , C.prettyShow optABVersionRange
+                    , "=/="
+                    , C.prettyShow newRange
+                    ]
+
+             -- write new version
+             putStrLn $ unwords [ "Adding bound to", fp ]
+             BS.writeFile fp new
+
+   return ()
+  where
+    mkHConn = do
+        sslCtx <- baselineContextSSL
+        pure $ HConn (openConnectionSSL sslCtx optHost 443) Nothing 0 0
+
+    runHConn act = do
+        hc0 <- mkHConn
+        flip evalStateT hc0 $ do
+            res <- act
+            closeHConn
+            return res
+
+    getNetrcContents :: IO (Maybe ByteString)
+    getNetrcContents = do
+        mhome <- lookupEnv "HOME"
+        case mhome of
+            Nothing -> return Nothing
+            Just "" -> return Nothing
+            Just ho -> do
+                let fnGpg = ho ++ "/.netrc.gpg"
+                let fn = ho ++ "/.netrc"
+                gpgExists <- doesFileExist fnGpg
+                if gpgExists
+                then readGpg fnGpg
+                else readPlain fn
+      where
+        readGpg fn = do
+            (ec, out, err) <- readProcessWithExitCode "gpg" ["--decrypt", fn] ""
+            case ec of
+                ExitSuccess   -> return (Just out)
+                ExitFailure _ -> BS.putStr err >> return Nothing
+
+        readPlain fn = do
+            ret <- tryIOError (BS.readFile fn)
+            case ret of
+                Left e | isDoesNotExistError e -> return Nothing
+                       | otherwise             -> ioError e
+                Right b -> return $! Just b
+
+    getHackageCreds :: IO (Maybe (ByteString,ByteString))
+    getHackageCreds = do
+        getNetrcContents >>= \case
+            Nothing -> pure Nothing
+            Just contents -> case parseNetRc "netrc" contents of
+                Left _ -> fail "Invalid ${HOME}/.netrc(.gpg) found"
+                Right NetRc {..} ->
+                    evaluate $ (\NetRcHost{..} -> (nrhLogin,nrhPassword))
+                               <$> listToMaybe (filter ((== optHost) . nrhName) nrHosts)
+
+
+    pkgDescToPkgIdXrev pdesc = force (BS8.pack pkgn, BS8.pack $ showVersion pkgv, read xrev :: PkgRev)
+      where
+        C.PackageIdentifier (C.unPackageName -> pkgn) pkgv = C.package . C.packageDescription $ pdesc
+        xrev = fromMaybe "0" . lookup "x-revision" . C.customFieldsPD . C.packageDescription $ pdesc
+
+    incrXrev :: ByteString -> ByteString
+    incrXrev cabdata0 = cabalEditXRev (xrev0+1) cabdata0
+      where
+        pdesc0 = parseGenericPackageDescription' cabdata0
+        (_,_,xrev0) = pkgDescToPkgIdXrev pdesc0
+
+    exitFailureWith e = do
+        putStrLn e
+        exitFailure
+
+    parseGenericPackageDescription' bs =
+        case snd $ C.runParseResult $ C.parseGenericPackageDescription bs of
+            Left (_, es) -> error $ List.intercalate "\n" $ map (C.showPError "<.cabal>") $ toList es
+            Right x      -> x
+
+    extractRange gpd pkgName = case vss of
+        []     -> C.noVersion
+        (v:vs) -> List.foldl' C.intersectVersionRanges v vs
+      where
+        vss = gpd ^.. LC.condLibrary . _Just . condTreeDataL . LC.targetBuildDepends . traverse . to ext . _Just
+        ext (C.Dependency pkgName' vr _)
+           | pkgName == pkgName' = Just vr
+           | otherwise           = Nothing
+
+    condTreeDataL :: Functor f => (a -> f a) -> C.CondTree v c a -> f (C.CondTree v c a)
+    condTreeDataL f (C.CondNode x c cs) = f x <&> \y -> C.CondNode y c cs
+
+
+-- | Try to clean-up HTML fragments to be more readable
+tidyHtml :: ByteString -> ByteString
+tidyHtml =
+    replace "&amp;"  "&"     . -- must be last entity substitution
+    replace "&gt;"   ">"     .
+    replace "&lt;"   "<"     .
+    replace "<p>"    ""      . -- tags must be replaced before entities
+    replace "</p>"   "\n"    .
+    replace "<li>"   "\n * " .
+    replace "</li>"  ""      .
+    replace "</pre>" "`"     .
+    replace "<pre>"  "`"     .
+    stripEnd
+  where
+    stripEnd = fst . BS8.spanEnd isSpace
+
+    replace :: ByteString -> ByteString -> ByteString -> ByteString
+    replace old new = BSL.toStrict . BSS.replace old new
+
+
+timeIt :: IO a -> IO (Double, a)
+timeIt act = do
+    t0  <- getTime
+    res <- act
+    t1  <- getTime
+    let !dt = t1 - t0
+    pure (dt, res)
+  where
+    getTime :: IO Double
+    getTime = realToFrac `fmap` getPOSIXTime
diff --git a/tests/Tests.hs b/tests/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Tests.hs
@@ -0,0 +1,61 @@
+{-# LANGUAGE TupleSections #-}
+module Main (main) where
+
+
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as LBS
+import Distribution.Server.Util.CabalRevisions (diffCabalRevisions', Change (..))
+import Distribution.Simple.Utils (toUTF8BS)
+import System.FilePath ((</>), (-<.>))
+import Test.Tasty (defaultMain, testGroup, TestTree)
+import Test.Tasty.Golden (goldenVsStringDiff)
+
+main :: IO ()
+main = defaultMain $ testGroup "Fixtures"
+    -- basic sanity tests
+    [ golden "tree-diff" 0 1
+    , golden "deepseq"   0 1
+
+    -- adding a new conditional section with restricted bounds
+    -- TODO: this is not allowed
+    , golden "SVGFonts" 0 2
+
+    , golden "semigroups" 0 2
+    ]
+
+golden :: String -> Int -> Int -> TestTree
+golden name mi ma = case pairs [mi .. ma] of
+    []       -> golden' name mi ma
+    [(x, y)] -> golden' name x y
+    ps       -> testGroup name
+        [ golden' name x y
+        | (x, y) <- ps
+        ]
+
+golden' :: String -> Int -> Int -> TestTree
+golden' name mi ma = goldenVsStringDiff name' diff gold $ do
+    o <- BS.readFile orig
+    e <- BS.readFile edit
+    return $ LBS.fromStrict $ toUTF8BS $ unlines $ case diffCabalRevisions' False o e of
+        Left err      -> [ "ERROR", err ]
+        Right changes -> "OK" : concatMap showChange changes
+  where
+    name' = unwords [ name, show mi, "->", show ma ]
+    orig = "fixtures" </> name -<.> (show mi ++ ".cabal")
+    edit = "fixtures" </> name -<.> (show ma ++ ".cabal")
+    gold = "fixtures" </> name -<.> (show mi ++ "." ++ show ma ++ ".diff")
+
+    diff ref new = ["diff", "-u", ref, new]
+
+    showChange (Change sev what from to) =
+        [ show sev
+        , what
+        , "- " ++ from
+        , "+ " ++ to
+        ]
+
+-- cartesian product without diagonal
+-- the idea is that revisions should be transitive and symmetric
+pairs :: [a] -> [(a,a)]
+pairs []     = []
+pairs (x:xs) = map (x,) xs ++ map (,x) xs ++ pairs xs
