[
  {
    "path": ".gitignore",
    "content": "# Ignore Gradle project-specific cache directory\n.gradle\n\n# Ignore Gradle build output directory\nbuild\n\nout\n\n.kotlintest\n.idea\n*.iml\n*.ipr\n*.iws\n\ninternal-test/src\n"
  },
  {
    "path": "LICENSE",
    "content": "Merge Request Integration Community Edition (CE) (c) 2019-present Nhat Phan <nhat.phan@ntworld.net> (https://github.com/nhat-phan)\n\nMerge Request Integration Community Edition (CE) is licensed under a\nCreative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.\n\nYou should have received a copy of the license along with this\nwork. If not, see <http://creativecommons.org/licenses/by-nc-nd/4.0/>.\n\nAttribution-NonCommercial-NoDerivatives 4.0 International\n\n=======================================================================\n\nCreative Commons Corporation (\"Creative Commons\") is not a law firm and\ndoes not provide legal services or legal advice. Distribution of\nCreative Commons public licenses does not create a lawyer-client or\nother relationship. Creative Commons makes its licenses and related\ninformation available on an \"as-is\" basis. Creative Commons gives no\nwarranties regarding its licenses, any material licensed under their\nterms and conditions, or any related information. Creative Commons\ndisclaims all liability for damages resulting from their use to the\nfullest extent possible.\n\nUsing Creative Commons Public Licenses\n\nCreative Commons public licenses provide a standard set of terms and\nconditions that creators and other rights holders may use to share\noriginal works of authorship and other material subject to copyright\nand certain other rights specified in the public license below. The\nfollowing considerations are for informational purposes only, are not\nexhaustive, and do not form part of our licenses.\n\n     Considerations for licensors: Our public licenses are\n     intended for use by those authorized to give the public\n     permission to use material in ways otherwise restricted by\n     copyright and certain other rights. Our licenses are\n     irrevocable. Licensors should read and understand the terms\n     and conditions of the license they choose before applying it.\n     Licensors should also secure all rights necessary before\n     applying our licenses so that the public can reuse the\n     material as expected. Licensors should clearly mark any\n     material not subject to the license. This includes other CC-\n     licensed material, or material used under an exception or\n     limitation to copyright. More considerations for licensors:\n\twiki.creativecommons.org/Considerations_for_licensors\n\n     Considerations for the public: By using one of our public\n     licenses, a licensor grants the public permission to use the\n     licensed material under specified terms and conditions. If\n     the licensor's permission is not necessary for any reason--for\n     example, because of any applicable exception or limitation to\n     copyright--then that use is not regulated by the license. Our\n     licenses grant only permissions under copyright and certain\n     other rights that a licensor has authority to grant. Use of\n     the licensed material may still be restricted for other\n     reasons, including because others have copyright or other\n     rights in the material. A licensor may make special requests,\n     such as asking that all changes be marked or described.\n     Although not required by our licenses, you are encouraged to\n     respect those requests where reasonable. More_considerations\n     for the public:\n\twiki.creativecommons.org/Considerations_for_licensees\n\n=======================================================================\n\nCreative Commons Attribution-NonCommercial-NoDerivatives 4.0\nInternational Public License\n\nBy exercising the Licensed Rights (defined below), You accept and agree\nto be bound by the terms and conditions of this Creative Commons\nAttribution-NonCommercial-NoDerivatives 4.0 International Public\nLicense (\"Public License\"). To the extent this Public License may be\ninterpreted as a contract, You are granted the Licensed Rights in\nconsideration of Your acceptance of these terms and conditions, and the\nLicensor grants You such rights in consideration of benefits the\nLicensor receives from making the Licensed Material available under\nthese terms and conditions.\n\n\nSection 1 -- Definitions.\n\n  a. Adapted Material means material subject to Copyright and Similar\n     Rights that is derived from or based upon the Licensed Material\n     and in which the Licensed Material is translated, altered,\n     arranged, transformed, or otherwise modified in a manner requiring\n     permission under the Copyright and Similar Rights held by the\n     Licensor. For purposes of this Public License, where the Licensed\n     Material is a musical work, performance, or sound recording,\n     Adapted Material is always produced where the Licensed Material is\n     synched in timed relation with a moving image.\n\n  b. Copyright and Similar Rights means copyright and/or similar rights\n     closely related to copyright including, without limitation,\n     performance, broadcast, sound recording, and Sui Generis Database\n     Rights, without regard to how the rights are labeled or\n     categorized. For purposes of this Public License, the rights\n     specified in Section 2(b)(1)-(2) are not Copyright and Similar\n     Rights.\n\n  c. Effective Technological Measures means those measures that, in the\n     absence of proper authority, may not be circumvented under laws\n     fulfilling obligations under Article 11 of the WIPO Copyright\n     Treaty adopted on December 20, 1996, and/or similar international\n     agreements.\n\n  d. Exceptions and Limitations means fair use, fair dealing, and/or\n     any other exception or limitation to Copyright and Similar Rights\n     that applies to Your use of the Licensed Material.\n\n  e. Licensed Material means the artistic or literary work, database,\n     or other material to which the Licensor applied this Public\n     License.\n\n  f. Licensed Rights means the rights granted to You subject to the\n     terms and conditions of this Public License, which are limited to\n     all Copyright and Similar Rights that apply to Your use of the\n     Licensed Material and that the Licensor has authority to license.\n\n  g. Licensor means the individual(s) or entity(ies) granting rights\n     under this Public License.\n\n  h. NonCommercial means not primarily intended for or directed towards\n     commercial advantage or monetary compensation. For purposes of\n     this Public License, the exchange of the Licensed Material for\n     other material subject to Copyright and Similar Rights by digital\n     file-sharing or similar means is NonCommercial provided there is\n     no payment of monetary compensation in connection with the\n     exchange.\n\n  i. Share means to provide material to the public by any means or\n     process that requires permission under the Licensed Rights, such\n     as reproduction, public display, public performance, distribution,\n     dissemination, communication, or importation, and to make material\n     available to the public including in ways that members of the\n     public may access the material from a place and at a time\n     individually chosen by them.\n\n  j. Sui Generis Database Rights means rights other than copyright\n     resulting from Directive 96/9/EC of the European Parliament and of\n     the Council of 11 March 1996 on the legal protection of databases,\n     as amended and/or succeeded, as well as other essentially\n     equivalent rights anywhere in the world.\n\n  k. You means the individual or entity exercising the Licensed Rights\n     under this Public License. Your has a corresponding meaning.\n\n\nSection 2 -- Scope.\n\n  a. License grant.\n\n       1. Subject to the terms and conditions of this Public License,\n          the Licensor hereby grants You a worldwide, royalty-free,\n          non-sublicensable, non-exclusive, irrevocable license to\n          exercise the Licensed Rights in the Licensed Material to:\n\n            a. reproduce and Share the Licensed Material, in whole or\n               in part, for NonCommercial purposes only; and\n\n            b. produce and reproduce, but not Share, Adapted Material\n               for NonCommercial purposes only.\n\n       2. Exceptions and Limitations. For the avoidance of doubt, where\n          Exceptions and Limitations apply to Your use, this Public\n          License does not apply, and You do not need to comply with\n          its terms and conditions.\n\n       3. Term. The term of this Public License is specified in Section\n          6(a).\n\n       4. Media and formats; technical modifications allowed. The\n          Licensor authorizes You to exercise the Licensed Rights in\n          all media and formats whether now known or hereafter created,\n          and to make technical modifications necessary to do so. The\n          Licensor waives and/or agrees not to assert any right or\n          authority to forbid You from making technical modifications\n          necessary to exercise the Licensed Rights, including\n          technical modifications necessary to circumvent Effective\n          Technological Measures. For purposes of this Public License,\n          simply making modifications authorized by this Section 2(a)\n          (4) never produces Adapted Material.\n\n       5. Downstream recipients.\n\n            a. Offer from the Licensor -- Licensed Material. Every\n               recipient of the Licensed Material automatically\n               receives an offer from the Licensor to exercise the\n               Licensed Rights under the terms and conditions of this\n               Public License.\n\n            b. No downstream restrictions. You may not offer or impose\n               any additional or different terms or conditions on, or\n               apply any Effective Technological Measures to, the\n               Licensed Material if doing so restricts exercise of the\n               Licensed Rights by any recipient of the Licensed\n               Material.\n\n       6. No endorsement. Nothing in this Public License constitutes or\n          may be construed as permission to assert or imply that You\n          are, or that Your use of the Licensed Material is, connected\n          with, or sponsored, endorsed, or granted official status by,\n          the Licensor or others designated to receive attribution as\n          provided in Section 3(a)(1)(A)(i).\n\n  b. Other rights.\n\n       1. Moral rights, such as the right of integrity, are not\n          licensed under this Public License, nor are publicity,\n          privacy, and/or other similar personality rights; however, to\n          the extent possible, the Licensor waives and/or agrees not to\n          assert any such rights held by the Licensor to the limited\n          extent necessary to allow You to exercise the Licensed\n          Rights, but not otherwise.\n\n       2. Patent and trademark rights are not licensed under this\n          Public License.\n\n       3. To the extent possible, the Licensor waives any right to\n          collect royalties from You for the exercise of the Licensed\n          Rights, whether directly or through a collecting society\n          under any voluntary or waivable statutory or compulsory\n          licensing scheme. In all other cases the Licensor expressly\n          reserves any right to collect such royalties, including when\n          the Licensed Material is used other than for NonCommercial\n          purposes.\n\n\nSection 3 -- License Conditions.\n\nYour exercise of the Licensed Rights is expressly made subject to the\nfollowing conditions.\n\n  a. Attribution.\n\n       1. If You Share the Licensed Material, You must:\n\n            a. retain the following if it is supplied by the Licensor\n               with the Licensed Material:\n\n                 i. identification of the creator(s) of the Licensed\n                    Material and any others designated to receive\n                    attribution, in any reasonable manner requested by\n                    the Licensor (including by pseudonym if\n                    designated);\n\n                ii. a copyright notice;\n\n               iii. a notice that refers to this Public License;\n\n                iv. a notice that refers to the disclaimer of\n                    warranties;\n\n                 v. a URI or hyperlink to the Licensed Material to the\n                    extent reasonably practicable;\n\n            b. indicate if You modified the Licensed Material and\n               retain an indication of any previous modifications; and\n\n            c. indicate the Licensed Material is licensed under this\n               Public License, and include the text of, or the URI or\n               hyperlink to, this Public License.\n\n          For the avoidance of doubt, You do not have permission under\n          this Public License to Share Adapted Material.\n\n       2. You may satisfy the conditions in Section 3(a)(1) in any\n          reasonable manner based on the medium, means, and context in\n          which You Share the Licensed Material. For example, it may be\n          reasonable to satisfy the conditions by providing a URI or\n          hyperlink to a resource that includes the required\n          information.\n\n       3. If requested by the Licensor, You must remove any of the\n          information required by Section 3(a)(1)(A) to the extent\n          reasonably practicable.\n\n\nSection 4 -- Sui Generis Database Rights.\n\nWhere the Licensed Rights include Sui Generis Database Rights that\napply to Your use of the Licensed Material:\n\n  a. for the avoidance of doubt, Section 2(a)(1) grants You the right\n     to extract, reuse, reproduce, and Share all or a substantial\n     portion of the contents of the database for NonCommercial purposes\n     only and provided You do not Share Adapted Material;\n\n  b. if You include all or a substantial portion of the database\n     contents in a database in which You have Sui Generis Database\n     Rights, then the database in which You have Sui Generis Database\n     Rights (but not its individual contents) is Adapted Material; and\n\n  c. You must comply with the conditions in Section 3(a) if You Share\n     all or a substantial portion of the contents of the database.\n\nFor the avoidance of doubt, this Section 4 supplements and does not\nreplace Your obligations under this Public License where the Licensed\nRights include other Copyright and Similar Rights.\n\n\nSection 5 -- Disclaimer of Warranties and Limitation of Liability.\n\n  a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE\n     EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS\n     AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF\n     ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,\n     IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,\n     WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR\n     PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,\n     ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT\n     KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT\n     ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.\n\n  b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE\n     TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,\n     NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,\n     INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,\n     COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR\n     USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN\n     ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR\n     DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR\n     IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.\n\n  c. The disclaimer of warranties and limitation of liability provided\n     above shall be interpreted in a manner that, to the extent\n     possible, most closely approximates an absolute disclaimer and\n     waiver of all liability.\n\n\nSection 6 -- Term and Termination.\n\n  a. This Public License applies for the term of the Copyright and\n     Similar Rights licensed here. However, if You fail to comply with\n     this Public License, then Your rights under this Public License\n     terminate automatically.\n\n  b. Where Your right to use the Licensed Material has terminated under\n     Section 6(a), it reinstates:\n\n       1. automatically as of the date the violation is cured, provided\n          it is cured within 30 days of Your discovery of the\n          violation; or\n\n       2. upon express reinstatement by the Licensor.\n\n     For the avoidance of doubt, this Section 6(b) does not affect any\n     right the Licensor may have to seek remedies for Your violations\n     of this Public License.\n\n  c. For the avoidance of doubt, the Licensor may also offer the\n     Licensed Material under separate terms or conditions or stop\n     distributing the Licensed Material at any time; however, doing so\n     will not terminate this Public License.\n\n  d. Sections 1, 5, 6, 7, and 8 survive termination of this Public\n     License.\n\n\nSection 7 -- Other Terms and Conditions.\n\n  a. The Licensor shall not be bound by any additional or different\n     terms or conditions communicated by You unless expressly agreed.\n\n  b. Any arrangements, understandings, or agreements regarding the\n     Licensed Material not stated herein are separate from and\n     independent of the terms and conditions of this Public License.\n\n\nSection 8 -- Interpretation.\n\n  a. For the avoidance of doubt, this Public License does not, and\n     shall not be interpreted to, reduce, limit, restrict, or impose\n     conditions on any use of the Licensed Material that could lawfully\n     be made without permission under this Public License.\n\n  b. To the extent possible, if any provision of this Public License is\n     deemed unenforceable, it shall be automatically reformed to the\n     minimum extent necessary to make it enforceable. If the provision\n     cannot be reformed, it shall be severed from this Public License\n     without affecting the enforceability of the remaining terms and\n     conditions.\n\n  c. No term or condition of this Public License will be waived and no\n     failure to comply consented to unless expressly agreed to by the\n     Licensor.\n\n  d. Nothing in this Public License constitutes or may be interpreted\n     as a limitation upon, or waiver of, any privileges and immunities\n     that apply to the Licensor or You, including from the legal\n     processes of any jurisdiction or authority.\n\n=======================================================================\n\nCreative Commons is not a party to its public\nlicenses. Notwithstanding, Creative Commons may elect to apply one of\nits public licenses to material it publishes and in those instances\nwill be considered the “Licensor.” The text of the Creative Commons\npublic licenses is dedicated to the public domain under the CC0 Public\nDomain Dedication. Except for the limited purpose of indicating that\nmaterial is shared under a Creative Commons public license or as\notherwise permitted by the Creative Commons policies published at\ncreativecommons.org/policies, Creative Commons does not authorize the\nuse of the trademark \"Creative Commons\" or any other trademark or logo\nof Creative Commons without its prior written consent including,\nwithout limitation, in connection with any unauthorized modifications\nto any of its public licenses or any other arrangements,\nunderstandings, or agreements concerning use of licensed material. For\nthe avoidance of doubt, this paragraph does not form part of the\npublic licenses.\n\nCreative Commons may be contacted at creativecommons.org.\n\n"
  },
  {
    "path": "README.md",
    "content": "## Merge Request Integration\n\nMerge Request Integration is a plugin which helps you to do Code Review right in your IDE.\n\n<img src=\"https://raw.githubusercontent.com/nhat-phan/merge-request-integration/master/images/v2019.3.2.gif\" alt=\"Merge Request Integration CE\" style=\"width: 100%\" />\n\n\nWhat you can do:\n\n- Filter Merge Requests which are assigned to you, waiting for your approval, etc\n- Check pipeline status and approval status.\n- Select and review 1 or all commits\n- Do code review, navigate code with Diff View right in your IDE.\n- Add, reply, resolve or delete comments\n- Approve/revoke your approval\n- More and more features will be coming soon :)\n\nCurrently the plugin supports GitLab only (gitlab cloud and self-hosted).\n\nYou can download the plugin on intellij plugins repository: \n[Community Edition](https://plugins.jetbrains.com/plugin/13607-merge-request-integration-ce--code-review-for-gitlab/),\n[Enterprise Edition](https://plugins.jetbrains.com/plugin/13615-merge-request-integration-ee--code-review-for-gitlab/)\n\n### How to set up Gitlab connection\n\n#### Create Gitlab Personal Access Tokens\n\nTo get the your Personal Access Token please follow these steps:\n\n<img src=\"https://raw.githubusercontent.com/nhat-phan/merge-request-integration/master/images/gitlab-1.png\" />\n\nStep 1: On top right corner > Settings\n\n<img src=\"https://raw.githubusercontent.com/nhat-phan/merge-request-integration/master/images/gitlab-2.png\" />\n\nStep 2: Click to Access Tokens menu\n\n<img src=\"https://raw.githubusercontent.com/nhat-phan/merge-request-integration/master/images/gitlab-3.png\" />\n\nStep 3: Create a Personal Access Tokens with api scope\n\n#### Config connection in your IDE Settings\n\nAfter creating the Personal Access Tokens:\n\n- Go to your IDE preferences (macOS: `⌘,` Windows: `Ctrl+Alt+S`)\n- Merge Request Integration > Gitlab\n- Fill data, then save. *Tip: you can use Starred/Membership/Own option to search your project quicker.* \n- Click refresh button of Merge Request Integration CE window if you don't see the connection.\n\nIf your project has more than 1 repository, just setup multiple connections.\n\n### License\n\nThe plugin is an open source released under Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International\nlicense.\n\nIt's totally free if you are using it for public repositories.\n\nFor private repositories, this plugin is a trial. How long is the trial period? Equal to WINRAR's trial period 🙈\n\n[Community Edition (CE)](https://plugins.jetbrains.com/plugin/13607-merge-request-integration-ce--code-review-for-gitlab/) \nis exactly the same as \n[Enterprise Edition (EE)](https://plugins.jetbrains.com/plugin/13615-merge-request-integration-ee--code-review-for-gitlab/). \nYou don't need to hack or find a cracked version.\nCracking software invites virus to your computer.\n\n### About me\n\nMy name is Nhat, I'm a software developer at [Personio](https://personio.com) \n(yes, \n[we are hiring](https://www.personio.com/about-personio/jobs/) \nall around the world, relocation to Munich is of course possible).\n\n### Sponsor\n\nIf you love this plugin, please support me by:\n\n- Buy an [Enterprise Edition](https://plugins.jetbrains.com/plugin/13615-merge-request-integration-ee--code-review-for-gitlab/), only 1$/month\n- Buy me a beer via [Paypal](https://paypal.me/phanhoangnhat) or [Patreon](https://www.patreon.com/nhat/creators).\n\nThanks in advance!\n\n### Attribution\n\n- Icons by [Font Awesome](https://fontawesome.com/) are licensed under \n[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)\n"
  },
  {
    "path": "build.gradle.kts",
    "content": "plugins {\n    // \"org.jetbrains.kotlin.jvm\"\n    kotlin(\"jvm\") version \"1.3.50\" apply false\n\n    // \"org.jetbrains.kotlin.kapt\"\n    kotlin(\"kapt\") version \"1.3.50\" apply false\n\n    // \"kotlinx-serialization\"\n    id(\"kotlinx-serialization\") version \"1.3.50\" apply false\n\n    id(\"org.jetbrains.intellij\") version \"0.4.12\" apply false\n}\n\nsubprojects {\n    if (name == \"contracts\") {\n        apply(plugin = \"org.jetbrains.kotlin.jvm\")\n        apply(plugin = \"org.jetbrains.kotlin.kapt\")\n        apply(plugin = \"kotlinx-serialization\")\n    }\n\n    if (name == \"merge-request-integration\") {\n        apply(plugin = \"org.jetbrains.kotlin.jvm\")\n        apply(plugin = \"org.jetbrains.kotlin.kapt\")\n        apply(plugin = \"kotlinx-serialization\")\n    }\n\n    if (name == \"merge-request-integration-core\") {\n        apply(plugin = \"org.jetbrains.intellij\")\n        apply(plugin = \"org.jetbrains.kotlin.jvm\")\n    }\n\n    if (name == \"merge-request-integration-ce\") {\n        apply(plugin = \"org.jetbrains.intellij\")\n        apply(plugin = \"org.jetbrains.kotlin.jvm\")\n    }\n\n    if (name == \"merge-request-integration-ee\") {\n        apply(plugin = \"org.jetbrains.intellij\")\n        apply(plugin = \"org.jetbrains.kotlin.jvm\")\n    }\n}"
  },
  {
    "path": "contracts/build.gradle.kts",
    "content": "val artifactGroup: String by project\nval artifactVersion: String by project\nval jvmTarget: String by project\nval foundationVersion: String by project\nval foundationProcessorVersion: String by project\nval jodaTimeVersion: String by project\nval kotlinxSerializationRuntimeVersion: String by project\n\ngroup = artifactGroup\nversion = artifactVersion\n\nrepositories {\n    jcenter()\n    mavenCentral()\n    mavenLocal()\n    maven(\"https://jitpack.io\")\n}\n\ndependencies {\n    implementation(kotlin(\"stdlib\"))\n    implementation(\"com.github.nhat-phan.foundation:foundation-jvm:$foundationVersion\")\n    implementation(\"joda-time:joda-time:$jodaTimeVersion\")\n    compile(\"org.jetbrains.kotlinx:kotlinx-serialization-runtime:$kotlinxSerializationRuntimeVersion\")\n\n    kapt(\"com.github.nhat-phan.foundation:foundation-processor:$foundationProcessorVersion\")\n    kaptTest(\"com.github.nhat-phan.foundation:foundation-processor:$foundationProcessorVersion\")\n\n    testImplementation(kotlin(\"test\"))\n    testImplementation(kotlin(\"test-junit\"))\n}\n\nkapt {\n    arguments {\n        arg(\"foundation.processor.mode\", \"contractOnly\")\n        arg(\"foundation.processor.settingsClass\", \"net.ntworld.mergeRequestIntegration.ContractData\")\n    }\n}\n\ntasks {\n    named<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>(\"compileKotlin\") {\n        kotlinOptions {\n            jvmTarget = jvmTarget\n        }\n    }\n}\n"
  },
  {
    "path": "contracts/settings.gradle.kts",
    "content": "rootProject.name = \"contracts\"\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/Approval.kt",
    "content": "package net.ntworld.mergeRequest\n\ninterface Approval {\n    val approved: Boolean\n\n    val approvalsRequired: Int\n\n    val approvalsLeft: Int\n\n    val approvers: List<UserInfo>\n\n    val suggestedApprovers: List<UserInfo>\n\n    val approvedBy: List<UserInfo>\n\n    val hasApproved: Boolean\n\n    val canApprove: Boolean\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/Change.kt",
    "content": "package net.ntworld.mergeRequest\n\ninterface Change {\n    val oldPath: String\n    val newPath: String\n    val aMode: String\n    val bMode: String\n    val newFile: Boolean\n    val renamedFile: Boolean\n    val deletedFile: Boolean\n\n    companion object\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/Comment.kt",
    "content": "package net.ntworld.mergeRequest\n\ninterface Comment {\n    val id: String\n\n    val parentId: String\n\n    val replyId: String\n\n    val body: String\n\n    val author: UserInfo\n\n    val position: CommentPosition?\n\n    val createdAt: DateTime\n\n    val updatedAt: DateTime\n\n    val resolvable: Boolean\n\n    val resolved: Boolean\n\n    val resolvedBy: UserInfo?\n\n    val isDraft: Boolean\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/CommentPosition.kt",
    "content": "package net.ntworld.mergeRequest\n\ninterface CommentPosition {\n    val baseHash: String\n\n    val startHash: String\n\n    val headHash: String\n\n    val oldPath: String?\n\n    val newPath: String?\n\n    val oldLine: Int?\n\n    val newLine: Int?\n\n    val source: CommentPositionSource\n\n    val changeType: CommentPositionChangeType\n\n    companion object\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/CommentPositionChangeType.kt",
    "content": "package net.ntworld.mergeRequest\n\nenum class CommentPositionChangeType{\n    UNKNOWN,\n    INSERTED,\n    DELETED,\n    MODIFIED\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/CommentPositionSource.kt",
    "content": "package net.ntworld.mergeRequest\n\nenum class CommentPositionSource {\n    UNKNOWN,\n    SERVER,\n    SINGLE_SIDE,\n    SIDE_BY_SIDE_LEFT,\n    SIDE_BY_SIDE_RIGHT,\n    UNIFIED\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/Commit.kt",
    "content": "package net.ntworld.mergeRequest\n\ninterface Commit {\n    val id: String\n\n    val message: String\n\n    val authorName: String\n\n    val authorEmail: String\n\n    val createdAt: String\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/DateTime.kt",
    "content": "package net.ntworld.mergeRequest\n\ntypealias DateTime = String"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/DateTimeSerializer.kt",
    "content": "@file:UseSerializers(DateTimeSerializer::class)\n\npackage net.ntworld.mergeRequest\n\nimport kotlinx.serialization.*\nimport kotlinx.serialization.internal.SerialClassDescImpl\nimport org.joda.time.DateTime\n\n/**\n * TODO: Cannot use until foundation support @SerializerFor(...)\n */\n@Serializer(forClass = DateTime::class)\nobject DateTimeSerializer : KSerializer<DateTime> {\n\n    override val descriptor: SerialDescriptor = SerialClassDescImpl(\"DateTime\")\n\n    override fun deserialize(decoder: Decoder): DateTime {\n        return DateTime(decoder.decodeString())\n    }\n\n    override fun serialize(encoder: Encoder, obj: DateTime) {\n        encoder.encodeString(obj.toString())\n    }\n\n}\n\n\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/DiffReference.kt",
    "content": "package net.ntworld.mergeRequest\n\ninterface DiffReference {\n    val baseHash: String\n\n    val headHash: String\n\n    val startHash: String\n\n    companion object\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/MergeRequest.kt",
    "content": "package net.ntworld.mergeRequest\n\ninterface MergeRequest: MergeRequestInfo {\n    val assignee: UserInfo?\n\n    val author: UserInfo\n\n    val diffReference: DiffReference?\n\n    val sourceBranch: String\n\n    val targetBranch: String\n\n    val upVotes: Int\n\n    val downVotes: Int\n\n    val commentsCount: Int\n\n    val isWorkInProgress: Boolean\n\n    val canMerged: Boolean\n\n    val mergedBy: UserInfo?\n\n    val closedBy: UserInfo?\n\n    val mergedAt: DateTime?\n\n    val closedAt: DateTime?\n\n    companion object\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/MergeRequestInfo.kt",
    "content": "package net.ntworld.mergeRequest\n\ninterface MergeRequestInfo {\n    val id: String\n\n    val provider: String\n\n    val projectId: String\n\n    val title: String\n\n    val description: String\n\n    val url: String\n\n    val state: MergeRequestState\n\n    val createdAt: DateTime\n\n    val updatedAt: DateTime\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/MergeRequestState.kt",
    "content": "package net.ntworld.mergeRequest\n\nenum class MergeRequestState {\n    ALL,\n    OPENED,\n    CLOSED,\n    MERGED\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/Pipeline.kt",
    "content": "package net.ntworld.mergeRequest\n\ninterface Pipeline {\n    val id: String\n\n    val hash: String\n\n    val ref: String\n\n    val status: PipelineStatus\n\n    val url: String\n\n    val createdAt: DateTime?\n\n    val updatedAt: DateTime?\n\n    companion object\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/PipelineStatus.kt",
    "content": "package net.ntworld.mergeRequest\n\nenum class PipelineStatus {\n    FAILED,\n    RUNNING,\n    PARTIAL_FAILED,\n    SUCCESS,\n    UNKNOWN,\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/Project.kt",
    "content": "package net.ntworld.mergeRequest\n\ninterface Project {\n    val id: String\n\n    val provider: ProviderInfo\n\n    val name: String\n\n    val path: String\n\n    val url: String\n\n    val avatarUrl: String\n\n    val visibility: ProjectVisibility\n\n    val repositoryHttpUrl: String\n\n    val repositorySshUrl: String\n\n    companion object\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/ProjectVisibility.kt",
    "content": "package net.ntworld.mergeRequest\n\nenum class ProjectVisibility {\n    PUBLIC,\n    PRIVATE\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/ProviderData.kt",
    "content": "package net.ntworld.mergeRequest\n\nimport net.ntworld.mergeRequest.api.ApiCredentials\n\ninterface ProviderData {\n    val id: String\n\n    val key: String\n\n    val name: String\n\n    val info: ProviderInfo\n\n    val credentials: ApiCredentials\n\n    val project: Project\n\n    val currentUser: User\n\n    val repository: String\n\n    val errorMessage: String?\n\n    val status: ProviderStatus\n\n    val hasApprovalFeature: Boolean\n        get() = false\n\n    val hasAssigneeFeature: Boolean\n        get() = false\n\n    companion object\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/ProviderInfo.kt",
    "content": "package net.ntworld.mergeRequest\n\ninterface ProviderInfo {\n    val id: String\n\n    val name: String\n\n    val iconPath: String\n\n    val icon2xPath: String\n\n    val icon3xPath: String\n\n    val icon4xPath: String\n\n    fun createCommentUrl(mergeRequestUrl: String, comment: Comment): String\n\n    fun formatMergeRequestId(mergeRequestId: String): String\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/ProviderStatus.kt",
    "content": "package net.ntworld.mergeRequest\n\nenum class ProviderStatus {\n    ACTIVE,\n    ERROR\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/User.kt",
    "content": "package net.ntworld.mergeRequest\n\ninterface User : UserInfo {\n    val email: String\n\n    val createdAt: DateTime\n\n    companion object\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/UserInfo.kt",
    "content": "package net.ntworld.mergeRequest\n\ninterface UserInfo {\n    val id: String\n\n    val name: String\n\n    val username: String\n\n    val avatarUrl: String?\n\n    val url: String\n\n    val status: UserStatus\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/UserStatus.kt",
    "content": "package net.ntworld.mergeRequest\n\nenum class UserStatus {\n    ACTIVE,\n    INACTIVE\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/api/ApiConnection.kt",
    "content": "package net.ntworld.mergeRequest.api\n\ninterface ApiConnection {\n    val url: String\n\n    val ignoreSSLCertificateErrors: Boolean\n\n    val login: String\n\n    val token: String\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/api/ApiCredentials.kt",
    "content": "package net.ntworld.mergeRequest.api\n\ninterface ApiCredentials : ApiConnection {\n    val projectId: String\n\n    val version: String\n\n    val info: String\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/api/ApiOptions.kt",
    "content": "package net.ntworld.mergeRequest.api\n\ninterface ApiOptions {\n    val enableRequestCache: Boolean\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/api/ApiProvider.kt",
    "content": "package net.ntworld.mergeRequest.api\n\nimport net.ntworld.mergeRequest.ProviderInfo\nimport net.ntworld.mergeRequest.User\nimport net.ntworld.mergeRequest.UserInfo\n\ninterface ApiProvider {\n    val info: ProviderInfo\n\n    val credentials: ApiCredentials\n\n    val cache: Cache\n\n    val user: UserApi\n\n    val mergeRequest: MergeRequestApi\n\n    val project: ProjectApi\n\n    val comment: CommentApi\n\n    val commit: CommitApi\n\n    fun setOptions(options: ApiOptions)\n\n    fun initialize(currentUser: UserInfo)\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/api/Cache.kt",
    "content": "package net.ntworld.mergeRequest.api\n\nimport org.joda.time.DateTime\n\ninterface Cache {\n    val defaultTTL: Int\n\n    fun <T> get(key: String): T\n\n    fun has(key: String): Boolean\n\n    fun remove(key: String)\n\n    fun put(key: String, value: Any, ttl: Int)\n\n    fun isExpiredAfter(key: String, datetime: DateTime): Boolean\n\n    fun put(key: String, value: Any) = put(key, value, defaultTTL)\n\n    fun set(key: String, value: Any) = put(key, value, defaultTTL)\n\n    fun set(key: String, value: Any, ttl: Int) = put(key, value, ttl)\n\n    @Suppress(\"UNCHECKED_CAST\")\n    fun<T> getOrRun(key: String, run: (() -> T)): T {\n        try {\n            if (!this.has(key)) {\n                return run()\n            }\n            return this.get(key)\n        } catch (exception: CacheNotFoundException) {\n            return run()\n        }\n    }\n\n    fun removeIfExpiredAfter(key: String, datetime: DateTime) {\n        if (isExpiredAfter(key, datetime)) {\n            remove(key)\n        }\n    }\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/api/CacheNotFoundException.kt",
    "content": "package net.ntworld.mergeRequest.api\n\nclass CacheNotFoundException : Throwable()"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/api/CommentApi.kt",
    "content": "package net.ntworld.mergeRequest.api\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequest.Project\n\ninterface CommentApi {\n    fun getAll(project: Project, mergeRequestId: String): List<Comment>\n\n    fun create(\n        project: Project,\n        mergeRequestId: String,\n        body: String,\n        position: CommentPosition?,\n        isDraft: Boolean\n    ): String?\n\n    fun reply(project: Project, mergeRequestId: String, repliedComment: Comment, body: String): String?\n\n    fun delete(project: Project, mergeRequestId: String, comment: Comment)\n\n    fun resolve(project: Project, mergeRequestId: String, comment: Comment)\n\n    fun unresolve(project: Project, mergeRequestId: String, comment: Comment)\n\n    fun update(project: Project, mergeRequestId: String, comment: Comment, body: String)\n\n    fun hasDraft(project: Project, mergeRequestId: String): Boolean {\n        return this.getDraftCount(project, mergeRequestId) > 0\n    }\n\n    fun getDraftCount(project: Project, mergeRequestId: String): Int\n\n    fun publishAllDraftComments(project: Project, mergeRequestId: String)\n\n    fun publishDraftComments(project: Project, mergeRequestId: String, commentIds: List<String>)\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/api/CommitApi.kt",
    "content": "package net.ntworld.mergeRequest.api\n\nimport net.ntworld.mergeRequest.Change\n\ninterface CommitApi {\n\n    fun getChanges(projectId: String, commitId: String): List<Change>\n\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/api/DraftCommentStorage.kt",
    "content": "package net.ntworld.mergeRequest.api\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequest.Project\n\ninterface DraftCommentStorage {\n    fun getAll(project: Project, mergeRequestId: String): List<Comment>\n\n    fun findById(project: Project, mergeRequestId: String, commentId: String): Comment?\n\n    fun create(project: Project, mergeRequestId: String, body: String, position: CommentPosition?): String?\n\n    fun update(project: Project, mergeRequestId: String, comment: Comment, body: String)\n\n    fun delete(project: Project, mergeRequestId: String, comment: Comment)\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/api/MergeRequestApi.kt",
    "content": "package net.ntworld.mergeRequest.api\n\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequest.query.GetMergeRequestFilter\n\ninterface MergeRequestApi {\n    fun find(projectId: String, mergeRequestId: String): MergeRequest?\n\n    fun approve(projectId: String, mergeRequestId: String, sha: String)\n\n    fun unapprove(projectId: String, mergeRequestId: String)\n\n    fun findApproval(projectId: String, mergeRequestId: String): Approval\n\n    fun getPipelines(projectId: String, mergeRequestId: String): List<Pipeline>\n\n    fun getCommits(projectId: String, mergeRequestId: String): List<Commit>\n\n    fun getChanges(projectId: String, mergeRequestId: String): List<Change>\n\n    fun search(\n        projectId: String,\n        currentUserId: String,\n        filterBy: GetMergeRequestFilter,\n        orderBy: MergeRequestOrdering,\n        page: Int,\n        itemsPerPage: Int\n    ): SearchResult\n\n    fun findOrFail(projectId: String, mergeRequestId: String): MergeRequest\n\n    interface SearchResult {\n        val data: List<MergeRequestInfo>\n\n        val totalPages: Int\n\n        val totalItems: Int\n\n        val currentPage: Int\n    }\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/api/MergeRequestOrdering.kt",
    "content": "package net.ntworld.mergeRequest.api\n\nenum class MergeRequestOrdering {\n    RECENTLY_UPDATED,\n    NEWEST,\n    OLDEST\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/api/ProjectApi.kt",
    "content": "package net.ntworld.mergeRequest.api\n\nimport net.ntworld.mergeRequest.Project\nimport net.ntworld.mergeRequest.UserInfo\n\ninterface ProjectApi {\n    fun find(projectId: String): Project?\n\n    fun getMembers(projectId: String): List<UserInfo>\n\n    fun findOrFail(projectId: String): Project {\n        val project = find(projectId)\n        if (null === project) {\n            throw Exception(\"Project $projectId not found.\")\n        }\n        return project\n    }\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/api/UserApi.kt",
    "content": "package net.ntworld.mergeRequest.api\n\nimport net.ntworld.mergeRequest.User\n\ninterface UserApi {\n    // @Deprecated(\"Not used, will be removed\", ReplaceWith(\"Nothing\"), DeprecationLevel.HIDDEN)\n    // fun find(id: String): UserInfo\n\n    fun me(): User\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/command/ApproveMergeRequestCommand.kt",
    "content": "package net.ntworld.mergeRequest.command\n\nimport net.ntworld.foundation.cqrs.Command\n\ninterface ApproveMergeRequestCommand : Command {\n    val providerId: String\n\n    val mergeRequestId: String\n\n    val sha: String\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/command/DeleteCommentCommand.kt",
    "content": "package net.ntworld.mergeRequest.command\n\nimport net.ntworld.foundation.cqrs.Command\nimport net.ntworld.mergeRequest.Comment\n\ninterface DeleteCommentCommand : Command {\n    val providerId: String\n\n    val mergeRequestId: String\n\n    val comment: Comment\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/command/ResolveCommentCommand.kt",
    "content": "package net.ntworld.mergeRequest.command\n\nimport net.ntworld.foundation.cqrs.Command\nimport net.ntworld.mergeRequest.Comment\n\ninterface ResolveCommentCommand : Command {\n    val providerId: String\n\n    val mergeRequestId: String\n\n    val comment: Comment\n\n    companion object\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/command/UnapproveMergeRequestCommand.kt",
    "content": "package net.ntworld.mergeRequest.command\n\nimport net.ntworld.foundation.cqrs.Command\n\ninterface UnapproveMergeRequestCommand : Command {\n    val providerId: String\n\n    val mergeRequestId: String\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/command/UnresolveCommentCommand.kt",
    "content": "package net.ntworld.mergeRequest.command\n\nimport net.ntworld.foundation.cqrs.Command\nimport net.ntworld.mergeRequest.Comment\n\ninterface UnresolveCommentCommand : Command {\n    val providerId: String\n\n    val mergeRequestId: String\n\n    val comment: Comment\n\n    companion object\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/query/FindApprovalQuery.kt",
    "content": "package net.ntworld.mergeRequest.query\n\nimport net.ntworld.foundation.cqrs.Query\n\ninterface FindApprovalQuery : QueryBase, Query<FindApprovalQueryResult> {\n    val mergeRequestId: String\n\n    companion object\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/query/FindApprovalQueryResult.kt",
    "content": "package net.ntworld.mergeRequest.query\n\nimport net.ntworld.foundation.cqrs.QueryResult\nimport net.ntworld.mergeRequest.Approval\n\ninterface FindApprovalQueryResult : QueryResult {\n    val approval: Approval\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/query/FindMergeRequestQuery.kt",
    "content": "package net.ntworld.mergeRequest.query\n\nimport net.ntworld.foundation.cqrs.Query\n\ninterface FindMergeRequestQuery: QueryBase, Query<FindMergeRequestQueryResult> {\n    val mergeRequestId: String\n\n    companion object\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/query/FindMergeRequestQueryResult.kt",
    "content": "package net.ntworld.mergeRequest.query\n\nimport net.ntworld.foundation.cqrs.QueryResult\nimport net.ntworld.mergeRequest.MergeRequest\n\ninterface FindMergeRequestQueryResult : QueryResult {\n    val mergeRequest: MergeRequest\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetCommentsQuery.kt",
    "content": "package net.ntworld.mergeRequest.query\n\nimport net.ntworld.foundation.cqrs.Query\n\ninterface GetCommentsQuery : QueryBase, Query<GetCommentsQueryResult> {\n    val mergeRequestId: String\n\n    companion object\n}\n\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetCommentsQueryResult.kt",
    "content": "package net.ntworld.mergeRequest.query\n\nimport net.ntworld.foundation.cqrs.QueryResult\nimport net.ntworld.mergeRequest.Comment\n\ninterface GetCommentsQueryResult : QueryResult {\n    val comments: List<Comment>\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetCommitsQuery.kt",
    "content": "package net.ntworld.mergeRequest.query\n\nimport net.ntworld.foundation.cqrs.Query\n\ninterface GetCommitsQuery : QueryBase,\n    Query<GetCommitsQueryResult> {\n    val mergeRequestId: String\n\n    companion object\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetCommitsQueryResult.kt",
    "content": "package net.ntworld.mergeRequest.query\n\nimport net.ntworld.foundation.cqrs.QueryResult\nimport net.ntworld.mergeRequest.Commit\n\ninterface GetCommitsQueryResult : QueryResult {\n    val commits: List<Commit>\n\n    companion object\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetMergeRequestFilter.kt",
    "content": "package net.ntworld.mergeRequest.query\n\nimport net.ntworld.mergeRequest.MergeRequestState\n\ninterface GetMergeRequestFilter {\n    val id: Int?\n\n    val state: MergeRequestState\n\n    val search: String\n\n    val authorId: String\n\n    val assigneeId: String\n\n    val approverIds: List<String>\n\n    val sourceBranch: String\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetMergeRequestsQuery.kt",
    "content": "package net.ntworld.mergeRequest.query\n\nimport net.ntworld.foundation.cqrs.Query\nimport net.ntworld.mergeRequest.api.MergeRequestOrdering\n\ninterface GetMergeRequestsQuery : QueryBase, Query<GetMergeRequestsQueryResult> {\n    val filterBy: GetMergeRequestFilter\n\n    val orderBy: MergeRequestOrdering\n\n    val page: Int\n\n    val itemsPerPage: Int\n\n    companion object\n}\n\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetMergeRequestsQueryResult.kt",
    "content": "package net.ntworld.mergeRequest.query\n\nimport net.ntworld.foundation.cqrs.QueryResult\nimport net.ntworld.mergeRequest.MergeRequestInfo\n\ninterface GetMergeRequestsQueryResult : QueryResult {\n    val mergeRequests: List<MergeRequestInfo>\n\n    val totalPages: Int\n\n    val totalItems: Int\n\n    val currentPage: Int\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetPipelinesQuery.kt",
    "content": "package net.ntworld.mergeRequest.query\n\nimport net.ntworld.foundation.cqrs.Query\n\ninterface GetPipelinesQuery : QueryBase,\n    Query<GetPipelinesQueryResult> {\n    val mergeRequestId: String\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetPipelinesQueryResult.kt",
    "content": "package net.ntworld.mergeRequest.query\n\nimport net.ntworld.foundation.cqrs.QueryResult\nimport net.ntworld.mergeRequest.Pipeline\n\ninterface GetPipelinesQueryResult : QueryResult {\n    val pipelines: List<Pipeline>\n\n    companion object\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetProjectMembersQuery.kt",
    "content": "package net.ntworld.mergeRequest.query\n\nimport net.ntworld.foundation.cqrs.Query\n\ninterface GetProjectMembersQuery : QueryBase, Query<GetProjectMembersQueryResult> {\n    companion object\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/query/GetProjectMembersQueryResult.kt",
    "content": "package net.ntworld.mergeRequest.query\n\nimport net.ntworld.foundation.cqrs.QueryResult\nimport net.ntworld.mergeRequest.UserInfo\n\ninterface GetProjectMembersQueryResult : QueryResult {\n    val members: List<UserInfo>\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/query/QueryBase.kt",
    "content": "package net.ntworld.mergeRequest.query\n\ninterface QueryBase {\n    val providerId: String\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/request/CreateCommentRequest.kt",
    "content": "package net.ntworld.mergeRequest.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequest.response.CreateCommentResponse\n\ninterface CreateCommentRequest : Request<CreateCommentResponse> {\n    val providerId: String\n\n    val mergeRequestId: String\n\n    val body: String\n\n    val position: CommentPosition?\n\n    val isDraft: Boolean\n\n    companion object\n}\n"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/request/PublishAllCommentsRequest.kt",
    "content": "package net.ntworld.mergeRequest.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.response.PublishAllCommentsResponse\n\ninterface PublishAllCommentsRequest : Request<PublishAllCommentsResponse> {\n    val providerId: String\n\n    val mergeRequestId: String\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/request/PublishCommentsRequest.kt",
    "content": "package net.ntworld.mergeRequest.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.response.PublishCommentsResponse\n\ninterface PublishCommentsRequest : Request<PublishCommentsResponse> {\n    val providerId: String\n\n    val mergeRequestId: String\n\n    val draftCommentIds: List<String>\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/request/ReplyCommentRequest.kt",
    "content": "package net.ntworld.mergeRequest.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.response.ReplyCommentResponse\n\ninterface ReplyCommentRequest : Request<ReplyCommentResponse> {\n    val providerId: String\n\n    val mergeRequestId: String\n\n    val repliedComment: Comment\n\n    val body: String\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/request/UpdateCommentRequest.kt",
    "content": "package net.ntworld.mergeRequest.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.response.UpdateCommentResponse\n\ninterface UpdateCommentRequest : Request<UpdateCommentResponse> {\n    val providerId: String\n\n    val mergeRequestId: String\n\n    val comment: Comment\n\n    val body: String\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/response/CreateCommentResponse.kt",
    "content": "package net.ntworld.mergeRequest.response\n\nimport net.ntworld.foundation.Response\n\ninterface CreateCommentResponse : Response {\n    val createdCommentId: String?\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/response/PublishAllCommentsResponse.kt",
    "content": "package net.ntworld.mergeRequest.response\n\nimport net.ntworld.foundation.Response\n\ninterface PublishAllCommentsResponse : Response {\n    val success: Boolean\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/response/PublishCommentsResponse.kt",
    "content": "package net.ntworld.mergeRequest.response\n\nimport net.ntworld.foundation.Response\n\ninterface PublishCommentsResponse : Response {\n    val success: Boolean\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/response/ReplyCommentResponse.kt",
    "content": "package net.ntworld.mergeRequest.response\n\nimport net.ntworld.foundation.Response\n\ninterface ReplyCommentResponse : Response {\n    val createdCommentId: String?\n\n    companion object\n}"
  },
  {
    "path": "contracts/src/main/kotlin/net/ntworld/mergeRequest/response/UpdateCommentResponse.kt",
    "content": "package net.ntworld.mergeRequest.response\n\nimport net.ntworld.foundation.Response\n\ninterface UpdateCommentResponse : Response {\n    val commentId: String?\n\n    companion object\n}"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Thu Oct 10 17:57:14 CEST 2019\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-5.3.1-all.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "artifactGroup=net.ntworld.nhat-phan.merge-request-integration\n\n# Please also change version number in\n# net.ntworld.mergeRequestIntegration.update.UpdateManager\neapRelease=false\nartifactVersion=2020.3.0\ntargetIDEVersion=2020.3.x\ncommunityEditionVersion=2020.3.0\nenterpriseEditionVersion=2020.3.0\n\n# intellijVersion=LATEST-EAP-SNAPSHOT\nintellijVersion=2020.3\nintellijSinceBuild=203\nintellijUntilBuild=203.*\n\njvmTarget=1.8\n\nfoundationVersion=0.6\nfoundationProcessorVersion=0.6.1\n\nkotlinxSerializationRuntimeVersion=0.11.1\njavaFakerVersion=1.0.0\n\njodaTimeVersion=2.10.5\n\nfuelVersion=2.2.1\n\ngitlab4jVersion=4.14.5\ngithubApiVersion=1.101\n\nprettyTimeVersion=4.0.1.Final\n\ncommonmarkVersion=0.13.0\n\nmockkVersion=1.9.3\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=$(save \"$@\")\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\n# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong\nif [ \"$(uname)\" = \"Darwin\" ] && [ \"$HOME\" = \"$PWD\" ]; then\n  cd \"$(dirname \"$0\")\"\nfi\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      http://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n\r\n@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:init\r\n@rem Get command-line arguments, handling Windows variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\n\r\n:win9xME_args\r\n@rem Slurp the command line arguments.\r\nset CMD_LINE_ARGS=\r\nset _SKIP=2\r\n\r\n:win9xME_args_slurp\r\nif \"x%~1\" == \"x\" goto execute\r\n\r\nset CMD_LINE_ARGS=%*\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "merge-request-integration/build.gradle.kts",
    "content": "val artifactGroup: String by project\nval artifactVersion: String by project\nval jvmTarget: String by project\nval foundationVersion: String by project\nval foundationProcessorVersion: String by project\nval kotlinxSerializationRuntimeVersion: String by project\nval javaFakerVersion: String by project\nval jodaTimeVersion: String by project\nval fuelVersion: String by project\nval gitlab4jVersion: String by project\nval githubApiVersion: String by project\nval prettyTimeVersion: String by project\nval commonmarkVersion: String by project\n\ngroup = artifactGroup\nversion = artifactVersion\n\nrepositories {\n    jcenter()\n    mavenCentral()\n    mavenLocal()\n    maven(\"https://jitpack.io\")\n}\n\ndependencies {\n    implementation(kotlin(\"stdlib\"))\n    implementation(\"com.github.nhat-phan.foundation:foundation-jvm:$foundationVersion\")\n    implementation(project(\":contracts\"))\n    implementation(\"joda-time:joda-time:$jodaTimeVersion\")\n    implementation(\"com.github.kittinunf.fuel:fuel:$fuelVersion\")\n    implementation(\"org.gitlab4j:gitlab4j-api:$gitlab4jVersion\")\n    implementation(\"org.kohsuke:github-api:$githubApiVersion\")\n    implementation(\"org.ocpsoft.prettytime:prettytime:$prettyTimeVersion\")\n\n    compile(\"com.atlassian.commonmark:commonmark:$commonmarkVersion\")\n    compile(\"org.jetbrains.kotlinx:kotlinx-serialization-runtime:$kotlinxSerializationRuntimeVersion\")\n    compile(\"com.github.javafaker:javafaker:$javaFakerVersion\")\n\n    kapt(\"com.github.nhat-phan.foundation:foundation-processor:$foundationProcessorVersion\")\n    kaptTest(\"com.github.nhat-phan.foundation:foundation-processor:$foundationProcessorVersion\")\n\n    testImplementation(kotlin(\"test\"))\n    testImplementation(kotlin(\"test-junit\"))\n    testImplementation(\"io.mockk:mockk:1.9\")\n    testImplementation(\"io.kotlintest:kotlintest-runner-junit5:3.3.0\")\n}\n\ntasks.withType<Test> {\n    useJUnitPlatform()\n}\n\nkapt {\n    arguments {\n        arg(\"foundation.processor.globalNamespace\", \"net.ntworld.mergeRequestIntegration\")\n    }\n}\n\ntasks {\n    named<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>(\"compileKotlin\") {\n        kotlinOptions {\n            jvmTarget = jvmTarget\n        }\n    }\n}\n"
  },
  {
    "path": "merge-request-integration/settings.gradle.kts",
    "content": "rootProject.name = \"merge-request-integration\"\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/DefaultProviderStorage.kt",
    "content": "package net.ntworld.mergeRequestIntegration\n\nimport net.ntworld.foundation.Infrastructure\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequest.api.ApiOptions\nimport net.ntworld.mergeRequest.api.ApiProvider\nimport net.ntworld.mergeRequestIntegration.exception.ProviderNotFoundException\nimport net.ntworld.mergeRequestIntegration.internal.ProjectImpl\nimport net.ntworld.mergeRequestIntegration.internal.ProviderDataImpl\nimport net.ntworld.mergeRequestIntegration.internal.UserImpl\nimport net.ntworld.mergeRequestIntegration.provider.MemoryCache\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.Gitlab\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabApiProvider\nimport java.util.*\n\nclass DefaultProviderStorage : ProviderStorage {\n    private val data = Collections.synchronizedMap(mutableMapOf<String, ProviderData>())\n    private val api = Collections.synchronizedMap(mutableMapOf<String, ApiProvider>())\n\n    override val registeredProviders\n        get() = data.values.toList()\n\n    override fun updateApiOptions(options: ApiOptions) {\n        api.forEach { it.value.setOptions(options) }\n    }\n\n    override fun register(\n        infrastructure: Infrastructure,\n        id: String,\n        key: String,\n        name: String,\n        info: ProviderInfo,\n        credentials: ApiCredentials,\n        repository: String\n    ): Pair<ProviderData, Throwable?> {\n        val api = createApiProvider(infrastructure = infrastructure, id = id, info = info, credentials = credentials)\n        var throwable: Throwable? = null\n        var message: String? = null\n        try {\n            val user = api.user.me()\n            api.initialize(user)\n\n            val project = api.project.find(credentials.projectId)\n            if (null !== project) {\n                val providerData = ProviderDataImpl(\n                    id = id,\n                    key = key,\n                    name = name,\n                    info = info,\n                    credentials = credentials,\n                    project = project,\n                    currentUser = user,\n                    repository = repository,\n                    errorMessage = null,\n                    status = ProviderStatus.ACTIVE\n                )\n                data[id] = providerData\n                return Pair(providerData, null)\n            }\n        } catch (exception: Throwable) {\n            message = exception.message\n            throwable = exception\n        }\n        val invalid = ProviderDataImpl(\n            id = id,\n            key = key,\n            name = name,\n            info = info,\n            credentials = credentials,\n            project = ProjectImpl(\"\", info, \"\", \"\", \"\", \"\", ProjectVisibility.PUBLIC, \"\", \"\"),\n            currentUser = UserImpl(\"\", \"[Error]\", \"<error>\", \"\", \"\", UserStatus.INACTIVE, \"\", \"\"),\n            repository = repository,\n            errorMessage = message,\n            status = ProviderStatus.ERROR\n        )\n        data[id] = invalid\n        return Pair(invalid, throwable)\n    }\n\n    override fun clear() {\n        data.clear()\n        api.clear()\n    }\n\n    private fun createApiProvider(\n        infrastructure: Infrastructure,\n        id: String,\n        info: ProviderInfo,\n        credentials: ApiCredentials\n    ): ApiProvider {\n        val created = when (info.id) {\n            Gitlab.id -> GitlabApiProvider(\n                infrastructure = infrastructure,\n                credentials = credentials,\n                cache = MemoryCache()\n            )\n            // Github.id -> GithubApiProvider(\n            //     infrastructure = infrastructure,\n            //     credentials = credentials,\n            //     cache = MemoryCache()\n            // )\n            else -> throw Exception(\"Cannot create ApiProvider ${info.id}\")\n        }\n        api[id] = created\n        return created\n    }\n\n    override fun findOrFail(id: String): Pair<ProviderData, ApiProvider> {\n        return Pair(findDataOrFail(id), findProviderOrFail(id))\n    }\n\n    private fun findDataOrFail(id: String): ProviderData {\n        val item = data[id]\n        return if (null !== item) {\n            item\n        } else {\n            throw ProviderNotFoundException()\n        }\n    }\n\n    private fun findProviderOrFail(id: String): ApiProvider {\n        val provider = api[id]\n        return if (null !== provider) {\n            provider\n        } else {\n            throw ProviderNotFoundException()\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/MergeRequestIntegrationInfrastructure.kt",
    "content": "package net.ntworld.mergeRequestIntegration\n\nimport net.ntworld.foundation.InfrastructureProvider\n\nclass MergeRequestIntegrationInfrastructure(\n    private val providerStorage: ProviderStorage\n) : InfrastructureProvider() {\n    private val included = listOf(\n        AutoGeneratedInfrastructureProvider(providerStorage)\n    )\n\n    init {\n        wire(this.root, this.included)\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/ProviderStorage.kt",
    "content": "package net.ntworld.mergeRequestIntegration\n\nimport net.ntworld.foundation.Infrastructure\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.ProviderInfo\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequest.api.ApiOptions\nimport net.ntworld.mergeRequest.api.ApiProvider\n\ninterface ProviderStorage {\n\n    val registeredProviders: List<ProviderData>\n\n    fun updateApiOptions(options: ApiOptions)\n\n    fun register(\n        infrastructure: Infrastructure,\n        id: String,\n        key: String,\n        name: String,\n        info: ProviderInfo,\n        credentials: ApiCredentials,\n        repository: String\n    ): Pair<ProviderData, Throwable?>\n\n    fun clear()\n\n    fun findOrFail(id: String): Pair<ProviderData, ApiProvider>\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/_const.kt",
    "content": "package net.ntworld.mergeRequestIntegration\n\nconst val DEFAULT_DATETIME = \"1970-01-01T00:00:00Z\""
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/commandHandler/ApproveMergeRequestCommandHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.commandHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.CommandHandler\nimport net.ntworld.mergeRequest.command.ApproveMergeRequestCommand\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\n\n@Handler\nclass ApproveMergeRequestCommandHandler(\n    private val providerStorage: ProviderStorage\n) : CommandHandler<ApproveMergeRequestCommand> {\n    override fun handle(command: ApproveMergeRequestCommand) {\n        val (data, api) = providerStorage.findOrFail(command.providerId)\n        api.mergeRequest.approve(\n            projectId = data.project.id,\n            mergeRequestId = command.mergeRequestId,\n            sha = command.sha\n        )\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/commandHandler/DeleteCommentCommandHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.commandHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.CommandHandler\nimport net.ntworld.mergeRequest.command.DeleteCommentCommand\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\n\n@Handler\nclass DeleteCommentCommandHandler(\n    private val providerStorage: ProviderStorage\n) : CommandHandler<DeleteCommentCommand> {\n    override fun handle(command: DeleteCommentCommand) {\n        val (data, api) = providerStorage.findOrFail(command.providerId)\n        api.comment.delete(data.project, command.mergeRequestId, command.comment)\n    }\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/commandHandler/ResolveCommentCommandHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.commandHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.CommandHandler\nimport net.ntworld.mergeRequest.command.ResolveCommentCommand\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\n\n@Handler\nclass ResolveCommentCommandHandler(\n    private val providerStorage: ProviderStorage\n) : CommandHandler<ResolveCommentCommand> {\n    override fun handle(command: ResolveCommentCommand) {\n        val (data, api) = providerStorage.findOrFail(command.providerId)\n        api.comment.resolve(data.project, command.mergeRequestId, command.comment)\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/commandHandler/UnapproveMergeRequestCommandHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.commandHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.CommandHandler\nimport net.ntworld.mergeRequest.command.UnapproveMergeRequestCommand\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\n\n@Handler\nclass UnapproveMergeRequestCommandHandler(\n    private val providerStorage: ProviderStorage\n) : CommandHandler<UnapproveMergeRequestCommand> {\n    override fun handle(command: UnapproveMergeRequestCommand) {\n        val (data, api) = providerStorage.findOrFail(command.providerId)\n        api.mergeRequest.unapprove(\n            projectId = data.project.id,\n            mergeRequestId = command.mergeRequestId\n        )\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/commandHandler/UnresolveCommentCommandHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.commandHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.CommandHandler\nimport net.ntworld.mergeRequest.command.UnresolveCommentCommand\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\n\n@Handler\nclass UnresolveCommentCommandHandler(\n    private val providerStorage: ProviderStorage\n) : CommandHandler<UnresolveCommentCommand> {\n    override fun handle(command: UnresolveCommentCommand) {\n        val (data, api) = providerStorage.findOrFail(command.providerId)\n        api.comment.unresolve(data.project, command.mergeRequestId, command.comment)\n    }\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/exception/InvalidCacheKeyException.kt",
    "content": "package net.ntworld.mergeRequestIntegration.exception\n\nimport java.lang.Exception\n\nclass InvalidCacheKeyException(message: String): Exception(message)"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/exception/InvalidTTLException.kt",
    "content": "package net.ntworld.mergeRequestIntegration.exception\n\nclass InvalidTTLException: Exception()"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/exception/ProviderNotFoundException.kt",
    "content": "package net.ntworld.mergeRequestIntegration.exception\n\nimport java.lang.Exception\n\nclass ProviderNotFoundException: Exception()"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/ApiOptionsImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegration.internal\n\nimport net.ntworld.mergeRequest.api.ApiOptions\n\ndata class ApiOptionsImpl(override val enableRequestCache: Boolean) : ApiOptions {\n    companion object {\n        val DEFAULT = ApiOptionsImpl(\n            enableRequestCache = true\n        )\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/ApprovalImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegration.internal\n\nimport net.ntworld.mergeRequest.Approval\nimport net.ntworld.mergeRequest.UserInfo\n\ndata class ApprovalImpl(\n    override val approved: Boolean,\n    override val approvalsRequired: Int,\n    override val approvalsLeft: Int,\n    override val suggestedApprovers: List<UserInfo>,\n    override val approvers: List<UserInfo>,\n    override val approvedBy: List<UserInfo>,\n    override val hasApproved: Boolean,\n    override val canApprove: Boolean\n) : Approval\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/ChangeImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegration.internal\n\nimport net.ntworld.mergeRequest.Change\n\ndata class ChangeImpl(\n    override val oldPath: String,\n    override val newPath: String,\n    override val aMode: String,\n    override val bMode: String,\n    override val newFile: Boolean,\n    override val renamedFile: Boolean,\n    override val deletedFile: Boolean\n) : Change"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/CommentImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegration.internal\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequest.DateTime\nimport net.ntworld.mergeRequest.UserInfo\n\ndata class CommentImpl(\n    override val id: String,\n    override val parentId: String,\n    override val replyId: String,\n    override val body: String,\n    override val author: UserInfo,\n    override val position: CommentPosition?,\n    override val createdAt: DateTime,\n    override val updatedAt: DateTime,\n    override val resolvable: Boolean,\n    override val resolved: Boolean,\n    override val resolvedBy: UserInfo?,\n    override val isDraft: Boolean\n) : Comment {\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/CommentPositionImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegration.internal\n\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequest.CommentPositionChangeType\nimport net.ntworld.mergeRequest.CommentPositionSource\n\ndata class CommentPositionImpl(\n    override val baseHash: String,\n    override val startHash: String,\n    override val headHash: String,\n    override val oldPath: String?,\n    override val newPath: String?,\n    override val oldLine: Int?,\n    override val newLine: Int?,\n    override val source: CommentPositionSource,\n    override val changeType: CommentPositionChangeType\n): CommentPosition"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/CommitImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegration.internal\n\nimport net.ntworld.mergeRequest.Commit\n\ndata class CommitImpl(\n    override val id: String,\n    override val message: String,\n    override val authorName: String,\n    override val authorEmail: String,\n    override val createdAt: String\n) : Commit"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/DiffReferenceImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegration.internal\n\nimport net.ntworld.mergeRequest.DiffReference\n\ndata class DiffReferenceImpl(\n    override val baseHash: String,\n    override val headHash: String,\n    override val startHash: String\n): DiffReference"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/MergeRequestImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegration.internal\n\nimport net.ntworld.mergeRequest.*\n\nclass MergeRequestImpl(\n    override val id: String,\n    override val provider: String,\n    override val projectId: String,\n    override val title: String,\n    override val description: String,\n    override val url: String,\n    override val state: MergeRequestState,\n    override val createdAt: DateTime,\n    override val updatedAt: DateTime,\n    override val assignee: UserInfo?,\n    override val author: UserInfo,\n    override val diffReference: DiffReference?,\n    override val sourceBranch: String,\n    override val targetBranch: String,\n    override val upVotes: Int,\n    override val downVotes: Int,\n    override val commentsCount: Int,\n    override val isWorkInProgress: Boolean,\n    override val canMerged: Boolean,\n    override val mergedBy: UserInfo?,\n    override val closedBy: UserInfo?,\n    override val mergedAt: DateTime?,\n    override val closedAt: DateTime?\n) : MergeRequest"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/MergeRequestInfoImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegration.internal\n\nimport net.ntworld.mergeRequest.DateTime\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.MergeRequestState\n\ndata class MergeRequestInfoImpl(\n    override val id: String,\n    override val provider: String,\n    override val projectId: String,\n    override val title: String,\n    override val description: String,\n    override val url: String,\n    override val state: MergeRequestState,\n    override val createdAt: DateTime,\n    override val updatedAt: DateTime\n) : MergeRequestInfo"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/MergeRequestSearchResultImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegration.internal\n\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.api.MergeRequestApi\n\ndata class MergeRequestSearchResultImpl(\n    override val data: List<MergeRequestInfo>,\n    override val totalPages: Int,\n    override val totalItems: Int,\n    override val currentPage: Int\n): MergeRequestApi.SearchResult"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/PipelineImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegration.internal\n\nimport net.ntworld.mergeRequest.DateTime\nimport net.ntworld.mergeRequest.Pipeline\nimport net.ntworld.mergeRequest.PipelineStatus\n\ndata class PipelineImpl(\n    override val id: String,\n    override val hash: String,\n    override val ref: String,\n    override val status: PipelineStatus,\n    override val url: String,\n    override val createdAt: DateTime?,\n    override val updatedAt: DateTime?\n) : Pipeline"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/ProjectImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegration.internal\n\nimport net.ntworld.mergeRequest.Project\nimport net.ntworld.mergeRequest.ProjectVisibility\nimport net.ntworld.mergeRequest.ProviderInfo\n\ndata class ProjectImpl(\n    override val id: String,\n    override val provider: ProviderInfo,\n    override val name: String,\n    override val path: String,\n    override val url: String,\n    override val avatarUrl: String,\n    override val visibility: ProjectVisibility,\n    override val repositoryHttpUrl: String,\n    override val repositorySshUrl: String\n): Project"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/ProviderDataImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegration.internal\n\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.Gitlab\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabUtil\n\ndata class ProviderDataImpl(\n    override val id: String,\n    override val key: String,\n    override val name: String,\n    override val info: ProviderInfo,\n    override val credentials: ApiCredentials,\n    override val project: Project,\n    override val currentUser: User,\n    override val repository: String,\n    override val errorMessage: String?,\n    override val status: ProviderStatus\n) : ProviderData {\n    override val hasApprovalFeature: Boolean\n        get() {\n            if (this.info.id != Gitlab.id) {\n                return true\n            }\n            if (GitlabUtil.hasMergeApprovalFeature(credentials)) {\n                return true\n            }\n            return false\n        }\n\n    override val hasAssigneeFeature: Boolean\n        get() {\n            if (this.info.id == Gitlab.id) {\n                return true\n            }\n            return false\n        }\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/UserImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegration.internal\n\nimport net.ntworld.mergeRequest.DateTime\nimport net.ntworld.mergeRequest.User\nimport net.ntworld.mergeRequest.UserStatus\n\ndata class UserImpl(\n    override val id: String,\n    override val name: String,\n    override val username: String,\n    override val avatarUrl: String?,\n    override val url: String,\n    override val status: UserStatus,\n    override val email: String,\n    override val createdAt: DateTime\n) : User"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/internal/UserInfoImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegration.internal\n\nimport net.ntworld.mergeRequest.UserInfo\nimport net.ntworld.mergeRequest.UserStatus\n\ndata class UserInfoImpl(\n    override val id: String,\n    override val name: String,\n    override val username: String,\n    override val avatarUrl: String?,\n    override val url: String,\n    override val status: UserStatus\n) : UserInfo {\n    companion object {\n        val None = UserInfoImpl(\n            id = \"\",\n            name = \"<None>\",\n            username = \"\",\n            avatarUrl = \"\",\n            url = \"\",\n            status = UserStatus.ACTIVE\n        )\n    }\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/DraftCommentApi.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequest.Project\nimport net.ntworld.mergeRequest.api.CommentApi\nimport net.ntworld.mergeRequest.api.DraftCommentStorage\n\nclass DraftCommentApi(private val api: CommentApi, private val storage: DraftCommentStorage): CommentApi {\n    override fun getAll(project: Project, mergeRequestId: String): List<Comment> {\n        val result = mutableListOf<Comment>()\n        result.addAll(api.getAll(project, mergeRequestId))\n        result.addAll(storage.getAll(project, mergeRequestId))\n\n        return result\n    }\n\n    override fun create(\n        project: Project,\n        mergeRequestId: String,\n        body: String,\n        position: CommentPosition?,\n        isDraft: Boolean\n    ): String? {\n        if (isDraft) {\n            return storage.create(project, mergeRequestId, body, position)\n        }\n        return api.create(project, mergeRequestId, body, position, false)\n    }\n\n    override fun reply(project: Project, mergeRequestId: String, repliedComment: Comment, body: String): String? {\n        return api.reply(project, mergeRequestId, repliedComment, body)\n    }\n\n    override fun delete(project: Project, mergeRequestId: String, comment: Comment) {\n        if (comment.isDraft) {\n            return storage.delete(project, mergeRequestId, comment)\n        }\n        return api.delete(project, mergeRequestId, comment)\n    }\n\n    override fun resolve(project: Project, mergeRequestId: String, comment: Comment) {\n        if (!comment.isDraft) {\n            return api.resolve(project, mergeRequestId, comment)\n        }\n    }\n\n    override fun unresolve(project: Project, mergeRequestId: String, comment: Comment) {\n        if (!comment.isDraft) {\n            return api.unresolve(project, mergeRequestId, comment)\n        }\n    }\n\n    override fun update(project: Project, mergeRequestId: String, comment: Comment, body: String) {\n        if (!comment.isDraft) {\n            return api.update(project, mergeRequestId, comment, body)\n        }\n        return storage.update(project, mergeRequestId, comment, body)\n    }\n\n    override fun getDraftCount(project: Project, mergeRequestId: String): Int {\n        return storage.getAll(project, mergeRequestId).count()\n    }\n\n    override fun publishAllDraftComments(project: Project, mergeRequestId: String) {\n        val comments = storage.getAll(project, mergeRequestId)\n        for (comment in comments) {\n            publishDraftComment(project, mergeRequestId, comment)\n        }\n    }\n\n    override fun publishDraftComments(project: Project, mergeRequestId: String, commentIds: List<String>) {\n        for (commentId in commentIds) {\n            val comment = storage.findById(project, mergeRequestId, commentId)\n            if (null !== comment) {\n                publishDraftComment(project, mergeRequestId, comment)\n            }\n        }\n    }\n\n    private fun publishDraftComment(project: Project, mergeRequestId: String, comment: Comment) {\n        if (comment.isDraft) {\n            api.create(project, mergeRequestId, comment.body, comment.position, false)\n            storage.delete(project, mergeRequestId, comment)\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/FuelClient.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider\n\nimport com.github.kittinunf.fuel.core.*\nimport com.github.kittinunf.result.Result\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.JsonConfiguration\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport java.security.cert.X509Certificate\nimport javax.net.ssl.HostnameVerifier\nimport javax.net.ssl.SSLContext\nimport javax.net.ssl.TrustManager\nimport javax.net.ssl.X509TrustManager\n\nabstract class FuelClient (\n    private val credentials: ApiCredentials\n) {\n    val json = Json(JsonConfiguration.Stable.copy(strictMode = false))\n\n    protected abstract fun injectAuthentication(httpRequest: Request): Request\n\n    protected fun makeRequestFactory(): RequestFactory.Convenience {\n        if (credentials.ignoreSSLCertificateErrors) {\n            return FuelManager().apply {\n                val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {\n                    override fun getAcceptedIssuers(): Array<X509Certificate>? = null\n                    override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit\n                    override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) = Unit\n                })\n\n                socketFactory = SSLContext.getInstance(\"SSL\").apply {\n                    init(null, trustAllCerts, java.security.SecureRandom())\n                }.socketFactory\n\n                hostnameVerifier = HostnameVerifier { _, _ -> true }\n            }\n        }\n        return FuelManager()\n    }\n\n    fun postJson(url: String, parameters: Parameters? = null): String {\n        return executeRequest(makeRequestFactory().post(url, parameters))\n    }\n\n    fun deleteJson(url: String, parameters: Parameters? = null): String {\n        return executeRequest(makeRequestFactory().delete(url, parameters))\n    }\n\n    fun putJson(url: String, parameters: Parameters? = null): String {\n        return executeRequest(makeRequestFactory().put(url, parameters))\n    }\n\n    fun getJson(url: String, parameters: Parameters? = null): String {\n        return executeRequest(makeRequestFactory().get(url, parameters))\n    }\n\n    protected fun executeRequest(request: Request) : String {\n        val httpRequest = injectAuthentication(request)\n        val (_, response, result) = httpRequest.responseString()\n        return when (result) {\n            is Result.Success -> {\n                result.value\n            }\n            is Result.Failure -> {\n                throw HttpException(response.statusCode, result.error.message ?: \"Unknown\")\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/MemoryCache.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider\n\nimport net.ntworld.mergeRequest.api.Cache\nimport net.ntworld.mergeRequest.api.CacheNotFoundException\nimport net.ntworld.mergeRequestIntegration.exception.InvalidCacheKeyException\nimport net.ntworld.mergeRequestIntegration.exception.InvalidTTLException\nimport org.joda.time.DateTime\nimport org.joda.time.DateTimeZone\nimport org.joda.time.LocalDateTime\n\nclass MemoryCache(ttl: Int? = null) : Cache {\n    private val data = mutableMapOf<String, CachedData>()\n\n    override var defaultTTL: Int = 0\n        private set\n\n    init {\n        defaultTTL = if (null === ttl) {\n            60000\n        } else {\n            if (ttl <= 0) {\n                throw InvalidTTLException()\n            }\n            ttl\n        }\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    override fun <T> get(key: String): T {\n        assertKeyIsValid(key)\n        if (!data.containsKey(key)) {\n            throw CacheNotFoundException()\n        }\n\n        val cachedItem = data[key]\n        if (null === cachedItem || isExpired(cachedItem.expired)) {\n            throw CacheNotFoundException()\n        }\n        return cachedItem.value as T\n    }\n\n    override fun has(key: String): Boolean {\n        assertKeyIsValid(key)\n        if (!data.containsKey(key)) {\n            return false\n        }\n        val cachedItem = data[key]\n        return null !== cachedItem && !isExpired(cachedItem.expired)\n    }\n\n    override fun remove(key: String) {\n        assertKeyIsValid(key)\n        data.remove(key)\n    }\n\n    override fun put(key: String, value: Any, ttl: Int) {\n        data[key] = CachedData(value, now() + ttl)\n    }\n\n    override fun isExpiredAfter(key: String, datetime: DateTime): Boolean {\n        assertKeyIsValid(key)\n        val cachedItem = data[key]\n        if (null === cachedItem) {\n            throw CacheNotFoundException()\n        }\n        return cachedItem.expired < toUtc(datetime).millis\n    }\n\n    private fun assertKeyIsValid(key: String) {\n        if (key.trim().isEmpty()) {\n            throw InvalidCacheKeyException(\"Key is invalid\")\n        }\n    }\n\n    internal fun isExpired(expired: Long): Boolean {\n        return expired < now()\n    }\n\n    internal fun now(): Long {\n        return toUtc(DateTime.now()).millis\n    }\n\n    private fun toUtc(datetime: DateTime): DateTime {\n        return LocalDateTime(datetime).toDateTime(DateTimeZone.UTC)\n    }\n\n    private data class CachedData(\n        val value: Any,\n        val expired: Long\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/MemoryDraftCommentStorage.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequest.Project\nimport net.ntworld.mergeRequest.UserInfo\nimport net.ntworld.mergeRequest.api.DraftCommentStorage\nimport net.ntworld.mergeRequestIntegration.internal.CommentImpl\nimport net.ntworld.mergeRequestIntegration.util.DateTimeUtil\nimport org.joda.time.DateTime\nimport java.util.*\n\nclass MemoryDraftCommentStorage(private val currentUser: UserInfo) : DraftCommentStorage {\n    private val data = mutableMapOf<String, MutableMap<String, Comment>>()\n\n    private fun getMutableMap(project: Project, mergeRequestId: String): MutableMap<String, Comment> {\n        val key = project.id + \"-\" + mergeRequestId\n        if (!data.contains(key)) {\n            data[key] = mutableMapOf()\n        }\n        return data[key] as MutableMap<String, Comment>\n    }\n\n    override fun getAll(project: Project, mergeRequestId: String): List<Comment> {\n        return getMutableMap(project, mergeRequestId).values.toList()\n    }\n\n    override fun findById(project: Project, mergeRequestId: String, commentId: String): Comment? {\n        val mutableMap = getMutableMap(project, mergeRequestId)\n        return mutableMap[commentId]\n    }\n\n    override fun create(project: Project, mergeRequestId: String, body: String, position: CommentPosition?): String? {\n        val mutableMap = getMutableMap(project, mergeRequestId)\n        val id = \"tmp:${UUID.randomUUID()}\"\n        mutableMap[id] = CommentImpl(\n            id = id,\n            parentId = UUID.randomUUID().toString(),\n            replyId = \"\",\n            body = body,\n            position = position,\n            createdAt = DateTimeUtil.fromDate(DateTime.now().toDate()),\n            updatedAt = DateTimeUtil.fromDate(DateTime.now().toDate()),\n            resolvable = false,\n            resolved = false,\n            resolvedBy = null,\n            author = currentUser,\n            isDraft = true\n        )\n        return id\n    }\n\n    override fun update(project: Project, mergeRequestId: String, comment: Comment, body: String) {\n        if (!comment.isDraft) {\n            return\n        }\n        val mutableMap = getMutableMap(project, mergeRequestId)\n        if (mutableMap.contains(comment.id)) {\n            mutableMap[comment.id] = CommentImpl(\n                id = comment.id,\n                parentId = comment.parentId,\n                replyId = \"\",\n                body = body,\n                position = comment.position,\n                createdAt = comment.createdAt,\n                updatedAt = DateTimeUtil.fromDate(DateTime.now().toDate()),\n                resolvable = false,\n                resolved = false,\n                resolvedBy = null,\n                author = currentUser,\n                isDraft = true\n            )\n        }\n    }\n\n    override fun delete(project: Project, mergeRequestId: String, comment: Comment) {\n        if (!comment.isDraft) {\n            return\n        }\n        val mutableMap = getMutableMap(project, mergeRequestId)\n        mutableMap.remove(comment.id)\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/MergeRequestApiDecorator.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider\n\nimport net.ntworld.mergeRequest.api.MergeRequestApi\n\nopen class MergeRequestApiDecorator(\n    private val wrappee: MergeRequestApi\n): MergeRequestApi by wrappee\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/ProviderException.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider\n\nimport net.ntworld.foundation.Error\n\nclass ProviderException(val error: Error) : Exception(error.message)\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/Transformer.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider\n\ninterface Transformer<T, R> {\n    fun transform(input: T): R\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/Github.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.ProviderInfo\n\nobject Github : ProviderInfo {\n\n    override val id: String = \"github\"\n\n    override val name: String = \"Github\"\n\n    override val iconPath: String = \"/icons/github.svg\"\n\n    override val icon2xPath: String = \"/icons/github@2x.svg\"\n\n    override val icon3xPath: String = \"/icons/github@3x.svg\"\n\n    override val icon4xPath: String = \"/icons/github@4x.svg\"\n\n    override fun createCommentUrl(mergeRequestUrl: String, comment: Comment): String {\n        return \"$mergeRequestUrl#comment_${comment.id}\"\n    }\n\n    override fun formatMergeRequestId(mergeRequestId: String): String {\n        return mergeRequestId\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubApiProvider.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github\n\nimport net.ntworld.foundation.Infrastructure\nimport net.ntworld.mergeRequest.ProviderInfo\nimport net.ntworld.mergeRequest.UserInfo\nimport net.ntworld.mergeRequest.api.*\n\nclass GithubApiProvider(\n    private val infrastructure: Infrastructure,\n    override val credentials: ApiCredentials,\n    override val cache: Cache\n) : ApiProvider {\n    private val myMergeRequestApi = GithubMergeRequestApi(infrastructure, credentials)\n\n    override val info: ProviderInfo = Github\n\n    override val user: UserApi = GithubUserApi(infrastructure, credentials)\n\n    override val mergeRequest: MergeRequestApi = myMergeRequestApi\n\n    override val project: ProjectApi = GithubProjectApi(infrastructure, credentials)\n\n    override val comment: CommentApi\n        get() = TODO(\"not implemented\")\n\n    override val commit: CommitApi\n        get() = TODO(\"Not yet implemented\")\n\n    override fun setOptions(options: ApiOptions) {\n    }\n\n    override fun initialize(currentUser: UserInfo) {\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubClient.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Request\nimport net.ntworld.foundation.Response\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport org.kohsuke.github.GitHub\nimport org.kohsuke.github.GitHubBuilder\nimport java.io.IOException\n\nobject GithubClient {\n    private fun makeGitHub(credentials: ApiCredentials) : GitHub {\n        return GitHubBuilder()\n            .withEndpoint(credentials.url)\n            .withPassword(credentials.login, credentials.token)\n            .build()\n    }\n\n    operator fun <T, R: Response> invoke(\n        request: T,\n        execute: (GitHub.(T) -> R),\n        failed: ((Error) -> R)\n    ) : R where T : Request<R>, T : GithubRequest {\n        return try {\n            execute.invoke(\n                makeGitHub(request.credentials),\n                request\n            )\n        } catch (exception: IOException) {\n            failed.invoke(\n                GithubFailedRequestError(\n                    exception.message ?: \"Failed request\",\n                    400\n                )\n            )\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubFailedRequestError.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github\n\nimport net.ntworld.foundation.Error\n\ndata class GithubFailedRequestError(\n    override val message: String,\n    override val code: Int\n): Error {\n    override val type: String = \"net.ntworld.mergeRequestIntegration.provider.github.GithubFailedRequestError\"\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubFuelClient.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github\n\nimport com.github.kittinunf.fuel.core.HttpException\nimport com.github.kittinunf.fuel.core.Request\nimport com.github.kittinunf.fuel.core.extensions.authentication\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.FuelClient\n\nclass GithubFuelClient private constructor(\n    private val credentials: ApiCredentials\n) : FuelClient(credentials) {\n    val searchIssuesUrl = \"${credentials.url}/search/issues\"\n\n    override fun injectAuthentication(httpRequest: Request): Request {\n        return httpRequest.authentication().basic(credentials.login, credentials.token)\n    }\n\n    companion object {\n        operator fun <T, R : Response> invoke(\n            request: T,\n            execute: (GithubFuelClient.(T) -> R),\n            failed: ((Error) -> R)\n        ): R where T : net.ntworld.foundation.Request<R>, T : GithubRequest {\n            return try {\n                val client = GithubFuelClient(request.credentials)\n                execute.invoke(client, request)\n            } catch (exception: HttpException) {\n                failed.invoke(\n                    GithubFailedRequestError(\n                        exception.message ?: \"Failed request\",\n                        500\n                    )\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubMergeRequestApi.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github\n\nimport net.ntworld.foundation.Infrastructure\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequest.api.MergeRequestApi\nimport net.ntworld.mergeRequest.api.MergeRequestOrdering\nimport net.ntworld.mergeRequest.query.GetMergeRequestFilter\nimport net.ntworld.mergeRequestIntegration.internal.MergeRequestSearchResultImpl\nimport net.ntworld.mergeRequestIntegration.provider.github.request.GithubSearchPRsRequest\nimport net.ntworld.mergeRequestIntegration.provider.github.transformer.GithubSearchPullRequestItemTransformer\nimport net.ntworld.mergeRequestIntegration.provider.github.vo.GithubProjectId\nimport net.ntworld.mergeRequestIntegration.provider.github.vo.GithubUserId\nimport kotlin.math.ceil\n\nclass GithubMergeRequestApi(\n    private val infrastructure: Infrastructure,\n    private val credentials: ApiCredentials\n) : MergeRequestApi {\n\n    override fun find(projectId: String, mergeRequestId: String): MergeRequest? {\n        TODO(\"not implemented\") //To change body of created functions use File | Settings | File Templates.\n    }\n\n    override fun approve(projectId: String, mergeRequestId: String, sha: String) {\n    }\n\n    override fun unapprove(projectId: String, mergeRequestId: String) {\n    }\n\n    override fun findApproval(projectId: String, mergeRequestId: String): Approval {\n        TODO(\"not implemented\") //To change body of created functions use File | Settings | File Templates.\n    }\n\n    override fun getPipelines(projectId: String, mergeRequestId: String): List<Pipeline> {\n        // TODO: get pipelines\n        return listOf()\n    }\n\n    override fun getCommits(projectId: String, mergeRequestId: String): List<Commit> {\n        // TODO: get commits\n        return listOf()\n    }\n\n    override fun getChanges(projectId: String, mergeRequestId: String): List<Change> {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun search(\n        projectId: String,\n        currentUserId: String,\n        filterBy: GetMergeRequestFilter,\n        orderBy: MergeRequestOrdering,\n        page: Int,\n        itemsPerPage: Int\n    ): MergeRequestApi.SearchResult {\n        val (sort, order) = resolveOrderAndSort(orderBy)\n        val reviewer = if (filterBy.approverIds.isNotEmpty() && filterBy.approverIds[0].isNotEmpty()) {\n            GithubUserId.parseLogin(filterBy.approverIds[0])\n        } else \"\"\n        val out = infrastructure.serviceBus() process GithubSearchPRsRequest(\n            credentials = credentials,\n            repo = GithubProjectId.parseFullName(credentials.projectId),\n            state = when (filterBy.state) {\n                MergeRequestState.ALL -> \"\"\n                MergeRequestState.OPENED -> PULL_REQUEST_STATE_OPEN\n                MergeRequestState.CLOSED -> PULL_REQUEST_STATE_CLOSED\n                MergeRequestState.MERGED -> PULL_REQUEST_STATE_MERGED\n            },\n            author = if (filterBy.authorId.isNotEmpty()) GithubUserId.parseLogin(filterBy.authorId) else \"\",\n            assignee = if (filterBy.assigneeId.isNotEmpty()) GithubUserId.parseLogin(filterBy.assigneeId) else \"\",\n            reviewer = reviewer,\n            term = filterBy.search,\n            sort = sort,\n            order = order,\n            page = page,\n            perPage = itemsPerPage\n        )\n\n        return if (out.hasError()) {\n            MergeRequestSearchResultImpl(listOf(), 0, 0, 0)\n        } else {\n            val response = out.getResponse()\n            val transformer = GithubSearchPullRequestItemTransformer(projectId)\n            MergeRequestSearchResultImpl(\n                data = response.result.items.map { transformer.transform(it) },\n                totalItems = response.result.totalCount,\n                totalPages = ceil(response.result.totalCount.toDouble() / itemsPerPage).toInt(),\n                currentPage = page\n            )\n        }\n    }\n\n    private fun resolveState(state: MergeRequestState): String = when(state) {\n        MergeRequestState.ALL -> \"all\"\n        MergeRequestState.OPENED -> \"open\"\n        MergeRequestState.CLOSED -> \"closed\"\n        MergeRequestState.MERGED -> \"merged\"\n    }\n\n    private fun resolveOrderAndSort(orderBy: MergeRequestOrdering): Pair<String, String> {\n        return when (orderBy) {\n            MergeRequestOrdering.RECENTLY_UPDATED -> Pair(\"updated\", \"desc\")\n            MergeRequestOrdering.NEWEST -> Pair(\"created\", \"desc\")\n            MergeRequestOrdering.OLDEST -> Pair(\"created\", \"asc\")\n        }\n    }\n\n    override fun findOrFail(projectId: String, mergeRequestId: String): MergeRequest {\n        val mergeRequest = find(projectId, mergeRequestId)\n        if (null === mergeRequest) {\n            throw Exception(\"MergeRequest $mergeRequestId not found.\")\n        }\n        return mergeRequest\n    }\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubProjectApi.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github\n\nimport net.ntworld.foundation.Infrastructure\nimport net.ntworld.mergeRequest.Project\nimport net.ntworld.mergeRequest.UserInfo\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequest.api.ProjectApi\nimport net.ntworld.mergeRequestIntegration.provider.github.request.GithubFindRepositoryRequest\nimport net.ntworld.mergeRequestIntegration.provider.github.transformer.GithubRepositoryTransformer\nimport net.ntworld.mergeRequestIntegration.provider.github.vo.GithubProjectId\n\nclass GithubProjectApi(\n    private val infrastructure: Infrastructure,\n    private val credentials: ApiCredentials\n) : ProjectApi {\n\n    override fun find(projectId: String): Project? {\n        val out = infrastructure.serviceBus() process GithubFindRepositoryRequest(\n            credentials, repositoryId = GithubProjectId.parseId(projectId).toString()\n        )\n\n        return if (out.hasError()) {\n            null\n        } else {\n            GithubRepositoryTransformer.transform(out.getResponse().repository)\n        }\n    }\n\n    override fun getMembers(projectId: String): List<UserInfo> {\n        // TODO: Get members of project\n        return listOf()\n    }\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github\n\nimport net.ntworld.mergeRequest.api.ApiCredentials\n\ninterface GithubRequest {\n    val credentials: ApiCredentials\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubUserApi.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github\n\nimport net.ntworld.foundation.Infrastructure\nimport net.ntworld.mergeRequest.User\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequest.api.UserApi\nimport net.ntworld.mergeRequestIntegration.provider.github.request.GithubFindCurrentUserRequest\nimport net.ntworld.mergeRequestIntegration.provider.github.transformer.GithubUserTransformer\n\nclass GithubUserApi(\n    private val infrastructure: Infrastructure,\n    private val credentials: ApiCredentials\n) : UserApi {\n\n    override fun me(): User {\n        val response = infrastructure.serviceBus() process GithubFindCurrentUserRequest(credentials) ifError {\n            throw Exception(\"Cannot find info of current user.\")\n        }\n        return GithubUserTransformer.transform(response.user)\n    }\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/GithubUtil.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github\n\nobject GithubUtil"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/_const.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github\n\nconst val PULL_REQUEST_STATE_OPEN = \"open\"\nconst val PULL_REQUEST_STATE_CLOSED = \"closed\"\nconst val PULL_REQUEST_STATE_MERGED = \"merged\""
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/model/PullRequestSearchItem.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class PullRequestSearchItem(\n    val id: Long,\n\n    val number: Int,\n\n    @SerialName(\"node_id\")\n    val nodeId: String,\n\n    val title: String,\n\n    val body: String,\n\n    @SerialName(\"html_url\")\n    val htmlUrl: String,\n\n    val state: String,\n\n    @SerialName(\"created_at\")\n    val createdAt: String,\n\n    @SerialName(\"updated_at\")\n    val updatedAt: String\n)"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/model/SearchPullRequestResult.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class SearchPullRequestResult(\n    @SerialName(\"total_count\")\n    val totalCount: Int,\n\n    @SerialName(\"incomplete_results\")\n    val incompleteResults: Boolean,\n\n    val items: List<PullRequestSearchItem>\n)"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/request/GithubFindCurrentUserRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.github.GithubRequest\nimport net.ntworld.mergeRequestIntegration.provider.github.response.GithubFindUserResponse\n\ndata class GithubFindCurrentUserRequest(\n    override val credentials: ApiCredentials\n): GithubRequest, Request<GithubFindUserResponse>"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/request/GithubFindRepositoryRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.github.GithubRequest\nimport net.ntworld.mergeRequestIntegration.provider.github.response.GithubFindRepositoryResponse\n\ndata class GithubFindRepositoryRequest(\n    override val credentials: ApiCredentials,\n    val repositoryId: String\n): GithubRequest, Request<GithubFindRepositoryResponse>\n\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/request/GithubSearchPRsRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.github.GithubRequest\nimport net.ntworld.mergeRequestIntegration.provider.github.response.GithubSearchPRsResponse\n\ndata class GithubSearchPRsRequest(\n    override val credentials: ApiCredentials,\n    val repo: String,\n    val state: String,\n    val author: String,\n    val reviewer: String,\n    val assignee: String,\n    val term: String,\n    val sort: String,\n    val order: String,\n    val page: Int = 0,\n    val perPage: Int = 10\n) : GithubRequest, Request<GithubSearchPRsResponse>\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/request/GithubSearchRepositoriesRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.github.GithubRequest\nimport net.ntworld.mergeRequestIntegration.provider.github.response.GithubSearchRepositoriesResponse\n\ndata class GithubSearchRepositoriesRequest(\n    override val credentials: ApiCredentials,\n    val term: String\n) : GithubRequest, Request<GithubSearchRepositoriesResponse>"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/requestHandler/GithubFindCurrentUserRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.github.GithubClient\nimport net.ntworld.mergeRequestIntegration.provider.github.request.GithubFindCurrentUserRequest\nimport net.ntworld.mergeRequestIntegration.provider.github.response.GithubFindUserResponse\nimport org.kohsuke.github.GHUser\n\n@Handler\nclass GithubFindCurrentUserRequestHandler : RequestHandler<GithubFindCurrentUserRequest, GithubFindUserResponse> {\n    override fun handle(request: GithubFindCurrentUserRequest): GithubFindUserResponse = GithubClient(\n        request = request,\n        execute = {\n            GithubFindUserResponse(error = null, user = myself)\n        },\n        failed = {\n            GithubFindUserResponse(error = it, user = GHUser())\n        }\n    )\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/requestHandler/GithubFindRepositoryRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.github.GithubClient\nimport net.ntworld.mergeRequestIntegration.provider.github.GithubUtil\nimport net.ntworld.mergeRequestIntegration.provider.github.request.GithubFindRepositoryRequest\nimport net.ntworld.mergeRequestIntegration.provider.github.response.GithubFindRepositoryResponse\nimport org.kohsuke.github.GHRepository\n\n@Handler\nclass GithubFindRepositoryRequestHandler : RequestHandler<GithubFindRepositoryRequest, GithubFindRepositoryResponse> {\n    override fun handle(request: GithubFindRepositoryRequest): GithubFindRepositoryResponse = GithubClient(\n        request = request,\n        execute = {\n            val repository = this.getRepositoryById(request.repositoryId)\n\n            GithubFindRepositoryResponse(error = null, repository = repository)\n        },\n        failed = {\n            GithubFindRepositoryResponse(error = it, repository = GHRepository())\n        }\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/requestHandler/GithubSearchPRsRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.requestHandler\n\nimport kotlinx.serialization.serializer\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.github.GithubFuelClient\nimport net.ntworld.mergeRequestIntegration.provider.github.model.SearchPullRequestResult\nimport net.ntworld.mergeRequestIntegration.provider.github.request.GithubSearchPRsRequest\nimport net.ntworld.mergeRequestIntegration.provider.github.response.GithubSearchPRsResponse\n\n@Handler\nclass GithubSearchPRsRequestHandler : RequestHandler<GithubSearchPRsRequest, GithubSearchPRsResponse> {\n\n    /**\n     * Because there is no way to do pagination manually with the library https://github.com/github-api/github-api/\n     * then I have to change the contract & use a custom client.\n     */\n    override fun handle(request: GithubSearchPRsRequest): GithubSearchPRsResponse = GithubFuelClient(\n        request = request,\n        execute = {\n            val q = mutableListOf<String>()\n            q.add(\"repo:${request.repo}\")\n            q.add(\"is:pr\")\n            q.add(\"state:${request.state}\")\n            if (request.author.isNotEmpty()) {\n                q.add(\"author:${request.author}\")\n            }\n            if (request.reviewer.isNotEmpty()) {\n                q.add(\"reviewed-by:${request.reviewer}\")\n            }\n            if (request.assignee.isNotEmpty()) {\n                q.add(\"assignee:${request.assignee}\")\n            }\n            if (request.term.trim().isNotEmpty()) {\n                q.add(request.term.trim())\n            }\n\n            val params: MutableList<Pair<String, Any?>> = mutableListOf(\n                Pair(\"q\", q.joinToString(\" \")),\n                Pair(\"sort\", request.sort),\n                Pair(\"order\", request.order),\n                Pair(\"page\", request.page),\n                Pair(\"per_page\", request.perPage)\n            )\n\n            val input = this.getJson(searchIssuesUrl, params)\n            GithubSearchPRsResponse(error = null, result = json.parse(\n                SearchPullRequestResult.serializer(),\n                input\n            ))\n        },\n        failed = {\n            GithubSearchPRsResponse(error = it, result = SearchPullRequestResult(\n                totalCount = 0,\n                incompleteResults = false,\n                items = listOf()\n            ))\n        }\n    )\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/requestHandler/GithubSearchRepositoriesRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.github.GithubClient\nimport net.ntworld.mergeRequestIntegration.provider.github.request.GithubSearchRepositoriesRequest\nimport net.ntworld.mergeRequestIntegration.provider.github.response.GithubSearchRepositoriesResponse\nimport org.kohsuke.github.GitHubBuilder\n\n@Handler\nclass GithubSearchRepositoriesRequestHandler : RequestHandler<GithubSearchRepositoriesRequest, GithubSearchRepositoriesResponse> {\n\n    override fun handle(request: GithubSearchRepositoriesRequest): GithubSearchRepositoriesResponse = GithubClient(\n        request = request,\n        execute = {\n            val search = this.searchRepositories().q(request.term)\n            val pager = search.list().withPageSize(10)\n            val data = pager.iterator().nextPage()\n            GithubSearchRepositoriesResponse(error = null, repositories = data)\n        },\n        failed = {\n            GithubSearchRepositoriesResponse(error = it, repositories = listOf())\n        }\n    )\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/response/GithubFindRepositoryResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport org.kohsuke.github.GHRepository\n\ndata class GithubFindRepositoryResponse(\n    override val error: Error?,\n    val repository: GHRepository\n) : Response\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/response/GithubFindUserResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport org.kohsuke.github.GHUser\n\ndata class GithubFindUserResponse(\n    override val error: Error?,\n    val user: GHUser\n): Response"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/response/GithubSearchPRsResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport net.ntworld.mergeRequestIntegration.provider.github.model.PullRequestSearchItem\nimport net.ntworld.mergeRequestIntegration.provider.github.model.SearchPullRequestResult\n\ndata class GithubSearchPRsResponse(\n    override val error: Error?,\n    val result: SearchPullRequestResult\n): Response\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/response/GithubSearchRepositoriesResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport org.kohsuke.github.GHRepository\n\ndata class GithubSearchRepositoriesResponse(\n    override val error: Error?,\n    val repositories: List<GHRepository>\n): Response"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/transformer/GithubRepositoryTransformer.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.transformer\n\nimport net.ntworld.mergeRequest.Project\nimport net.ntworld.mergeRequest.ProjectVisibility\nimport net.ntworld.mergeRequestIntegration.internal.ProjectImpl\nimport net.ntworld.mergeRequestIntegration.provider.github.Github\nimport net.ntworld.mergeRequestIntegration.provider.Transformer\nimport net.ntworld.mergeRequestIntegration.provider.github.vo.GithubProjectId\nimport org.kohsuke.github.GHRepository\n\nobject GithubRepositoryTransformer :\n    Transformer<GHRepository, Project> {\n    override fun transform(input: GHRepository): Project = ProjectImpl(\n        // Unlike gitlab, github works based on :owner/:repo rather than id\n        // So to keep everything works as expected, we have to add :owner/:repo information\n        // Please use GithubProjectId value-object to work with generate/parsing user id\n        id = GithubProjectId(id = input.id, owner = input.ownerName, repo = input.name).getValue(),\n        provider = Github,\n        name = input.name,\n        path = input.fullName,\n        url = input.url.toString(),\n        visibility = if (input.isPrivate) ProjectVisibility.PRIVATE else ProjectVisibility.PUBLIC,\n        avatarUrl = \"\",\n        repositoryHttpUrl = input.htmlUrl.toString(),\n        repositorySshUrl = input.sshUrl\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/transformer/GithubSearchPullRequestItemTransformer.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.transformer\n\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.MergeRequestState\nimport net.ntworld.mergeRequestIntegration.internal.MergeRequestInfoImpl\nimport net.ntworld.mergeRequestIntegration.provider.Transformer\nimport net.ntworld.mergeRequestIntegration.provider.github.Github\nimport net.ntworld.mergeRequestIntegration.provider.github.PULL_REQUEST_STATE_CLOSED\nimport net.ntworld.mergeRequestIntegration.provider.github.PULL_REQUEST_STATE_MERGED\nimport net.ntworld.mergeRequestIntegration.provider.github.PULL_REQUEST_STATE_OPEN\nimport net.ntworld.mergeRequestIntegration.provider.github.model.PullRequestSearchItem\nimport net.ntworld.mergeRequestIntegration.provider.github.vo.GithubMergeRequestId\n\nclass GithubSearchPullRequestItemTransformer(\n    private val projectId: String\n) : Transformer<PullRequestSearchItem, MergeRequestInfo> {\n    override fun transform(input: PullRequestSearchItem): MergeRequestInfo = MergeRequestInfoImpl(\n        id = GithubMergeRequestId(input.id, input.number, input.nodeId).getValue(),\n        provider = Github.id,\n        projectId = projectId,\n        title = input.title,\n        description = input.body,\n        url = input.htmlUrl,\n        state = when (input.state) {\n            PULL_REQUEST_STATE_OPEN -> MergeRequestState.OPENED\n            PULL_REQUEST_STATE_MERGED -> MergeRequestState.MERGED\n            PULL_REQUEST_STATE_CLOSED -> MergeRequestState.CLOSED\n            else -> MergeRequestState.CLOSED\n        },\n        createdAt = input.createdAt,\n        updatedAt = input.updatedAt\n    )\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/transformer/GithubUserTransformer.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.transformer\n\nimport net.ntworld.mergeRequest.User\nimport net.ntworld.mergeRequest.UserStatus\nimport net.ntworld.mergeRequestIntegration.DEFAULT_DATETIME\nimport net.ntworld.mergeRequestIntegration.internal.UserImpl\nimport net.ntworld.mergeRequestIntegration.provider.Transformer\nimport net.ntworld.mergeRequestIntegration.provider.github.vo.GithubUserId\nimport net.ntworld.mergeRequestIntegration.util.DateTimeUtil\nimport org.kohsuke.github.GHUser\n\nobject GithubUserTransformer : Transformer<GHUser, User> {\n    override fun transform(input: GHUser): User = UserImpl(\n        // Unlike gitlab, github works based on login rather than id\n        // So to keep everything works as expected, we have to add username into id information\n        // Please use GithubUserId value-object to work with generate/parsing user id\n        id = GithubUserId(input.id, input.login).getValue(),\n        name = if (input.name.isNullOrBlank()) input.login else input.name,\n        username = input.login,\n        avatarUrl = input.avatarUrl,\n        url = input.htmlUrl.toString(),\n        status = UserStatus.ACTIVE,\n        email = if (input.email.isNullOrBlank()) \"\" else input.email,\n        createdAt = if (null === input.createdAt) DEFAULT_DATETIME else DateTimeUtil.fromDate(input.createdAt)\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/vo/GithubMergeRequestId.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.vo\n\ndata class GithubMergeRequestId(\n    val id: Long,\n    val number: Int,\n    val nodeId: String\n) {\n    fun getValue() : String {\n        return \"$id:$number:$nodeId\"\n    }\n\n    companion object {\n        fun parse(input: String): GithubMergeRequestId {\n            val parts = input.split(\":\")\n            if (parts.size != 3) {\n                throw Exception(\"Invalid Github mergeRequestId\")\n            }\n            return GithubMergeRequestId(\n                id = parts[0].toLong(),\n                number = parts[1].toInt(),\n                nodeId = parts[2]\n            )\n        }\n\n        fun parseId(mergeRequestId: String) : Long {\n            return parse(mergeRequestId).id\n        }\n\n        fun parseNumber(mergeRequestId: String) : Int {\n            return parse(mergeRequestId).number\n        }\n\n        fun parseNodeId(mergeRequestId: String) : String {\n            return parse(mergeRequestId).nodeId\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/vo/GithubProjectId.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.vo\n\ndata class GithubProjectId(\n    val id: Long,\n    val owner: String,\n    val repo: String\n) {\n    fun getValue() : String {\n        return \"$id:$owner/$repo\"\n    }\n\n    companion object {\n        fun parse(input: String): GithubProjectId {\n            val parts = input.split(\":\")\n            if (parts.size != 2) {\n                throw Exception(\"Invalid Github projectId\")\n            }\n            val info = parts[1].split(\"/\")\n            if (info.size != 2) {\n                throw Exception(\"Invalid Github projectId\")\n            }\n            return GithubProjectId(\n                id = parts[0].toLong(), owner = info[0], repo = info[1]\n            )\n        }\n\n        fun parseId(projectId: String) : Long {\n            return parse(projectId).id\n        }\n\n        fun parseFullName(projectId: String): String {\n            val data = parse(projectId)\n            return \"${data.owner}/${data.repo}\"\n        }\n\n        fun parseOwner(projectId: String) : String {\n            return parse(projectId).owner\n        }\n\n        fun parseRepo(projectId: String): String {\n            return parse(projectId).repo\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/github/vo/GithubUserId.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.github.vo\n\ndata class GithubUserId(\n    val id: Long,\n    val login: String\n) {\n    fun getValue() : String {\n        return \"$id:$login\"\n    }\n\n    companion object {\n        fun parse(input: String): GithubUserId {\n            val parts = input.split(\":\")\n            if (parts.size != 2) {\n                throw Exception(\"Invalid Github userId\")\n            }\n            return GithubUserId(\n                id = parts[0].toLong(), login = parts[1]\n            )\n        }\n\n        fun parseId(input: String): Long {\n            return parse(input).id\n        }\n\n        fun parseLogin(input: String): String {\n            return parse(input).login\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/Gitlab.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.ProviderInfo\n\nobject Gitlab : ProviderInfo {\n\n    override val id: String = \"gitlab\"\n\n    override val name: String = \"GitLab\"\n\n    override val iconPath: String = \"/icons/gitlab.svg\"\n\n    override val icon2xPath: String = \"/icons/gitlab@2x.svg\"\n\n    override val icon3xPath: String = \"/icons/gitlab@3x.svg\"\n\n    override val icon4xPath: String = \"/icons/gitlab@4x.svg\"\n\n    @Synchronized\n    override fun createCommentUrl(mergeRequestUrl: String, comment: Comment): String {\n        return \"$mergeRequestUrl#note_${comment.id}\"\n    }\n\n    @Synchronized\n    override fun formatMergeRequestId(mergeRequestId: String): String {\n        return \"!$mergeRequestId\"\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabApiProvider.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab\n\nimport net.ntworld.foundation.Infrastructure\nimport net.ntworld.mergeRequest.ProviderInfo\nimport net.ntworld.mergeRequest.UserInfo\nimport net.ntworld.mergeRequest.api.*\nimport net.ntworld.mergeRequestIntegration.provider.DraftCommentApi\nimport net.ntworld.mergeRequestIntegration.provider.MemoryDraftCommentStorage\n\nclass GitlabApiProvider(\n    private val infrastructure: Infrastructure,\n\n    override val credentials: ApiCredentials,\n\n    override val cache: Cache\n) : ApiProvider {\n    private var myCommentApi: CommentApi? = null\n    private val myMergeRequestApi = GitlabMergeRequestApiCache(\n        GitlabMergeRequestApi(infrastructure, credentials), cache\n    )\n\n    override val info: ProviderInfo = Gitlab\n\n    override val user: UserApi = GitlabUserApi(infrastructure, credentials)\n\n    override val mergeRequest: MergeRequestApi = myMergeRequestApi\n\n    override val project: ProjectApi = GitlabProjectApi(infrastructure, credentials)\n\n    override val comment: CommentApi\n        get() = myCommentApi!!\n\n    override val commit: CommitApi = GitlabCommitApi(infrastructure, credentials)\n\n    override fun setOptions(options: ApiOptions) {\n        myMergeRequestApi.options = options\n    }\n\n    override fun initialize(currentUser: UserInfo) {\n        this.myCommentApi = DraftCommentApi(\n            GitlabCommentApi(infrastructure, credentials),\n            MemoryDraftCommentStorage(currentUser)\n        )\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabClient.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Request\nimport net.ntworld.foundation.Response\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabSearchProjectsRequest\nimport org.gitlab4j.api.Constants\nimport org.gitlab4j.api.GitLabApi\nimport org.gitlab4j.api.GitLabApiException\nimport org.glassfish.jersey.client.ClientProperties.CONNECT_TIMEOUT\nimport org.glassfish.jersey.client.ClientProperties.READ_TIMEOUT\n\nclass GitlabClient {\n    companion object {\n        private fun makeGitLabApi(credentials: ApiCredentials): GitLabApi {\n            val config: HashMap<String, Any> = HashMap()\n            config.put(READ_TIMEOUT, 10000)\n            config.put(CONNECT_TIMEOUT, 10000)\n            val api = GitLabApi(\n                credentials.url,\n                Constants.TokenType.PRIVATE,\n                credentials.token,\n                null,\n                config\n            )\n            if (credentials.ignoreSSLCertificateErrors) {\n                api.ignoreCertificateErrors = true\n            }\n            return api\n        }\n\n        operator fun <T, R : Response> invoke(\n            request: T,\n            execute: (GitLabApi.(T) -> R),\n            failed: ((Error) -> R)\n        ): R where T : Request<R>, T : GitlabRequest {\n            return try {\n                execute.invoke(\n                    makeGitLabApi(request.credentials),\n                    request\n                )\n            } catch (exception: GitLabApiException) {\n                failed.invoke(\n                    GitlabFailedRequestError(\n                        exception.message ?: \"Failed request\",\n                        exception.httpStatus\n                    )\n                )\n            }\n        }\n\n        operator fun <T> invoke(\n            credentials: ApiCredentials,\n            execute: (GitLabApi.() -> T),\n            failed: ((Error) -> T)\n        ): T {\n            return try {\n                execute.invoke(\n                    makeGitLabApi(credentials)\n                )\n            } catch (exception: GitLabApiException) {\n                failed.invoke(\n                    GitlabFailedRequestError(\n                        exception.message ?: \"Failed request\",\n                        exception.httpStatus\n                    )\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabCommentApi.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab\n\nimport net.ntworld.foundation.Infrastructure\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequest.api.CommentApi\nimport net.ntworld.mergeRequestIntegration.provider.ProviderException\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.command.*\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabCreateNoteRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetMRCommentsRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetMRDiscussionsRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabReplyNoteRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.GitlabCommentTransformer\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.GitlabDiscussionTransformer\nimport org.gitlab4j.api.models.Position\nimport java.util.logging.Level\nimport java.util.logging.Logger\n\nclass GitlabCommentApi(\n    private val infrastructure: Infrastructure,\n    private val credentials: ApiCredentials\n) : CommentApi {\n\n    companion object {\n        private val log: Logger = Logger.getLogger(\"GitlabCommentApi\")\n    }\n\n    override fun getAll(project: Project, mergeRequestId: String): List<Comment> {\n        val request = GitlabGetMRDiscussionsRequest(\n            credentials = credentials,\n            mergeRequestInternalId = mergeRequestId.toInt()\n        )\n        val response = infrastructure.serviceBus() process request ifError {\n            throw Exception(it.message)\n        }\n        val comments = mutableListOf<Comment>()\n        response.discussions.forEach {\n            comments.addAll(GitlabDiscussionTransformer.transform(it))\n        }\n        return comments\n    }\n\n    private fun getAllGraphQL(project: Project, mergeRequestId: String): List<Comment> {\n        val fullPath = findProjectFullPath(project)\n        val comments = mutableListOf<Comment>()\n        var endCursor = \"\"\n        do {\n            val request = GitlabGetMRCommentsRequest(\n                credentials = credentials,\n                projectFullPath = fullPath,\n                mergeRequestInternalId = mergeRequestId.toInt(),\n                endCursor = endCursor\n            )\n            val response = infrastructure.serviceBus() process request ifError {\n                throw Exception(it.message)\n            }\n\n            val payload = response.payload\n            if (null !== payload) {\n                endCursor = payload.data.project.mergeRequest.notes.pageInfo.endCursor\n                payload.data.project.mergeRequest.notes.nodes.forEach {\n                    if (!it.system) {\n                        comments.add(GitlabCommentTransformer.transform(it))\n                    }\n                }\n            }\n        } while (null === payload || payload.data.project.mergeRequest.notes.pageInfo.hasNextPage)\n\n        return comments\n    }\n\n    override fun create(\n        project: Project,\n        mergeRequestId: String,\n        body: String,\n        position: CommentPosition?,\n        isDraft: Boolean\n    ): String? {\n        if (isDraft) {\n            println(body)\n            println(position)\n            // TODO: do something\n            return \"\"\n        }\n\n        val createdCommentId = if (null === position) {\n            createGeneralComment(mergeRequestId, body)\n        } else {\n            createPositionComment(mergeRequestId, body, position)\n        }\n        return if (createdCommentId == 0) null else createdCommentId.toString()\n    }\n\n    private fun createGeneralComment(mergeRequestId: String, body: String): Int {\n        val request = GitlabCreateNoteRequest(\n            credentials = credentials,\n            mergeRequestInternalId = mergeRequestId.toInt(),\n            body = body,\n            position = null\n        )\n        val response = infrastructure.serviceBus() process request ifError {\n            throw ProviderException(it)\n        }\n        return response.createdCommentId\n    }\n\n    private fun createPositionComment(mergeRequestId: String, body: String, commentPosition: CommentPosition) : Int {\n        val position = makePosition(commentPosition)\n        if (commentPosition.changeType != CommentPositionChangeType.UNKNOWN) {\n            if (commentPosition.source == CommentPositionSource.SIDE_BY_SIDE_LEFT) {\n                position.newLine = null\n                position.newPath = null\n            }\n            if (commentPosition.source == CommentPositionSource.SIDE_BY_SIDE_RIGHT) {\n                position.oldLine = null\n                position.oldPath = null\n            }\n        }\n        val request = GitlabCreateNoteRequest(\n            credentials = credentials,\n            mergeRequestInternalId = mergeRequestId.toInt(),\n            body = body,\n            position = position\n        )\n        val out = infrastructure.serviceBus() process request\n        if (out.hasError()) {\n            throw ProviderException(out.getResponse().error!!)\n        }\n        return out.getResponse().createdCommentId\n    }\n\n    private fun makePosition(position: CommentPosition): Position {\n        val model = Position()\n        model.baseSha = position.baseHash\n        model.headSha = position.headHash\n        model.startSha = position.startHash\n        model.oldPath = if (null === position.oldLine || position.oldLine!! < 0) null else position.oldPath\n        model.newPath = if (null === position.newLine || position.newLine!! < 0) null else position.newPath\n        model.oldLine = if (null === position.oldLine || position.oldLine!! < 0) null else position.oldLine\n        model.newLine = if (null === position.newLine || position.newLine!! < 0) null else position.newLine\n        model.positionType = Position.PositionType.TEXT\n        return model\n    }\n\n    override fun reply(project: Project, mergeRequestId: String, repliedComment: Comment, body: String): String? {\n        val request = GitlabReplyNoteRequest(\n            credentials = credentials,\n            mergeRequestInternalId = mergeRequestId.toInt(),\n            discussionId = repliedComment.parentId,\n            noteId = repliedComment.id.toInt(),\n            body = body\n        )\n\n        val response = infrastructure.serviceBus() process request ifError {\n            throw ProviderException(it)\n        }\n        return response.createdCommentId.toString()\n    }\n\n    override fun delete(project: Project, mergeRequestId: String, comment: Comment) {\n        val command = GitlabDeleteNoteCommand(\n            credentials = credentials,\n            mergeRequestInternalId = mergeRequestId.toInt(),\n            discussionId = comment.parentId,\n            noteId = comment.id.toInt()\n        )\n        infrastructure.commandBus() process command\n    }\n\n    override fun resolve(project: Project, mergeRequestId: String, comment: Comment) {\n        infrastructure.commandBus() process GitlabResolveNoteCommand(\n            credentials = credentials,\n            mergeRequestInternalId = mergeRequestId.toInt(),\n            discussionId = comment.parentId,\n            resolve = true\n        )\n    }\n\n    override fun unresolve(project: Project, mergeRequestId: String, comment: Comment) {\n        infrastructure.commandBus() process GitlabResolveNoteCommand(\n            credentials = credentials,\n            mergeRequestInternalId = mergeRequestId.toInt(),\n            discussionId = comment.parentId,\n            resolve = false\n        )\n    }\n\n    override fun update(project: Project, mergeRequestId: String, comment: Comment, body: String) {\n        infrastructure.commandBus() process GitlabUpdateDiffNoteCommand(\n            credentials = credentials,\n            mergeRequestInternalId = mergeRequestId.toInt(),\n            discussionId = comment.parentId,\n            body = body,\n            noteId = comment.id.toInt()\n        )\n    }\n\n    override fun getDraftCount(project: Project, mergeRequestId: String): Int {\n        throw Exception(\"Not implemented in GitlabCommentApi, see DraftCommentApi\")\n    }\n\n    override fun publishAllDraftComments(project: Project, mergeRequestId: String) {\n        throw Exception(\"Not implemented in GitlabCommentApi, see DraftCommentApi\")\n    }\n\n    override fun publishDraftComments(project: Project, mergeRequestId: String, commentIds: List<String>) {\n        throw Exception(\"Not implemented in GitlabCommentApi, see DraftCommentApi\")\n    }\n\n    private fun findProjectFullPath(project: Project): String {\n        val url = project.url.replace(credentials.url, \"\")\n        return if (url.startsWith(\"/\")) url.substring(1) else url\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabCommitApi.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab\n\nimport net.ntworld.foundation.Infrastructure\nimport net.ntworld.mergeRequest.Change\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequest.api.CommitApi\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetCommitChangesRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.GitlabDiffTransformer\n\nclass GitlabCommitApi(\n    private val infrastructure: Infrastructure,\n    private val credentials: ApiCredentials\n) : CommitApi {\n\n    override fun getChanges(projectId: String, commitId: String): List<Change> {\n        val out = infrastructure.serviceBus() process GitlabGetCommitChangesRequest(\n            credentials = credentials,\n            commitSha = commitId\n        )\n        return if (out.hasError()) {\n            listOf()\n        } else {\n            out.getResponse().changes.map { GitlabDiffTransformer.transform(it) }\n        }\n    }\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabCredentials.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab\n\nimport net.ntworld.mergeRequest.api.ApiCredentials\n\ndata class GitlabCredentials(\n    override val url: String,\n\n    override val token: String,\n\n    override val projectId: String,\n\n    override val login: String = \"\",\n\n    override val version: String = \"v4\",\n\n    override val info: String = \"\",\n\n    override val ignoreSSLCertificateErrors: Boolean = false\n): ApiCredentials {\n\n    companion object {\n        val Empty = GitlabCredentials(\"\", \"\", \"\", \"\")\n    }\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabFailedRequestError.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab\n\nimport net.ntworld.foundation.Error\n\ndata class GitlabFailedRequestError(\n    override val message: String,\n    override val code: Int\n): Error {\n    override val type: String = \"net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFailedRequestError\"\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabFailedRequestException.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab\n\nimport net.ntworld.foundation.Error\n\nclass GitlabFailedRequestException(error: Error) : Throwable()"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabFuelClient.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab\n\nimport com.github.kittinunf.fuel.core.*\nimport com.github.kittinunf.result.Result\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.JsonConfiguration\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.FuelClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.model.GraphqlRequest\nimport java.security.cert.X509Certificate\nimport javax.net.ssl.HostnameVerifier\nimport javax.net.ssl.SSLContext\nimport javax.net.ssl.TrustManager\nimport javax.net.ssl.X509TrustManager\n\nclass GitlabFuelClient private constructor(\n    private val credentials: ApiCredentials\n) : FuelClient(credentials) {\n    override fun injectAuthentication(httpRequest: Request): Request {\n        return httpRequest.header(\"PRIVATE-TOKEN\", credentials.token)\n    }\n\n    val baseUrl: String = when (credentials.version) {\n        \"v4\" -> \"${credentials.url}/api/v4\"\n        else -> throw Exception(\"Not supported\")\n    }\n\n    val baseProjectUrl: String = when (credentials.version) {\n        \"v4\" -> \"${credentials.url}/api/v4/projects/${credentials.projectId}\"\n        else -> throw Exception(\"Not supported\")\n    }\n\n    fun callGraphQL(graphqlRequest: String): String {\n        val httpRequest = makeRequestFactory().post(\"${credentials.url}/api/graphql\")\n        httpRequest.header(\"Authorization\", \"Bearer ${credentials.token}\")\n        httpRequest.header(\"Content-Type\", \"application/json\")\n        httpRequest.header(\"Accept\", \"application/json\")\n        httpRequest.body(graphqlRequest)\n        val (_, response, result) = httpRequest.responseString()\n        return when (result) {\n            is Result.Success -> {\n                result.value\n            }\n            is Result.Failure -> {\n                throw HttpException(response.statusCode, result.error.message ?: \"Unknown\")\n            }\n        }\n    }\n\n    fun callGraphQL(query: String, variables: Map<String, String?>): String {\n        val graphqlRequest = GraphqlRequest(\n            query = query,\n            variables = variables\n        )\n        return this.callGraphQL(json.stringify(GraphqlRequest.serializer(), graphqlRequest))\n    }\n\n    companion object {\n        operator fun <T, R : Response> invoke(\n            request: T,\n            execute: (GitlabFuelClient.(T) -> R),\n            failed: ((Error) -> R)\n        ): R where T : net.ntworld.foundation.Request<R>, T : GitlabRequest {\n            return try {\n                val client = GitlabFuelClient(request.credentials)\n                execute.invoke(client, request)\n            } catch (exception: HttpException) {\n                failed.invoke(\n                    GitlabFailedRequestError(\n                        exception.message ?: \"Failed request\",\n                        500\n                    )\n                )\n            }\n        }\n\n        operator fun <T> invoke(\n            credentials: ApiCredentials,\n            execute: (GitlabFuelClient.() -> T),\n            failed: ((Error) -> T)\n        ) : T {\n            return try {\n                val client = GitlabFuelClient(credentials)\n                execute.invoke(client)\n            } catch (exception: HttpException) {\n                failed.invoke(\n                    GitlabFailedRequestError(\n                        exception.message ?: \"Failed request\",\n                        500\n                    )\n                )\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabMergeRequestApi.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab\n\nimport net.ntworld.foundation.Infrastructure\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequest.api.MergeRequestApi\nimport net.ntworld.mergeRequest.api.MergeRequestOrdering\nimport net.ntworld.mergeRequest.query.GetMergeRequestFilter\nimport net.ntworld.mergeRequestIntegration.internal.MergeRequestSearchResultImpl\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.command.GitlabApproveMRCommand\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.command.GitlabUnapproveMRCommand\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.*\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.*\nimport org.gitlab4j.api.Constants\n\nclass GitlabMergeRequestApi(\n    private val infrastructure: Infrastructure,\n    private val credentials: ApiCredentials\n) : MergeRequestApi {\n    override fun find(projectId: String, mergeRequestId: String): MergeRequest? {\n        val out = infrastructure.serviceBus() process GitlabFindMRRequest(\n            credentials = credentials,\n            mergeRequestInternalId = mergeRequestId\n        )\n        return if (out.hasError()) {\n            null\n        } else {\n            GitlabMRTransformer.transform(out.getResponse().mergeRequest)\n        }\n    }\n\n    override fun approve(projectId: String, mergeRequestId: String, sha: String) {\n        infrastructure.commandBus() process GitlabApproveMRCommand(\n            credentials = credentials,\n            mergeRequestInternalId = mergeRequestId.toInt(),\n            sha = sha\n        )\n    }\n\n    override fun unapprove(projectId: String, mergeRequestId: String) {\n        infrastructure.commandBus() process GitlabUnapproveMRCommand(\n            credentials = credentials,\n            mergeRequestInternalId = mergeRequestId.toInt()\n        )\n    }\n\n    override fun findApproval(projectId: String, mergeRequestId: String): Approval {\n        val out = infrastructure.serviceBus() process GitlabFindMRApprovalRequest(\n            credentials = credentials,\n            mergeRequestInternalId = mergeRequestId.toInt()\n        )\n        return GitlabApprovalTransformer.transform(out.getResponse().approval)\n    }\n\n    override fun getPipelines(projectId: String, mergeRequestId: String): List<Pipeline> {\n        val out = infrastructure.serviceBus() process GitlabGetMRPipelinesRequest(\n            credentials = credentials,\n            mergeRequestInternalId = mergeRequestId.toInt()\n        )\n        return if (out.hasError()) {\n            listOf()\n        } else {\n            out.getResponse().pipelines.map { GitlabPipelineTransformer.transform(it) }\n        }\n    }\n\n    override fun getCommits(projectId: String, mergeRequestId: String): List<Commit> {\n        val out = infrastructure.serviceBus() process GitlabGetMRCommitsRequest(\n            credentials = credentials,\n            mergeRequestInternalId = mergeRequestId.toInt()\n        )\n        return if (out.hasError()) {\n            listOf()\n        } else {\n            out.getResponse().commits.map { GitlabCommitTransformer.transform(it) }\n        }\n    }\n\n    override fun getChanges(projectId: String, mergeRequestId: String): List<Change> {\n        val out = infrastructure.serviceBus() process GitlabGetMRChangesRequest(\n            credentials = credentials,\n            mergeRequestInternalId = mergeRequestId.toInt()\n        )\n        return if (out.hasError()) {\n            listOf()\n        } else {\n            out.getResponse().changes.map { GitlabDiffTransformer.transform(it) }\n        }\n    }\n\n    override fun search(\n        projectId: String,\n        currentUserId: String,\n        filterBy: GetMergeRequestFilter,\n        orderBy: MergeRequestOrdering,\n        page: Int,\n        itemsPerPage: Int\n    ): MergeRequestApi.SearchResult {\n        val (order, sort) = resolveOrderAndSort(orderBy)\n        val out = infrastructure.serviceBus() process GitlabSearchMRsRequest(\n            credentials = credentials,\n            state = resolveState(filterBy.state),\n            filterById = filterBy.id,\n            search = filterBy.search,\n            authorId = filterBy.authorId,\n            assigneeId = filterBy.assigneeId,\n            approverIds = filterBy.approverIds,\n            sourceBranch = filterBy.sourceBranch,\n            orderBy = order,\n            sort = sort,\n            page = page,\n            perPage = itemsPerPage\n        )\n\n        return if (out.hasError()) {\n            MergeRequestSearchResultImpl(listOf(), 0, 0, 0)\n        } else {\n            val response = out.getResponse()\n            MergeRequestSearchResultImpl(\n                data = response.mergeRequests.map { GitlabMRSimpleTransformer.transform(it) },\n                totalItems = response.totalItems,\n                totalPages = response.totalPages,\n                currentPage = response.currentPage\n            )\n        }\n    }\n\n    override fun findOrFail(projectId: String, mergeRequestId: String): MergeRequest  {\n        val mergeRequest = find(projectId, mergeRequestId)\n        if (null === mergeRequest) {\n            throw Exception(\"MergeRequest $mergeRequestId not found.\")\n        }\n        return mergeRequest\n    }\n\n    private fun resolveState(state: MergeRequestState): Constants.MergeRequestState = when (state) {\n        MergeRequestState.ALL -> Constants.MergeRequestState.ALL\n        MergeRequestState.OPENED -> Constants.MergeRequestState.OPENED\n        MergeRequestState.CLOSED -> Constants.MergeRequestState.CLOSED\n        MergeRequestState.MERGED -> Constants.MergeRequestState.MERGED\n    }\n\n    private fun resolveOrderAndSort(\n        orderBy: MergeRequestOrdering\n    ): Pair<Constants.MergeRequestOrderBy, Constants.SortOrder> {\n        return when (orderBy) {\n            MergeRequestOrdering.RECENTLY_UPDATED -> Pair(\n                Constants.MergeRequestOrderBy.UPDATED_AT,\n                Constants.SortOrder.DESC\n            )\n            MergeRequestOrdering.NEWEST -> Pair(Constants.MergeRequestOrderBy.CREATED_AT, Constants.SortOrder.DESC)\n            MergeRequestOrdering.OLDEST -> Pair(Constants.MergeRequestOrderBy.CREATED_AT, Constants.SortOrder.ASC)\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabMergeRequestApiCache.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab\n\nimport net.ntworld.mergeRequest.MergeRequest\nimport net.ntworld.mergeRequest.api.*\nimport net.ntworld.mergeRequest.query.GetMergeRequestFilter\nimport net.ntworld.mergeRequestIntegration.internal.ApiOptionsImpl\nimport net.ntworld.mergeRequestIntegration.provider.MergeRequestApiDecorator\nimport org.joda.time.DateTime\n\nclass GitlabMergeRequestApiCache(\n    private val api: MergeRequestApi,\n    private val cache: Cache\n) : MergeRequestApiDecorator(api) {\n    var options: ApiOptions = ApiOptionsImpl.DEFAULT\n\n    override fun findOrFail(projectId: String, mergeRequestId: String): MergeRequest {\n        if (!options.enableRequestCache) {\n            return super.findOrFail(projectId, mergeRequestId)\n        }\n\n        val key = makeFindCacheKey(mergeRequestId)\n        return cache.getOrRun(key) {\n            val mergeRequest = super.findOrFail(projectId, mergeRequestId)\n\n            cache.set(key, mergeRequest)\n            mergeRequest\n        }\n    }\n\n    override fun search(\n        projectId: String,\n        currentUserId: String,\n        filterBy: GetMergeRequestFilter,\n        orderBy: MergeRequestOrdering,\n        page: Int,\n        itemsPerPage: Int\n    ): MergeRequestApi.SearchResult {\n        val result = super.search(projectId, currentUserId, filterBy, orderBy, page, itemsPerPage)\n        if (options.enableRequestCache) {\n            result.data.forEach {\n                try {\n                    val key = makeFindCacheKey(it.id)\n                    if (cache.isExpiredAfter(key, DateTime(it.updatedAt))) {\n                        cache.remove(key)\n                    }\n                } catch (cacheNotFound: CacheNotFoundException) {\n                }\n            }\n        }\n        return result\n    }\n\n    private fun makeFindCacheKey(mergeRequestId: String) = \"MR:find:$mergeRequestId\"\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabProjectApi.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab\n\nimport net.ntworld.foundation.Infrastructure\nimport net.ntworld.mergeRequest.Project\nimport net.ntworld.mergeRequest.UserInfo\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequest.api.ProjectApi\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabFindProjectRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetProjectMembersRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.GitlabMemberTransformer\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.GitlabProjectTransformer\n\nclass GitlabProjectApi(\n    private val infrastructure: Infrastructure,\n    private val credentials: ApiCredentials\n) : ProjectApi {\n\n    override fun find(projectId: String): Project? {\n        val out = infrastructure.serviceBus() process GitlabFindProjectRequest(credentials, projectId.toInt())\n\n        return if (out.hasError()) {\n            null\n        } else {\n            GitlabProjectTransformer.transform(out.getResponse().project)\n        }\n    }\n\n    override fun getMembers(projectId: String): List<UserInfo> {\n        val out = infrastructure.serviceBus() process GitlabGetProjectMembersRequest(credentials)\n\n        return if (out.hasError()) {\n            listOf()\n        } else {\n            out.getResponse().members.map { GitlabMemberTransformer.transform(it) }\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab\n\nimport net.ntworld.mergeRequest.api.ApiCredentials\n\ninterface GitlabRequest {\n    val credentials: ApiCredentials\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabUserApi.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab\n\nimport net.ntworld.foundation.Infrastructure\nimport net.ntworld.mergeRequest.User\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequest.api.UserApi\nimport net.ntworld.mergeRequestIntegration.provider.ProviderException\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabFindCurrentUserRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.GitlabUserTransformer\n\nclass GitlabUserApi(\n    private val infrastructure: Infrastructure,\n    private val credentials: ApiCredentials\n) : UserApi {\n\n    override fun me(): User {\n        val response = infrastructure.serviceBus() process GitlabFindCurrentUserRequest(credentials) ifError {\n            throw ProviderException(it)\n        }\n        return GitlabUserTransformer.transform(response.user)\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/GitlabUtil.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab\n\nimport net.ntworld.mergeRequest.UserStatus\nimport net.ntworld.mergeRequest.api.ApiCredentials\n\nobject GitlabUtil {\n    fun findUserStatus(state: String): UserStatus {\n        return when (state) {\n            USER_STATE_ACTIVE -> UserStatus.ACTIVE\n            USER_STATE_INACTIVE -> UserStatus.INACTIVE\n            else -> UserStatus.INACTIVE\n        }\n    }\n\n    fun hasMergeApprovalFeature(credentials: ApiCredentials): Boolean {\n        return credentials.info.contains(GITLAB_HAS_MERGE_APPROVAL_FEATURE)\n    }\n\n    fun getMergeApprovalFeatureInfo() = GITLAB_HAS_MERGE_APPROVAL_FEATURE\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/_const.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab\n\nconst val GITLAB_HAS_MERGE_APPROVAL_FEATURE = \"mergeApprovalFeature:true\"\nconst val MERGE_REQUEST_STATE_OPENED = \"opened\"\nconst val MERGE_REQUEST_STATE_CLOSED = \"closed\"\nconst val MERGE_REQUEST_STATE_MERGED = \"merged\"\n\nconst val USER_STATE_ACTIVE = \"active\"\nconst val USER_STATE_INACTIVE = \"inactive\"\n\nconst val MERGE_STATUS_CAN_BE_MERGED = \"can_be_merged\"\n\nconst val PIPELINE_STATUS_RUNNING = \"running\"\nconst val PIPELINE_STATUS_FAILED = \"failed\"\nconst val PIPELINE_STATUS_SUCCESS = \"success\"\nconst val PIPELINE_STATUS_PARTIAL_FAILED = \"partial_failed\""
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/command/GitlabApproveMRCommand.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.command\n\nimport net.ntworld.foundation.cqrs.Command\nimport net.ntworld.mergeRequest.api.ApiCredentials\n\ndata class GitlabApproveMRCommand(\n    val credentials: ApiCredentials,\n    val mergeRequestInternalId: Int,\n    val sha: String\n) : Command\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/command/GitlabCreateDiffNoteCommand.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.command\n\nimport net.ntworld.foundation.cqrs.Command\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest\n\ndata class GitlabCreateDiffNoteCommand(\n    val credentials: ApiCredentials,\n    val mergeRequestInternalId: Int,\n    val body: String,\n    val position: CommentPosition\n) : Command\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/command/GitlabDeleteNoteCommand.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.command\n\nimport net.ntworld.foundation.cqrs.Command\nimport net.ntworld.mergeRequest.api.ApiCredentials\n\ndata class GitlabDeleteNoteCommand(\n    val credentials: ApiCredentials,\n    val mergeRequestInternalId: Int,\n    val discussionId: String,\n    val noteId: Int\n) : Command\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/command/GitlabResolveNoteCommand.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.command\n\nimport net.ntworld.foundation.cqrs.Command\nimport net.ntworld.mergeRequest.api.ApiCredentials\n\ndata class GitlabResolveNoteCommand(\n    val credentials: ApiCredentials,\n    val mergeRequestInternalId: Int,\n    val discussionId: String,\n    val resolve: Boolean\n) : Command\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/command/GitlabUnapproveMRCommand.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.command\n\nimport net.ntworld.foundation.cqrs.Command\nimport net.ntworld.mergeRequest.api.ApiCredentials\n\ndata class GitlabUnapproveMRCommand(\n    val credentials: ApiCredentials,\n    val mergeRequestInternalId: Int\n) : Command\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/command/GitlabUpdateDiffNoteCommand.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.command\n\nimport net.ntworld.foundation.cqrs.Command\nimport net.ntworld.mergeRequest.api.ApiCredentials\n\nclass GitlabUpdateDiffNoteCommand(\n    val credentials: ApiCredentials,\n    val mergeRequestInternalId: Int,\n    val discussionId: String,\n    val noteId: Int,\n    val body: String\n): Command"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/commandHandler/GitlabApproveMRCommandHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.commandHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.CommandHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.command.GitlabApproveMRCommand\nimport org.gitlab4j.api.Constants\nimport org.gitlab4j.api.GitLabApi\n\n@Handler\nclass GitlabApproveMRCommandHandler : CommandHandler<GitlabApproveMRCommand> {\n\n    override fun handle(command: GitlabApproveMRCommand) {\n        val api = GitLabApi(command.credentials.url, Constants.TokenType.PRIVATE, command.credentials.token)\n        api.mergeRequestApi.approveMergeRequest(\n            command.credentials.projectId.toInt(),\n            command.mergeRequestInternalId,\n            command.sha\n        )\n    }\n\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/commandHandler/GitlabCreateDiffNoteCommandHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.commandHandler\n\nimport kotlinx.serialization.Serializable\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.CommandHandler\nimport net.ntworld.foundation.util.UUIDGenerator\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFuelClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.command.GitlabCreateDiffNoteCommand\n\n@Handler\nclass GitlabCreateDiffNoteCommandHandler : CommandHandler<GitlabCreateDiffNoteCommand> {\n\n    override fun handle(command: GitlabCreateDiffNoteCommand) = GitlabFuelClient(\n        credentials = command.credentials,\n        execute = {\n            val clientMutationId = generateClientMutationId()\n            val graphqlRequest = CustomGraphqlRequest(\n                query = mutation,\n                variables = CustomVariables(\n                    clientMutationId = clientMutationId,\n                    body = attachFooterToBody(command.body, clientMutationId),\n                    mrIid = \"gid://gitlab/MergeRequest/${command.mergeRequestInternalId}\",\n                    headSha = command.position.headHash,\n                    baseSha = command.position.baseHash,\n                    startSha = command.position.startHash,\n                    oldPath = command.position.oldPath,\n                    oldLine = command.position.oldLine,\n                    newPath = command.position.newPath,\n                    newLine = command.position.newLine\n                )\n            )\n            this.callGraphQL(json.stringify(CustomGraphqlRequest.serializer(), graphqlRequest))\n            Unit\n        },\n        failed = {\n            println(it)\n            Unit\n        }\n    )\n\n    private fun generateClientMutationId(): String {\n        return \"MRI:${UUIDGenerator.generate()}\"\n    }\n\n    private fun attachFooterToBody(body: String, clientMutationId: String): String {\n        return body\n    }\n\n    @Serializable\n    data class CustomGraphqlRequest(\n        val query: String,\n        val variables: CustomVariables\n    )\n\n    @Serializable\n    data class CustomVariables(\n        val clientMutationId: String,\n        val body: String,\n        val mrIid: String,\n        val headSha: String,\n        val baseSha: String,\n        val startSha: String,\n        val oldPath: String?,\n        val oldLine: Int?,\n        val newPath: String?,\n        val newLine: Int?\n    )\n\n    private val mutation = \"\"\"\nmutation createComment(\n  $${\"clientMutationId\"}: String, \n  $${\"body\"}: String!, \n  $${\"mrIid\"}: ID!,\n  $${\"headSha\"}: String!,\n  $${\"baseSha\"}: String,\n  $${\"startSha\"}: String!,\n  $${\"oldPath\"}: String,\n  $${\"oldLine\"}: Int,\n  $${\"newPath\"}: String,\n  $${\"newLine\"}: Int!\n) {\n  createDiffNote(input: {\n    body: $${\"body\"}\n    noteableId: $${\"mrIid\"},\n    clientMutationId: $${\"clientMutationId\"},\n    position: {\n      headSha: $${\"headSha\"},\n      baseSha: $${\"baseSha\"},\n      startSha: $${\"startSha\"},\n      paths: {\n        oldPath: $${\"oldPath\"},\n        newPath: $${\"newPath\"}\n      },\n      oldLine: $${\"oldLine\"},\n      newLine: $${\"newLine\"}\n    }\n  }) {\n    clientMutationId\n  }\n}\n\"\"\"\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/commandHandler/GitlabDeleteNoteCommandHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.commandHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.CommandHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFuelClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.command.GitlabDeleteNoteCommand\n\n@Handler\nclass GitlabDeleteNoteCommandHandler : CommandHandler<GitlabDeleteNoteCommand> {\n\n    override fun handle(command: GitlabDeleteNoteCommand) = GitlabFuelClient(\n        credentials = command.credentials,\n        execute = {\n            this.deleteJson(\"$baseProjectUrl/merge_requests/${command.mergeRequestInternalId}/discussions/${command.discussionId}/notes/${command.noteId}\")\n            Unit\n        },\n        failed = {}\n    )\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/commandHandler/GitlabResolveNoteCommandHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.commandHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.CommandHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFuelClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.command.GitlabResolveNoteCommand\n\n@Handler\nclass GitlabResolveNoteCommandHandler : CommandHandler<GitlabResolveNoteCommand> {\n\n    override fun handle(command: GitlabResolveNoteCommand) = GitlabFuelClient(\n        credentials = command.credentials,\n        execute = {\n            val params = listOf(\n                Pair(\"resolved\", command.resolve)\n            )\n            this.putJson(\n                \"$baseProjectUrl/merge_requests/${command.mergeRequestInternalId}/discussions/${command.discussionId}\",\n                params\n            )\n            Unit\n        },\n        failed = {\n            println(it)\n        }\n    )\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/commandHandler/GitlabUnapproveMRCommandHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.commandHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.CommandHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.command.GitlabUnapproveMRCommand\nimport org.gitlab4j.api.Constants\nimport org.gitlab4j.api.GitLabApi\n\n@Handler\nclass GitlabUnapproveMRCommandHandler: CommandHandler<GitlabUnapproveMRCommand> {\n\n    override fun handle(command: GitlabUnapproveMRCommand) {\n        val api = GitLabApi(command.credentials.url, Constants.TokenType.PRIVATE, command.credentials.token)\n        api.mergeRequestApi.unapproveMergeRequest(\n            command.credentials.projectId.toInt(),\n            command.mergeRequestInternalId\n        )\n    }\n\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/commandHandler/GitlabUpdateDiffNoteCommandHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.commandHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.CommandHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFuelClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.command.GitlabUpdateDiffNoteCommand\n\n@Handler\nclass GitlabUpdateDiffNoteCommandHandler : CommandHandler<GitlabUpdateDiffNoteCommand> {\n    override fun handle(command: GitlabUpdateDiffNoteCommand) = GitlabFuelClient(\n        credentials = command.credentials,\n        execute = {\n            val params = listOf(\n                Pair(\"body\", command.body)\n            )\n            this.putJson(\n                \"$baseProjectUrl/merge_requests/${command.mergeRequestInternalId}/discussions/${command.discussionId}/notes/${command.noteId}\",\n                params\n            )\n            Unit\n        },\n        failed = {\n            println(it)\n        }\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/model/ApprovalModel.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ApprovalModel(\n    val approved: Boolean,\n\n    @SerialName(\"approvals_required\")\n    val approvalsRequired: Int,\n\n    @SerialName(\"approvals_left\")\n    val approvalsLeft: Int,\n\n    @SerialName(\"suggested_approvers\")\n    val suggestedApprovers: List<UserInfoModel>,\n\n    val approvers: List<ApproverModel>,\n\n    @SerialName(\"approved_by\")\n    val approvedBy: List<ApproverModel>,\n\n    @SerialName(\"user_has_approved\")\n    val hasApproved: Boolean,\n\n    @SerialName(\"user_can_approve\")\n    val canApprove: Boolean\n) {\n    companion object {\n        val Empty = ApprovalModel(\n            approved = false,\n            approvalsRequired = 0,\n            approvalsLeft = 0,\n            suggestedApprovers = listOf(),\n            approvers = listOf(),\n            approvedBy = listOf(),\n            hasApproved = false,\n            canApprove = false\n        )\n    }\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/model/ApproverModel.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ApproverModel(\n    val user: UserInfoModel\n)"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/model/GetCommentsPayload.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GetCommentsPayload(\n    val data: Data\n) {\n    @Serializable\n    data class Data(\n        val project: Project\n    )\n\n    @Serializable\n    data class Project(\n        val id: String,\n        val name: String,\n        val mergeRequest: MergeRequest\n    )\n\n    @Serializable\n    data class MergeRequest(\n        val id: String,\n        val iid: String,\n        val notes: NoteCollection\n    )\n\n    @Serializable\n    data class NoteCollection(\n        val pageInfo: NotesPageInfo,\n        val nodes: List<Note>\n    )\n\n    @Serializable\n    data class NotesPageInfo(\n        val endCursor: String,\n        val startCursor: String,\n        val hasNextPage: Boolean,\n        val hasPreviousPage: Boolean\n    )\n\n    @Serializable\n    data class Note(\n        val id: String,\n        val body: String,\n        val bodyHtml: String,\n        val author: User,\n        val resolvable: Boolean,\n        val resolvedAt: String?,\n        val resolvedBy: User?,\n        val system: Boolean,\n        val createdAt: String,\n        val updatedAt: String,\n        val position: NotePosition?\n    )\n\n    @Serializable\n    data class User(\n        val name: String,\n        val username: String,\n        val webUrl: String,\n        val avatarUrl: String\n    )\n\n    @Serializable\n    data class NotePosition(\n        val diffRefs: DiffRef,\n        val filePath: String,\n        val newLine: Int?,\n        val oldLine: Int?,\n        val newPath: String?,\n        val oldPath: String?,\n        val positionType: String\n    )\n\n    @Serializable\n    data class DiffRef(\n        val baseSha: String,\n        val startSha: String,\n        val headSha: String\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/model/GraphqlRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GraphqlRequest(\n    val query: String,\n    val variables: Map<String, String?>\n)\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/model/PipelineModel.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class PipelineModel(\n    val id: Int,\n\n    val sha: String,\n\n    val ref: String,\n\n    val status: String,\n\n    @SerialName(\"created_at\")\n    val createdAt: String? = null,\n\n    @SerialName(\"updated_at\")\n    val updatedAt: String? = null,\n\n    @SerialName(\"web_url\")\n    val webUrl: String\n)"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/model/ReplyCommentPayload.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ReplyCommentPayload(\n    val id: Int\n)\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/model/UserInfoModel.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class UserInfoModel(\n    val id: Int,\n\n    val name: String,\n\n    val username: String,\n\n    @SerialName(\"avatar_url\")\n    val avatarUrl: String,\n\n    @SerialName(\"web_url\")\n    val webUrl: String,\n\n    val state: String\n)"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabCreateNoteRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabCreateNoteResponse\nimport org.gitlab4j.api.models.Position\n\ndata class GitlabCreateNoteRequest(\n    override val credentials: ApiCredentials,\n    val mergeRequestInternalId: Int,\n    val position: Position?,\n    val body: String\n) : GitlabRequest, Request<GitlabCreateNoteResponse>\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabFindCurrentUserRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindUserResponse\n\ndata class GitlabFindCurrentUserRequest(\n    override val credentials: ApiCredentials\n): GitlabRequest, Request<GitlabFindUserResponse>"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabFindMRApprovalRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindMRApprovalResponse\n\ndata class GitlabFindMRApprovalRequest(\n    override val credentials: ApiCredentials,\n    val mergeRequestInternalId: Int\n) : GitlabRequest, Request<GitlabFindMRApprovalResponse>"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabFindMRRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindMRResponse\n\n// FIXME: projectId, mergeRequestInternalId in this package belong to Gitlab, then it should be an integer not String\ndata class GitlabFindMRRequest(\n    override val credentials: ApiCredentials,\n    val mergeRequestInternalId: String\n): GitlabRequest, Request<GitlabFindMRResponse>"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabFindProjectRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindProjectResponse\n\ndata class GitlabFindProjectRequest(\n    override val credentials: ApiCredentials,\n    val projectId: Int,\n    val projectPath: String = \"\"\n) : GitlabRequest, Request<GitlabFindProjectResponse>\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabFindUserRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindUserResponse\n\n// FIXME: userId in this package belong to Gitlab, then it should be an integer not String\ndata class GitlabFindUserRequest(\n    override val credentials: ApiCredentials,\n    val userId: String\n) : GitlabRequest, Request<GitlabFindUserResponse>\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabGetCommitChangesRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetCommitChangesResponse\n\ndata class GitlabGetCommitChangesRequest(\n    override val credentials: ApiCredentials,\n    val commitSha: String\n): GitlabRequest, Request<GitlabGetCommitChangesResponse>"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabGetMRChangesRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRChangesResponse\n\ndata class GitlabGetMRChangesRequest(\n    override val credentials: ApiCredentials,\n    val mergeRequestInternalId: Int\n): GitlabRequest, Request<GitlabGetMRChangesResponse>"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabGetMRCommentsRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRCommentsResponse\n\ndata class GitlabGetMRCommentsRequest(\n    override val credentials: ApiCredentials,\n    val projectFullPath: String,\n    val endCursor: String,\n    val mergeRequestInternalId: Int\n) : GitlabRequest, Request<GitlabGetMRCommentsResponse>\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabGetMRCommitsRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRCommitsResponse\n\ndata class GitlabGetMRCommitsRequest(\n    override val credentials: ApiCredentials,\n    val mergeRequestInternalId: Int\n): GitlabRequest, Request<GitlabGetMRCommitsResponse>"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabGetMRDiscussionsRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRDiscussionsResponse\n\ndata class GitlabGetMRDiscussionsRequest(\n    override val credentials: ApiCredentials,\n    val mergeRequestInternalId: Int\n) : GitlabRequest, Request<GitlabGetMRDiscussionsResponse>\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabGetMRPipelinesRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRPipelinesResponse\n\ndata class GitlabGetMRPipelinesRequest(\n    override val credentials: ApiCredentials,\n    val mergeRequestInternalId: Int\n): GitlabRequest, Request<GitlabGetMRPipelinesResponse>"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabGetProjectMembersRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetProjectMembersResponse\n\ndata class GitlabGetProjectMembersRequest(\n    override val credentials: ApiCredentials\n): GitlabRequest, Request<GitlabGetProjectMembersResponse>\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabReplyNoteRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabReplyNoteResponse\n\ndata class GitlabReplyNoteRequest(\n    override val credentials: ApiCredentials,\n    val mergeRequestInternalId: Int,\n    val discussionId: String,\n    val noteId: Int,\n    val body: String\n) : GitlabRequest, Request<GitlabReplyNoteResponse>\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabSearchMRsRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabSearchMRsResponse\nimport org.gitlab4j.api.Constants\n\n// FIXME: authorId, assigneeId... in this package belong to Gitlab, then it should be an integer not String\ndata class GitlabSearchMRsRequest(\n    override val credentials: ApiCredentials,\n    val state: Constants.MergeRequestState,\n    val filterById: Int?,\n    val search: String,\n    val authorId: String,\n    val assigneeId: String,\n    val approverIds: List<String>,\n    val sourceBranch: String,\n    val orderBy: Constants.MergeRequestOrderBy = Constants.MergeRequestOrderBy.UPDATED_AT,\n    val sort: Constants.SortOrder,\n    val page: Int = 0,\n    val perPage: Int = 100\n): GitlabRequest, Request<GitlabSearchMRsResponse>\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/request/GitlabSearchProjectsRequest.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.request\n\nimport net.ntworld.foundation.Request\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabSearchProjectsResponse\n\ndata class GitlabSearchProjectsRequest(\n    override val credentials: ApiCredentials,\n    val term: String,\n    val owner: Boolean = false,\n    val starred: Boolean = false,\n    val membership: Boolean = false\n) : GitlabRequest, Request<GitlabSearchProjectsResponse>\n\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabCreateNoteRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabCreateNoteRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabCreateNoteResponse\nimport java.util.*\n\n@Handler\nclass GitlabCreateNoteRequestHandler : RequestHandler<GitlabCreateNoteRequest, GitlabCreateNoteResponse> {\n\n    override fun handle(request: GitlabCreateNoteRequest): GitlabCreateNoteResponse = GitlabClient(\n        request = request,\n        execute = {\n            val result = this.discussionsApi.createMergeRequestDiscussion(\n                request.credentials.projectId.toInt(),\n                request.mergeRequestInternalId,\n                request.body,\n                Date(),\n                null, // This is a bug from the API client, null is okay\n                request.position\n            )\n            if (result.notes.isNotEmpty()) {\n                GitlabCreateNoteResponse(error = null, createdCommentId = result.notes.first().id)\n            } else {\n                GitlabCreateNoteResponse(error = null, createdCommentId = 0)\n            }\n        },\n        failed = {\n            GitlabCreateNoteResponse(error = it, createdCommentId = 0)\n        }\n    )\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabFindCurrentUserRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabFindCurrentUserRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindUserResponse\nimport org.gitlab4j.api.models.User\n\n@Handler\nclass GitlabFindCurrentUserRequestHandler\n    : RequestHandler<GitlabFindCurrentUserRequest, GitlabFindUserResponse> {\n\n    override fun handle(request: GitlabFindCurrentUserRequest): GitlabFindUserResponse = GitlabClient(\n        request = request,\n        execute = {\n            GitlabFindUserResponse(error = null, user = this.userApi.currentUser)\n        },\n        failed = {\n            GitlabFindUserResponse(error = it, user = User())\n        }\n    )\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabFindMRApprovalRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFuelClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.model.ApprovalModel\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabFindMRApprovalRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindMRApprovalResponse\n\n@Handler\nclass GitlabFindMRApprovalRequestHandler : RequestHandler<GitlabFindMRApprovalRequest, GitlabFindMRApprovalResponse> {\n    override fun handle(request: GitlabFindMRApprovalRequest): GitlabFindMRApprovalResponse = GitlabFuelClient(\n        request = request,\n        execute = {\n            val response = this.getJson(\n                \"${this.baseProjectUrl}/merge_requests/${request.mergeRequestInternalId}/approvals\"\n            )\n\n            GitlabFindMRApprovalResponse(\n                error = null,\n                approval = this.json.parse(ApprovalModel.serializer(), response)\n            )\n        },\n        failed = {\n            GitlabFindMRApprovalResponse(it, ApprovalModel.Empty)\n        }\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabFindMRRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabFindMRRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindMRResponse\nimport org.gitlab4j.api.models.MergeRequest\n\n@Handler\nclass GitlabFindMRRequestHandler : RequestHandler<GitlabFindMRRequest, GitlabFindMRResponse> {\n    override fun handle(request: GitlabFindMRRequest): GitlabFindMRResponse = GitlabClient(\n        request = request,\n        execute = {\n            val mr = this.mergeRequestApi.getMergeRequest(\n                request.credentials.projectId.toInt(),\n                request.mergeRequestInternalId.toInt()\n            )\n            GitlabFindMRResponse(null, mr)\n        },\n        failed = {\n            GitlabFindMRResponse(it, MergeRequest())\n        }\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabFindProjectRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabFindProjectRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindProjectResponse\nimport org.gitlab4j.api.models.Project\n\n@Handler\nclass GitlabFindProjectRequestHandler : RequestHandler<GitlabFindProjectRequest, GitlabFindProjectResponse> {\n\n    override fun handle(request: GitlabFindProjectRequest): GitlabFindProjectResponse = GitlabClient(\n        request = request,\n        execute = {\n            val project = if (request.projectPath.isNotBlank()) {\n                this.projectApi.getProject(request.projectPath)\n            } else {\n                this.projectApi.getProject(request.projectId)\n            }\n\n            GitlabFindProjectResponse(error = null, project = project)\n        },\n        failed = {\n            GitlabFindProjectResponse(error = it, project = Project())\n        }\n    )\n\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabFindUserRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFailedRequestError\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabFindUserRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabFindUserResponse\nimport org.gitlab4j.api.models.User\n\n@Handler\nclass GitlabFindUserRequestHandler : RequestHandler<GitlabFindUserRequest, GitlabFindUserResponse> {\n    override fun handle(request: GitlabFindUserRequest): GitlabFindUserResponse = GitlabClient(\n        request = request,\n        execute = {\n            val user = this.userApi.getUser(request.userId.toInt())\n            if (null == user) {\n                GitlabFindUserResponse(\n                    error = GitlabFailedRequestError(\"User not found\", 404),\n                    user = User()\n                )\n            } else {\n                GitlabFindUserResponse(error = null, user = user)\n            }\n        },\n        failed = {\n            GitlabFindUserResponse(error = it, user = User())\n        }\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabGetCommitChangesRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetCommitChangesRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetCommitChangesResponse\n\n@Handler\nclass GitlabGetCommitChangesRequestHandler : RequestHandler<GitlabGetCommitChangesRequest, GitlabGetCommitChangesResponse> {\n    override fun handle(request: GitlabGetCommitChangesRequest): GitlabGetCommitChangesResponse = GitlabClient(\n        request = request,\n        execute = {\n            val diff = this.commitsApi.getDiff(\n                request.credentials.projectId.toInt(), request.commitSha\n            )\n            GitlabGetCommitChangesResponse(error = null, changes = diff)\n        },\n        failed = {\n            GitlabGetCommitChangesResponse(error = it, changes = listOf())\n        }\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabGetMRChangesRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetMRChangesRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRChangesResponse\n\n@Handler\nclass GitlabGetMRChangesRequestHandler : RequestHandler<GitlabGetMRChangesRequest, GitlabGetMRChangesResponse> {\n\n    override fun handle(request: GitlabGetMRChangesRequest): GitlabGetMRChangesResponse = GitlabClient(\n        request = request,\n        execute = {\n            val mr = this.mergeRequestApi.getMergeRequestChanges(\n                request.credentials.projectId.toInt(), request.mergeRequestInternalId\n            )\n            GitlabGetMRChangesResponse(error = null, changes = mr.changes)\n        },\n        failed = {\n            GitlabGetMRChangesResponse(error = it, changes = listOf())\n        }\n    )\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabGetMRCommentsRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFuelClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.model.GetCommentsPayload\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetMRCommentsRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRCommentsResponse\n\n@Handler\nclass GitlabGetMRCommentsRequestHandler : RequestHandler<GitlabGetMRCommentsRequest, GitlabGetMRCommentsResponse> {\n    override fun handle(request: GitlabGetMRCommentsRequest): GitlabGetMRCommentsResponse = GitlabFuelClient(\n        request = request,\n        execute = {\n            val response = this.callGraphQL(query = query, variables = mapOf(\n                \"projectPath\" to request.projectFullPath,\n                \"mrIid\" to request.mergeRequestInternalId.toString(),\n                \"endCursor\" to request.endCursor\n            ))\n\n            GitlabGetMRCommentsResponse(\n                error = null,\n                payload = this.json.parse(GetCommentsPayload.serializer(), response)\n            )\n        },\n        failed = {\n            GitlabGetMRCommentsResponse(error = it, payload = null)\n        }\n    )\n\n    private val query = \"\"\"\nquery GetNotes($${\"projectPath\"}: ID!, $${\"mrIid\"}: String, $${\"endCursor\"}: String){\n  project(fullPath: $${\"projectPath\"}) {\n    id,\n    name,\n    \n    mergeRequest(iid: $${\"mrIid\"}) {\n      id\n      iid\n      notes(after: $${\"endCursor\"}) {\n        pageInfo {\n          endCursor\n          startCursor\n          hasNextPage\n          hasPreviousPage\n        }\n        nodes {\n          id\n          body\n          bodyHtml\n          author {\n            name\n            username\n            webUrl\n            avatarUrl\n          }\n          resolvable\n          resolvedAt\n          resolvedBy {\n            name\n            username\n            webUrl\n            avatarUrl\n          }\n          system\n          createdAt\n          updatedAt\n          position {\n            diffRefs {\n              baseSha,\n              startSha,\n              headSha\n            }\n            filePath\n            newLine\n            oldLine\n            newPath\n            oldPath\n            positionType\n          }\n        }\n      }\n    }\n  }\n}\n\"\"\"\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabGetMRCommitsRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetMRCommitsRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRCommitsResponse\n\n@Handler\nclass GitlabGetMRCommitsRequestHandler : RequestHandler<GitlabGetMRCommitsRequest, GitlabGetMRCommitsResponse> {\n\n    override fun handle(request: GitlabGetMRCommitsRequest): GitlabGetMRCommitsResponse = GitlabClient(\n        request = request,\n        execute = {\n            val commits = this.mergeRequestApi.getCommits(\n                request.credentials.projectId.toInt(), request.mergeRequestInternalId\n            )\n            GitlabGetMRCommitsResponse(error = null, commits = commits)\n        },\n        failed = {\n            GitlabGetMRCommitsResponse(error = it, commits = listOf())\n        }\n    )\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabGetMRDiscussionsRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetMRDiscussionsRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRDiscussionsResponse\n\n@Handler\nclass GitlabGetMRDiscussionsRequestHandler\n    : RequestHandler<GitlabGetMRDiscussionsRequest, GitlabGetMRDiscussionsResponse> {\n\n    override fun handle(request: GitlabGetMRDiscussionsRequest): GitlabGetMRDiscussionsResponse = GitlabClient(\n        request = request,\n        execute = {\n            val discussions = this.discussionsApi.getMergeRequestDiscussions(\n                request.credentials.projectId.toInt(),\n                request.mergeRequestInternalId\n            )\n            GitlabGetMRDiscussionsResponse(error = null, discussions = discussions)\n        },\n        failed = {\n            GitlabGetMRDiscussionsResponse(error = it, discussions = listOf())\n        }\n    )\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabGetMRPipelinesRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler\n\nimport kotlinx.serialization.list\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFuelClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.model.PipelineModel\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetMRPipelinesRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetMRPipelinesResponse\n\n@Handler\nclass GitlabGetMRPipelinesRequestHandler : RequestHandler<GitlabGetMRPipelinesRequest, GitlabGetMRPipelinesResponse> {\n    override fun handle(request: GitlabGetMRPipelinesRequest): GitlabGetMRPipelinesResponse = GitlabFuelClient(\n        request = request,\n        execute = {\n            val response = this.getJson(\n                \"${this.baseProjectUrl}/merge_requests/${request.mergeRequestInternalId}/pipelines\"\n            )\n\n            GitlabGetMRPipelinesResponse(\n                error = null,\n                pipelines = this.json.parse(PipelineModel.serializer().list, response)\n            )\n        },\n        failed = {\n            GitlabGetMRPipelinesResponse(error = it, pipelines = listOf())\n        }\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabGetProjectMembersRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabGetProjectMembersRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabGetProjectMembersResponse\n\n@Handler\nclass GitlabGetProjectMembersRequestHandler :\n    RequestHandler<GitlabGetProjectMembersRequest, GitlabGetProjectMembersResponse> {\n\n    override fun handle(request: GitlabGetProjectMembersRequest): GitlabGetProjectMembersResponse = GitlabClient(\n        request = request,\n        execute = {\n            val members = this.projectApi.getAllMembers(request.credentials.projectId.toInt())\n            GitlabGetProjectMembersResponse(error = null, members = members)\n        },\n        failed = {\n            GitlabGetProjectMembersResponse(error = it, members = listOf())\n        }\n    )\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabReplyNoteRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabFuelClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.model.ReplyCommentPayload\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabReplyNoteRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabReplyNoteResponse\n\n@Handler\nclass GitlabReplyNoteRequestHandler : RequestHandler<GitlabReplyNoteRequest, GitlabReplyNoteResponse> {\n\n    override fun handle(request: GitlabReplyNoteRequest) = GitlabFuelClient(\n        request = request,\n        execute = {\n            val url = \"${baseProjectUrl}/merge_requests/${request.mergeRequestInternalId}/discussions/${request.discussionId}/notes\"\n            val parameters = listOf(\n                Pair(\"body\", request.body)\n            )\n            val result = this.postJson(url = url, parameters = parameters)\n            val payload = json.parse(ReplyCommentPayload.serializer(), result)\n\n            GitlabReplyNoteResponse(error = null, createdCommentId = payload.id)\n        },\n        failed = {\n            GitlabReplyNoteResponse(error = it, createdCommentId = 0)\n        }\n    )\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabSearchMRsRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabUtil\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabSearchMRsRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabSearchMRsResponse\nimport org.gitlab4j.api.GitLabApiForm\nimport org.gitlab4j.api.models.MergeRequestFilter\n\n@Handler\nclass GitlabSearchMRsRequestHandler : RequestHandler<GitlabSearchMRsRequest, GitlabSearchMRsResponse> {\n    override fun handle(request: GitlabSearchMRsRequest): GitlabSearchMRsResponse = GitlabClient(\n        request = request,\n        execute = {\n            val filter = buildMergeRequestFilter(request)\n            val pager = this.mergeRequestApi.getMergeRequests(filter, request.perPage)\n            val result = pager.page(if (request.page <= pager.totalPages) request.page else pager.totalPages)\n            GitlabSearchMRsResponse(\n                error = null, mergeRequests = result,\n                totalPages = pager.totalPages,\n                totalItems = pager.totalItems,\n                currentPage = pager.currentPage\n            )\n        },\n        failed = {\n            GitlabSearchMRsResponse(\n                error = it,\n                mergeRequests = listOf(),\n                totalPages = 0,\n                totalItems = 0,\n                currentPage = 0\n            )\n        }\n    )\n\n    internal fun buildMergeRequestFilter(request: GitlabSearchMRsRequest): MyMergeRequestFilter {\n        val filter = MyMergeRequestFilter()\n        if (request.sourceBranch.isNotEmpty()) {\n            filter.sourceBranch = request.sourceBranch\n        }\n        filter.state = request.state\n        filter.projectId = request.credentials.projectId.toInt()\n\n        setFilterByIdIfNotEmpty(filter, request)\n        setSearchFilterParamIfNotEmpty(filter, request)\n        setAuthorFilterParamIfNotEmpty(filter, request)\n        setAssigneeFilterParamIfNotEmpty(filter, request)\n        setApproverIdsParamIfNotEmpty(filter, request)\n\n        filter.orderBy = request.orderBy\n        filter.sort = request.sort\n        return filter\n    }\n\n    private fun setFilterByIdIfNotEmpty(filter: MyMergeRequestFilter, request: GitlabSearchMRsRequest) {\n        if (null !== request.filterById && request.filterById > 0) {\n            filter.withIids(listOf(request.filterById))\n        }\n    }\n\n    private fun setSearchFilterParamIfNotEmpty(filter: MyMergeRequestFilter, request: GitlabSearchMRsRequest) {\n        if (request.search.isNotEmpty()) {\n            filter.search = request.search\n        }\n    }\n\n    private fun setAuthorFilterParamIfNotEmpty(filter: MyMergeRequestFilter, request: GitlabSearchMRsRequest) {\n        if (request.authorId.isNotEmpty()) {\n            filter.authorId = request.authorId.toInt()\n        }\n    }\n\n    private fun setAssigneeFilterParamIfNotEmpty(filter: MyMergeRequestFilter, request: GitlabSearchMRsRequest) {\n        if (request.assigneeId.isNotEmpty()) {\n            filter.assigneeId = request.assigneeId.toInt()\n        }\n    }\n\n    private fun setApproverIdsParamIfNotEmpty(filter: MyMergeRequestFilter, request: GitlabSearchMRsRequest) {\n        if (GitlabUtil.hasMergeApprovalFeature(request.credentials)) {\n            val approverIds = request.approverIds\n                .filter { it.isNotEmpty() }\n                .map { it.toInt() }\n            if (approverIds.isNotEmpty()) {\n                filter.approverIds = approverIds\n            }\n        }\n    }\n\n    internal class MyMergeRequestFilter : MergeRequestFilter() {\n        var approverIds: List<Int> = listOf()\n\n        override fun getQueryParams(): GitLabApiForm {\n            val form = super.getQueryParams()\n            form.withParam(\"author_id\", this.authorId)\n            val approvers = approverIds.filter { it > 0 }\n            if (approvers.isNotEmpty()) {\n                form.withParam(\"approver_ids\", approvers)\n            }\n            return form\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/requestHandler/GitlabSearchProjectsRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabClient\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabSearchProjectsRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.response.GitlabSearchProjectsResponse\nimport org.gitlab4j.api.Constants\n\n@Handler\nclass GitlabSearchProjectsRequestHandler : RequestHandler<GitlabSearchProjectsRequest, GitlabSearchProjectsResponse> {\n\n    override fun handle(request: GitlabSearchProjectsRequest): GitlabSearchProjectsResponse = GitlabClient(\n        request = request,\n        execute = {\n            val projects = if (it.term.isEmpty()) {\n                this.projectApi.getProjects(10).first()\n            } else {\n                this.projectApi.getProjects(\n                    false,\n                    null,\n                    Constants.ProjectOrderBy.CREATED_AT,\n                    Constants.SortOrder.ASC,\n                    request.term,\n                    false,\n                    request.owner,\n                    request.membership,\n                    request.starred,\n                    false,\n                    10\n                ).first()\n            }\n\n            GitlabSearchProjectsResponse(error = null, projects = projects)\n        },\n        failed = {\n            GitlabSearchProjectsResponse(error = it, projects = listOf())\n        }\n    )\n\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabCreateNoteResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\n\ndata class GitlabCreateNoteResponse(\n    override val error: Error?,\n    val createdCommentId: Int\n) : Response"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabFindMRApprovalResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.model.ApprovalModel\n\ndata class GitlabFindMRApprovalResponse(\n    override val error: Error?,\n    val approval: ApprovalModel\n) : Response"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabFindMRResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport org.gitlab4j.api.models.MergeRequest\n\ndata class GitlabFindMRResponse(\n    override val error: Error?,\n    val mergeRequest: MergeRequest\n) :Response"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabFindProjectResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport org.gitlab4j.api.models.Project as GitlabProject\n\ndata class GitlabFindProjectResponse(\n    override val error: Error?,\n    val project: GitlabProject\n) : Response\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabFindUserResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport org.gitlab4j.api.models.User\n\ndata class GitlabFindUserResponse(\n    override val error: Error?,\n    val user: User\n): Response"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabGetCommitChangesResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport org.gitlab4j.api.models.Diff\n\ndata class GitlabGetCommitChangesResponse(\n    override val error: Error?,\n    val changes: List<Diff>\n) : Response\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabGetMRChangesResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport org.gitlab4j.api.models.Diff\n\ndata class GitlabGetMRChangesResponse(\n    override val error: Error?,\n    val changes: List<Diff>\n) : Response\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabGetMRCommentsResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.model.GetCommentsPayload\n\ndata class GitlabGetMRCommentsResponse(\n    override val error: Error?,\n    val payload: GetCommentsPayload?\n) : Response\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabGetMRCommitsResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport org.gitlab4j.api.models.Commit\n\ndata class GitlabGetMRCommitsResponse(\n    override val error: Error?,\n    val commits: List<Commit>\n) : Response\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabGetMRDiscussionsResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport org.gitlab4j.api.models.Discussion\n\ndata class GitlabGetMRDiscussionsResponse(\n    override val error: Error?,\n    val discussions: List<Discussion>\n) : Response\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabGetMRPipelinesResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.model.PipelineModel\n\ndata class GitlabGetMRPipelinesResponse(\n    override val error: Error?,\n    val pipelines: List<PipelineModel>\n) : Response\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabGetProjectMembersResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport org.gitlab4j.api.models.Member\n\ndata class GitlabGetProjectMembersResponse(\n    override val error: Error?,\n    val members: List<Member>\n): Response\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabReplyNoteResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\n\ndata class GitlabReplyNoteResponse(\n    override val error: Error?,\n    val createdCommentId: Int\n) : Response"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabSearchMRsResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport org.gitlab4j.api.models.MergeRequest\n\ndata class GitlabSearchMRsResponse(\n    override val error: Error?,\n    val mergeRequests: List<MergeRequest>,\n    val totalPages: Int,\n    val totalItems: Int,\n    val currentPage: Int\n): Response\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/response/GitlabSearchProjectsResponse.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.response\n\nimport net.ntworld.foundation.Error\nimport net.ntworld.foundation.Response\nimport org.gitlab4j.api.models.Project as GitlabProject\n\ndata class GitlabSearchProjectsResponse(\n    override val error: Error?,\n    val projects: List<GitlabProject>\n) : Response\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabApprovalTransformer.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer\n\nimport net.ntworld.mergeRequest.Approval\nimport net.ntworld.mergeRequest.UserInfo\nimport net.ntworld.mergeRequestIntegration.internal.ApprovalImpl\nimport net.ntworld.mergeRequestIntegration.internal.UserInfoImpl\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabUtil\nimport net.ntworld.mergeRequestIntegration.provider.Transformer\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.model.ApprovalModel\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.model.ApproverModel\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.model.UserInfoModel\n\nobject GitlabApprovalTransformer :\n    Transformer<ApprovalModel, Approval> {\n    override fun transform(input: ApprovalModel): Approval = ApprovalImpl(\n        approved = input.approved,\n        approvalsRequired = input.approvalsRequired,\n        approvalsLeft = input.approvalsLeft,\n        suggestedApprovers = input.suggestedApprovers.map { transformUserInfo(it) },\n        approvers = input.approvers.map { transformApprover(it) },\n        approvedBy = input.approvedBy.map { transformApprover(it) },\n        hasApproved = input.hasApproved,\n        canApprove = input.canApprove\n    )\n\n    private fun transformApprover(input: ApproverModel): UserInfo = transformUserInfo(input.user)\n\n    private fun transformUserInfo(input: UserInfoModel): UserInfo = UserInfoImpl(\n        id = input.id.toString(),\n        name = input.name,\n        username = input.username,\n        avatarUrl = input.avatarUrl,\n        url = input.webUrl,\n        status = GitlabUtil.findUserStatus(input.state)\n    )\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabCommentTransformer.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPositionChangeType\nimport net.ntworld.mergeRequest.CommentPositionSource\nimport net.ntworld.mergeRequest.UserStatus\nimport net.ntworld.mergeRequestIntegration.internal.CommentImpl\nimport net.ntworld.mergeRequestIntegration.internal.CommentPositionImpl\nimport net.ntworld.mergeRequestIntegration.internal.UserInfoImpl\nimport net.ntworld.mergeRequestIntegration.provider.Transformer\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.model.GetCommentsPayload\n\nobject GitlabCommentTransformer :\n    Transformer<GetCommentsPayload.Note, Comment> {\n\n    override fun transform(input: GetCommentsPayload.Note): Comment = CommentImpl(\n        id = input.id,\n        parentId = \"\",\n        replyId = input.id,\n        body = input.body,\n        author = UserInfoImpl(\n            id = \"\",\n            name = input.author.name,\n            username = input.author.username,\n            url = input.author.webUrl,\n            avatarUrl = input.author.avatarUrl,\n            status = UserStatus.ACTIVE\n        ),\n        createdAt = input.createdAt,\n        updatedAt = input.updatedAt,\n        resolvedBy = if (null !== input.resolvedBy) {\n            UserInfoImpl(\n                id = \"\",\n                name = input.author.name,\n                username = input.author.username,\n                url = input.author.webUrl,\n                avatarUrl = input.author.avatarUrl,\n                status = UserStatus.ACTIVE\n            )\n        } else null,\n        resolved = null !== input.resolvedBy,\n        resolvable = input.resolvable,\n        position = if (null !== input.position) {\n            CommentPositionImpl(\n                startHash = input.position.diffRefs.startSha,\n                baseHash = input.position.diffRefs.baseSha,\n                headHash = input.position.diffRefs.headSha,\n                oldPath = input.position.oldPath,\n                newPath = input.position.newPath,\n                oldLine = input.position.oldLine,\n                newLine = input.position.newLine,\n                source = CommentPositionSource.SERVER,\n                changeType = CommentPositionChangeType.UNKNOWN\n            )\n        } else null,\n        isDraft = false\n    )\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabCommitTransformer.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer\n\nimport net.ntworld.mergeRequest.Commit\nimport net.ntworld.mergeRequestIntegration.internal.CommitImpl\nimport net.ntworld.mergeRequestIntegration.provider.Transformer\nimport net.ntworld.mergeRequestIntegration.util.DateTimeUtil\nimport org.gitlab4j.api.models.Commit as CommitModel\n\nobject GitlabCommitTransformer :\n    Transformer<CommitModel, Commit> {\n    override fun transform(input: CommitModel): Commit = CommitImpl(\n        id = input.id,\n        message = input.message,\n        authorEmail = input.authorEmail,\n        authorName = input.authorName,\n        createdAt = DateTimeUtil.fromDate(input.createdAt)\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabDiffRefTransformer.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer\n\nimport net.ntworld.mergeRequest.DiffReference\nimport net.ntworld.mergeRequestIntegration.internal.DiffReferenceImpl\nimport net.ntworld.mergeRequestIntegration.provider.Transformer\nimport org.gitlab4j.api.models.DiffRef\n\nobject GitlabDiffRefTransformer:\n    Transformer<DiffRef, DiffReference> {\n    override fun transform(input: DiffRef): DiffReference = DiffReferenceImpl(\n        baseHash = input.baseSha,\n        headHash = input.headSha,\n        startHash = input.startSha\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabDiffTransformer.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer\n\nimport net.ntworld.mergeRequest.Change\nimport net.ntworld.mergeRequestIntegration.internal.ChangeImpl\nimport net.ntworld.mergeRequestIntegration.provider.Transformer\nimport org.gitlab4j.api.models.Diff\n\nobject GitlabDiffTransformer : Transformer<Diff, Change> {\n    override fun transform(input: Diff): Change = ChangeImpl(\n        oldPath = input.oldPath,\n        newPath = input.newPath,\n        aMode = input.aMode,\n        bMode = input.bMode,\n        newFile = input.newFile,\n        renamedFile = input.renamedFile,\n        deletedFile = input.deletedFile\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabDiscussionTransformer.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPositionChangeType\nimport net.ntworld.mergeRequest.CommentPositionSource\nimport net.ntworld.mergeRequest.UserStatus\nimport net.ntworld.mergeRequestIntegration.internal.CommentImpl\nimport net.ntworld.mergeRequestIntegration.internal.CommentPositionImpl\nimport net.ntworld.mergeRequestIntegration.internal.UserInfoImpl\nimport net.ntworld.mergeRequestIntegration.provider.Transformer\nimport net.ntworld.mergeRequestIntegration.util.DateTimeUtil\nimport org.gitlab4j.api.models.Discussion\nimport org.gitlab4j.api.models.Note\n\nobject GitlabDiscussionTransformer :\n    Transformer<Discussion, List<Comment>> {\n\n    override fun transform(input: Discussion): List<Comment> {\n        return input.notes.filter { !it.system }.map {\n            transformItem(input, it)\n        }\n    }\n\n    private fun transformItem(discussion: Discussion, input: Note) = CommentImpl(\n        id = input.id.toString(),\n        parentId = discussion.id,\n        replyId = input.id.toString(),\n        body = input.body,\n        author = UserInfoImpl(\n            id = input.author.id.toString(),\n            name = input.author.name,\n            username = input.author.username,\n            url = input.author.webUrl,\n            avatarUrl = input.author.avatarUrl,\n            status = UserStatus.ACTIVE\n        ),\n        createdAt = DateTimeUtil.fromDate(input.createdAt),\n        updatedAt = DateTimeUtil.fromDate(input.updatedAt),\n        resolvedBy = if (null !== input.resolvedBy) {\n            UserInfoImpl(\n                id = input.resolvedBy.id.toString(),\n                name = input.resolvedBy.name,\n                username = input.resolvedBy.username,\n                url = input.resolvedBy.webUrl,\n                avatarUrl = input.resolvedBy.avatarUrl,\n                status = UserStatus.ACTIVE\n            )\n        } else null,\n        resolved = if (null === input.resolved) false else input.resolved,\n        resolvable = input.resolvable,\n        position = if (null !== input.position) {\n            CommentPositionImpl(\n                startHash = input.position.startSha,\n                baseHash = input.position.baseSha,\n                headHash = input.position.headSha,\n                oldPath = input.position.oldPath,\n                newPath = input.position.newPath,\n                oldLine = input.position.oldLine,\n                newLine = input.position.newLine,\n                source = CommentPositionSource.SERVER,\n                changeType = CommentPositionChangeType.UNKNOWN\n            )\n        } else null,\n        isDraft = false\n    )\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabMRSimpleTransformer.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer\n\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.MergeRequestState\nimport net.ntworld.mergeRequestIntegration.internal.MergeRequestInfoImpl\nimport net.ntworld.mergeRequestIntegration.provider.Transformer\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.*\nimport net.ntworld.mergeRequestIntegration.util.DateTimeUtil\nimport org.gitlab4j.api.models.MergeRequest\n\nobject GitlabMRSimpleTransformer :\n    Transformer<MergeRequest, MergeRequestInfo> {\n    override fun transform(input: MergeRequest): MergeRequestInfo = MergeRequestInfoImpl(\n        id = input.iid.toString(),\n        provider = Gitlab.id,\n        projectId = input.projectId.toString(),\n        title = input.title,\n        description = input.description,\n        url = input.webUrl,\n        state = when (input.state) {\n            MERGE_REQUEST_STATE_OPENED -> MergeRequestState.OPENED\n            MERGE_REQUEST_STATE_MERGED -> MergeRequestState.MERGED\n            MERGE_REQUEST_STATE_CLOSED -> MergeRequestState.CLOSED\n            else -> MergeRequestState.CLOSED\n        },\n        createdAt = DateTimeUtil.fromDate(input.createdAt),\n        updatedAt = DateTimeUtil.fromDate(input.updatedAt)\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabMRTransformer.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer\n\nimport net.ntworld.mergeRequest.MergeRequest\nimport net.ntworld.mergeRequest.MergeRequestState\nimport net.ntworld.mergeRequestIntegration.internal.MergeRequestImpl\nimport net.ntworld.mergeRequestIntegration.provider.Transformer\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.*\nimport net.ntworld.mergeRequestIntegration.util.DateTimeUtil\nimport org.gitlab4j.api.models.MergeRequest as MergeRequestModel\n\nobject GitlabMRTransformer :\n    Transformer<MergeRequestModel, MergeRequest> {\n    override fun transform(input: MergeRequestModel): MergeRequest = MergeRequestImpl(\n        id = input.iid.toString(),\n        provider = Gitlab.id,\n        projectId = input.projectId.toString(),\n        title = input.title,\n        description = input.description,\n        url = input.webUrl,\n        state = when (input.state) {\n            MERGE_REQUEST_STATE_OPENED -> MergeRequestState.OPENED\n            MERGE_REQUEST_STATE_MERGED -> MergeRequestState.MERGED\n            MERGE_REQUEST_STATE_CLOSED -> MergeRequestState.CLOSED\n            else -> MergeRequestState.CLOSED\n        },\n        createdAt = DateTimeUtil.fromDate(input.createdAt),\n        updatedAt = DateTimeUtil.fromDate(input.updatedAt),\n        assignee = if (null !== input.assignee) GitlabMemberTransformer.transform(input.assignee) else null,\n        author = GitlabMemberTransformer.transform(input.author),\n        diffReference = GitlabDiffRefTransformer.transform(input.diffRefs),\n        sourceBranch = input.sourceBranch,\n        targetBranch = input.targetBranch,\n        upVotes = input.upvotes,\n        downVotes = input.downvotes,\n        commentsCount = input.userNotesCount,\n        isWorkInProgress = input.workInProgress,\n        canMerged = input.mergeStatus == MERGE_STATUS_CAN_BE_MERGED,\n        mergedBy = if (null !== input.mergedBy) GitlabMemberTransformer.transform(input.mergedBy) else null,\n        closedBy = if (null !== input.closedBy) GitlabMemberTransformer.transform(input.closedBy) else null,\n        mergedAt = if (null !== input.mergedAt) DateTimeUtil.fromDate(input.mergedAt) else null,\n        closedAt = if (null !== input.closedAt) DateTimeUtil.fromDate(input.closedAt) else null\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabMemberTransformer.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer\n\nimport net.ntworld.mergeRequest.UserInfo\nimport net.ntworld.mergeRequestIntegration.internal.UserInfoImpl\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabUtil\nimport net.ntworld.mergeRequestIntegration.provider.Transformer\nimport org.gitlab4j.api.models.AbstractUser\n\nobject GitlabMemberTransformer : Transformer<AbstractUser<*>, UserInfo> {\n    override fun transform(input: AbstractUser<*>): UserInfo = UserInfoImpl(\n        id = input.getId().toString(),\n        name = input.getName(),\n        username = input.getUsername(),\n        avatarUrl = input.getAvatarUrl(),\n        url = input.getWebUrl(),\n        status = GitlabUtil.findUserStatus(input.getState())\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabPipelineTransformer.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer\n\nimport net.ntworld.mergeRequest.Pipeline\nimport net.ntworld.mergeRequest.PipelineStatus\nimport net.ntworld.mergeRequestIntegration.internal.PipelineImpl\nimport net.ntworld.mergeRequestIntegration.provider.Transformer\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.*\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.model.PipelineModel\n\nobject GitlabPipelineTransformer :\n    Transformer<PipelineModel, Pipeline> {\n    override fun transform(input: PipelineModel): Pipeline = PipelineImpl(\n        id = input.id.toString(),\n        hash = input.sha,\n        ref = input.ref,\n        status = findStatus(input.status),\n        url = input.webUrl,\n        createdAt = input.createdAt,\n        updatedAt = input.updatedAt\n    )\n\n    private fun findStatus(status: String): PipelineStatus {\n        if (status == PIPELINE_STATUS_FAILED) {\n            return PipelineStatus.FAILED\n        }\n        if (status == PIPELINE_STATUS_PARTIAL_FAILED) {\n            return PipelineStatus.PARTIAL_FAILED\n        }\n        if (status == PIPELINE_STATUS_SUCCESS) {\n            return PipelineStatus.SUCCESS\n        }\n        if (status == PIPELINE_STATUS_RUNNING) {\n            return PipelineStatus.RUNNING\n        }\n        return PipelineStatus.UNKNOWN\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabProjectTransformer.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer\n\nimport net.ntworld.mergeRequest.Project\nimport net.ntworld.mergeRequest.ProjectVisibility\nimport net.ntworld.mergeRequestIntegration.internal.ProjectImpl\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.Gitlab\nimport net.ntworld.mergeRequestIntegration.provider.Transformer\nimport org.gitlab4j.api.models.Visibility\nimport org.gitlab4j.api.models.Project as GitlabProject\n\nobject GitlabProjectTransformer :\n    Transformer<GitlabProject, Project> {\n\n    override fun transform(input: GitlabProject): Project = ProjectImpl(\n        id = input.id.toString(),\n        provider = Gitlab,\n        name = input.name,\n        path = input.path,\n        url = input.webUrl,\n        visibility = if (input.visibility == Visibility.PUBLIC) ProjectVisibility.PUBLIC else ProjectVisibility.PRIVATE,\n        avatarUrl = input.avatarUrl ?: \"\",\n        repositoryHttpUrl = input.httpUrlToRepo,\n        repositorySshUrl = input.sshUrlToRepo\n    )\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabUserInfoTransformer.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer\n\nimport net.ntworld.mergeRequest.UserInfo\nimport net.ntworld.mergeRequestIntegration.internal.UserInfoImpl\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabUtil\nimport net.ntworld.mergeRequestIntegration.provider.Transformer\nimport org.gitlab4j.api.models.User as UserModel\n\nobject GitlabUserInfoTransformer:\n    Transformer<UserModel, UserInfo> {\n    override fun transform(input: UserModel): UserInfo = UserInfoImpl(\n        id = input.id.toString(),\n        name = input.name,\n        username = input.username,\n        avatarUrl = input.avatarUrl,\n        url = input.webUrl,\n        status = GitlabUtil.findUserStatus(input.state)\n    )\n\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/provider/gitlab/transformer/GitlabUserTransformer.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider.gitlab.transformer\n\nimport net.ntworld.mergeRequest.User\nimport net.ntworld.mergeRequestIntegration.internal.UserImpl\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabUtil\nimport net.ntworld.mergeRequestIntegration.provider.Transformer\nimport org.gitlab4j.api.models.User as UserModel\n\nobject GitlabUserTransformer:\n    Transformer<UserModel, User> {\n    override fun transform(input: UserModel): User = UserImpl(\n        id = input.id.toString(),\n        name = input.name,\n        username = input.username,\n        avatarUrl = input.avatarUrl,\n        url = input.webUrl,\n        status = GitlabUtil.findUserStatus(input.state),\n        email = input.email,\n        createdAt = input.createdAt.toString()\n    )\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/queryHandler/FindApprovalQueryHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.queryHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.QueryHandler\nimport net.ntworld.mergeRequest.query.FindApprovalQuery\nimport net.ntworld.mergeRequest.query.FindApprovalQueryResult\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\nimport net.ntworld.mergeRequestIntegration.make\n\n@Handler\nclass FindApprovalQueryHandler(\n    private val providerStorage: ProviderStorage\n) : QueryHandler<FindApprovalQuery, FindApprovalQueryResult> {\n    override fun handle(query: FindApprovalQuery): FindApprovalQueryResult {\n        val (data, api) = providerStorage.findOrFail(query.providerId)\n        return FindApprovalQueryResult.make(\n            approval = api.mergeRequest.findApproval(data.project.id, query.mergeRequestId)\n        )\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/queryHandler/FindMergeRequestQueryHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.queryHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.QueryHandler\nimport net.ntworld.mergeRequest.query.FindMergeRequestQuery\nimport net.ntworld.mergeRequest.query.FindMergeRequestQueryResult\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\nimport net.ntworld.mergeRequestIntegration.make\n\n@Handler\nclass FindMergeRequestQueryHandler(\n    private val providerStorage: ProviderStorage\n): QueryHandler<FindMergeRequestQuery, FindMergeRequestQueryResult> {\n\n    override fun handle(query: FindMergeRequestQuery): FindMergeRequestQueryResult {\n        val (data, api) = providerStorage.findOrFail(query.providerId)\n        return FindMergeRequestQueryResult.make(\n            mergeRequest = api.mergeRequest.findOrFail(data.project.id, query.mergeRequestId)\n        )\n    }\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/queryHandler/GetCommentsQueryHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.queryHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.QueryHandler\nimport net.ntworld.mergeRequest.query.GetCommentsQuery\nimport net.ntworld.mergeRequest.query.GetCommentsQueryResult\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\nimport net.ntworld.mergeRequestIntegration.make\n\n@Handler\nclass GetCommentsQueryHandler(\n    private val providerStorage: ProviderStorage\n) : QueryHandler<GetCommentsQuery, GetCommentsQueryResult> {\n\n    override fun handle(query: GetCommentsQuery): GetCommentsQueryResult {\n        val (data, api) = providerStorage.findOrFail(query.providerId)\n        return GetCommentsQueryResult.make(\n            comments = api.comment.getAll(data.project, query.mergeRequestId)\n        )\n    }\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/queryHandler/GetCommitsQueryHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.queryHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.QueryHandler\nimport net.ntworld.mergeRequest.query.GetCommitsQuery\nimport net.ntworld.mergeRequest.query.GetCommitsQueryResult\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\nimport net.ntworld.mergeRequestIntegration.make\n\n@Handler\nclass GetCommitsQueryHandler(\n    private val providerStorage: ProviderStorage\n) : QueryHandler<GetCommitsQuery, GetCommitsQueryResult> {\n\n    override fun handle(query: GetCommitsQuery): GetCommitsQueryResult {\n        val (data, api) = providerStorage.findOrFail(query.providerId)\n        return GetCommitsQueryResult.make(\n            commits = api.mergeRequest.getCommits(data.project.id, query.mergeRequestId)\n        )\n    }\n\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/queryHandler/GetMergeRequestsQueryHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.queryHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.QueryHandler\nimport net.ntworld.mergeRequest.query.GetMergeRequestsQuery\nimport net.ntworld.mergeRequest.query.GetMergeRequestsQueryResult\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\nimport net.ntworld.mergeRequestIntegration.make\n\n@Handler\nclass GetMergeRequestsQueryHandler(\n    private val providerStorage: ProviderStorage\n) : QueryHandler<GetMergeRequestsQuery, GetMergeRequestsQueryResult> {\n\n    override fun handle(query: GetMergeRequestsQuery): GetMergeRequestsQueryResult {\n        val (data, api) = providerStorage.findOrFail(query.providerId)\n        val result = api.mergeRequest.search(\n            projectId = data.project.id,\n            currentUserId = data.currentUser.id,\n            filterBy = query.filterBy,\n            orderBy = query.orderBy,\n            page = query.page,\n            itemsPerPage = query.itemsPerPage\n        )\n        return GetMergeRequestsQueryResult.make(\n            mergeRequests = result.data,\n            totalPages = result.totalPages,\n            totalItems = result.totalItems,\n            currentPage = result.currentPage\n        )\n    }\n\n}\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/queryHandler/GetPipelinesQueryHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.queryHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.QueryHandler\nimport net.ntworld.mergeRequest.query.GetPipelinesQuery\nimport net.ntworld.mergeRequest.query.GetPipelinesQueryResult\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\nimport net.ntworld.mergeRequestIntegration.make\n\n@Handler\nclass GetPipelinesQueryHandler(\n    private val providerStorage: ProviderStorage\n) : QueryHandler<GetPipelinesQuery, GetPipelinesQueryResult> {\n\n    override fun handle(query: GetPipelinesQuery): GetPipelinesQueryResult {\n        val (data, api) = providerStorage.findOrFail(query.providerId)\n        return GetPipelinesQueryResult.make(\n            pipelines = api.mergeRequest.getPipelines(data.project.id, query.mergeRequestId)\n        )\n    }\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/queryHandler/GetProjectMembersQueryHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.queryHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.cqrs.QueryHandler\nimport net.ntworld.mergeRequest.query.GetProjectMembersQuery\nimport net.ntworld.mergeRequest.query.GetProjectMembersQueryResult\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\nimport net.ntworld.mergeRequestIntegration.make\n\n@Handler\nclass GetProjectMembersQueryHandler(\n    private val providerStorage: ProviderStorage\n) : QueryHandler<GetProjectMembersQuery, GetProjectMembersQueryResult> {\n\n    override fun handle(query: GetProjectMembersQuery): GetProjectMembersQueryResult {\n        val (data, api) = providerStorage.findOrFail(query.providerId)\n        return GetProjectMembersQueryResult.make(api.project.getMembers(data.project.id))\n    }\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/requestHandler/CreateCommentRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequest.request.CreateCommentRequest\nimport net.ntworld.mergeRequest.response.CreateCommentResponse\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegration.provider.ProviderException\n\n@Handler\nclass CreateCommentRequestHandler(\n    private val providerStorage: ProviderStorage\n) : RequestHandler<CreateCommentRequest, CreateCommentResponse> {\n\n    override fun handle(request: CreateCommentRequest): CreateCommentResponse {\n        val (data, api) = providerStorage.findOrFail(request.providerId)\n        return try {\n            val createdCommentId = api.comment.create(\n                project = data.project,\n                mergeRequestId = request.mergeRequestId,\n                body = request.body,\n                position = request.position,\n                isDraft = request.isDraft\n            )\n            CreateCommentResponse.make(error = null, createdCommentId = createdCommentId)\n        } catch (exception: ProviderException) {\n            CreateCommentResponse.make(error = exception.error, createdCommentId = null)\n        }\n    }\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/requestHandler/PublishAllCommentsRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequest.request.PublishAllCommentsRequest\nimport net.ntworld.mergeRequest.response.PublishAllCommentsResponse\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegration.provider.ProviderException\n\n@Handler\nclass PublishAllCommentsRequestHandler(\n    private val providerStorage: ProviderStorage\n): RequestHandler<PublishAllCommentsRequest, PublishAllCommentsResponse> {\n\n    override fun handle(request: PublishAllCommentsRequest): PublishAllCommentsResponse {\n        val (data, api) = providerStorage.findOrFail(request.providerId)\n        return try {\n            api.comment.publishAllDraftComments(\n                project = data.project,\n                mergeRequestId = request.mergeRequestId\n            )\n            PublishAllCommentsResponse.make(error = null, success = true)\n        } catch (exception: ProviderException) {\n            PublishAllCommentsResponse.make(error = exception.error, success = false)\n        }\n    }\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/requestHandler/PublishCommentsRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequest.request.PublishCommentsRequest\nimport net.ntworld.mergeRequest.response.PublishCommentsResponse\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegration.provider.ProviderException\n\n@Handler\nclass PublishCommentsRequestHandler(\n    private val providerStorage: ProviderStorage\n): RequestHandler<PublishCommentsRequest, PublishCommentsResponse> {\n\n    override fun handle(request: PublishCommentsRequest): PublishCommentsResponse {\n        val (data, api) = providerStorage.findOrFail(request.providerId)\n        return try {\n            api.comment.publishDraftComments(\n                project = data.project,\n                mergeRequestId = request.mergeRequestId,\n                commentIds = request.draftCommentIds\n            )\n            PublishCommentsResponse.make(error = null, success = true)\n        } catch (exception: ProviderException) {\n            PublishCommentsResponse.make(error = exception.error, success = false)\n        }\n    }\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/requestHandler/ReplyCommentRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequest.request.ReplyCommentRequest\nimport net.ntworld.mergeRequest.response.ReplyCommentResponse\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegration.provider.ProviderException\n\n@Handler\nclass ReplyCommentRequestHandler(\n    private val providerStorage: ProviderStorage\n) : RequestHandler<ReplyCommentRequest, ReplyCommentResponse> {\n\n    override fun handle(request: ReplyCommentRequest): ReplyCommentResponse {\n        val (data, api) = providerStorage.findOrFail(request.providerId)\n        return try {\n            val createdCommentId = api.comment.reply(\n                project = data.project,\n                mergeRequestId = request.mergeRequestId,\n                repliedComment = request.repliedComment,\n                body = request.body\n            )\n            ReplyCommentResponse.make(error = null, createdCommentId = createdCommentId)\n        } catch (exception: ProviderException) {\n            ReplyCommentResponse.make(error = exception.error, createdCommentId = null)\n        }\n    }\n\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/requestHandler/UpdateCommentRequestHandler.kt",
    "content": "package net.ntworld.mergeRequestIntegration.requestHandler\n\nimport net.ntworld.foundation.Handler\nimport net.ntworld.foundation.RequestHandler\nimport net.ntworld.mergeRequest.request.UpdateCommentRequest\nimport net.ntworld.mergeRequest.response.UpdateCommentResponse\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegration.provider.ProviderException\n\n@Handler\nclass UpdateCommentRequestHandler(\n    private val providerStorage: ProviderStorage\n): RequestHandler<UpdateCommentRequest, UpdateCommentResponse> {\n    override fun handle(request: UpdateCommentRequest): UpdateCommentResponse {\n        val (data, api) = providerStorage.findOrFail(request.providerId)\n        return try {\n            api.comment.update(\n                project = data.project,\n                mergeRequestId = request.mergeRequestId,\n                comment = request.comment,\n                body = request.body\n            )\n            UpdateCommentResponse.make(error = null, commentId = request.comment.id)\n        } catch (exception: ProviderException) {\n            UpdateCommentResponse.make(error = exception.error, commentId = null)\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/update/UpdateManager.kt",
    "content": "package net.ntworld.mergeRequestIntegration.update\n\nimport com.github.kittinunf.fuel.core.FuelManager\nimport com.github.kittinunf.fuel.core.ResponseResultOf\nimport com.github.kittinunf.result.Result\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.JsonConfiguration\nimport kotlinx.serialization.list\nimport java.util.*\n\nobject UpdateManager {\n    private const val CURRENT_VERSION = \"2020.1.2\"\n    private const val METADATA_URL = \"https://nhat-phan.github.io/updates/merge-request-integration/metadata.json\"\n    private const val CHECK_INTERVAL = 3600000 // Every 1 hour\n\n    private val myJson: Json = Json(JsonConfiguration.Stable.copy(strictMode = false))\n    private var myLastCheckDate : Date? = null\n\n    fun shouldGetAvailableUpdates(): Boolean {\n        val lastCheck = myLastCheckDate\n        if (null === lastCheck) {\n            return true\n        }\n        val difference = Date().time - lastCheck.time\n        return difference > CHECK_INTERVAL\n    }\n\n    private fun makeGetRequest(url: String): ResponseResultOf<String> {\n        return FuelManager().get(url).responseString()\n    }\n\n    fun getAvailableUpdates(): List<String> {\n        try {\n            val (_, _, result) = makeGetRequest(METADATA_URL)\n            return when (result) {\n                is Result.Success -> {\n                    myLastCheckDate = Date()\n                    buildAvailableUpdates(result.value)\n                }\n                is Result.Failure -> {\n                    myLastCheckDate = Date()\n                    listOf()\n                }\n            }\n        } catch (exception: Exception) {\n            myLastCheckDate = Date()\n            return listOf()\n        }\n    }\n\n    private fun buildAvailableUpdates(input: String): List<String> {\n        val metadata = myJson.parse(UpdateMetadata.serializer().list, input).sortedBy { it.id }\n        val currentVersion = metadata.firstOrNull { it.version == CURRENT_VERSION }\n        if (null === currentVersion) {\n            return listOf()\n        }\n        val updates = metadata.filter { it.id > currentVersion.id && it.active }\n        return updates.map {\n            try {\n                val (_, _, result) = makeGetRequest(it.changesUrl)\n                when (result) {\n                    is Result.Success -> {\n                        result.value\n                            .replace(\"<html lang=\\\"en\\\">\", \"<h2>${it.version}</h2>\")\n                            .replace(\"</html>\", \"\")\n                    }\n                    is Result.Failure -> {\n                        \"\"\n                    }\n                }\n            } catch (exception: Exception) {\n                \"\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/update/UpdateMetadata.kt",
    "content": "package net.ntworld.mergeRequestIntegration.update\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class UpdateMetadata(\n    val id: Int,\n    val version: String,\n    val changesUrl: String,\n    val active: Boolean\n)\n"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/util/DateTimeUtil.kt",
    "content": "package net.ntworld.mergeRequestIntegration.util\n\nimport net.ntworld.mergeRequest.DateTime\nimport org.joda.time.DateTimeZone\nimport org.joda.time.format.ISODateTimeFormat\nimport org.ocpsoft.prettytime.PrettyTime\nimport java.text.SimpleDateFormat\nimport java.util.*\n\nobject DateTimeUtil {\n    private val timezone = TimeZone.getTimeZone(\"UTC\")\n    private val toStringDateFormat by lazy {\n        val df = SimpleDateFormat(\"yyyy-MM-dd HH:mm\")\n        df.timeZone = timezone\n        df\n    }\n    private val convertDateFormat by lazy {\n        val df = SimpleDateFormat(\"yyyy-MM-dd'T'HH:mm:ss'Z'\")\n        df.timeZone = timezone\n        df\n    }\n    private val parser = ISODateTimeFormat.dateTimeParser()\n    private val localTimeZone = DateTimeZone.getDefault()\n    private val prettyTime = PrettyTime()\n\n    @Synchronized\n    fun fromDate(date: Date): DateTime = convertDateFormat.format(date)\n\n    @Synchronized\n    fun toDate(datetime: DateTime): Date {\n        return parser.parseDateTime(datetime).withZone(localTimeZone).toDate()\n    }\n\n    @Synchronized\n    fun formatDate(date: Date): String = toStringDateFormat.format(date) ?: \"\"\n\n    @Synchronized\n    fun toPretty(date: Date): String {\n        return prettyTime.format(date)\n    }\n\n    @Synchronized\n    fun toPretty(datetime: DateTime): String {\n        return prettyTime.format(toDate(datetime))\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/main/kotlin/net/ntworld/mergeRequestIntegration/util/SavedFiltersUtil.kt",
    "content": "package net.ntworld.mergeRequestIntegration.util\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.JsonConfiguration\nimport net.ntworld.mergeRequest.MergeRequestState\nimport net.ntworld.mergeRequest.api.MergeRequestOrdering\nimport net.ntworld.mergeRequest.query.GetMergeRequestFilter\nimport net.ntworld.mergeRequest.query.generated.GetMergeRequestFilterImpl\n\nclass SavedFiltersUtil {\n    @Serializable\n    private class SavedFilters constructor(\n        val state: String,\n        val search: String,\n        val authorId: String,\n        val assigneeId: String,\n        val approverIds: List<String>,\n        val ordering: String\n    ) {\n        fun toPair(): Pair<GetMergeRequestFilter, MergeRequestOrdering> {\n            return Pair(\n                GetMergeRequestFilterImpl(\n                    id = null,\n                    state = when (state) {\n                        \"all\" -> MergeRequestState.ALL\n                        \"opened\" -> MergeRequestState.OPENED\n                        \"closed\" -> MergeRequestState.CLOSED\n                        \"merged\" -> MergeRequestState.MERGED\n                        else -> MergeRequestState.OPENED\n                    },\n                    search = search,\n                    authorId = authorId,\n                    assigneeId = assigneeId,\n                    approverIds = approverIds,\n                    sourceBranch = \"\"\n                ),\n                when (ordering) {\n                    \"recently-updated\" -> MergeRequestOrdering.RECENTLY_UPDATED\n                    \"newest\" -> MergeRequestOrdering.NEWEST\n                    \"oldest\" -> MergeRequestOrdering.OLDEST\n                    else -> MergeRequestOrdering.RECENTLY_UPDATED\n                }\n            )\n        }\n    }\n\n    companion object {\n        @JvmStatic\n        private val json = Json(JsonConfiguration.Stable.copy(strictMode = false))\n\n        @JvmStatic\n        fun stringify(filter: GetMergeRequestFilter, ordering: MergeRequestOrdering): String {\n            val data = SavedFilters(\n                state = when (filter.state) {\n                    MergeRequestState.ALL -> \"all\"\n                    MergeRequestState.OPENED -> \"opened\"\n                    MergeRequestState.CLOSED -> \"closed\"\n                    MergeRequestState.MERGED -> \"merged\"\n                },\n                search = filter.search,\n                authorId = filter.authorId,\n                assigneeId = filter.assigneeId,\n                approverIds = filter.approverIds,\n                ordering = when (ordering) {\n                    MergeRequestOrdering.RECENTLY_UPDATED -> \"recently-updated\"\n                    MergeRequestOrdering.NEWEST -> \"newest\"\n                    MergeRequestOrdering.OLDEST -> \"oldest\"\n                }\n            )\n            return json.stringify(SavedFilters.serializer(), data)\n        }\n\n        @JvmStatic\n        fun parse(input: String) : Pair<GetMergeRequestFilter, MergeRequestOrdering>? {\n            return try {\n                val data = json.parse(SavedFilters.serializer(), input)\n                data.toPair()\n            } catch (exception: Exception) {\n                null\n            }\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration/src/test/kotlin/net/ntworld/mergeRequestIntegration/provider/MemoryCacheTests.kt",
    "content": "package net.ntworld.mergeRequestIntegration.provider\n\nimport io.kotlintest.specs.DescribeSpec\nimport io.mockk.every\nimport io.mockk.spyk\nimport net.ntworld.mergeRequest.api.Cache\nimport net.ntworld.mergeRequest.api.CacheNotFoundException\nimport net.ntworld.mergeRequestIntegration.exception.InvalidCacheKeyException\nimport net.ntworld.mergeRequestIntegration.exception.InvalidTTLException\nimport org.joda.time.DateTime\nimport kotlin.test.assertEquals\nimport kotlin.test.assertFailsWith\nimport kotlin.test.assertFalse\nimport kotlin.test.assertTrue\n\nclass MemoryCacheTests : DescribeSpec({\n    fun makeMemoryCache(defaultTTL: Int? = null) = MemoryCache(ttl = defaultTTL)\n    fun makeMemoryCacheSpy() = spyk(MemoryCache())\n\n    describe(\".defaultTTL\") {\n        context(\"use default value\") {\n            it(\"return 60 seconds\") {\n                val cache = makeMemoryCache()\n\n                val defaultTimeToLive = cache.defaultTTL\n\n                assertEquals(60000, defaultTimeToLive)\n            }\n        }\n\n        context(\"use constructable valid value\") {\n            it(\"return given value\") {\n                val cache = makeMemoryCache(1)\n\n                val defaultTimeToLive = cache.defaultTTL\n\n                assertEquals(1, defaultTimeToLive)\n            }\n        }\n\n        context(\"use constructable invalid value\") {\n            it(\"throws InvalidTTLException\") {\n                assertFailsWith<InvalidTTLException> {\n                    makeMemoryCache(0)\n                }\n            }\n        }\n    }\n\n    describe(\".get()\") {\n        context(\"invalid key\") {\n            it(\"throws InvalidCacheKeyException with empty key\") {\n                val cache = makeMemoryCache()\n\n                assertFailsWith<InvalidCacheKeyException> {\n                    cache.get<Any>(\"\")\n                }\n            }\n\n            it(\"throws InvalidCacheKeyException with key contains all space characters\") {\n                val cache = makeMemoryCache()\n\n                assertFailsWith<InvalidCacheKeyException> {\n                    cache.get<Any>(\" \")\n                }\n            }\n        }\n\n        context(\"does not exists\") {\n            it(\"throws CacheNotFoundException\") {\n                val cache = makeMemoryCache()\n\n                assertFailsWith<CacheNotFoundException> {\n                    cache.get(\"key-not-found\")\n                }\n            }\n        }\n\n        context(\"exists, still alive\") {\n            it(\"returns set value\") {\n                val cache = makeMemoryCache()\n                cache.set(\"abcd\", \"value\")\n\n                val result: String = cache.get(\"abcd\")\n\n                assertEquals(\"value\", result)\n            }\n        }\n\n        context(\"exists, expired\") {\n            it(\"throws CacheNotFoundException\") {\n                val cache = makeMemoryCacheSpy()\n                every { cache.isExpired(any()) } returns true\n                cache.set(\"abc\", \"\")\n\n                assertFailsWith<CacheNotFoundException> {\n                    cache.get(\"abc\")\n                }\n            }\n        }\n    }\n\n    describe(\".has()\") {\n        context(\"invalid key\") {\n            it(\"throws InvalidCacheKeyException with empty key\") {\n                val cache = makeMemoryCache()\n\n                assertFailsWith<InvalidCacheKeyException> {\n                    cache.has(\"\")\n                }\n            }\n\n            it(\"throws InvalidCacheKeyException with key contains all space characters\") {\n                val cache = makeMemoryCache()\n\n                assertFailsWith<InvalidCacheKeyException> {\n                    cache.has(\" \")\n                }\n            }\n        }\n\n        context(\"does not exists\") {\n            it(\"returns false\") {\n                val cache = makeMemoryCache()\n\n                val result = cache.has(\"key-not-found\")\n\n                assertFalse(result)\n            }\n        }\n\n        context(\"key exists, still alive\") {\n            it(\"returns true\") {\n                val cache = makeMemoryCache()\n                cache.set(\"abc\", \"\")\n\n                val result = cache.has(\"abc\")\n\n                assertTrue(result)\n            }\n        }\n\n        context(\"key exists, expired\") {\n            it(\"returns false\") {\n                val cache = makeMemoryCacheSpy()\n                every { cache.isExpired(any()) } returns true\n                cache.set(\"abc\", \"\")\n\n                val result = cache.has(\"abc\")\n\n                assertFalse(result)\n            }\n        }\n    }\n\n    describe(\".remove()\") {\n        context(\"invalid key\") {\n            it(\"throws InvalidCacheKeyException with empty key\") {\n                val cache = makeMemoryCache()\n\n                assertFailsWith<InvalidCacheKeyException> {\n                    cache.remove(\"\")\n                }\n            }\n\n            it(\"throws InvalidCacheKeyException with key contains all space characters\") {\n                val cache = makeMemoryCache()\n\n                assertFailsWith<InvalidCacheKeyException> {\n                    cache.remove(\" \")\n                }\n            }\n        }\n\n        context(\"valid key\") {\n            it(\"removes given key\") {\n                val cache = makeMemoryCache()\n                cache.set(\"abc\", \"\")\n\n                val beforeRemoving = cache.has(\"abc\")\n                cache.remove(\"abc\")\n                val afterRemoving = cache.has(\"abc\")\n\n                assertTrue(beforeRemoving)\n                assertFalse(afterRemoving)\n            }\n        }\n    }\n\n    describe(\".isExpiredAfter()\") {\n        context(\"invalid key\") {\n            it(\"throws InvalidCacheKeyException with empty key\") {\n                val cache = makeMemoryCache()\n\n                assertFailsWith<InvalidCacheKeyException> {\n                    cache.isExpiredAfter(\"\", DateTime.now())\n                }\n            }\n\n            it(\"throws InvalidCacheKeyException with key contains all space characters\") {\n                val cache = makeMemoryCache()\n\n                assertFailsWith<InvalidCacheKeyException> {\n                    cache.isExpiredAfter(\" \", DateTime.now())\n                }\n            }\n        }\n\n        context(\"key not found\") {\n            it(\"returns true\") {\n                val cache = makeMemoryCache()\n\n                assertFailsWith<CacheNotFoundException> {\n                    cache.isExpiredAfter(\"abc\", DateTime.now())\n                }\n            }\n        }\n\n        context(\"key found\") {\n            it(\"returns false if not expired yet\") {\n                val cache = makeMemoryCache()\n                cache.set(\"abc\", \"value\")\n\n                val result = cache.isExpiredAfter(\"abc\", DateTime.now())\n\n                assertFalse(result)\n            }\n\n            it(\"returns true if the key already expired\") {\n                val cache = makeMemoryCache()\n                cache.set(\"abc\", \"value\")\n\n                val result = cache.isExpiredAfter(\"abc\", DateTime.now().plus(86400))\n\n                assertTrue(result)\n            }\n        }\n    }\n})\n"
  },
  {
    "path": "merge-request-integration/src/test/resources/update-metadata.json",
    "content": "[\n  {\n    \"id\": 1,\n    \"version\": \"2019.3.0\",\n    \"changesUrl\": \"https://nhat-phan.github.io/updates/merge-request-integration/changes/2019.3.0.html\",\n    \"active\": true\n  },\n  {\n    \"id\": 2,\n    \"version\": \"2019.3.1\",\n    \"changesUrl\": \"https://nhat-phan.github.io/updates/merge-request-integration/changes/2019.3.1.html\",\n    \"active\": false\n  }\n]"
  },
  {
    "path": "merge-request-integration-ce/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.tasks.KotlinCompile\n\nval artifactGroup: String by project\nval targetIDEVersion: String by project\nval communityEditionVersion: String by project\nval intellijVersion: String by project\nval jvmTarget: String by project\nval foundationVersion: String by project\nval gitlab4jVersion: String by project\nval githubApiVersion: String by project\nval prettyTimeVersion: String by project\nval commonmarkVersion: String by project\nval intellijSinceBuild: String by project\nval intellijUntilBuild: String by project\nval eapRelease: String by project\n\ngroup = artifactGroup\nversion = if (eapRelease == \"false\") {\n    \"$communityEditionVersion-built-for-ide-$targetIDEVersion\"\n} else {\n    \"$communityEditionVersion-eap-$eapRelease-for-ide-$targetIDEVersion\"\n}\n\nrepositories {\n    jcenter()\n    mavenCentral()\n    maven(\"https://jitpack.io\")\n}\n\ndependencies {\n    implementation(\"org.jetbrains.kotlin:kotlin-stdlib\")\n    implementation(\"com.github.nhat-phan.foundation:foundation-jvm:$foundationVersion\")\n    implementation(\"org.gitlab4j:gitlab4j-api:$gitlab4jVersion\")\n    implementation(\"org.kohsuke:github-api:$githubApiVersion\")\n    implementation(\"org.ocpsoft.prettytime:prettytime:$prettyTimeVersion\")\n    compile(\"com.atlassian.commonmark:commonmark:$commonmarkVersion\")\n\n    implementation(project(\":contracts\"))\n    implementation(project(\":merge-request-integration-core\"))\n    implementation(project(\":merge-request-integration\"))\n}\n\n// See https://github.com/JetBrains/gradle-intellij-plugin/\nintellij {\n    version = intellijVersion\n    updateSinceUntilBuild = true\n    setPlugins(\"git4idea\")\n}\n\nval compileKotlin: KotlinCompile by tasks\nval compileTestKotlin: KotlinCompile by tasks\n\ncompileKotlin.kotlinOptions {\n    jvmTarget = jvmTarget\n}\n\ncompileTestKotlin.kotlinOptions {\n    jvmTarget = jvmTarget\n}\n\ntasks {\n    named<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>(\"compileKotlin\") {\n        kotlinOptions {\n            jvmTarget = jvmTarget\n        }\n    }\n\n    named<org.jetbrains.intellij.tasks.PatchPluginXmlTask>(\"patchPluginXml\") {\n        val version = if (!communityEditionVersion.endsWith(\"eap\"))\n            communityEditionVersion else communityEditionVersion.substring(0, communityEditionVersion.length - 3)\n        changeNotes(htmlFixer(\"./merge-request-integration-ce/doc/release-notes.$version.html\"))\n        pluginDescription(htmlFixer(\"./merge-request-integration-ce/doc/description.html\"))\n        sinceBuild(intellijSinceBuild)\n        untilBuild(intellijUntilBuild)\n    }\n}\n\nfun htmlFixer(filename: String): String {\n    if (!File(filename).exists()) {\n        throw Exception(\"File $filename not found.\")\n    }\n    return File(filename).readText().replace(\"<html lang=\\\"en\\\">\", \"\").replace(\"</html>\", \"\")\n}"
  },
  {
    "path": "merge-request-integration-ce/doc/description.html",
    "content": "<html lang=\"en\">\n\n<p>Merge Request Integration CE is an open-source plugin for JetBrains IDEs which helps you</p>\n<ul>\n    <li>Do code review right in your IDE.</li>\n    <li>Address review comments from your colleagues.</li>\n</ul>\n<br />\n<p>What you can do:</p>\n\n<ul>\n    <li>Check approval statuses of merge requests which are waiting for your approval.</li>\n    <li>Filter Merge Requests which are assigned to you, belongs to your colleagues, etc</li>\n    <li>Check pipeline status and approval status.</li>\n    <li>Do code review, navigate code with Diff View right in your IDE.</li>\n    <li>Address review comments, navigate comments in editors.</li>\n    <li>Add and reply a comment</li>\n    <li>Approve/revoke your approval</li>\n    <li>More and more features will be coming soon :)</li>\n</ul>\n\n<br />\n<p>Currently the plugin supports GitLab only (gitlab cloud and self-hosted).</p>\n\n<h3>How to setup Gitlab connection</h3>\n\n<p>You need a personal api token. To get the token please follow these steps:</p>\n\n<ul>\n    <li>Log in to your Gitlab site</li>\n    <li>Go to Settings > Access Token and create a personal access token</li>\n    <li>Go to your IDE preferences, Merge Request Integration > Gitlab</li>\n    <li>Fill data, then save and click refresh button of Merge Request Integration CE window</li>\n</ul>\n\n<h3>License</h3>\n\n<p>\nThe plugin is an open source released under Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International\nlicense.\n</p>\n<br />\n<p>It's totally free if you are using it for public repositories on gitlab.com.</p>\n<br />\n<p>For private repositories, this plugin is a trial. How long is the trial period? Equal to WINRAR's trial period 🙈</p>\n<br />\n<p>\n    Community Edition (CE) is exactly the same as Enterprise Edition (EE).\n    You don't need to hack or find a cracked version.\n    Cracking software invites virus to your computer.\n</p>\n\n<h3>About me</h3>\n\n<p>\n    My name is Nhat, I'm a software developer at\n    <a href=\"https://personio.com/\">Personio</a>\n    (yes, <a href=\"https://www.personio.com/about-personio/jobs/\">we are hiring</a>\n    all around the world, relocation to Munich is of course possible).\n</p>\n\n<h3>Sponsor</h3>\n\n<p>If you love this plugin, please support me by:</p>\n\n<ul>\n    <li>Buy an <a href=\"https://plugins.jetbrains.com/plugin/13615-merge-request-integration-ee--code-review-for-gitlab/\">Enterprise Edition</a>, only 1$/month</li>\n    <li>\n        Buy me a beer via\n        <a href=\"https://paypal.me/phanhoangnhat\">Paypal</a>\n        or\n        <a href=\"https://www.patreon.com/nhat/creators\">Patreon</a>.\n    </li>\n</ul>\n\n<br />\n<p>Thanks in advance!</p>\n\n<h3>Attribution</h3>\n\n<ul>\n    <li>\n        Icons by\n        <a href=\"https://fontawesome.com/\">Font Awesome</a>\n        are licensed under\n        <a href=\"https://creativecommons.org/licenses/by/4.0/\">CC BY 4.0</a>\n    </li>\n</ul>\n</html>"
  },
  {
    "path": "merge-request-integration-ce/doc/release-notes.2019.3.0.html",
    "content": "<html lang=\"en\">\n    <ul>\n        <li>Initial release :)</li>\n    </ul>\n</html>"
  },
  {
    "path": "merge-request-integration-ce/doc/release-notes.2019.3.1.html",
    "content": "<html lang=\"en\">\n    <ul>\n        <li>Update Enterprise Edition url</li>\n    </ul>\n</html>"
  },
  {
    "path": "merge-request-integration-ce/doc/release-notes.2019.3.2.html",
    "content": "<html lang=\"en\">\n    <ul>\n        <li>Introduce commit list and changes tree</li>\n        <li>Resolve and delete comments</li>\n        <li>Display comments on editors</li>\n        <li>Bug fixes</li>\n    </ul>\n</html>"
  },
  {
    "path": "merge-request-integration-ce/doc/release-notes.2019.3.3.html",
    "content": "<html lang=\"en\">\n    <h3>Notable changes</h3>\n    <ul>\n        <li>Improved style of list</li>\n        <li>Improved approval panel which only opens if there is information to be displayed</li>\n        <li>Replaced deprecated api</li>\n        <li>Fixed bug #4: Cannot leave a comment in unified view or on Windows</li>\n        <li>Fixed bug #5: Cannot open comment editor if IDE does not support Markdown</li>\n        <li>Fixed bug: Token on Windows cannot be saved</li>\n        <li>Fixed grammatical error on GitLab configuration panel</li>\n    </ul>\n\n    <h3>Acknowledgement</h3>\n    <ul>\n        <li>Thank <a href=\"https://github.com/Savak\">@Savak</a>, <a href=\"https://github.com/SToto98\">@SToto98</a> for reporting bug #4</li>\n        <li>Thank <a href=\"https://github.com/smallcreep\">@smallcreep</a> for reporting bug #5</li>\n        <li>Thank <a href=\"https://plugins.jetbrains.com/author/7857882b-94e6-4d52-a3fd-801afb8ca0e2\">Artem Kuchyn</a> for reporting \"Token on Windows cannot be saved\" bug.</li>\n        <li>Thank Jay for correcting grammar.</li>\n    </ul>\n</html>"
  },
  {
    "path": "merge-request-integration-ce/doc/release-notes.2019.3.4.html",
    "content": "<html lang=\"en\">\n    <h3>Notable changes</h3>\n    <ul>\n        <li>Supported self-signed certificate</li>\n        <li>Supported cache option for finding merge request to improve performance</li>\n        <li>New implementation of configuration panel</li>\n        <li>Fixed #1: Support multi repositories project</li>\n        <li>Fixed #9: Connection list cannot be saved</li>\n        <li>Fixed #16: Pipeline is not displayed on Gitlab v12.2.4</li>\n        <li>Fixed bug: Empty description string on some actions</li>\n        <li>Fixed bug: UpdateManager requires weird format of metadata</li>\n    </ul>\n\n    <h3>Acknowledgement</h3>\n    <ul>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/answer-huang\">answer-huang</a>\n            for contributing to source code, the commit\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/commit/e0bdc259785fad3f11ecde1849acf32a36dc4281\">e0bdc25</a>,\n            Pull Request\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/pull/11\">#11</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/turnik\">turnik</a>\n            for contributing to source code, the commit\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/commit/9e4f7b6824963c46af1fee595de53d6363dac9f2\">9e4f7b6</a>,\n            Pull Request\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/pull/18\">#18</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/Marthym\">Marthym</a>\n            for reporting bug\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/1\">#1</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/mmjurov\">mmjurov</a>\n            for reporting bug\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/9\">#9</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/vdsirotkin\">vdsirotkin</a>\n            for reporting bug\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/16\">#16</a>\n        </li>\n    </ul>\n</html>"
  },
  {
    "path": "merge-request-integration-ce/doc/release-notes.2019.3.5.html",
    "content": "<html lang=\"en\">\n    <h3>Notable changes</h3>\n    <ul>\n        <li>Hot-fix #21: Gitlab configuration page is empty for new users who have no connections data</li>\n    </ul>\n\n    <h3>Acknowledgement</h3>\n    <ul>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/Tillerino\">Tillerino</a>,\n            <a target=\"_blank\" href=\"https://github.com/Alzhurav\">Alzhurav</a>,\n            <a target=\"_blank\" href=\"https://github.com/dylanfontaine\">dylanfontaine</a>,\n            <a target=\"_blank\" href=\"https://github.com/evangalen\">evangalen</a>,\n            <a target=\"_blank\" href=\"https://plugins.jetbrains.com/author/894191e2-5627-4e8a-a5ac-ee79b15f955a\">Ahsan Javed</a>\n            for reporting bug\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/21\">#21</a>\n        </li>\n    </ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ce/doc/release-notes.2020.1.0.html",
    "content": "<html lang=\"en\">\n    <h3>Notable changes</h3>\n    <ul>\n        <ul>\n            <li>Feature: introduce create-general-comment button</li>\n            <li>Option: allow doing code review without checking out the target branch</li>\n            <li>Option: open diff changes if the number of changes is less than configurable value</li>\n            <li>Option: keep selected values of filters and order state in Merge Request list</li>\n            <li>UX: display a comment after creating/replying comment</li>\n            <li>UX: select project automatically based on remote url in configuration</li>\n            <li>UX: remove left border of Tabs</li>\n            <li>Bug fix: cannot start an IDE if CE and EE are installed simultaneously</li>\n        </ul>\n    </ul>\n\n    <h3>Acknowledgement</h3>\n    <ul>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/riclucio\">riclucio</a>\n            for requesting features\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/33\">#31</a>,\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/33\">#33</a>,\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/35\">#35</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/turnik\">turnik</a>\n            for requesting option\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/40\">#40</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/riclucio\">riclucio</a>\n            for reporting problem\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/37\">#37</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/Tillerino\">Tillerino</a>\n            for reporting problem\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/37\">#37</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/intgr\">intgr</a>\n            for reporting problem\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/20\">#20</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/zlodes\">zlodes</a>\n            for contributing to source code, the commit\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/commit/dfdc4f9e0c4366cc555fb723e82bc922eb06cdfc\">dfdc4f9</a>,\n            Pull Request\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/pull/30\">#30</a>\n        </li>\n    </ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ce/doc/release-notes.2020.1.1.html",
    "content": "<html lang=\"en\">\n    <h3>Notable changes</h3>\n    <ul>\n        <li>Hotfix: Crashes on Intellij IDEs v2020.1 EAP</li>\n    </ul>\n\n    <h3>Acknowledgement</h3>\n    <ul>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/alexeyzimarev\">alexeyzimarev</a>,\n            <a target=\"_blank\" href=\"https://github.com/tyger\">tyger</a>\n            for reporting problem\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/41\">#41</a>\n        </li>\n    </ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ce/doc/release-notes.2020.1.2.html",
    "content": "<html lang=\"en\">\n    <h3>Notable changes</h3>\n    <ul>\n        <li>Feature: Display, resolve, delete, reply, write comments in diff view</li>\n        <li>Feature: Open diff view when selecting a group in comments tree</li>\n        <li>Bug fix: Duplicated/wrong comment position</li>\n    </ul>\n\n    <h3>Acknowledgement</h3>\n    <ul>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/turnik\">turnik</a>\n            for contributing the new comment architecture\n        </li>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/theotherp\">theotherp</a>,\n            <a target=\"_blank\" href=\"https://github.com/BlacKCaT27\">BlacKCaT27</a>\n            for requesting feature\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/3\">#3</a>\n        </li>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/Pegietix\">Pegietix</a>,\n            <a target=\"_blank\" href=\"https://github.com/s-hildebrandt\">s-hildebrandt</a>\n            for reporting issue\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/23\">#23</a>\n        </li>\n    </ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ce/doc/release-notes.2020.1.3.html",
    "content": "<html lang=\"en\">\n    <h3>Notable changes</h3>\n    <ul>\n        <li>Branch project based on IDE versions, deploy plugin to EAP channel for 2020.1 EAP IDEs</li>\n        <li>Display approval status in merge request list</li>\n        <li>Improve UI response when deleting and adding a new comment</li>\n        <li>Sync comment collection when a comment gets changed in diff view</li>\n        <li>Fix #44: error when comments get destroyed in the event thread</li>\n    </ul>\n\n    <h3>Acknowledgement</h3>\n    <ul>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/PlatonovLex\">PlatonovLex</a>\n            for reporting issue\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/44\">#44</a>\n        </li>\n    </ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ce/doc/release-notes.2020.1.4.html",
    "content": "<html lang=\"en\">\n    <h3>Notable changes</h3>\n    <ul>\n        <li>Bug fixed: comments are not shown in diff view on Windows</li>\n        <li>Bug fixed: comments are filtered out when the MR has too many commits</li>\n    </ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ce/doc/release-notes.2020.1.5.html",
    "content": "<html lang=\"en\">\n    <h2>Notable changes</h2>\n    <ul>\n        <li>Support IDEA 2020.1, Android Studio 4.0.0</li>\n        <li>\n            Introduce new Comments tab which\n            <ul>\n                <li>Has all current functionalities</li>\n                <li>Can open diff view when you select a file</li>\n                <li>Can jump to code when you travel the Comments tree</li>\n            </ul>\n        </li>\n        <li>Open diff view when you click to Changes tree in Commits tab</li>\n        <li>Display merge request id in Home tab, Merge Request tree and Info tab</li>\n        <li>Fix #17: Cannot do Code Review on Android Studio</li>\n        <li>Fix #54: Merge Request tree should be invoked in application thread when get reloaded</li>\n        <li>Bug fixes</li>\n    </ul>\n\n    <h3>Acknowledgement</h3>\n    <ul>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/peper0\">peper0</a>,\n            <a target=\"_blank\" href=\"https://github.com/smec-cgint\">smec-cgint</a>\n            for requesting feature\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/14\">#14</a>\n        </li>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/LionZXY\">LionZXY</a>,\n            <a target=\"_blank\" href=\"https://github.com/mochadwi\">mochadwi</a>\n            for reporting issue\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/17\">#17</a>\n        </li>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/JaimeFreireB3I\">JaimeFreireB3I</a>\n            for reporting issue\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/54\">#54</a>\n        </li>\n    </ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ce/doc/release-notes.2020.2.0.html",
    "content": "<html lang=\"en\">\n    <h2>Notable changes</h2>\n    <ul>\n        <li>Support IDEA 2020.2</li>\n        <li>\n            Introduce new Rework flow which helps\n            <ul>\n                <li>Display comments and file changes if current branch matches your MR</li>\n                <li>Jump and address comments more easily</li>\n            </ul>\n            <div>Note: because of technical difficulty Reply button only works when open a dialog</div>\n        </li>\n        <li>Introduce options to hide UpVote, DownVote or State buttons in toolbar.</li>\n        <li>Improve configuration area.</li>\n        <li>Throw all exceptions which may happen inside plugin.</li>\n        <li>Fix #65: loading data and refreshing not work properly</li>\n        <li>Fix #88: fetching merge requests error for some timezones</li>\n    </ul>\n\n    <h3>Acknowledgement</h3>\n    <ul>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/lohengrin332\">lohengrin332</a>,\n            <a target=\"_blank\" href=\"https://github.com/jmatthiesen81\">jmatthiesen81</a>\n            for reporting\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/86\">#86</a>\n        </li>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/johnhenaot\">johnhenaot</a>,\n            <a target=\"_blank\" href=\"https://github.com/pchelk1n\">pchelk1n</a>,\n            <a target=\"_blank\" href=\"https://github.com/tlttnz2018\">tlttnz2018</a>\n            for reporting\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/87\">#87</a>\n        </li>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/srinathanantha\">srinathanantha</a>\n            for reporting\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/65\">#65</a>\n        </li>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/LuccaPrado\">LuccaPrado</a>\n            for reporting\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/88\">#88</a>\n        </li>\n    </ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ce/doc/release-notes.2020.3.0.html",
    "content": "<html lang=\"en\">\n<h2>Notable changes</h2>\n<ul>\n    <li>Support IDEA 2020.3.x</li>\n    <li>Support search by ID when ID is typed in the search box</li>\n    <li>Introduce edit comment functionality</li>\n    <li>Introduce draft comment feature which can temporarily save and push when Code Review is stopped</li>\n    <li>Fix #93: Crash with empty avatar</li>\n    <li>Fix #98: Crash when project gets closed</li>\n</ul>\n\n<h3>Acknowledgement</h3>\n<ul>\n    <li>\n        Thank\n        <a target=\"_blank\" href=\"https://github.com/sleepyprof\">sleepyprof</a>,\n        <a target=\"_blank\" href=\"https://github.com/PeskovV\">PeskovV</a>,\n        <a target=\"_blank\" href=\"https://github.com/ctammes\">ctammes</a>\n        for reporting\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/93\">#93</a>\n    </li>\n\n    <li>\n        Thank\n        <a target=\"_blank\" href=\"https://github.com/dragetd\">dragetd</a>,\n        <a target=\"_blank\" href=\"https://github.com/MaDKaTZe\">MaDKaTZe</a>\n        for reporting\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/98\">#98</a>\n    </li>\n\n    <li>\n        Thank\n        <a target=\"_blank\" href=\"https://github.com/regevbr\">regevbr</a>,\n        <a target=\"_blank\" href=\"https://github.com/jsbh05\">jsbh05</a>,\n        <a target=\"_blank\" href=\"https://github.com/artitrue\">artitrue</a>,\n        <a target=\"_blank\" href=\"https://github.com/jmatthiesen81\">jmatthiesen81</a>,\n        <a target=\"_blank\" href=\"https://github.com/krishnaku\">krishnaku</a>,\n        <a target=\"_blank\" href=\"https://github.com/drewoko\">drewoko</a>,\n        <a target=\"_blank\" href=\"https://github.com/petromir\">petromir</a>,\n        <a target=\"_blank\" href=\"https://github.com/mjhamiltonus\">mjhamiltonus</a>,\n        <a target=\"_blank\" href=\"https://github.com/pchelk1n\">pchelk1n</a>\n        for reporting\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/102\">#102</a>\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/115\">#115</a>\n    </li>\n\n    <li>\n        Thank\n        <a target=\"_blank\" href=\"https://github.com/martinsefcik\">martinsefcik</a>,\n        <a target=\"_blank\" href=\"https://github.com/rrajkomar\">rrajkomar</a>,\n        <a target=\"_blank\" href=\"https://github.com/nailuoGG\">nailuoGG</a>\n        for requesting\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/69\">#69</a>,\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/71\">#71</a>,\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/118\">#118</a>\n    </li>\n\n    <li>\n        Thank\n        <a target=\"_blank\" href=\"https://github.com/bironran\">bironran</a>,\n        <a target=\"_blank\" href=\"https://github.com/Borginator\">Borginator</a>\n        for requesting\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/89\">#89</a>\n    </li>\n\n    <li>\n        Thank\n        <a target=\"_blank\" href=\"https://github.com/pionl\">pionl</a>\n        for requesting\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/92\">#92</a>\n    </li>\n</ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ce/settings.gradle.kts",
    "content": "rootProject.name = \"merge-request-integration-ce\"\n"
  },
  {
    "path": "merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/CommunityApplicationServiceProvider.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeCE\n\nimport com.intellij.openapi.components.ServiceManager\nimport com.intellij.openapi.components.State\nimport com.intellij.openapi.components.Storage\nimport com.intellij.openapi.project.Project\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.AbstractApplicationServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\n\n@State(name = \"MergeRequestIntegrationApplicationLevel\", storages = [(Storage(\"merge-request-integration.xml\"))])\nclass CommunityApplicationServiceProvider: AbstractApplicationServiceProvider() {\n    override fun findProjectServiceProvider(project: Project): ProjectServiceProvider {\n        val service = ServiceManager.getService(project, CommunityProjectServiceProvider::class.java)\n        registerProjectServiceProvider(service)\n\n        return service\n    }\n\n    override val singleMRToolWindowName: String = \"Merge Request CE\"\n}"
  },
  {
    "path": "merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/CommunityProjectServiceProvider.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeCE\n\nimport com.intellij.openapi.components.ServiceManager\nimport com.intellij.openapi.components.State\nimport com.intellij.openapi.components.Storage\nimport com.intellij.openapi.project.Project\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.AbstractProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider\n\n@State(name = \"MergeRequestIntegrationProjectLevel\", storages = [(Storage(\"merge-request-integration-ce.xml\"))])\nclass CommunityProjectServiceProvider(project: Project) : AbstractProjectServiceProvider(project) {\n\n    override val applicationServiceProvider: ApplicationServiceProvider = ServiceManager.getService(\n        CommunityApplicationServiceProvider::class.java\n    )\n\n    init {\n        initWithApplicationServiceProvider(applicationServiceProvider)\n    }\n}"
  },
  {
    "path": "merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/Configuration.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeCE\n\nimport com.intellij.openapi.components.ServiceManager\nimport net.ntworld.mergeRequestIntegrationIde.ui.configuration.ConfigurationBase\n\nclass Configuration: ConfigurationBase(ServiceManager.getService(CommunityApplicationServiceProvider::class.java)) {\n    override fun getId(): String {\n        return \"merge-request-integration-ce\"\n    }\n\n    override fun getDisplayName(): String {\n        return \"Merge Request Integration CE\"\n    }\n}"
  },
  {
    "path": "merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/DiffExtension.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeCE\n\nimport com.intellij.openapi.components.ServiceManager\nimport net.ntworld.mergeRequestIntegrationIde.diff.DiffExtensionBase\n\nclass DiffExtension : DiffExtensionBase(\n    ServiceManager.getService(CommunityApplicationServiceProvider::class.java)\n)"
  },
  {
    "path": "merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/DiffViewAddCommentAction.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeCE\n\nimport net.ntworld.mergeRequestIntegrationIde.diff.DiffViewAddCommentActionBase\n\nclass DiffViewAddCommentAction : DiffViewAddCommentActionBase()"
  },
  {
    "path": "merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/DiffViewToggleCommentsAction.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeCE\n\nimport net.ntworld.mergeRequestIntegrationIde.diff.DiffViewToggleCommentsActionBase\n\nclass DiffViewToggleCommentsAction : DiffViewToggleCommentsActionBase()"
  },
  {
    "path": "merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/GithubConnectionsConfigurable.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeCE\n\nimport com.intellij.openapi.components.ServiceManager\nimport com.intellij.openapi.project.Project\nimport net.ntworld.mergeRequestIntegrationIde.ui.configuration.GithubConnectionsConfigurableBase\n\nclass GithubConnectionsConfigurable(project: Project): GithubConnectionsConfigurableBase(\n    ServiceManager.getService(project, CommunityProjectServiceProvider::class.java)\n) {\n    override fun getId(): String = \"MRI:github-ce\"\n\n    override fun getDisplayName(): String = \"Github\"\n}"
  },
  {
    "path": "merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/GitlabConnectionsConfigurable.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeCE\n\nimport com.intellij.openapi.components.ServiceManager\nimport com.intellij.openapi.project.Project\nimport net.ntworld.mergeRequestIntegrationIde.ui.configuration.GitlabConnectionsConfigurableBase\n\nclass GitlabConnectionsConfigurable(project: Project) : GitlabConnectionsConfigurableBase(\n    ServiceManager.getService(project, CommunityProjectServiceProvider::class.java)\n) {\n    override fun getId(): String = \"MRI:gitlab-ce\"\n\n    override fun getDisplayName(): String = \"Gitlab\"\n}\n"
  },
  {
    "path": "merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/MainToolWindowFactory.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeCE\n\nimport com.intellij.openapi.components.ServiceManager\nimport net.ntworld.mergeRequestIntegrationIde.ui.MainToolWindowFactoryBase\n\nclass MainToolWindowFactory : MainToolWindowFactoryBase(\n    ServiceManager.getService(CommunityApplicationServiceProvider::class.java)\n)"
  },
  {
    "path": "merge-request-integration-ce/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeCE/SingleMRToolWindowFactory.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeCE\n\nimport com.intellij.openapi.components.ServiceManager\nimport net.ntworld.mergeRequestIntegrationIde.toolWindow.SingleMRToolWindowFactoryBase\n\nclass SingleMRToolWindowFactory : SingleMRToolWindowFactoryBase(\n    ServiceManager.getService(CommunityApplicationServiceProvider::class.java)\n)"
  },
  {
    "path": "merge-request-integration-ce/src/main/resources/META-INF/plugin.xml",
    "content": "<idea-plugin>\n    <id>net.ntworld.nhat-phan.merge-request-integration-ce</id>\n    <name>Merge Request Integration CE - Code Review for GitLab</name>\n    <vendor>ntworld</vendor>\n\n    <depends>com.intellij.modules.platform</depends>\n    <depends>com.intellij.modules.lang</depends>\n    <depends>com.intellij.modules.vcs</depends>\n    <depends>Git4Idea</depends>\n\n    <extensions defaultExtensionNs=\"com.intellij\">\n        <applicationService serviceImplementation=\"net.ntworld.mergeRequestIntegrationIdeCE.CommunityApplicationServiceProvider\"/>\n        <projectService serviceImplementation=\"net.ntworld.mergeRequestIntegrationIdeCE.CommunityProjectServiceProvider\"/>\n        <diff.DiffExtension implementation=\"net.ntworld.mergeRequestIntegrationIdeCE.DiffExtension\"/>\n\n        <projectConfigurable id=\"merge-request-integration-ce\"\n                             displayName=\"Merge Request Integration CE\"\n                             instance=\"net.ntworld.mergeRequestIntegrationIdeCE.Configuration\"/>\n        <projectConfigurable groupId=\"merge-request-integration-ce\"\n                             id=\"MRI:gitlab-ce\"\n                             displayName=\"Gitlab\"\n                             instance=\"net.ntworld.mergeRequestIntegrationIdeCE.GitlabConnectionsConfigurable\"/>\n        <!--\n        <projectConfigurable groupId=\"merge-request-integration-ce\"\n                             id=\"MRI:github-ce\"\n                             displayName=\"Github\"\n                             instance=\"net.ntworld.mergeRequestIntegrationIdeCE.GithubConnectionsConfigurable\"/>\n        -->\n\n        <toolWindow id=\"Merge Request Integration CE\"\n                    secondary=\"false\"\n                    anchor=\"bottom\"\n                    icon=\"/icons/main-tool-window.svg\"\n                    canCloseContents=\"true\"\n                    factoryClass=\"net.ntworld.mergeRequestIntegrationIdeCE.MainToolWindowFactory\"/>\n\n        <toolWindow id=\"Merge Request CE\"\n                    secondary=\"true\"\n                    anchor=\"left\"\n                    icon=\"/icons/single-mr-tool-window.svg\"\n                    factoryClass=\"net.ntworld.mergeRequestIntegrationIdeCE.SingleMRToolWindowFactory\"/>\n    </extensions>\n\n    <actions>\n        <action class=\"net.ntworld.mergeRequestIntegrationIdeCE.DiffViewToggleCommentsAction\"\n                text=\"Toggle Comments\"\n                description=\"Toggle comments to this line\">\n            <keyboard-shortcut first-keystroke=\"alt V\" keymap=\"$default\"/>\n            <add-to-group group-id=\"Diff.EditorPopupMenu\" relative-to-action=\"Annotate\" anchor=\"after\" />\n        </action>\n        <action class=\"net.ntworld.mergeRequestIntegrationIdeCE.DiffViewAddCommentAction\"\n                text=\"Add Comment\"\n                description=\"Add comment to this line\">\n            <keyboard-shortcut first-keystroke=\"alt C\" keymap=\"$default\"/>\n            <add-to-group group-id=\"Diff.EditorPopupMenu\" relative-to-action=\"Annotate\" anchor=\"after\" />\n        </action>\n    </actions>\n</idea-plugin>"
  },
  {
    "path": "merge-request-integration-core/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.tasks.KotlinCompile\n\nval artifactGroup: String by project\nval artifactVersion: String by project\nval intellijVersion: String by project\nval jvmTarget: String by project\nval foundationVersion: String by project\nval gitlab4jVersion: String by project\nval githubApiVersion: String by project\nval prettyTimeVersion: String by project\nval commonmarkVersion: String by project\nval mockkVersion: String by project\n\ngroup = artifactGroup\nversion = artifactVersion\n\nrepositories {\n    jcenter()\n    mavenCentral()\n    maven(\"https://jitpack.io\")\n}\n\ndependencies {\n    implementation(\"org.jetbrains.kotlin:kotlin-stdlib\")\n    implementation(\"com.github.nhat-phan.foundation:foundation-jvm:$foundationVersion\")\n    implementation(\"org.gitlab4j:gitlab4j-api:$gitlab4jVersion\")\n    implementation(\"org.kohsuke:github-api:$githubApiVersion\")\n    implementation(\"org.ocpsoft.prettytime:prettytime:$prettyTimeVersion\")\n    compile(\"com.atlassian.commonmark:commonmark:$commonmarkVersion\")\n\n    implementation(project(\":contracts\"))\n    implementation(project(\":merge-request-integration\"))\n\n    testImplementation(kotlin(\"test\"))\n    testImplementation(kotlin(\"test-junit\"))\n    testImplementation(\"io.mockk:mockk:$mockkVersion\")\n}\n\nintellij {\n    version = intellijVersion\n    setPlugins(\"git4idea\")\n}\n\nval compileKotlin: KotlinCompile by tasks\nval compileTestKotlin: KotlinCompile by tasks\n\ncompileKotlin.kotlinOptions {\n    jvmTarget = jvmTarget\n}\n\ncompileTestKotlin.kotlinOptions {\n    jvmTarget = jvmTarget\n}\n\ntasks {\n    named<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>(\"compileKotlin\") {\n        kotlinOptions {\n            jvmTarget = jvmTarget\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/settings.gradle.kts",
    "content": "rootProject.name = \"merge-request-integration-core\"\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/AbstractModel.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nimport com.intellij.util.EventDispatcher\nimport java.util.*\n\nabstract class AbstractModel<DataListener: EventListener>: Model<DataListener> {\n    protected abstract val dispatcher: EventDispatcher<DataListener>\n\n    override fun addDataListener(listener: DataListener) = dispatcher.addListener(listener)\n\n    override fun removeDataListener(listener: DataListener) = dispatcher.removeListener(listener)\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/AbstractPresenter.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nimport com.intellij.util.EventDispatcher\nimport java.util.*\n\nabstract class AbstractPresenter<T: EventListener> : Presenter<T> {\n    protected abstract val dispatcher: EventDispatcher<T>\n\n    override fun addListener(listener: T) = dispatcher.addListener(listener)\n\n    override fun removeListener(listener: T) = dispatcher.removeListener(listener)\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/AbstractSimpleModel.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nimport com.intellij.util.EventDispatcher\nimport java.util.*\n\nabstract class AbstractSimpleModel: SimpleModel {\n    protected val dispatcher = EventDispatcher.create(EventListener::class.java)\n\n    override fun addDataListener(listener: EventListener) = dispatcher.addListener(listener)\n\n    override fun removeDataListener(listener: EventListener) = dispatcher.removeListener(listener)\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/AbstractSimplePresenter.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nimport com.intellij.util.EventDispatcher\nimport java.util.*\n\nabstract class AbstractSimplePresenter : SimplePresenter {\n    protected val dispatcher = EventDispatcher.create(EventListener::class.java)\n\n    override fun addListener(listener: EventListener) = dispatcher.addListener(listener)\n\n    override fun removeListener(listener: EventListener) = dispatcher.removeListener(listener)\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/AbstractSimpleView.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nimport com.intellij.util.EventDispatcher\nimport java.util.*\n\nabstract class AbstractSimpleView: SimpleView {\n    protected val dispatcher = EventDispatcher.create(EventListener::class.java)\n\n    override fun addActionListener(listener: EventListener) = dispatcher.addListener(listener)\n\n    override fun removeActionListener(listener: EventListener) = dispatcher.removeListener(listener)\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/AbstractView.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nimport com.intellij.util.EventDispatcher\nimport java.util.*\n\nabstract class AbstractView<ActionListener: EventListener>: View<ActionListener> {\n    protected abstract val dispatcher: EventDispatcher<ActionListener>\n\n    override fun addActionListener(listener: ActionListener) = dispatcher.addListener(listener)\n\n    override fun removeActionListener(listener: ActionListener) = dispatcher.removeListener(listener)\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/Component.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nimport javax.swing.JComponent\n\ninterface Component {\n    val component: JComponent\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ComponentFactory.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.component.PaginationToolbar\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.CommentComponentFactory\nimport net.ntworld.mergeRequestIntegrationIde.toolWindow.CommentsToolWindowTab\nimport net.ntworld.mergeRequestIntegrationIde.toolWindow.FilesToolWindowTab\n\ninterface ComponentFactory {\n    val toolWindowTabs: ToolWindowTabFactory\n\n    val commentComponents: CommentComponentFactory\n\n    fun makePaginationToolbar(displayRefreshButton: Boolean = false): PaginationToolbar\n\n    interface ToolWindowTabFactory {\n        fun makeFilesToolWindowTab(providerData: ProviderData, isCodeReviewChanges: Boolean): FilesToolWindowTab\n\n        fun makeCommentsToolWindowTab(\n            providerData: ProviderData,\n            mergeRequestInfo: MergeRequestInfo,\n            comments: List<Comment>\n        ): CommentsToolWindowTab\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/DataChangedSource.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nenum class DataChangedSource {\n    UI,\n    NOTIFIER\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/DefaultComponentFactory.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.component.PaginationToolbar\nimport net.ntworld.mergeRequestIntegrationIde.component.PaginationToolbarImpl\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.CommentComponentFactory\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.CommentComponentFactoryImpl\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.toolWindow.CommentsToolWindowTab\nimport net.ntworld.mergeRequestIntegrationIde.toolWindow.FilesToolWindowTab\nimport net.ntworld.mergeRequestIntegrationIde.toolWindow.internal.CommentsToolWindowTabImpl\nimport net.ntworld.mergeRequestIntegrationIde.toolWindow.internal.FilesToolWindowTabImpl\n\nclass DefaultComponentFactory(\n    private val projectServiceProvider: ProjectServiceProvider\n) : ComponentFactory {\n    override val toolWindowTabs: ComponentFactory.ToolWindowTabFactory = MyToolWindowTabFactory(projectServiceProvider)\n\n    override val commentComponents: CommentComponentFactory = CommentComponentFactoryImpl(projectServiceProvider)\n\n    override fun makePaginationToolbar(displayRefreshButton: Boolean): PaginationToolbar {\n        return PaginationToolbarImpl(displayRefreshButton)\n    }\n\n    private class MyToolWindowTabFactory(\n        private val projectServiceProvider: ProjectServiceProvider\n    ) : ComponentFactory.ToolWindowTabFactory {\n        override fun makeFilesToolWindowTab(\n            providerData: ProviderData,\n            isCodeReviewChanges: Boolean\n        ): FilesToolWindowTab {\n            return FilesToolWindowTabImpl(projectServiceProvider, providerData, isCodeReviewChanges)\n        }\n\n        override fun makeCommentsToolWindowTab(\n            providerData: ProviderData,\n            mergeRequestInfo: MergeRequestInfo,\n            comments: List<Comment>\n        ): CommentsToolWindowTab {\n            return CommentsToolWindowTabImpl(projectServiceProvider, providerData, mergeRequestInfo, comments)\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/IdeInfrastructure.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nimport net.ntworld.foundation.InfrastructureProvider\nimport net.ntworld.mergeRequestIntegration.MergeRequestIntegrationInfrastructure\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\n\nclass IdeInfrastructure(\n    private val providerStorage: ProviderStorage\n) : InfrastructureProvider() {\n    private val included = listOf(\n        MergeRequestIntegrationInfrastructure(providerStorage)\n    )\n\n    init {\n        wire(this.root, included)\n    }\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/Model.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nimport java.util.*\n\ninterface Model<DataListener : EventListener> {\n    fun addDataListener(listener: DataListener)\n\n    fun removeDataListener(listener: DataListener)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/Presenter.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nimport java.util.*\n\ninterface Presenter<T : EventListener> {\n    fun addListener(listener: T)\n\n    fun removeListener(listener: T)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/SimpleModel.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nimport java.util.*\n\ninterface SimpleModel : Model<EventListener>\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/SimplePresenter.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nimport java.util.*\n\ninterface SimplePresenter : Presenter<EventListener>"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/SimpleView.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nimport java.util.*\n\ninterface SimpleView : View<EventListener>\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/View.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nimport java.util.*\n\ninterface View<ActionListener : EventListener> {\n\n    fun addActionListener(listener: ActionListener)\n\n    fun removeActionListener(listener: ActionListener)\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/_const.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde\n\nimport com.intellij.openapi.diagnostic.Logger\n\nconst val ENTERPRISE_EDITION_URL = \"https://plugins.jetbrains.com/plugin/13615-merge-request-integration-ee--code-review-for-gitlab/\"\nconst val SEARCH_MERGE_REQUEST_ITEMS_PER_PAGE = 10\n\nconst val SINGLE_MR_CHANGES_WHEN_DOING_CODE_REVIEW_NAME = \"Changed Files\"\nconst val SINGLE_MR_REWORK_CHANGES_PREFIX = \"Files\"\nconst val SINGLE_MR_REWORK_COMMENTS_PREFIX = \"Comments\"\n\nprivate const val DEBUG = false\n\nval logger = Logger.getInstance(\"MRI\")\nfun debug(message: String) {\n    if (DEBUG) {\n        println(message)\n        logger.debug(message)\n    }\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/compatibility/IntellijIdeApi.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.compatibility\n\nimport com.intellij.openapi.editor.ex.EditorEx\nimport com.intellij.openapi.editor.impl.EditorEmbeddedComponentManager\nimport com.intellij.openapi.project.Project\nimport com.intellij.vcs.log.impl.VcsLogManager\nimport gnu.trove.TIntFunction\n\ninterface IntellijIdeApi {\n    fun findLeftLineNumberConverter(editor: EditorEx): TIntFunction\n\n    fun findRightLineNumberConverter(editor: EditorEx): TIntFunction\n\n    fun makeEditorEmbeddedComponentManagerProperties(offset: Int): EditorEmbeddedComponentManager.Properties\n\n    fun getVcsLogManager(ideaProject: Project, action: ((VcsLogManager) -> Unit))\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/compatibility/Version193Adapter.kt",
    "content": "//package net.ntworld.mergeRequestIntegrationIde.compatibility\n//\n//import com.intellij.openapi.editor.ex.EditorEx\n//import com.intellij.openapi.editor.impl.EditorEmbeddedComponentManager\n//import com.intellij.vcs.log.impl.VcsLogContentUtil\n//import com.intellij.vcs.log.impl.VcsLogManager\n//import gnu.trove.TIntFunction\n//\n//class Version193Adapter : IntellijIdeApi {\n//    private val resizePolicy by lazy {\n//        val constructors = EditorEmbeddedComponentManager.ResizePolicy::class.java.declaredConstructors\n//        for (ctor in constructors) {\n//            ctor.isAccessible = true\n//            return@lazy ctor.newInstance(0) as EditorEmbeddedComponentManager.ResizePolicy\n//        }\n//        return@lazy EditorEmbeddedComponentManager.ResizePolicy.any()\n//    }\n//\n//    override fun findLeftLineNumberConverter(editor: EditorEx): TIntFunction {\n//        val myLineNumberConvertor = editor.gutter.javaClass.getDeclaredField(\"myLineNumberConvertor\")\n//        myLineNumberConvertor.isAccessible = true\n//        return myLineNumberConvertor.get(editor.gutter) as TIntFunction\n//    }\n//\n//    override fun findRightLineNumberConverter(editor: EditorEx): TIntFunction {\n//        val myAdditionalLineNumberConvertor = editor.gutter.javaClass.getDeclaredField(\n//            \"myAdditionalLineNumberConvertor\"\n//        )\n//        myAdditionalLineNumberConvertor.isAccessible = true\n//        return myAdditionalLineNumberConvertor.get(editor.gutter) as TIntFunction\n//    }\n//\n//    override fun makeEditorEmbeddedComponentManagerProperties(offset: Int): EditorEmbeddedComponentManager.Properties {\n//        return EditorEmbeddedComponentManager.Properties(\n//            resizePolicy,\n//            true,\n//            false,\n//            0,\n//            offset\n//        )\n//    }\n//\n//    override fun getVcsLogManager(ideaProject: Project, action: (VcsLogManager) -> Unit) {\n//        val log = VcsLogContentUtil.getOrCreateLog(ideaProject)\n//        if (null === log) {\n//            return\n//        }\n//        action.invoke(log)\n//    }\n//}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/compatibility/Version201Adapter.kt",
    "content": "//package net.ntworld.mergeRequestIntegrationIde.compatibility\n//\n//import com.intellij.openapi.editor.ex.EditorEx\n//import com.intellij.openapi.editor.impl.EditorEmbeddedComponentManager\n//import com.intellij.openapi.editor.impl.LineNumberConverterAdapter\n//import com.intellij.openapi.project.Project\n//import com.intellij.vcs.log.impl.VcsLogContentUtil\n//import com.intellij.vcs.log.impl.VcsLogManager\n//import gnu.trove.TIntFunction\n//\n//class Version201Adapter : IntellijIdeApi {\n//    override fun findLeftLineNumberConverter(editor: EditorEx): TIntFunction {\n//        val myLineNumberConverter = editor.gutter.javaClass.getDeclaredField(\"myLineNumberConverter\")\n//        myLineNumberConverter.isAccessible = true\n//        val adapter = myLineNumberConverter.get(editor.gutter) as LineNumberConverterAdapter\n//        return TIntFunction { p0 -> adapter.convert(editor, p0) ?: -1 }\n//    }\n//\n//    override fun findRightLineNumberConverter(editor: EditorEx): TIntFunction {\n//        val myAdditionalLineNumberConverter = editor.gutter.javaClass.getDeclaredField(\n//            \"myAdditionalLineNumberConverter\"\n//        )\n//        myAdditionalLineNumberConverter.isAccessible = true\n//        val adapter = myAdditionalLineNumberConverter.get(editor.gutter) as LineNumberConverterAdapter\n//        return TIntFunction { p0 -> adapter.convert(editor, p0) ?: -1 }\n//    }\n//\n//    override fun makeEditorEmbeddedComponentManagerProperties(offset: Int): EditorEmbeddedComponentManager.Properties {\n//        return EditorEmbeddedComponentManager.Properties(\n//            EditorEmbeddedComponentManager.ResizePolicy.none(),\n//            null,\n//            true,\n//            false,\n//            0,\n//            offset\n//        )\n//    }\n//\n//    override fun getVcsLogManager(ideaProject: Project, action: (VcsLogManager) -> Unit) {\n//        val log = VcsLogContentUtil.getOrCreateLog(ideaProject)\n//        if (null === log) {\n//            return\n//        }\n//        action.invoke(log)\n//    }\n//}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/compatibility/Version203Adapter.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.compatibility\n\nimport com.intellij.openapi.editor.ex.EditorEx\nimport com.intellij.openapi.editor.impl.EditorEmbeddedComponentManager\nimport com.intellij.openapi.editor.impl.LineNumberConverterAdapter\nimport com.intellij.openapi.project.Project\nimport com.intellij.vcs.log.impl.VcsLogManager\nimport com.intellij.vcs.log.impl.VcsProjectLog\nimport gnu.trove.TIntFunction\n\nclass Version203Adapter: IntellijIdeApi {\n    override fun findLeftLineNumberConverter(editor: EditorEx): TIntFunction {\n        val myLineNumberConverter = editor.gutter.javaClass.getDeclaredField(\"myLineNumberConverter\")\n        myLineNumberConverter.isAccessible = true\n        val adapter = myLineNumberConverter.get(editor.gutter) as LineNumberConverterAdapter\n        return TIntFunction { p0 -> adapter.convert(editor, p0) ?: -1 }\n    }\n\n    override fun findRightLineNumberConverter(editor: EditorEx): TIntFunction {\n        val myAdditionalLineNumberConverter = editor.gutter.javaClass.getDeclaredField(\n            \"myAdditionalLineNumberConverter\"\n        )\n        myAdditionalLineNumberConverter.isAccessible = true\n        val adapter = myAdditionalLineNumberConverter.get(editor.gutter) as LineNumberConverterAdapter\n        return TIntFunction { p0 -> adapter.convert(editor, p0) ?: -1 }\n    }\n\n    override fun makeEditorEmbeddedComponentManagerProperties(offset: Int): EditorEmbeddedComponentManager.Properties {\n        return EditorEmbeddedComponentManager.Properties(\n            EditorEmbeddedComponentManager.ResizePolicy.none(),\n            null,\n            true,\n            false,\n            0,\n            offset\n        )\n    }\n\n    override fun getVcsLogManager(ideaProject: Project, action: (VcsLogManager) -> Unit) {\n        VcsProjectLog.runWhenLogIsReady(ideaProject) {\n            action.invoke(it)\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/Icons.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component\n\nimport com.intellij.openapi.util.IconLoader\n\nobject Icons {\n\n    val Comments = IconLoader.getIcon(\"/icons/comments.svg\", this::class.java)\n    val ThumbsUp = IconLoader.getIcon(\"/icons/thumbs-up.svg\", this::class.java)\n    val ThumbsDown = IconLoader.getIcon(\"/icons/thumbs-down.svg\", this::class.java)\n    val PipelineRunning = IconLoader.getIcon(\"/icons/clock.svg\", this::class.java)\n    val PipelineFailed = IconLoader.getIcon(\"/icons/exclamation-circle.svg\", this::class.java)\n    val PipelineSuccess = IconLoader.getIcon(\"/icons/check-circle.svg\", this::class.java)\n    val NoApproval = IconLoader.getIcon(\"/icons/square-gray.svg\", this::class.java)\n    val RequiredApproval = IconLoader.getIcon(\"/icons/square-yellow.svg\", this::class.java)\n    val Approved = IconLoader.getIcon(\"/icons/check-square.svg\", this::class.java)\n    val ExternalLink = IconLoader.getIcon(\"/icons/external-link-alt.svg\", this::class.java)\n    val Description = IconLoader.getIcon(\"/icons/file-alt.svg\", this::class.java)\n    val StateMerged = IconLoader.getIcon(\"/icons/door-closed-green.svg\", this::class.java)\n    val StateClosed = IconLoader.getIcon(\"/icons/door-closed-red.svg\", this::class.java)\n    val StateOpened = IconLoader.getIcon(\"/icons/door-open.svg\", this::class.java)\n    val HasComment = IconLoader.getIcon(\"/icons/sticky-note.svg\", this::class.java)\n    val ReplyComment = IconLoader.getIcon(\"/icons/reply.svg\", this::class.java)\n    val Trash = IconLoader.getIcon(\"/icons/trash.svg\", this::class.java)\n    val Edit = IconLoader.getIcon(\"/icons/edit.svg\", this::class.java)\n    val Resolve = IconLoader.getIcon(\"/icons/check-circle-gray.svg\", this::class.java)\n    val Resolved = IconLoader.getIcon(\"/icons/check-circle.svg\", this::class.java)\n    val CaretDown = IconLoader.getIcon(\"/icons/chevron-down.svg\", this::class.java)\n    val CaretRight = IconLoader.getIcon(\"/icons/chevron-right.svg\", this::class.java)\n    val LegalWarning = IconLoader.getIcon(\"/icons/exclamation-triangle.svg\", this::class.java)\n\n    object Gutter {\n        val Empty = IconLoader.getIcon(\"/icons/1px.svg\", this::class.java)\n        val Comment = IconLoader.getIcon(\"/icons/gutter-comment.svg\", this::class.java)\n        val Comments = IconLoader.getIcon(\"/icons/gutter-comments.svg\", this::class.java)\n        val AddComment = IconLoader.getIcon(\"/icons/gutter-plus-small.svg\", this::class.java)\n        val WritingComment = IconLoader.getIcon(\"/icons/gutter-writing-comment.svg\", this::class.java)\n        val HasDraft = IconLoader.getIcon(\"/icons/edit.svg\", this::class.java)\n    }\n\n    object TreeNode {\n        val ResolvedComment = IconLoader.getIcon(\"/icons/check-square.svg\", this::class.java)\n        val UnresolvedComment = IconLoader.getIcon(\"/icons/square-yellow.svg\", this::class.java)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/PaginationToolbar.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component\n\nimport net.ntworld.mergeRequestIntegrationIde.Component\nimport java.util.*\n\ninterface PaginationToolbar : Component {\n    fun enable()\n\n    fun disable()\n\n    fun getCurrentPage(): Int\n\n    fun setData(page: Int, totalPages: Int, totalItems: Int)\n\n    fun addListener(listener: Listener)\n\n    fun removeListener(listener: Listener)\n\n    interface Listener : EventListener {\n        fun changePage(page: Int)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/PaginationToolbarImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component\n\nimport com.intellij.icons.AllIcons\nimport com.intellij.openapi.actionSystem.ActionManager\nimport com.intellij.openapi.actionSystem.AnAction\nimport com.intellij.openapi.actionSystem.AnActionEvent\nimport com.intellij.openapi.actionSystem.DefaultActionGroup\nimport net.miginfocom.swing.MigLayout\nimport com.intellij.util.EventDispatcher\nimport javax.swing.JComponent\nimport javax.swing.JLabel\nimport javax.swing.JPanel\n\ninternal class PaginationToolbarImpl(private val displayRefreshButton: Boolean = false) : PaginationToolbar {\n    private val dispatcher = EventDispatcher.create(PaginationToolbar.Listener::class.java)\n\n    private var myEnabled: Boolean = true\n    private var myIsValid: Boolean = true\n    private var myPage: Int = 0\n    private var myTotalPages: Int = 0\n    private var myTotalItems: Int = 0\n\n    private val myRefreshButtonAction = MyRefreshButtonAction(this)\n    private val myFirstAction = MyFirstAction(this)\n    private val myPrevAction = MyPrevAction(this)\n    private val myNextAction = MyNextAction(this)\n    private val myLastAction = MyLastAction(this)\n\n    private val myInfo = JLabel()\n    private val myPanel by lazy {\n        val panel = JPanel(MigLayout(\"ins 0, fill\", \"[left, fill]push[right]\", \"center\"))\n\n        val actionGroup = DefaultActionGroup()\n        if (displayRefreshButton) {\n            actionGroup.add(myRefreshButtonAction)\n            actionGroup.addSeparator()\n        }\n        actionGroup.add(myFirstAction)\n        actionGroup.add(myPrevAction)\n        actionGroup.add(myNextAction)\n        actionGroup.add(myLastAction)\n\n        val toolbar = ActionManager.getInstance().createActionToolbar(\n            \"${this::class.java.canonicalName}/toolbar-right\", actionGroup, true\n        )\n\n        panel.add(myInfo)\n        panel.add(toolbar.component)\n        panel\n    }\n\n    override val component: JComponent = myPanel\n\n    override fun enable() {\n        myEnabled = true\n        updateInfo()\n    }\n\n    override fun disable() {\n        myEnabled = false\n        updateInfo()\n    }\n\n    override fun getCurrentPage(): Int {\n        return if (myPage <= 0) 1 else myPage\n    }\n\n    override fun setData(page: Int, totalPages: Int, totalItems: Int) {\n        myIsValid = true\n        if (page <= 0 || totalPages <= 0 || totalItems <= 0) {\n            myIsValid = false\n        }\n\n        if (page > totalPages) {\n            myIsValid = false\n        }\n\n        myPage = page\n        myTotalPages = totalPages\n        myTotalItems = totalItems\n        updateInfo()\n    }\n\n    override fun addListener(listener: PaginationToolbar.Listener) {\n        dispatcher.addListener(listener)\n    }\n\n    override fun removeListener(listener: PaginationToolbar.Listener) {\n        dispatcher.removeListener(listener)\n    }\n\n    private fun updateInfo() {\n        if (myEnabled) {\n            myInfo.text = \" Displaying page $myPage/$myTotalPages. Total items: $myTotalItems\"\n        } else {\n            myInfo.text = \" Loading data...\"\n        }\n    }\n\n    private class MyRefreshButtonAction(private val self: PaginationToolbarImpl) :\n        AnAction(\"Refresh\", \"Refresh\", AllIcons.Actions.Refresh) {\n\n        override fun actionPerformed(e: AnActionEvent) {\n            self.dispatcher.multicaster.changePage(self.myPage)\n        }\n\n        override fun update(e: AnActionEvent) {\n            e.presentation.isEnabled = self.myEnabled && self.myIsValid\n        }\n    }\n\n    private class MyFirstAction(private val self: PaginationToolbarImpl) :\n        AnAction(\"First\", \"Go to the first page\", AllIcons.Actions.Play_first) {\n\n        override fun actionPerformed(e: AnActionEvent) {\n            self.setData(1, self.myTotalPages, self.myTotalItems)\n            self.dispatcher.multicaster.changePage(self.myPage)\n        }\n\n        override fun update(e: AnActionEvent) {\n            e.presentation.isEnabled = self.myEnabled && self.myIsValid && self.myPage != 1\n        }\n    }\n\n    private class MyPrevAction(private val self: PaginationToolbarImpl) :\n        AnAction(\"Previous\", \"Previous page\", AllIcons.Actions.Play_back) {\n\n        override fun actionPerformed(e: AnActionEvent) {\n            self.setData(self.myPage - 1, self.myTotalPages, self.myTotalItems)\n            self.dispatcher.multicaster.changePage(self.myPage)\n        }\n\n        override fun update(e: AnActionEvent) {\n            e.presentation.isEnabled = self.myEnabled && self.myIsValid && self.myPage > 1 && self.myTotalPages > 1\n        }\n    }\n\n    private class MyNextAction(private val self: PaginationToolbarImpl) :\n        AnAction(\"Next\", \"Next page\", AllIcons.Actions.Play_forward) {\n\n        override fun actionPerformed(e: AnActionEvent) {\n            self.setData(self.myPage + 1, self.myTotalPages, self.myTotalItems)\n            self.dispatcher.multicaster.changePage(self.myPage)\n        }\n\n        override fun update(e: AnActionEvent) {\n            e.presentation.isEnabled = self.myEnabled && self.myIsValid && self.myPage < self.myTotalPages\n        }\n    }\n\n    private class MyLastAction(private val self: PaginationToolbarImpl) :\n        AnAction(\"Last\", \"Go to the last page\", AllIcons.Actions.Play_last) {\n\n        override fun actionPerformed(e: AnActionEvent) {\n            self.setData(self.myTotalPages, self.myTotalPages, self.myTotalItems)\n            self.dispatcher.multicaster.changePage(self.myPage)\n        }\n\n        override fun update(e: AnActionEvent) {\n            e.presentation.isEnabled = self.myEnabled && self.myIsValid && self.myPage < self.myTotalPages\n        }\n    }\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/CommentComponent.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.comment\n\nimport net.ntworld.mergeRequestIntegrationIde.Component\n\ninterface CommentComponent : Component {\n\n    fun hideMoveToDialogButtons()\n\n    fun showMoveToDialogButtons()\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/CommentComponentFactory.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.comment\n\nimport com.intellij.openapi.project.Project\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\n\ninterface CommentComponentFactory {\n    fun makeGroup(\n        providerData: ProviderData,\n        mergeRequestInfo: MergeRequestInfo,\n        ideaProject: Project,\n        borderTop: Boolean,\n        groupId: String,\n        comments: List<Comment>,\n        options: Options\n    ) : GroupComponent\n\n    fun makeComment(\n        groupComponent: GroupComponent,\n        providerData: ProviderData,\n        mergeRequestInfo: MergeRequestInfo,\n        comment: Comment,\n        indent: Int,\n        options: Options\n    ): CommentComponent\n\n    fun makeEditor(\n        ideaProject: Project,\n        type: EditorComponent.Type,\n        indent: Int,\n        borderLeftRight: Int,\n        showCancelAction: Boolean,\n        isDoingCodeReview: Boolean\n    ): EditorComponent\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/CommentComponentFactoryImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.comment\n\nimport com.intellij.openapi.project.Project as IdeaProject\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\n\ninternal class CommentComponentFactoryImpl(\n    private val projectServiceProvider: ProjectServiceProvider\n) : CommentComponentFactory {\n\n    override fun makeGroup(\n        providerData: ProviderData,\n        mergeRequestInfo: MergeRequestInfo,\n        ideaProject: IdeaProject,\n        borderTop: Boolean,\n        groupId: String,\n        comments: List<Comment>,\n        options: Options\n    ) : GroupComponent {\n        return GroupComponentImpl(\n            this,\n            borderTop,\n            providerData,\n            mergeRequestInfo,\n            ideaProject,\n            groupId,\n            comments,\n            options\n        )\n    }\n\n    override fun makeComment(\n        groupComponent: GroupComponent,\n        providerData: ProviderData,\n        mergeRequestInfo: MergeRequestInfo,\n        comment: Comment,\n        indent: Int,\n        options: Options\n    ): CommentComponent {\n        return CommentComponentImpl(\n            this,\n            projectServiceProvider,\n            groupComponent,\n            providerData,\n            mergeRequestInfo,\n            comment,\n            indent,\n            options\n        )\n    }\n\n    override fun makeEditor(\n        ideaProject: IdeaProject,\n        type: EditorComponent.Type,\n        indent: Int,\n        borderLeftRight: Int,\n        showCancelAction: Boolean,\n        isDoingCodeReview: Boolean\n    ): EditorComponent {\n        return EditorComponentImpl(\n            ideaProject,\n            type,\n            indent,\n            borderLeftRight,\n            showCancelAction,\n            isDoingCodeReview\n        )\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/CommentComponentImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.comment\n\nimport com.intellij.icons.AllIcons\nimport com.intellij.ide.BrowserUtil\nimport com.intellij.ide.util.TipUIUtil\nimport com.intellij.openapi.actionSystem.ActionManager\nimport com.intellij.openapi.actionSystem.AnAction\nimport com.intellij.openapi.actionSystem.AnActionEvent\nimport com.intellij.openapi.actionSystem.DefaultActionGroup\nimport com.intellij.openapi.ui.DialogBuilder\nimport com.intellij.openapi.ui.DialogWrapper\nimport com.intellij.openapi.ui.Messages\nimport com.intellij.openapi.ui.SimpleToolWindowPanel\nimport com.intellij.ui.JBColor\nimport com.intellij.ui.components.Label\nimport net.miginfocom.swing.MigLayout\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegration.util.DateTimeUtil\nimport net.ntworld.mergeRequestIntegrationIde.ENTERPRISE_EDITION_URL\nimport net.ntworld.mergeRequestIntegrationIde.component.Icons\nimport net.ntworld.mergeRequestIntegrationIde.component.dialog.LegalWarningDialog\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.util.HtmlHelper\nimport java.awt.Color\nimport java.awt.Cursor\nimport java.awt.event.MouseEvent\nimport java.awt.event.MouseListener\nimport javax.swing.BorderFactory\nimport javax.swing.JComponent\nimport javax.swing.JPanel\n\nclass CommentComponentImpl(\n    private val factory: CommentComponentFactory,\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val groupComponent: GroupComponent,\n    private val providerData: ProviderData,\n    private val mergeRequestInfo: MergeRequestInfo,\n    private val comment: Comment,\n    private val indent: Int,\n    private val options: Options\n) : CommentComponent {\n    private var displayMoveToDialog: Boolean = options.showMoveToDialog\n    private val myPanel = SimpleToolWindowPanel(true, false)\n    private val myNameLabel = Label(comment.author.name)\n    private val myUsernameLabel = Label(\"@${comment.author.username}\")\n    private val myNameSeparatorLabel = Label(\"·\")\n    private var myUsePrettyTime: Boolean = true\n\n    private var isPublishingDraft: Boolean = false\n    private var myEditEditor: EditorComponent? = null\n    private val myWebView = TipUIUtil.createBrowser() as TipUIUtil.Browser\n    private val myHtmlTemplate = CommentComponentImpl::class.java.getResource(\n        \"/templates/mr.comment.html\"\n    ).readText()\n\n    private val myEditEditorListener = object: EditorComponent.EventListener {\n        override fun onEditorFocused(editor: EditorComponent) {\n        }\n\n        override fun onEditorResized(editor: EditorComponent) {\n\n        }\n\n        override fun onCancelClicked(editor: EditorComponent) {\n            myWebView.component.isVisible = true\n            myPanel.setContent(myWebView.component)\n\n            groupComponent.editEditorDestroyed(comment, editor)\n            myEditEditor = null\n        }\n\n        override fun onSubmitClicked(editor: EditorComponent, isDraft: Boolean) {\n            val text = editor.text.trim()\n            if (text.isNotEmpty() && text !== comment.body) {\n                groupComponent.requestEditComment(comment, text)\n            }\n        }\n    }\n\n    private val myTimeAction = MyTimeAction(this)\n    private val myDraftStatusAction = MyDraftStatusAction(this)\n    private val myOpenInBrowserAction = MyOpenInBrowserAction(this)\n    private val myReplyAction = MyReplyAction(this)\n    private val myDeleteAction = MyDeleteAction(this)\n    private val myEditAction = MyEditAction(this)\n    private val myMoveToDialogAction = MyMoveToDialogAction(this)\n    private val myLegalWarningAction = MyLegalWarningAction(this)\n\n    private class MyResolveAction(private val self: CommentComponentImpl) : AnAction() {\n        override fun actionPerformed(e: AnActionEvent) {\n            self.groupComponent.requestToggleResolvedStateOfComment(self.comment)\n        }\n\n        override fun update(e: AnActionEvent) {\n            super.update(e)\n            if (self.comment.resolved) {\n                e.presentation.icon = Icons.Resolved\n                e.presentation.description = \"Unresolve thread\"\n                val resolvedBy = self.comment.resolvedBy\n                if (null !== resolvedBy) {\n                    e.presentation.text = \"Resolved by ${resolvedBy.name}\"\n                }\n            } else {\n                e.presentation.icon = Icons.Resolve\n                e.presentation.text = \"Resolve thead\"\n                e.presentation.description = \"Mark thread as resolved\"\n            }\n        }\n    }\n    private val myResolveAction = MyResolveAction(this)\n\n    private val myNameMouseListener = object : MouseListener {\n        override fun mouseReleased(e: MouseEvent?) {}\n        override fun mouseEntered(e: MouseEvent?) {}\n        override fun mousePressed(e: MouseEvent?) {}\n        override fun mouseExited(e: MouseEvent?) {}\n\n        override fun mouseClicked(e: MouseEvent?) {\n            groupComponent.collapse = !groupComponent.collapse\n            myNameLabel.icon = if (groupComponent.collapse) Icons.CaretRight else Icons.CaretDown\n        }\n    }\n\n    override val component: JComponent = myPanel\n\n    init {\n        if (indent == 0 && groupComponent.comments.size > 1) {\n            myNameLabel.icon = if (groupComponent.collapse) Icons.CaretRight else Icons.CaretDown\n            myNameLabel.addMouseListener(myNameMouseListener)\n            myNameLabel.cursor = Cursor.getDefaultCursor()\n        }\n\n        myUsernameLabel.foreground = Color(153, 153, 153)\n        myWebView.text = buildHtml(providerData, comment)\n        myPanel.toolbar = createToolbar()\n        myPanel.setContent(myWebView.component)\n\n        myPanel.border = BorderFactory.createMatteBorder(\n            0, indent * 40 + options.borderLeftRight, 1, options.borderLeftRight, JBColor.border()\n        )\n    }\n\n    private fun hideWebViewAndShowEditEditor()\n    {\n        myWebView.component.isVisible = false\n        if (null === this.myEditEditor) {\n            val createdEditor = factory.makeEditor(\n                projectServiceProvider.project,\n                EditorComponent.Type.EDIT,\n                0,\n                borderLeftRight = 0,\n                showCancelAction = true,\n                isDoingCodeReview = projectServiceProvider.isDoingCodeReview()\n            )\n            groupComponent.editEditorCreated(comment, createdEditor)\n\n            createdEditor.addListener(myEditEditorListener)\n            createdEditor.text = comment.body + \"\\n\"\n            myEditEditor = createdEditor\n        }\n        myPanel.setContent(this.myEditEditor!!.component)\n        myEditEditor!!.focus()\n    }\n\n    override fun hideMoveToDialogButtons() {\n        displayMoveToDialog = false\n    }\n\n    override fun showMoveToDialogButtons() {\n        displayMoveToDialog = true\n    }\n\n    private fun createToolbar(): JComponent {\n        val panel = JPanel(MigLayout(\"ins 0, fill\", \"5[left]5[left]5[left]0[left, fill]push[right]\", \"center\"))\n\n        val leftActionGroupTwo = DefaultActionGroup()\n        leftActionGroupTwo.add(myTimeAction)\n        leftActionGroupTwo.add(myDraftStatusAction)\n        val leftToolbarTwo = ActionManager.getInstance().createActionToolbar(\n            \"${CommentComponentImpl::class.java.canonicalName}/toolbar-left-two\",\n            leftActionGroupTwo,\n            true\n        )\n\n        val rightActionGroup = DefaultActionGroup()\n        if (options.showMoveToDialog) {\n            rightActionGroup.add(myMoveToDialogAction)\n            rightActionGroup.addSeparator()\n        }\n\n        if (providerData.currentUser.id == comment.author.id) {\n            rightActionGroup.add(myEditAction)\n            rightActionGroup.add(myDeleteAction)\n            if (!comment.isDraft) {\n                rightActionGroup.addSeparator()\n            }\n        }\n\n        if (!comment.isDraft) {\n            rightActionGroup.add(myOpenInBrowserAction)\n            rightActionGroup.addSeparator()\n            if (comment.resolvable) {\n                rightActionGroup.add(myResolveAction)\n            }\n            rightActionGroup.add(myReplyAction)\n        }\n\n        if (!projectServiceProvider.applicationServiceProvider.isLegal(providerData)) {\n            rightActionGroup.addSeparator()\n            rightActionGroup.add(myLegalWarningAction)\n        }\n\n        val rightToolbar = ActionManager.getInstance().createActionToolbar(\n            \"${CommentComponentImpl::class.java.canonicalName}/toolbar-right\",\n            rightActionGroup,\n            true\n        )\n\n        panel.add(myNameLabel)\n        panel.add(myUsernameLabel)\n        panel.add(myNameSeparatorLabel)\n        panel.add(leftToolbarTwo.component)\n        panel.add(rightToolbar.component)\n        return panel\n    }\n\n    private fun buildHtml(providerData: ProviderData, comment: Comment): String {\n        val output = myHtmlTemplate\n            .replace(\"{{content}}\", HtmlHelper.convertFromMarkdown(comment.body))\n\n        return HtmlHelper.resolveRelativePath(providerData, output)\n    }\n\n    private class MyMoveToDialogAction(private val self: CommentComponentImpl): AnAction(\n        \"Open in a Dialog\", \"\", AllIcons.Actions.MoveToWindow\n    ) {\n        override fun actionPerformed(e: AnActionEvent) {\n            self.groupComponent.requestOpenDialog()\n        }\n\n        override fun update(e: AnActionEvent) {\n            super.update(e)\n            e.presentation.isVisible = self.displayMoveToDialog\n        }\n    }\n\n    private class MyTimeAction(private val self: CommentComponentImpl) : AnAction(null, null, null) {\n        override fun actionPerformed(e: AnActionEvent) {\n            self.myUsePrettyTime = !self.myUsePrettyTime\n        }\n\n        override fun update(e: AnActionEvent) {\n            e.presentation.text = if (self.myUsePrettyTime) {\n                DateTimeUtil.toPretty(DateTimeUtil.toDate(self.comment.updatedAt))\n            } else {\n                DateTimeUtil.formatDate(DateTimeUtil.toDate(self.comment.updatedAt))\n            }\n            e.presentation.isVisible = !self.comment.isDraft\n        }\n\n        override fun useSmallerFontForTextInToolbar(): Boolean = false\n        override fun displayTextInToolbar() = true\n    }\n\n    private class MyDraftStatusAction(private val self: CommentComponentImpl) : AnAction(\n        \"Draft, click to publish this comment\", \"This is a draft comment, click to publish\", null\n    ) {\n        override fun actionPerformed(e: AnActionEvent) {\n            if (self.isPublishingDraft) {\n                return\n            }\n            self.isPublishingDraft = true\n            self.groupComponent.publishDraftComment(self.comment)\n        }\n\n        override fun update(e: AnActionEvent) {\n            e.presentation.isVisible = self.comment.isDraft\n            if (self.comment.isDraft && self.isPublishingDraft) {\n                e.presentation.text = \"Publishing...\"\n            } else {\n                e.presentation.text = \"Draft, click to publish this comment\"\n            }\n        }\n\n        override fun useSmallerFontForTextInToolbar(): Boolean = false\n        override fun displayTextInToolbar() = true\n    }\n\n    private class MyOpenInBrowserAction(private val self: CommentComponentImpl): AnAction(\n        \"View in browser\", \"Open and view the comment in browser\", Icons.ExternalLink\n    ) {\n        override fun actionPerformed(e: AnActionEvent) {\n            BrowserUtil.open(self.providerData.info.createCommentUrl(self.mergeRequestInfo.url, self.comment))\n        }\n    }\n\n    private class MyReplyAction(private val self: CommentComponentImpl): AnAction(\n        \"Reply\", \"Reply this comment\", Icons.ReplyComment\n    ) {\n        override fun actionPerformed(e: AnActionEvent) {\n            self.groupComponent.showReplyEditor()\n            if (self.options.openEditorInDialog) {\n                self.groupComponent.requestOpenDialog()\n            }\n        }\n    }\n\n    private class MyDeleteAction(private val self: CommentComponentImpl): AnAction(\n        \"Delete comment\", \"Delete comment\", Icons.Trash\n    ) {\n        override fun actionPerformed(e: AnActionEvent) {\n            val result = Messages.showYesNoDialog(\n                \"Do you want to delete the comment?\", \"Are you sure\", Messages.getQuestionIcon()\n            )\n            if (result == Messages.YES) {\n                self.groupComponent.requestDeleteComment(self.comment)\n            }\n        }\n    }\n\n    private class MyEditAction(private val self: CommentComponentImpl): AnAction(\n        \"Edit comment\", \"Edit comment\", AllIcons.Actions.Edit\n    ) {\n        override fun actionPerformed(e: AnActionEvent) {\n            self.hideWebViewAndShowEditEditor()\n            if (self.options.openEditorInDialog) {\n                self.groupComponent.requestOpenDialog()\n            }\n        }\n\n        override fun update(e: AnActionEvent) {\n            e.presentation.isEnabled = null === self.myEditEditor\n        }\n    }\n\n    private class MyLegalWarningAction(private val self: CommentComponentImpl): AnAction(\n        \"Illegal\",\n        \"You cannot use CE for private repositories, please buy Enterprise Edition, only 1\\$/month.\",\n        Icons.LegalWarning\n    ) {\n        override fun actionPerformed(e: AnActionEvent) {\n            val builder = DialogBuilder()\n            builder.title(\"Please buy Enterprise Edition\")\n            builder.setCenterPanel(LegalWarningDialog().component)\n            builder.addOkAction()\n            builder.okAction.setText(\"Buy Enterprise Edition\")\n\n            val code = builder.show()\n\n            // This line run after the dialog is closed\n            if (code == DialogWrapper.OK_EXIT_CODE) {\n                BrowserUtil.open(ENTERPRISE_EDITION_URL)\n            }\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/CommentEvent.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.comment\n\nimport net.ntworld.mergeRequest.Comment\nimport java.util.*\n\ninterface CommentEvent: EventListener {\n\n    fun onDeleteCommentRequested(comment: Comment)\n\n    fun onResolveCommentRequested(comment: Comment)\n\n    fun onUnresolveCommentRequested(comment: Comment)\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/CommentEventPropagator.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.comment\n\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.Comment\n\nclass CommentEventPropagator<T: CommentEvent>(\n    private val dispatcher: EventDispatcher<T>\n) : CommentEvent {\n\n    override fun onDeleteCommentRequested(comment: Comment) {\n        dispatcher.multicaster.onDeleteCommentRequested(comment)\n    }\n\n    override fun onResolveCommentRequested(comment: Comment) {\n        dispatcher.multicaster.onResolveCommentRequested(comment)\n    }\n\n    override fun onUnresolveCommentRequested(comment: Comment) {\n        dispatcher.multicaster.onUnresolveCommentRequested(comment)\n    }\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/EditorComponent.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.comment\n\nimport com.intellij.openapi.Disposable\nimport net.ntworld.mergeRequestIntegrationIde.Component\n\ninterface EditorComponent : Component, Disposable {\n    var text: String\n\n    var addCommentNowButtonText: String\n\n    var addCommentNowButtonDesc: String\n\n    var startAReviewButtonText: String\n\n    var startAReviewButtonDesc: String\n\n    var isVisible: Boolean\n\n    fun focus()\n\n    fun drawBorderTop(display: Boolean)\n\n    fun addListener(listener: EventListener)\n\n    interface EventListener: java.util.EventListener {\n        fun onEditorFocused(editor: EditorComponent)\n\n        fun onEditorResized(editor: EditorComponent)\n\n        fun onCancelClicked(editor: EditorComponent)\n\n        fun onSubmitClicked(editor: EditorComponent, isDraft: Boolean)\n    }\n\n    enum class Type {\n        NEW_DISCUSSION,\n        EDIT,\n        REPLY\n    }\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/EditorComponentImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.comment\n\nimport com.intellij.openapi.actionSystem.*\nimport com.intellij.openapi.editor.impl.DocumentImpl\nimport com.intellij.openapi.project.Project as IdeaProject\nimport com.intellij.ui.EditorSettingsProvider\nimport com.intellij.ui.EditorTextField\nimport com.intellij.ui.JBColor\nimport com.intellij.util.EventDispatcher\nimport net.miginfocom.swing.MigLayout\nimport net.ntworld.mergeRequestIntegrationIde.ui.util.CustomSimpleToolWindowPanel\nimport net.ntworld.mergeRequestIntegrationIde.util.FileTypeUtil\nimport java.awt.event.ComponentAdapter\nimport java.awt.event.ComponentEvent\nimport java.awt.event.FocusEvent\nimport java.awt.event.FocusListener\nimport javax.swing.BorderFactory\nimport javax.swing.JComponent\nimport javax.swing.JPanel\n\nclass EditorComponentImpl(\n    private val ideaProject: IdeaProject,\n    private val type: EditorComponent.Type,\n    val indent: Int,\n    private val borderLeftRight: Int = 1,\n    private val showCancelAction: Boolean = true,\n    private val isDoingCodeReview: Boolean = false\n) : EditorComponent {\n    private val dispatcher = EventDispatcher.create(EditorComponent.EventListener::class.java)\n    private val myPanel = CustomSimpleToolWindowPanel(vertical = true, borderless = false)\n    private val myDocument = DocumentImpl(\"\")\n    private val myEditorSettingsProvider = EditorSettingsProvider { editor ->\n        if (null !== editor) {\n            editor.settings.isLineNumbersShown = true\n            editor.settings.isFoldingOutlineShown = true\n            editor.settings.isUseSoftWraps = true\n\n            editor.setHorizontalScrollbarVisible(false)\n            editor.setVerticalScrollbarVisible(true)\n        }\n    }\n    private val myEditorTextField by lazy {\n        val textField = EditorTextField(myDocument, ideaProject, FileTypeUtil.markdownFileType)\n        textField.setOneLineMode(false)\n        textField.addSettingsProvider(myEditorSettingsProvider)\n        textField\n    }\n    private val myComponentListener = object: ComponentAdapter() {\n        override fun componentResized(e: ComponentEvent?) {\n            dispatcher.multicaster.onEditorResized(this@EditorComponentImpl)\n        }\n    }\n\n    private class MyCancelAction(private val self: EditorComponentImpl) : AnAction(\n        \"Cancel\", \"Cancel and delete this comment\", null\n    ) {\n        override fun actionPerformed(e: AnActionEvent) {\n            self.dispatcher.multicaster.onCancelClicked(self)\n        }\n\n        override fun displayTextInToolbar() = true\n    }\n    private val myCancelAction = MyCancelAction(this)\n\n    private class MyAddCommentNowAction(private val self: EditorComponentImpl) : AnAction(\n        \"Add comment now\", \"Add comment to the current position\", null\n    ) {\n        override fun actionPerformed(e: AnActionEvent) {\n            if (self.myEditorTextField.text.trim().isNotBlank()) {\n                self.dispatcher.multicaster.onSubmitClicked(self, false)\n            }\n        }\n\n        override fun update(e: AnActionEvent) {\n            when (self.type) {\n                EditorComponent.Type.NEW_DISCUSSION -> {\n                    e.presentation.text = self.addCommentNowButtonText\n                    e.presentation.description = self.addCommentNowButtonDesc\n                }\n                EditorComponent.Type.REPLY -> {\n                    e.presentation.text = \"Reply\"\n                    e.presentation.description = \"Reply to current discussion thread\"\n                }\n                EditorComponent.Type.EDIT -> {\n                    e.presentation.text = \"Save\"\n                    e.presentation.description = \"Update current comment\"\n                }\n            }\n        }\n\n        override fun useSmallerFontForTextInToolbar(): Boolean = false\n        override fun displayTextInToolbar() = true\n    }\n    private class MyStartAReviewAction(private val self: EditorComponentImpl) : AnAction(\n        \"Save as Draft\", \"Save a comment for now then publish (all) comments later\", null\n    ) {\n        override fun actionPerformed(e: AnActionEvent) {\n            if (self.myEditorTextField.text.trim().isNotBlank()) {\n                self.dispatcher.multicaster.onSubmitClicked(self, true)\n            }\n        }\n\n        override fun update(e: AnActionEvent) {\n            when (self.type) {\n                EditorComponent.Type.NEW_DISCUSSION -> {\n                    e.presentation.text = self.startAReviewButtonText\n                    e.presentation.description = self.startAReviewButtonDesc\n                    e.presentation.isVisible = true\n                }\n                EditorComponent.Type.REPLY, EditorComponent.Type.EDIT -> {\n                    e.presentation.isVisible = false\n                }\n            }\n\n            if (self.isDoingCodeReview) {\n                e.presentation.text = \"Start a review\"\n            } else {\n                e.presentation.text = \"Save as Draft\"\n            }\n        }\n\n        override fun useSmallerFontForTextInToolbar(): Boolean = false\n        override fun displayTextInToolbar() = true\n    }\n    private val myAddCommentNowAction = MyAddCommentNowAction(this)\n    private val myStartAReviewAction = MyStartAReviewAction(this)\n\n    private val myEditorFocusListener = object: FocusListener {\n        override fun focusLost(e: FocusEvent?) {\n            val editor = myEditorTextField.editor\n            if (null === editor) {\n                return\n            }\n            if (myEditorTextField.text.isBlank() || myEditorTextField.text == EMPTY_TEXT_REPLACED) {\n                myEditorTextField.text = \"\"\n                myPanel.toolbar!!.isVisible = false\n                dispatcher.multicaster.onEditorResized(this@EditorComponentImpl)\n            }\n        }\n\n        override fun focusGained(e: FocusEvent?) {\n            if (myEditorTextField.text.isBlank()) {\n                myEditorTextField.text = EMPTY_TEXT_REPLACED\n            }\n            myPanel.toolbar!!.isVisible = true\n            dispatcher.multicaster.onEditorResized(this@EditorComponentImpl)\n            dispatcher.multicaster.onEditorFocused(this@EditorComponentImpl)\n        }\n    }\n\n    init {\n        when(type) {\n            EditorComponent.Type.NEW_DISCUSSION -> myEditorTextField.setPlaceholder(\"Start a new discussion...\")\n            EditorComponent.Type.REPLY -> myEditorTextField.setPlaceholder(\"Reply...\")\n        }\n\n        myPanel.setContent(myEditorTextField)\n        myPanel.toolbar = createToolbar()\n        myPanel.toolbar!!.isVisible = false\n\n        drawBorderTop(false)\n        myEditorTextField.addComponentListener(myComponentListener)\n        myEditorTextField.addFocusListener(myEditorFocusListener)\n    }\n\n    override val component: JComponent = myPanel\n\n    override var text: String\n        get() = myEditorTextField.text\n        set(value) {\n            myEditorTextField.text = value\n        }\n\n    override var addCommentNowButtonText: String = \"Add comment now\"\n    override var addCommentNowButtonDesc: String = \"Add comment to the current position\"\n\n    override var startAReviewButtonText: String = \"Start a review\"\n    override var startAReviewButtonDesc: String = \"Save a comment for now then publish all comments later\"\n\n    override var isVisible: Boolean\n        get() = myPanel.isVisible\n        set(value) {\n            myPanel.isVisible = value\n        }\n\n    override fun focus() {\n        myEditorTextField.grabFocus()\n    }\n\n    override fun drawBorderTop(display: Boolean) {\n        myPanel.border = BorderFactory.createMatteBorder(\n            if (display) 1 else 0, indent * 40 + borderLeftRight, 1, borderLeftRight, JBColor.border()\n        )\n    }\n\n    override fun addListener(listener: EditorComponent.EventListener) = dispatcher.addListener(listener)\n\n    override fun dispose() {\n        dispatcher.listeners.clear()\n    }\n\n    private fun createToolbar(): JComponent {\n        val panel = JPanel(MigLayout(\"ins 0, fill\", \"15[left]push[right]5\", \"center\"))\n\n        val leftActionGroup = DefaultActionGroup()\n        leftActionGroup.add(myStartAReviewAction)\n        leftActionGroup.addSeparator()\n        leftActionGroup.add(myAddCommentNowAction)\n        if (showCancelAction) {\n            leftActionGroup.addSeparator()\n            leftActionGroup.add(myCancelAction)\n        }\n\n        val leftToolbar = ActionManager.getInstance().createActionToolbar(\n            \"${CommentComponentImpl::class.java.canonicalName}/toolbar-left\",\n            leftActionGroup,\n            true\n        )\n\n        val rightActionGroup = DefaultActionGroup()\n        val rightToolbar = ActionManager.getInstance().createActionToolbar(\n            \"${CommentComponentImpl::class.java.canonicalName}/toolbar-left\",\n            rightActionGroup,\n            true\n        )\n\n        panel.add(leftToolbar.component)\n        panel.add(rightToolbar.component)\n        return panel\n    }\n\n    companion object {\n        const val EMPTY_TEXT_REPLACED = \"\\n\"\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/GroupComponent.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.comment\n\nimport com.intellij.openapi.Disposable\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequestIntegrationIde.Component\n\ninterface GroupComponent : Component, Disposable {\n    val id: String\n\n    var comments: List<Comment>\n\n    var collapse: Boolean\n\n    fun requestOpenDialog()\n\n    fun requestDeleteComment(comment: Comment)\n\n    fun requestEditComment(comment: Comment, content: String)\n\n    fun requestToggleResolvedStateOfComment(comment: Comment)\n\n    fun resetReplyEditor()\n\n    fun showReplyEditor()\n\n    fun destroyReplyEditor()\n\n    fun editEditorCreated(comment: Comment, editor: EditorComponent)\n\n    fun editEditorDestroyed(comment: Comment, editor: EditorComponent)\n\n    fun publishDraftComment(comment: Comment)\n\n    fun addListener(listener: EventListener)\n\n    fun hideMoveToDialogButtons()\n\n    fun showMoveToDialogButtons()\n\n    interface EventListener : java.util.EventListener, CommentEvent {\n        fun onResized()\n\n        fun onOpenDialogClicked()\n\n        fun onEditorCreated(groupId: String, editor: EditorComponent)\n\n        fun onEditorDestroyed(groupId: String, editor: EditorComponent)\n\n        fun onReplyCommentRequested(comment: Comment, content: String)\n\n        fun onEditCommentRequested(comment: Comment, content: String)\n\n        fun onPublishDraftCommentRequested(comment: Comment)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/GroupComponentImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.comment\n\nimport com.intellij.openapi.ui.Messages\nimport com.intellij.openapi.project.Project as IdeaProject\nimport com.intellij.ui.JBColor\nimport com.intellij.ui.components.Panel\nimport com.intellij.util.EventDispatcher\nimport com.intellij.util.ui.JBUI\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport javax.swing.BorderFactory\nimport javax.swing.BoxLayout\nimport javax.swing.JComponent\n\nclass GroupComponentImpl(\n    private val factory: CommentComponentFactory,\n    private val borderTop: Boolean,\n    private val providerData: ProviderData,\n    private val mergeRequestInfo: MergeRequestInfo,\n    private val project: IdeaProject,\n    override val id: String,\n    comments: List<Comment>,\n    private val options: Options\n) : GroupComponent {\n    private val dispatcher = EventDispatcher.create(GroupComponent.EventListener::class.java)\n    private val myBoxLayoutPanel = JBUI.Panels.simplePanel()\n    private val myPanel = Panel()\n    private val myCommentComponents = mutableListOf<CommentComponent>()\n    private var myEditor: EditorComponent? = null\n    private var myEditorEventListener = object : EditorComponent.EventListener {\n        override fun onEditorFocused(editor: EditorComponent) {\n        }\n\n        override fun onEditorResized(editor: EditorComponent) {\n            dispatcher.multicaster.onResized()\n        }\n\n        override fun onCancelClicked(editor: EditorComponent) {\n            val shouldDestroy = if (editor.text.isNotBlank()) {\n                val result = Messages.showYesNoDialog(\n                    \"Do you want to delete the whole content?\", \"Are you sure\", Messages.getQuestionIcon()\n                )\n                result == Messages.YES\n            } else true\n\n            if (shouldDestroy) {\n                destroyReplyEditor()\n                dispatcher.multicaster.onResized()\n            } else {\n                editor.focus()\n            }\n        }\n\n        override fun onSubmitClicked(editor: EditorComponent, isDraft: Boolean) {\n            dispatcher.multicaster.onReplyCommentRequested(comments.first(), editor.text)\n        }\n    }\n\n    override var comments: List<Comment> = comments\n        set(value) {\n            if (updateComments(field, value)) {\n                field = value\n            }\n        }\n\n    init {\n        myPanel.layout = BoxLayout(myPanel, BoxLayout.Y_AXIS)\n        if (borderTop) {\n            myPanel.border = BorderFactory.createMatteBorder(1, 0, 0, 0, JBColor.border())\n        }\n        myBoxLayoutPanel.addToCenter(myPanel)\n\n        rerenderComments(comments)\n\n    }\n\n    override var collapse: Boolean = false\n        set(value) {\n            field = value\n            if (value) {\n                myPanel.components.forEachIndexed { index, component ->\n                    if (index != 0) {\n                        component.isVisible = false\n                    }\n                }\n                val editor = myEditor\n                if (null !== editor) {\n                    editor.isVisible = false\n                }\n            } else {\n                myPanel.components.forEachIndexed { index, component ->\n                    if (index != 0) {\n                        component.isVisible = true\n                    }\n                }\n                val editor = myEditor\n                if (null !== editor) {\n                    editor.isVisible = true\n                }\n            }\n            dispatcher.multicaster.onResized()\n        }\n\n    override fun requestOpenDialog() {\n        dispatcher.multicaster.onOpenDialogClicked()\n    }\n\n    override fun requestDeleteComment(comment: Comment) {\n        dispatcher.multicaster.onDeleteCommentRequested(comment)\n    }\n\n    override fun requestEditComment(comment: Comment, content: String) {\n        dispatcher.multicaster.onEditCommentRequested(comment, content)\n    }\n\n    override fun requestToggleResolvedStateOfComment(comment: Comment) {\n        if (comment.resolved) {\n            dispatcher.multicaster.onUnresolveCommentRequested(comment)\n        } else {\n            dispatcher.multicaster.onResolveCommentRequested(comment)\n        }\n    }\n\n    override val component: JComponent = myBoxLayoutPanel\n\n    override fun resetReplyEditor() {\n        destroyReplyEditor()\n        dispatcher.multicaster.onResized()\n    }\n\n    override fun showReplyEditor() {\n        val editor = myEditor\n        if (null === editor) {\n            val createdEditor = factory.makeEditor(\n                project,\n                EditorComponent.Type.REPLY,\n                1,\n                options.borderLeftRight,\n                showCancelAction = true,\n                isDoingCodeReview = false\n            )\n            dispatcher.multicaster.onEditorCreated(this.id, createdEditor)\n            // createdEditor.drawBorderTop(true)\n            createdEditor.addListener(myEditorEventListener)\n\n            myBoxLayoutPanel.addToBottom(createdEditor.component)\n            createdEditor.focus()\n            myEditor = createdEditor\n        } else {\n            editor.focus()\n        }\n    }\n\n    override fun destroyReplyEditor() {\n        val editor = myEditor\n        if (null !== editor) {\n            editor.dispose()\n            myBoxLayoutPanel.remove(editor.component)\n            dispatcher.multicaster.onEditorDestroyed(this.id, editor)\n            myEditor = null\n        }\n    }\n\n    override fun editEditorCreated(comment: Comment, editor: EditorComponent)\n    {\n        dispatcher.multicaster.onEditorCreated(this.id, editor)\n    }\n\n    override fun editEditorDestroyed(comment: Comment, editor: EditorComponent)\n    {\n        editor.dispose()\n        dispatcher.multicaster.onEditorDestroyed(this.id, editor)\n        dispatcher.multicaster.onResized()\n    }\n\n    override fun publishDraftComment(comment: Comment) {\n        dispatcher.multicaster.onPublishDraftCommentRequested(comment)\n    }\n\n    override fun addListener(listener: GroupComponent.EventListener) = dispatcher.addListener(listener)\n\n    override fun hideMoveToDialogButtons() {\n        myCommentComponents.forEach { it.hideMoveToDialogButtons() }\n    }\n\n    override fun showMoveToDialogButtons() {\n        myCommentComponents.forEach { it.showMoveToDialogButtons() }\n    }\n\n    override fun dispose() {\n        dispatcher.listeners.clear()\n    }\n\n    private fun updateComments(old: List<Comment>, new: List<Comment>): Boolean {\n        rerenderComments(new)\n        return true\n        // TODO: Find the way to not update comments if it doesn't change\n//        if (old.size != new.size) {\n//            rerenderComments(new)\n//            return true\n//        }\n//        for (i in 0 until old.lastIndex) {\n//            val oldItem = old[i]\n//            val newItem = new[i]\n//            if (oldItem != newItem) {\n//                rerenderComments(new)\n//                println(\"Change, rerender\")\n//                return true\n//            }\n//        }\n//        println(\"Not change, do nothing\")\n//        return false\n    }\n\n    private fun rerenderComments(items: List<Comment>) {\n        myPanel.removeAll()\n        myCommentComponents.clear()\n\n        items.forEachIndexed { index, comment ->\n            val commentComponent = factory.makeComment(\n                this,\n                providerData,\n                mergeRequestInfo,\n                comment,\n                if (index == 0) 0 else 1,\n                options\n            )\n\n            myPanel.add(commentComponent.component)\n            myCommentComponents.add(commentComponent)\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/comment/Options.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.comment\n\ndata class Options(\n    val borderLeftRight: Int = 1,\n    val showMoveToDialog: Boolean = true,\n    val openEditorInDialog: Boolean = false\n)"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/dialog/LegalWarningDialog.form",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<form xmlns=\"http://www.intellij.com/uidesigner/form/\" version=\"1\" bind-to-class=\"net.ntworld.mergeRequestIntegrationIde.component.dialog.LegalWarningDialog\">\n  <grid id=\"27dc6\" binding=\"myWrapper\" layout-manager=\"GridLayoutManager\" row-count=\"5\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n    <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n    <constraints>\n      <xy x=\"20\" y=\"20\" width=\"584\" height=\"182\"/>\n    </constraints>\n    <properties/>\n    <border type=\"none\"/>\n    <children>\n      <component id=\"4e9d3\" class=\"javax.swing.JLabel\" binding=\"myLegalWarning\">\n        <constraints>\n          <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n        </constraints>\n        <properties>\n          <foreground color=\"-6015167\"/>\n          <text value=\"You cannot use CE for private repositories, please buy Enterprise Edition, only 1$/month.\"/>\n        </properties>\n      </component>\n      <vspacer id=\"48a38\">\n        <constraints>\n          <grid row=\"4\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n        </constraints>\n      </vspacer>\n      <component id=\"e53e7\" class=\"javax.swing.JLabel\" binding=\"myWords1\">\n        <constraints>\n          <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n        </constraints>\n        <properties>\n          <text value=\"The plugin is still working properly so you don't need to hack or find a cracked version.\"/>\n        </properties>\n      </component>\n      <component id=\"51b61\" class=\"javax.swing.JLabel\" binding=\"myWords2\">\n        <constraints>\n          <grid row=\"2\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n        </constraints>\n        <properties>\n          <text value=\"Cracking software invites virus to your computer. \"/>\n        </properties>\n      </component>\n      <component id=\"72f32\" class=\"javax.swing.JLabel\" binding=\"myWords3\">\n        <constraints>\n          <grid row=\"3\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n        </constraints>\n        <properties>\n          <text value=\"It would be nice if you purchase the paid version. This would help me constantly improve the plugin.\"/>\n        </properties>\n      </component>\n    </children>\n  </grid>\n</form>\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/dialog/LegalWarningDialog.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.dialog;\n\nimport net.ntworld.mergeRequestIntegrationIde.Component\nimport javax.swing.*;\n\nclass LegalWarningDialog: Component {\n    var myWrapper: JPanel? = null\n    var myLegalWarning: JLabel? = null\n    var myWords1: JLabel? = null\n    var myWords2: JLabel? = null\n    var myWords3: JLabel? = null\n\n    override val component: JComponent = myWrapper!!\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/gutter/GutterActionType.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.gutter\n\nenum class GutterActionType {\n    ADD,\n    TOGGLE\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/gutter/GutterIconRenderer.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.gutter\n\nimport com.intellij.diff.util.Side\n\ninterface GutterIconRenderer {\n    val visibleLineLeft: Int?\n\n    val visibleLineRight: Int?\n\n    val logicalLine: Int\n\n    val side: Side\n\n    fun setState(state: GutterState)\n\n    fun triggerAddAction()\n\n    fun triggerToggleAction()\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/gutter/GutterIconRendererActionListener.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.gutter\n\ninterface GutterIconRendererActionListener {\n    fun performGutterIconRendererAction(gutterIconRenderer: GutterIconRenderer, type: GutterActionType)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/gutter/GutterIconRendererFactory.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.gutter\n\nimport com.intellij.diff.util.Side\nimport com.intellij.openapi.editor.Editor\nimport com.intellij.openapi.editor.markup.RangeHighlighter\n\nobject GutterIconRendererFactory {\n\n    fun makeGutterIconRenderer(\n        highlighter: RangeHighlighter,\n        showAddIcon: Boolean,\n        logicalLine: Int,\n        visibleLineLeft: Int?,\n        visibleLineRight: Int?,\n        side: Side,\n        actionListener: GutterIconRendererActionListener\n    ): GutterIconRenderer {\n        val gutterIconRenderer = GutterIconRendererImpl(\n            showAddIcon, visibleLineLeft, visibleLineRight, logicalLine, side, actionListener\n        )\n        highlighter.gutterIconRenderer = gutterIconRenderer\n        return gutterIconRenderer\n    }\n\n    fun findGutterIconRenderer(editor: Editor): GutterIconRenderer? {\n        val logicalLine = editor.caretModel.logicalPosition.line\n        for (highlighter in editor.markupModel.allHighlighters) {\n            val gutterRenderer = highlighter.gutterIconRenderer\n            if (gutterRenderer !is GutterIconRenderer || gutterRenderer.logicalLine != logicalLine) {\n                continue\n            }\n            return gutterRenderer\n        }\n        return null\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/gutter/GutterIconRendererImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.gutter\n\nimport com.intellij.diff.util.Side\nimport com.intellij.openapi.actionSystem.AnAction\nimport com.intellij.openapi.actionSystem.AnActionEvent\nimport net.ntworld.mergeRequestIntegrationIde.component.Icons\nimport com.intellij.openapi.editor.markup.GutterIconRenderer as GutterIconRendererClass\n\nclass GutterIconRendererImpl(\n    private val showAddIcon: Boolean,\n    override val visibleLineLeft: Int?,\n    override val visibleLineRight: Int?,\n    override val logicalLine: Int,\n    override val side: Side,\n    private val actionListener: GutterIconRendererActionListener\n) : GutterIconRenderer, GutterIconRendererClass() {\n    private var icon = if (showAddIcon) Icons.Gutter.AddComment else Icons.Gutter.Empty\n    private var desc = \"\"\n\n    private class MyClickAction(private val self: GutterIconRendererImpl) : AnAction() {\n        override fun actionPerformed(e: AnActionEvent) {\n            if (self.icon == Icons.Gutter.AddComment || self.icon == Icons.Gutter.Empty) {\n                self.actionListener.performGutterIconRendererAction(self, GutterActionType.ADD)\n            } else {\n                self.actionListener.performGutterIconRendererAction(self, GutterActionType.TOGGLE)\n            }\n        }\n    }\n\n    private val clickAction = MyClickAction(this)\n\n    override fun setState(state: GutterState) {\n        when (state) {\n            GutterState.NO_COMMENT -> {\n                icon = if (showAddIcon) Icons.Gutter.AddComment else Icons.Gutter.Empty\n                desc = if (showAddIcon) \"Add new comment\" else \"\"\n            }\n            GutterState.THREAD_HAS_SINGLE_COMMENT -> {\n                icon = Icons.Gutter.Comment\n                desc = \"Toggle comment thread\"\n            }\n            GutterState.THREAD_HAS_MULTI_COMMENTS -> {\n                icon = Icons.Gutter.Comments\n                desc = \"Toggle comment thread\"\n            }\n            GutterState.WRITING -> {\n                icon = Icons.Gutter.WritingComment\n                desc = \"Toggle comment thread & continue writing your comment\"\n            }\n            GutterState.HAS_DRAFT -> {\n                icon = Icons.Gutter.HasDraft\n                desc = \"Toggle comment thread to see draft comment\"\n            }\n        }\n    }\n\n    override fun triggerAddAction() {\n        actionListener.performGutterIconRendererAction(this, GutterActionType.ADD)\n    }\n\n    override fun triggerToggleAction() {\n        actionListener.performGutterIconRendererAction(this, GutterActionType.TOGGLE)\n    }\n\n    override fun getClickAction(): AnAction = clickAction\n    override fun getIcon() = icon\n    override fun getTooltipText(): String? = desc\n    override fun isNavigateAction() = icon != Icons.Gutter.Empty\n    override fun hashCode(): Int = System.identityHashCode(this)\n    override fun equals(other: Any?): Boolean = other == this\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/gutter/GutterPosition.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.gutter\n\nimport net.ntworld.mergeRequestIntegrationIde.diff.DiffView\n\ndata class GutterPosition(\n    val editorType: DiffView.EditorType,\n    val changeType: DiffView.ChangeType,\n    val oldLine: Int?,\n    val newLine: Int?,\n    val oldPath: String?,\n    val newPath: String?,\n    val baseHash: String? = null,\n    val startHash: String? = null,\n    val headHash: String? = null\n)\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/gutter/GutterState.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.gutter\n\nenum class GutterState {\n    NO_COMMENT,\n    THREAD_HAS_SINGLE_COMMENT,\n    THREAD_HAS_MULTI_COMMENTS,\n    WRITING,\n    HAS_DRAFT\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/thread/ThreadFactory.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.thread\n\nimport com.intellij.diff.util.Side\nimport com.intellij.openapi.editor.ex.EditorEx\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\n\nobject ThreadFactory {\n    fun makeModel(comments: List<Comment>): ThreadModel {\n        return ThreadModelImpl(comments, false)\n    }\n\n    fun makeView(\n        projectServiceProvider: ProjectServiceProvider,\n        editor: EditorEx,\n        providerData: ProviderData,\n        mergeRequestInfo: MergeRequestInfo,\n        logicalLine: Int,\n        side: Side,\n        position: GutterPosition,\n        replyInDialog: Boolean = false\n    ): ThreadView {\n        return ThreadViewImpl(\n            projectServiceProvider, editor, providerData, mergeRequestInfo, logicalLine, side, position, replyInDialog\n        )\n    }\n\n    fun makePresenter(model: ThreadModel, view: ThreadView): ThreadPresenter {\n        return ThreadPresenterImpl(model, view)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/thread/ThreadModel.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.thread\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequestIntegrationIde.Model\nimport java.util.*\n\ninterface ThreadModel : Model<ThreadModel.DataListener> {\n    var comments: List<Comment>\n\n    var visible: Boolean\n\n    var showEditor: Boolean\n\n    fun resetEditor(comment: Comment?)\n\n    interface DataListener : EventListener {\n        fun onCommentsChanged(comments: List<Comment>)\n\n        fun onVisibilityChanged(visibility: Boolean)\n\n        fun onEditorVisibilityChanged(visibility: Boolean)\n\n        fun onEditorReset(comment: Comment?)\n    }\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/thread/ThreadModelImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.thread\n\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequestIntegrationIde.AbstractModel\n\nclass ThreadModelImpl(\n    comments: List<Comment>,\n     visibility: Boolean\n) : AbstractModel<ThreadModel.DataListener>(), ThreadModel {\n    override val dispatcher = EventDispatcher.create(ThreadModel.DataListener::class.java)\n\n    override var comments: List<Comment> = comments\n        set(value) {\n            field = value\n            dispatcher.multicaster.onCommentsChanged(value)\n        }\n\n    override var visible: Boolean = visibility\n        set(value) {\n            if (field == value) {\n                return\n            }\n            field = value\n            dispatcher.multicaster.onVisibilityChanged(value)\n        }\n\n    override var showEditor: Boolean = false\n        set(value) {\n            field = value\n            dispatcher.multicaster.onEditorVisibilityChanged(value)\n        }\n\n    override fun resetEditor(comment: Comment?) {\n        dispatcher.multicaster.onEditorReset(comment)\n    }\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/thread/ThreadPresenter.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.thread\n\nimport com.intellij.diff.util.Side\nimport com.intellij.openapi.Disposable\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequestIntegrationIde.Presenter\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.CommentEvent\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition\n\ninterface ThreadPresenter : Presenter<ThreadPresenter.EventListener>, Disposable {\n    val model: ThreadModel\n\n    val view: ThreadView\n\n    interface EventListener: CommentEvent {\n        fun onMainEditorClosed(threadPresenter: ThreadPresenter)\n\n        fun onEditCommentRequested(comment: Comment, content: String)\n\n        fun onReplyCommentRequested(content: String, repliedComment: Comment, logicalLine: Int, side: Side)\n\n        fun onCreateCommentRequested(content: String, position: GutterPosition, logicalLine: Int, side: Side, isDraft: Boolean)\n\n        fun onPublishDraftCommentRequested(comment: Comment)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/thread/ThreadPresenterImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.thread\n\nimport com.intellij.diff.util.Side\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequestIntegrationIde.AbstractPresenter\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.CommentEvent\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.CommentEventPropagator\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition\nimport net.ntworld.mergeRequestIntegrationIde.util.CommentUtil\n\nclass ThreadPresenterImpl(\n    override val model: ThreadModel,\n    override val view: ThreadView\n) : AbstractPresenter<ThreadPresenter.EventListener>(), ThreadPresenter, ThreadModel.DataListener {\n    override val dispatcher = EventDispatcher.create(ThreadPresenter.EventListener::class.java)\n    private val myCommentEventPropagator =\n        CommentEventPropagator(dispatcher)\n    private val myThreadViewActionListener = object : ThreadView.ActionListener,\n        CommentEvent by myCommentEventPropagator {\n        override fun onMainEditorClosed() {\n            dispatcher.multicaster.onMainEditorClosed(this@ThreadPresenterImpl)\n        }\n\n        override fun onEditCommentRequested(comment: Comment, content: String) {\n            dispatcher.multicaster.onEditCommentRequested(comment, content)\n        }\n\n        override fun onPublishDraftCommentRequested(comment: Comment) {\n            dispatcher.multicaster.onPublishDraftCommentRequested(comment)\n        }\n\n        override fun onCreateCommentRequested(\n            content: String, logicalLine: Int, side: Side,\n            repliedComment: Comment?, position: GutterPosition?,\n            isDraft: Boolean\n        ) {\n            if (null !== repliedComment) {\n                dispatcher.multicaster.onReplyCommentRequested(content, repliedComment, logicalLine, side)\n            }\n            if (null !== position) {\n                dispatcher.multicaster.onCreateCommentRequested(content, position, logicalLine, side, isDraft)\n            }\n        }\n    }\n\n    init {\n        model.addDataListener(this)\n        view.addActionListener(myThreadViewActionListener)\n        view.initialize()\n        onCommentsChanged(model.comments)\n    }\n\n    override fun dispose() {\n        view.dispose()\n    }\n\n    override fun onCommentsChanged(comments: List<Comment>) {\n        val groups = CommentUtil.groupCommentsByThreadId(model.comments)\n        val currentGroups = view.getAllGroupOfCommentsIds().toMutableSet()\n\n        groups.forEach { (id, items) ->\n            if (view.hasGroupOfComments(id)) {\n                view.updateGroupOfComments(id, items)\n            } else {\n                view.addGroupOfComments(id, items)\n            }\n            currentGroups.remove(id)\n        }\n\n        currentGroups.forEach { id ->\n            view.deleteGroupOfComments(id)\n        }\n\n        if (model.visible) {\n            view.show()\n        } else {\n            view.hide()\n        }\n    }\n\n    override fun onVisibilityChanged(visibility: Boolean) {\n        if (model.visible) {\n            view.show()\n            if (model.showEditor) {\n                view.showEditor()\n            }\n        } else {\n            view.hide()\n        }\n    }\n\n    override fun onEditorVisibilityChanged(visibility: Boolean) {\n        if (visibility) {\n            view.show()\n            view.showEditor()\n        }\n    }\n\n    override fun onEditorReset(comment: Comment?) {\n        if (null === comment) {\n            model.showEditor = false\n            view.resetMainEditor()\n        } else {\n            view.resetEditorOfGroup(comment.parentId)\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/thread/ThreadView.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.thread\n\nimport com.intellij.diff.util.Side\nimport com.intellij.openapi.Disposable\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequestIntegrationIde.View\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.CommentEvent\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition\n\ninterface ThreadView : View<ThreadView.ActionListener>, Disposable {\n    val logicalLine: Int\n\n    val side: Side\n\n    val position: GutterPosition\n\n    fun initialize()\n\n    fun getAllGroupOfCommentsIds(): Set<String>\n\n    fun hasGroupOfComments(groupId: String): Boolean\n\n    fun addGroupOfComments(groupId: String, comments: List<Comment>)\n\n    fun updateGroupOfComments(groupId: String, comments: List<Comment>)\n\n    fun deleteGroupOfComments(groupId: String)\n\n    fun resetMainEditor()\n\n    fun resetEditorOfGroup(groupId: String)\n\n    fun showEditor()\n\n    fun show()\n\n    fun hide()\n\n    interface ActionListener : CommentEvent {\n        fun onMainEditorClosed()\n\n        fun onEditCommentRequested(comment: Comment, content: String)\n\n        fun onPublishDraftCommentRequested(comment: Comment)\n\n        fun onCreateCommentRequested(\n            content: String,\n            logicalLine: Int,\n            side: Side,\n            repliedComment: Comment?,\n            position: GutterPosition?,\n            isDraft: Boolean\n        )\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/component/thread/ThreadViewImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.component.thread\n\nimport com.intellij.diff.util.Side\nimport com.intellij.openapi.Disposable\nimport com.intellij.openapi.editor.ex.EditorEx\nimport com.intellij.openapi.editor.ex.util.EditorUtil\nimport com.intellij.openapi.editor.impl.EditorEmbeddedComponentManager\nimport com.intellij.openapi.editor.impl.EditorImpl\nimport com.intellij.openapi.editor.impl.view.FontLayoutService\nimport com.intellij.openapi.ui.DialogBuilder\nimport com.intellij.openapi.ui.Messages\nimport com.intellij.openapi.util.Disposer\nimport com.intellij.ui.components.JBScrollPane\nimport com.intellij.ui.components.Panel\nimport com.intellij.util.EventDispatcher\nimport com.intellij.util.ui.JBUI\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.AbstractView\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.*\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport java.awt.Cursor\nimport java.awt.Dimension\nimport java.awt.Font\nimport java.awt.GridBagLayout\nimport java.awt.event.ComponentAdapter\nimport java.awt.event.ComponentEvent\nimport javax.swing.BoxLayout\nimport javax.swing.JComponent\nimport javax.swing.JPanel\nimport javax.swing.ScrollPaneConstants\nimport kotlin.math.ceil\nimport kotlin.math.max\nimport kotlin.math.min\n\nclass ThreadViewImpl(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val editor: EditorEx,\n    private val providerData: ProviderData,\n    private val mergeRequestInfo: MergeRequestInfo,\n    override val logicalLine: Int,\n    override val side: Side,\n    override val position: GutterPosition,\n    private val replyInDialog: Boolean\n) : AbstractView<ThreadView.ActionListener>(), ThreadView {\n    override val dispatcher = EventDispatcher.create(ThreadView.ActionListener::class.java)\n\n    private val myThreadPanel = Panel()\n    private val myBoxLayoutPanel = JBUI.Panels.simplePanel()\n    private val myComponent = MyComponent(myBoxLayoutPanel)\n    private val myWrapper = JPanel()\n    private val myEditorWidthWatcher = EditorTextWidthWatcher()\n    private val myCreatedEditors = mutableMapOf<String, EditorComponent>()\n    private val myGroups = mutableMapOf<String, GroupComponent>()\n    private val myEditor by lazy {\n        val editorComponent = projectServiceProvider.componentFactory.commentComponents.makeEditor(\n            editor.project!!, EditorComponent.Type.NEW_DISCUSSION, 0, borderLeftRight = 1, showCancelAction = true,\n            isDoingCodeReview = projectServiceProvider.isDoingCodeReview()\n        )\n        editorComponent.isVisible = false\n        editorComponent.addListener(myEditorComponentEventListener)\n        Disposer.register(this@ThreadViewImpl, editorComponent)\n\n        myCreatedEditors[\"\"] = editorComponent\n        myBoxLayoutPanel.addToBottom(editorComponent.component)\n        editorComponent\n    }\n    private val myEditorComponentEventListener = object : EditorComponent.EventListener {\n        override fun onEditorFocused(editor: EditorComponent) {}\n\n        override fun onEditorResized(editor: EditorComponent) {\n            myEditorWidthWatcher.updateWidthForAllInlays()\n        }\n\n        override fun onCancelClicked(editor: EditorComponent) {\n            val mainEditor = myCreatedEditors[\"\"]\n            if (null !== mainEditor && editor === mainEditor) {\n                val shouldHide = if (mainEditor.text.isNotBlank()) {\n                    val result = Messages.showYesNoDialog(\n                        \"Do you want to delete the whole content?\", \"Are you sure\", Messages.getQuestionIcon()\n                    )\n                    result == Messages.YES\n                } else true\n\n                if (shouldHide) {\n                    mainEditor.text = \"\"\n                    mainEditor.isVisible = false\n                    myEditorWidthWatcher.updateWidthForAllInlays()\n                    dispatcher.multicaster.onMainEditorClosed()\n                } else {\n                    mainEditor.focus()\n                }\n            }\n        }\n\n        override fun onSubmitClicked(editor: EditorComponent, isDraft: Boolean) {\n            val mainEditor = myCreatedEditors[\"\"]\n            if (null !== mainEditor && editor === mainEditor) {\n                dispatcher.multicaster.onCreateCommentRequested(\n                    mainEditor.text,\n                    logicalLine,\n                    side,\n                    repliedComment = null,\n                    position = position,\n                    isDraft = isDraft\n                )\n            }\n        }\n    }\n    private val myCommentEventPropagator = CommentEventPropagator(dispatcher)\n    private val myGroupComponentEventListener = object : GroupComponent.EventListener,\n        CommentEvent by myCommentEventPropagator {\n        override fun onResized() {\n            myEditorWidthWatcher.updateWidthForAllInlays()\n        }\n\n        override fun onOpenDialogClicked() {\n            val builder = DialogBuilder()\n            builder.setCenterPanel(myComponent)\n\n            myEditorWidthWatcher.updateWidthForAllInlays()\n            myGroups.forEach { it.value.hideMoveToDialogButtons() }\n\n            builder.removeAllActions()\n            builder.addOkAction()\n            builder.resizable(true)\n            builder.show()\n\n            // This line run after the dialog is closed\n            myGroups.forEach { it.value.showMoveToDialogButtons() }\n            myWrapper.add(myComponent)\n        }\n\n        override fun onEditorCreated(groupId: String, editor: EditorComponent) {\n            editor.addListener(myEditorComponentEventListener)\n            Disposer.register(this@ThreadViewImpl, editor)\n\n            myCreatedEditors[groupId] = editor\n        }\n\n        override fun onEditorDestroyed(groupId: String, editor: EditorComponent) {\n            myCreatedEditors.remove(groupId)\n        }\n\n        override fun onReplyCommentRequested(comment: Comment, content: String) {\n            dispatcher.multicaster.onCreateCommentRequested(\n                content,\n                logicalLine,\n                side,\n                repliedComment = comment,\n                position = null,\n                isDraft = false\n            )\n        }\n\n        override fun onEditCommentRequested(comment: Comment, content: String) {\n            dispatcher.multicaster.onEditCommentRequested(comment, content)\n        }\n\n        override fun onPublishDraftCommentRequested(comment: Comment) {\n            dispatcher.multicaster.onPublishDraftCommentRequested(comment)\n        }\n    }\n\n    init {\n        myBoxLayoutPanel.addToCenter(myThreadPanel)\n        myThreadPanel.layout = BoxLayout(myThreadPanel, BoxLayout.Y_AXIS)\n\n        myComponent.isVisible = false\n        myComponent.cursor = Cursor.getDefaultCursor()\n\n        myWrapper.layout = GridBagLayout()\n        myWrapper.add(myComponent)\n    }\n\n    override fun dispose() {\n        editor.scrollPane.viewport.removeComponentListener(myEditorWidthWatcher)\n        myCreatedEditors.values.forEach { it.dispose() }\n    }\n\n    override fun initialize() {\n        editor.scrollPane.viewport.addComponentListener(myEditorWidthWatcher)\n        Disposer.register(this, Disposable {\n            editor.scrollPane.viewport.removeComponentListener(myEditorWidthWatcher)\n        })\n\n        val editorEmbeddedComponentManager = EditorEmbeddedComponentManager.getInstance()\n        val offset = editor.document.getLineEndOffset(logicalLine)\n        editorEmbeddedComponentManager.addComponent(\n            editor,\n            myWrapper,\n            projectServiceProvider.intellijIdeApi.makeEditorEmbeddedComponentManagerProperties(offset)\n        )\n\n        EditorUtil.disposeWithEditor(editor, this)\n    }\n\n    override fun getAllGroupOfCommentsIds(): Set<String> {\n        return myGroups.keys.toSet()\n    }\n\n    override fun hasGroupOfComments(groupId: String): Boolean = myGroups.containsKey(groupId)\n\n    override fun addGroupOfComments(groupId: String, comments: List<Comment>) {\n        val group = projectServiceProvider.componentFactory.commentComponents.makeGroup(\n            providerData, mergeRequestInfo, editor.project!!, myGroups.isEmpty(), groupId, comments,\n            Options(borderLeftRight = 1, showMoveToDialog = true, openEditorInDialog = replyInDialog)\n        )\n        group.addListener(myGroupComponentEventListener)\n        Disposer.register(this, group)\n\n        myGroups[groupId] = group\n        myThreadPanel.add(group.component)\n    }\n\n    override fun updateGroupOfComments(groupId: String, comments: List<Comment>) {\n        val group = myGroups[groupId]\n        if (null !== group) {\n            group.comments = comments\n        }\n    }\n\n    override fun deleteGroupOfComments(groupId: String) {\n        val group = myGroups[groupId]\n        if (null !== group) {\n            myThreadPanel.remove(group.component)\n            group.dispose()\n            myGroups.remove(groupId)\n        }\n    }\n\n    override fun resetMainEditor() {\n        myEditor.text = \"\"\n        myEditor.isVisible = false\n        myEditorWidthWatcher.updateWidthForAllInlays()\n    }\n\n    override fun resetEditorOfGroup(groupId: String) {\n        val group = myGroups[groupId]\n        if (null !== group) {\n            group.resetReplyEditor()\n        }\n    }\n\n    override fun showEditor() {\n        myEditor.isVisible = true\n        myEditor.focus()\n        myEditor.drawBorderTop(myGroups.isEmpty())\n        myEditorWidthWatcher.updateWidthForAllInlays()\n    }\n\n    override fun show() {\n        myComponent.isVisible = true\n        myEditorWidthWatcher.updateWidthForAllInlays()\n    }\n\n    override fun hide() {\n        myComponent.isVisible = false\n        myEditorWidthWatcher.updateWidthForAllInlays()\n    }\n\n    private inner class MyComponent(private val component: JComponent) : JBScrollPane(component) {\n        init {\n            isOpaque = false\n            viewport.isOpaque = false\n\n            border = JBUI.Borders.empty()\n            viewportBorder = JBUI.Borders.empty()\n\n            horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER\n            verticalScrollBar.preferredSize = Dimension(0, 0)\n            setViewportView(component)\n\n            component.addComponentListener(object : ComponentAdapter() {\n                override fun componentResized(e: ComponentEvent) {\n                    return dispatchEvent(ComponentEvent(component, ComponentEvent.COMPONENT_RESIZED))\n                }\n            })\n        }\n\n        override fun getPreferredSize(): Dimension {\n            return Dimension(\n                myEditorWidthWatcher.editorTextWidth,\n                if (component.isVisible) component.preferredSize.height else 0\n            )\n        }\n    }\n\n    private inner class EditorTextWidthWatcher : ComponentAdapter() {\n        var editorTextWidth: Int = 0\n\n        private val maximumEditorTextWidth: Int\n        private val verticalScrollbarFlipped: Boolean\n\n        init {\n            val metrics = (editor as EditorImpl).getFontMetrics(Font.PLAIN)\n            val spaceWidth = FontLayoutService.getInstance().charWidth2D(metrics, ' '.toInt())\n            maximumEditorTextWidth = ceil(spaceWidth * (editor.settings.getRightMargin(editor.project)) - 1).toInt()\n\n            val scrollbarFlip = editor.scrollPane.getClientProperty(JBScrollPane.Flip::class.java)\n            verticalScrollbarFlipped = scrollbarFlip == JBScrollPane.Flip.HORIZONTAL ||\n                scrollbarFlip == JBScrollPane.Flip.BOTH\n        }\n\n        override fun componentResized(e: ComponentEvent) = updateWidthForAllInlays()\n        override fun componentHidden(e: ComponentEvent) = updateWidthForAllInlays()\n        override fun componentShown(e: ComponentEvent) = updateWidthForAllInlays()\n\n        fun updateWidthForAllInlays() {\n            val newWidth = calcWidth()\n            editorTextWidth = newWidth\n\n            myWrapper.dispatchEvent(ComponentEvent(myWrapper, ComponentEvent.COMPONENT_RESIZED))\n            myWrapper.invalidate()\n        }\n\n        private fun calcWidth(): Int {\n            val visibleEditorTextWidth =\n                editor.scrollPane.viewport.width - getVerticalScrollbarWidth() - getGutterTextGap()\n            return min(max(visibleEditorTextWidth, 0), maximumEditorTextWidth)\n        }\n\n        private fun getVerticalScrollbarWidth(): Int {\n            val width = editor.scrollPane.verticalScrollBar.width\n            return if (!verticalScrollbarFlipped) width * 2 else width\n        }\n\n        private fun getGutterTextGap(): Int {\n            return if (verticalScrollbarFlipped) {\n                val gutter = editor.gutterComponentEx\n                gutter.width - gutter.whitespaceSeparatorOffset\n            } else 0\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/configuration/README.md",
    "content": "## Notes\n\nI'm going to refactor everything to MVP architecture and DDD style.\n\nEach module is a separated domain (DDD), each domain can have a sub-domain.\n\nFor now these are the domain:\n\n- configuration: For configuration views\n- mergeRequest : For merge request views -> this is big domain\n    - Comment  : sub-domain of merge Request\n    - ...\n- diff         : domain for everything related to diff view\n- home         : domain for home tab \n\nEach domain has:\n\n- model\n- view\n- presenter\n- util          (if needed)\n- internal      (if needed)\n- entity        (if needed)\n- vos           (if needed)\n- ...\n\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/configuration/vos/GitRemotePathInfo.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.configuration.vos\n\nimport java.net.URL\n\nclass GitRemotePathInfo(private val input: String) {\n    var namespace: String = \"\"\n        private set\n\n    var project: String = \"\"\n        private set\n\n    var isValid: Boolean = false\n        private set\n\n    init {\n        val inputLowerCase = input.toLowerCase()\n        if (inputLowerCase.startsWith(\"https://\") || inputLowerCase.startsWith(\"http://\")) {\n            parseHttpUrl(input)\n        }\n        if (inputLowerCase.startsWith(\"git@\")) {\n            parseSshUrl(input)\n        }\n    }\n\n    private fun parseSshUrl(input: String) {\n        val index = input.lastIndexOf(':')\n        if (index < 0) {\n            return\n        }\n        val parts = input.substring(index + 1).split('/')\n        if (parts.size != 2) {\n            return\n        }\n        namespace = parts[0]\n        project = if (parts[1].endsWith(\".git\")) parts[1].substring(0, parts[1].length - 4) else parts[1]\n        isValid = true\n    }\n\n    private fun parseHttpUrl(input: String) {\n        val url = URL(input)\n        val parts = url.path.split('/')\n        if (parts.size != 3) {\n            return\n        }\n        namespace = parts[1]\n        project = if (parts[2].endsWith(\".git\")) parts[2].substring(0, parts[2].length - 4) else parts[2]\n        isValid = true\n    }\n\n    override fun toString(): String {\n        return \"$namespace/$project\"\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/AbstractDiffView.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.diff\n\nimport com.intellij.diff.tools.util.base.DiffViewerBase\nimport com.intellij.diff.tools.util.base.DiffViewerListener\nimport com.intellij.diff.util.Side\nimport com.intellij.diff.util.TextDiffType\nimport com.intellij.openapi.editor.Editor\nimport com.intellij.openapi.editor.ex.EditorEx\nimport com.intellij.openapi.editor.markup.TextAttributes\nimport com.intellij.openapi.util.Disposer\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.AbstractView\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.CommentEvent\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.CommentEventPropagator\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.*\nimport net.ntworld.mergeRequestIntegrationIde.component.thread.ThreadFactory\nimport net.ntworld.mergeRequestIntegrationIde.component.thread.ThreadModel\nimport net.ntworld.mergeRequestIntegrationIde.component.thread.ThreadPresenter\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\n\nabstract class AbstractDiffView<V : DiffViewerBase>(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val viewerBase: DiffViewerBase\n) : AbstractView<DiffView.ActionListener>(), DiffView<V> {\n    final override val dispatcher = EventDispatcher.create(DiffView.ActionListener::class.java)\n    private val diffViewerListener = object : DiffViewerListener() {\n        override fun onInit() = dispatcher.multicaster.onInit()\n        override fun onDispose() = dispatcher.multicaster.onDispose()\n        override fun onBeforeRediff() = dispatcher.multicaster.onBeforeRediff()\n        override fun onAfterRediff() = dispatcher.multicaster.onAfterRediff()\n        override fun onRediffAborted() = dispatcher.multicaster.onRediffAborted()\n    }\n    private val myGutterIconRenderersOfLeft = mutableMapOf<Int, GutterIconRenderer>()\n    private val myGutterIconRenderersOfRight = mutableMapOf<Int, GutterIconRenderer>()\n\n    private val myThreadOfLeft = mutableMapOf<Int, ThreadPresenter>()\n    private val myThreadOfRight = mutableMapOf<Int, ThreadPresenter>()\n    private val myCommentEventPropagator = CommentEventPropagator(dispatcher)\n    private val myThreadPresenterEventListener = object : ThreadPresenter.EventListener,\n        CommentEvent by myCommentEventPropagator {\n        override fun onMainEditorClosed(threadPresenter: ThreadPresenter) {\n            val renderer = findGutterIconRenderer(threadPresenter.view.logicalLine, threadPresenter.view.side)\n            if (null !== renderer) {\n                if (threadPresenter.model.comments.isEmpty()) {\n                    renderer.setState(GutterState.NO_COMMENT)\n                } else {\n                    if (threadPresenter.model.comments.filter { it.isDraft }.isNotEmpty()) {\n                        renderer.setState(GutterState.HAS_DRAFT)\n                    } else {\n                        renderer.setState(\n                            if (threadPresenter.model.comments.size == 1)\n                                GutterState.THREAD_HAS_SINGLE_COMMENT\n                            else\n                                GutterState.THREAD_HAS_MULTI_COMMENTS\n                        )\n                    }\n                }\n            }\n        }\n\n        override fun onEditCommentRequested(comment: Comment, content: String) {\n            dispatcher.multicaster.onEditCommentRequested(comment, content)\n        }\n\n        override fun onReplyCommentRequested(content: String, repliedComment: Comment, logicalLine: Int, side: Side) {\n            if (content.trim().isNotEmpty()) {\n                dispatcher.multicaster.onReplyCommentRequested(content, repliedComment, logicalLine, side)\n            }\n        }\n\n        override fun onCreateCommentRequested(content: String, position: GutterPosition, logicalLine: Int, side: Side, isDraft: Boolean) {\n            if (content.trim().isNotEmpty()) {\n                dispatcher.multicaster.onCreateCommentRequested(content, position, logicalLine, side, isDraft)\n            }\n        }\n\n        override fun onPublishDraftCommentRequested(comment: Comment) {\n            dispatcher.multicaster.onPublishDraftCommentRequested(comment)\n        }\n    }\n\n    protected val myGutterIconRendererActionListener = object : GutterIconRendererActionListener {\n        override fun performGutterIconRendererAction(gutterIconRenderer: GutterIconRenderer, type: GutterActionType) {\n            dispatcher.multicaster.onGutterActionPerformed(gutterIconRenderer, type, DiffView.DisplayCommentMode.TOGGLE)\n        }\n    }\n\n    init {\n        viewerBase.addListener(diffViewerListener)\n    }\n\n    protected abstract fun convertVisibleLineToLogicalLine(visibleLine: Int, side: Side): Int\n\n    override fun destroyExistingComments(excludedVisibleLines: Set<Int>, side: Side) {\n        val excludedLogicalLines = excludedVisibleLines\n            .map { convertVisibleLineToLogicalLine(it, side) }\n            .filter { it >= 0 }\n\n        val map = if (side == Side.LEFT) myThreadOfLeft else myThreadOfRight\n        val removedKeys = mutableListOf<Int>()\n        for (entry in map) {\n            if (excludedLogicalLines.contains(entry.key)) {\n                continue\n            }\n            entry.value.model.comments = listOf()\n            removedKeys.add(entry.key)\n        }\n        removedKeys.forEach { map.remove(it) }\n    }\n\n    override fun showAllComments() = changeAllCommentsVisibility(true)\n\n    override fun hideAllComments() = changeAllCommentsVisibility(false)\n\n    override fun resetGutterIcons() {\n        for (renderer in myGutterIconRenderersOfLeft.values) {\n            renderer.setState(GutterState.NO_COMMENT)\n        }\n        for (renderer in myGutterIconRenderersOfRight.values) {\n            renderer.setState(GutterState.NO_COMMENT)\n        }\n    }\n\n    override fun resetEditorOnLine(logicalLine: Int, side: Side, repliedComment: Comment?) {\n        val map = if (side == Side.LEFT) myThreadOfLeft else myThreadOfRight\n\n        val thread = map[logicalLine]\n        if (null !== thread) {\n            thread.model.resetEditor(repliedComment)\n        }\n    }\n\n    override fun dispose() {\n        myGutterIconRenderersOfLeft.clear()\n        myGutterIconRenderersOfRight.clear()\n    }\n\n    protected fun initializeThreadOnLineIfNotAvailable(\n        providerData: ProviderData,\n        mergeRequestInfo: MergeRequestInfo,\n        editor: EditorEx,\n        position: GutterPosition,\n        logicalLine: Int,\n        side: Side,\n        comments: List<Comment>\n    ) {\n        val map = if (side == Side.LEFT) myThreadOfLeft else myThreadOfRight\n        if (!map.containsKey(logicalLine)) {\n            val model = ThreadFactory.makeModel(comments)\n            val view = ThreadFactory.makeView(\n                projectServiceProvider, editor, providerData, mergeRequestInfo, logicalLine, side, position\n            )\n            val presenter = ThreadFactory.makePresenter(model, view)\n\n            presenter.addListener(myThreadPresenterEventListener)\n            Disposer.register(this, presenter)\n\n            map[logicalLine] = presenter\n        }\n    }\n\n    protected fun registerGutterIconRenderer(renderer: GutterIconRenderer) {\n        val map = if (renderer.side == Side.LEFT) myGutterIconRenderersOfLeft else myGutterIconRenderersOfRight\n\n        map[renderer.logicalLine] = renderer\n    }\n\n    protected fun findGutterIconRenderer(logicalLine: Int, side: Side): GutterIconRenderer? {\n        val map = if (side == Side.LEFT) myGutterIconRenderersOfLeft else myGutterIconRenderersOfRight\n\n        return map[logicalLine]\n    }\n\n    override fun displayComments(visibleLine: Int, side: Side, mode: DiffView.DisplayCommentMode) {\n        val logicalLine = convertVisibleLineToLogicalLine(visibleLine, side)\n        if (-1 != logicalLine) {\n            assertThreadAvailable(logicalLine, side) {\n                displayComments(it.model, logicalLine, side, mode)\n            }\n        }\n    }\n\n    override fun displayComments(renderer: GutterIconRenderer, mode: DiffView.DisplayCommentMode) {\n        assertThreadAvailable(renderer.logicalLine, renderer.side) {\n            displayComments(it.model, renderer.logicalLine, renderer.side, mode)\n        }\n    }\n\n    protected fun displayComments(\n        model: ThreadModel,\n        logicalLine: Int,\n        side: Side,\n        mode: DiffView.DisplayCommentMode\n    ) {\n        when (mode) {\n            DiffView.DisplayCommentMode.TOGGLE -> model.visible = !model.visible\n            DiffView.DisplayCommentMode.SHOW -> model.visible = true\n            DiffView.DisplayCommentMode.HIDE -> model.visible = false\n        }\n        setWritingStateOfGutterIconRenderer(model, logicalLine, side)\n    }\n\n    override fun displayEditorOnLine(logicalLine: Int, side: Side) {\n        assertThreadAvailable(logicalLine, side) {\n            it.model.showEditor = true\n            setWritingStateOfGutterIconRenderer(it.model, logicalLine, side)\n        }\n    }\n\n    protected fun updateComments(renderer: GutterIconRenderer, comments: List<Comment>) {\n        assertThreadAvailable(renderer.logicalLine, renderer.side) {\n            it.model.comments = comments\n        }\n        updateGutterIcon(renderer, comments)\n    }\n\n    // TODO: Duplicate EditorManagerImpl.updateGutterIcon maybe move to Util class\n    protected fun updateGutterIcon(renderer: GutterIconRenderer, comments: List<Comment>) {\n        val state = if (comments.isEmpty()) {\n            GutterState.NO_COMMENT\n        } else {\n            if (comments.filter { it.isDraft }.isNotEmpty()) {\n                GutterState.HAS_DRAFT\n            } else {\n                if (comments.size == 1) GutterState.THREAD_HAS_SINGLE_COMMENT else GutterState.THREAD_HAS_MULTI_COMMENTS\n            }\n        }\n\n        renderer.setState(state)\n    }\n\n    private fun changeAllCommentsVisibility(displayed: Boolean) {\n        val mode = if (displayed) DiffView.DisplayCommentMode.SHOW else DiffView.DisplayCommentMode.HIDE\n        for (renderer in myGutterIconRenderersOfLeft.values) {\n            dispatcher.multicaster.onGutterActionPerformed(\n                renderer, GutterActionType.TOGGLE, mode\n            )\n        }\n        for (renderer in myGutterIconRenderersOfRight.values) {\n            dispatcher.multicaster.onGutterActionPerformed(\n                renderer, GutterActionType.TOGGLE, mode\n            )\n        }\n    }\n\n    private fun setWritingStateOfGutterIconRenderer(model: ThreadModel, logicalLine: Int, side: Side) {\n        val renderer = findGutterIconRenderer(logicalLine, side)\n        if (null !== renderer && model.showEditor) {\n            renderer.setState(GutterState.WRITING)\n        }\n    }\n\n    private fun assertThreadAvailable(logicalLine: Int, side: Side, invoker: ((ThreadPresenter) -> Unit)) {\n        val map = if (side == Side.LEFT) myThreadOfLeft else myThreadOfRight\n        val thread = map[logicalLine]\n        if (null !== thread) {\n            invoker.invoke(thread)\n        }\n    }\n\n    protected fun findThreadPresenter(logicalLine: Int, side: Side): ThreadPresenter? {\n        val map = if (side == Side.LEFT) myThreadOfLeft else myThreadOfRight\n        return map[logicalLine]\n    }\n\n    protected fun findChangeType(editor: EditorEx, logicalLine: Int): DiffView.ChangeType {\n        if (editor.isDisposed) {\n            return DiffView.ChangeType.UNKNOWN\n        }\n        val guessChangeTypeByColorFunction = makeGuessChangeTypeByColorFunction(editor)\n        val highlighters = editor.markupModel.allHighlighters\n        var type = DiffView.ChangeType.UNKNOWN\n        for (highlighter in highlighters) {\n            val startLogicalPosition = editor.offsetToLogicalPosition(highlighter.startOffset)\n            val endLogicalPosition = editor.offsetToLogicalPosition(highlighter.endOffset)\n            if (startLogicalPosition.line > logicalLine || logicalLine > endLogicalPosition.line) {\n                continue\n            }\n\n            val guessType = guessChangeTypeByColorFunction(highlighter.textAttributes)\n            if (guessType != DiffView.ChangeType.UNKNOWN) {\n                type = guessType\n            }\n        }\n        return type\n    }\n\n    private fun collectThreadIds(comments: List<Comment>): MutableSet<String> {\n        val result = mutableSetOf<String>()\n        comments.forEach {\n            result.add(it.parentId)\n        }\n        return result\n    }\n\n    private fun makeGuessChangeTypeByColorFunction(editor: Editor): ((TextAttributes?) -> DiffView.ChangeType) {\n        val insertedColor = TextDiffType.INSERTED.getColor(editor).rgb\n        val insertedIgnoredColor = TextDiffType.INSERTED.getIgnoredColor(editor).rgb\n        val deletedColor = TextDiffType.DELETED.getColor(editor).rgb\n        val deletedIgnoredColor = TextDiffType.DELETED.getIgnoredColor(editor).rgb\n        val modifiedColor = TextDiffType.MODIFIED.getColor(editor).rgb\n        val modifiedIgnoredColor = TextDiffType.MODIFIED.getIgnoredColor(editor).rgb\n\n        return {\n            if (null === it) {\n                DiffView.ChangeType.UNKNOWN\n            } else {\n                val bgColor = it.backgroundColor\n                if (null === bgColor) {\n                    DiffView.ChangeType.UNKNOWN\n                } else {\n                    when (bgColor.rgb) {\n                        insertedColor, insertedIgnoredColor -> DiffView.ChangeType.INSERTED\n                        modifiedColor, modifiedIgnoredColor -> DiffView.ChangeType.MODIFIED\n                        deletedColor, deletedIgnoredColor -> DiffView.ChangeType.DELETED\n                        else -> DiffView.ChangeType.UNKNOWN\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/CommentPoint.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.diff\n\nimport net.ntworld.mergeRequest.Comment\n\ndata class CommentPoint(\n    val line: Int,\n    val comment: Comment\n) {\n    val id: String = comment.id\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffExtensionBase.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.diff\n\nimport com.intellij.diff.DiffContext\nimport com.intellij.diff.DiffExtension\nimport com.intellij.diff.FrameDiffTool\nimport com.intellij.diff.requests.DiffRequest\nimport com.intellij.diff.tools.util.base.DiffViewerBase\nimport com.intellij.diff.util.DiffUserDataKeys\nimport com.intellij.openapi.actionSystem.AnAction\nimport com.intellij.openapi.actionSystem.AnActionEvent\nimport com.intellij.openapi.actionSystem.ToggleAction\nimport com.intellij.openapi.project.Project as IdeaProject\nimport com.intellij.openapi.vcs.changes.Change\nimport com.intellij.openapi.vcs.changes.actions.diff.ChangeDiffRequestProducer\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.component.Icons\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.util.TextChoiceUtil\n\nopen class DiffExtensionBase(\n    private val applicationServiceProvider: ApplicationServiceProvider\n) : DiffExtension() {\n\n    override fun onViewerCreated(viewer: FrameDiffTool.DiffViewer, context: DiffContext, request: DiffRequest) {\n        val presenter = createPresenter(viewer, request)\n        if (null === presenter) {\n            return\n        }\n        context.putUserData(\n            DiffUserDataKeys.CONTEXT_ACTIONS, listOf(\n                MyToggleAllCommentsAction(\n                    presenter,\n                    applicationServiceProvider.settingsManager.displayCommentsInDiffView\n                ),\n                MyToggleResolvedCommentsAction(\n                    presenter,\n                    false\n                ),\n                MyShowDraftCommentsOnlyAction(\n                    presenter,\n                    false\n                ),\n                MyPublishDraftCommentsAction(\n                    presenter\n                )\n            )\n        )\n    }\n\n    private fun findReviewContext(projectServiceProvider: ProjectServiceProvider, request: DiffRequest): ReviewContext? {\n        val reviewContext = request.getUserData(ReviewContext.KEY)\n        if (null !== reviewContext) {\n            return reviewContext\n        }\n\n        return projectServiceProvider.reviewContextManager.findDoingCodeReviewContext()\n    }\n\n    private fun createPresenter(viewer: FrameDiffTool.DiffViewer, request: DiffRequest): DiffPresenter? {\n        return if (viewer is DiffViewerBase) {\n            assertViewerIsValid(viewer) { project, change ->\n                val projectServiceProvider = applicationServiceProvider.findProjectServiceProvider(project)\n                val reviewContext = findReviewContext(projectServiceProvider, request)\n                if (null === reviewContext) {\n                    return@assertViewerIsValid null\n                }\n\n                val model = DiffFactory.makeDiffModel(projectServiceProvider, reviewContext, change)\n                val view = DiffFactory.makeView(projectServiceProvider, viewer, change)\n                if (null !== view && null !== model) {\n                    DiffFactory.makeDiffPresenter(\n                        projectServiceProvider = projectServiceProvider,\n                        model = model,\n                        view = view\n                    )\n                } else {\n                    null\n                }\n            }\n        } else null\n    }\n\n    private fun assertViewerIsValid(\n        viewer: DiffViewerBase,\n        invoker: ((IdeaProject, Change) -> DiffPresenter?)\n    ): DiffPresenter? {\n        val project = viewer.project\n        val change = viewer.request.getUserData(ChangeDiffRequestProducer.CHANGE_KEY)\n        if (null === change || null === project) {\n            return null\n        }\n        return invoker.invoke(project, change)\n    }\n\n    private class MyToggleAllCommentsAction(\n        private val presenter: DiffPresenter,\n        initializedState: Boolean\n    ) : ToggleAction(\"Toggle Comments\", \"Toggle all comments in this diff view\", Icons.Comments) {\n        private var myShowAll = initializedState\n\n        override fun isSelected(e: AnActionEvent): Boolean {\n            return myShowAll\n        }\n\n        override fun setSelected(e: AnActionEvent, state: Boolean) {\n            myShowAll = state\n            if (myShowAll) {\n                presenter.view.showAllComments()\n            } else {\n                presenter.view.hideAllComments()\n            }\n        }\n    }\n\n    private class MyPublishDraftCommentsAction(\n        private val presenter: DiffPresenter\n    ) : AnAction(\"Publish draft comments\", \"Publish all draft comments in this diff view\", null) {\n        override fun actionPerformed(e: AnActionEvent) {\n            presenter.publishAllDraftComments()\n        }\n\n        override fun update(e: AnActionEvent) {\n            if (presenter.model.draftComments.isNotEmpty()) {\n                e.presentation.isVisible = true\n                e.presentation.text = \"Publish \" + TextChoiceUtil.draftComment(presenter.model.draftComments.count())\n            } else {\n                e.presentation.isVisible = false\n            }\n        }\n\n        override fun displayTextInToolbar() = true\n    }\n\n    private class MyShowDraftCommentsOnlyAction(\n        private val presenter: DiffPresenter,\n        initializedState: Boolean\n    ) : ToggleAction(\"Draft Comments Only\", \"Only show draft comments\", Icons.Edit) {\n        private var myOnlyDraft = initializedState\n\n        override fun isSelected(e: AnActionEvent): Boolean {\n            return myOnlyDraft\n        }\n\n        override fun setSelected(e: AnActionEvent, state: Boolean) {\n            myOnlyDraft = state\n            presenter.model.rebuildCommentsWhenOnlyShowDraftChanged(myOnlyDraft)\n        }\n\n        override fun update(e: AnActionEvent) {\n            super.update(e)\n            e.presentation.isVisible = presenter.model.draftComments.isNotEmpty()\n        }\n    }\n\n    private class MyToggleResolvedCommentsAction(\n        private val presenter: DiffPresenter,\n        initializedState: Boolean\n    ) : ToggleAction(\"Show Resolved Comments\", \"Toggle resolved comments in this diff view\", Icons.Resolved) {\n        private var myShown = initializedState\n\n        override fun isSelected(e: AnActionEvent): Boolean {\n            return myShown\n        }\n\n        override fun setSelected(e: AnActionEvent, state: Boolean) {\n            myShown = state\n            presenter.model.rebuildCommentsWhenShowResolvedChanged(myShown)\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffFactory.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.diff\n\nimport com.intellij.diff.tools.fragmented.UnifiedDiffViewer\nimport com.intellij.diff.tools.simple.SimpleOnesideDiffViewer\nimport com.intellij.diff.tools.util.base.DiffViewerBase\nimport com.intellij.diff.tools.util.side.TwosideTextDiffViewer\nimport com.intellij.diff.util.Side\nimport com.intellij.openapi.vcs.changes.Change\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\n\nobject DiffFactory {\n    fun makeDiffPresenter(\n        projectServiceProvider: ProjectServiceProvider, model: DiffModel, view: DiffView<*>\n    ): DiffPresenter {\n        return DiffPresenterImpl(projectServiceProvider, model, view)\n    }\n\n    fun makeView(\n        projectServiceProvider: ProjectServiceProvider,\n        viewer: DiffViewerBase,\n        change: Change\n    ): DiffView<*>? {\n        return when (viewer) {\n            is SimpleOnesideDiffViewer -> makeSimpleOneSideDiffViewer(projectServiceProvider, viewer, change)\n            is TwosideTextDiffViewer -> makeTwoSideTextDiffViewer(projectServiceProvider, viewer, change)\n            is UnifiedDiffViewer -> makeUnifiedDiffView(projectServiceProvider, viewer, change)\n            else -> null\n        }\n    }\n\n    private fun makeSimpleOneSideDiffViewer(\n        projectServiceProvider: ProjectServiceProvider, viewer: SimpleOnesideDiffViewer, change: Change\n    ): DiffView<SimpleOnesideDiffViewer>? {\n        return SimpleOneSideDiffView(projectServiceProvider, viewer, change, when(change.type) {\n            Change.Type.DELETED -> Side.LEFT\n            Change.Type.NEW -> Side.RIGHT\n            else -> throw Exception(\"Invalid change type\")\n        })\n    }\n\n    private fun makeTwoSideTextDiffViewer(\n        projectServiceProvider: ProjectServiceProvider, viewer: TwosideTextDiffViewer, change: Change\n    ): DiffView<TwosideTextDiffViewer>? {\n        return TwoSideTextDiffView(projectServiceProvider, viewer, change)\n    }\n\n    private fun makeUnifiedDiffView(\n        projectServiceProvider: ProjectServiceProvider, viewer: UnifiedDiffViewer, change: Change\n    ): DiffView<UnifiedDiffViewer>? {\n        return UnifiedDiffView(projectServiceProvider, viewer, change)\n    }\n\n    fun makeDiffModel(projectServiceProvider: ProjectServiceProvider, reviewContext: ReviewContext, change: Change): DiffModel? {\n        return DiffModelImpl(projectServiceProvider, reviewContext, change, false, onlyShowDraftComments = false)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffModel.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.diff\n\nimport com.intellij.openapi.Disposable\nimport com.intellij.openapi.vcs.changes.Change\nimport com.intellij.util.messages.MessageBusConnection\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequestIntegrationIde.DataChangedSource\nimport net.ntworld.mergeRequestIntegrationIde.Model\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\nimport java.util.*\n\ninterface DiffModel : Model<DiffModel.DataListener>, Disposable {\n    val reviewContext: ReviewContext\n\n    val providerData: ProviderData\n\n    val mergeRequestInfo: MergeRequestInfo\n\n    val messageBusConnection: MessageBusConnection\n\n    val diffReference: DiffReference?\n\n    val commits: List<Commit>\n\n    val change: Change\n\n    val draftComments: List<Comment>\n\n    val commentsOnBeforeSide: List<CommentPoint>\n\n    val commentsOnAfterSide: List<CommentPoint>\n\n    var displayResolvedComments: Boolean\n\n    var onlyShowDraftComments: Boolean\n\n    fun rebuildCommentsWhenShowResolvedChanged(showResolved: Boolean)\n\n    fun rebuildCommentsWhenOnlyShowDraftChanged(onlyShowDraft: Boolean)\n\n    interface DataListener : EventListener {\n        fun onCommentsUpdated(source: DataChangedSource)\n    }\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffModelImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.diff\n\nimport com.intellij.openapi.util.Disposer\nimport com.intellij.openapi.vcs.changes.Change\nimport com.intellij.openapi.vcs.changes.ContentRevision\nimport com.intellij.util.EventDispatcher\nimport com.intellij.util.messages.MessageBusConnection\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequestIntegrationIde.AbstractModel\nimport net.ntworld.mergeRequestIntegrationIde.DataChangedSource\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.MergeRequestDataNotifier\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil\n\nclass DiffModelImpl(\n    private val projectServiceProvider: ProjectServiceProvider,\n    override val reviewContext: ReviewContext,\n    override val change: Change,\n    override var displayResolvedComments: Boolean,\n    override var onlyShowDraftComments: Boolean\n) : AbstractModel<DiffModel.DataListener>(), DiffModel {\n    override val dispatcher = EventDispatcher.create(DiffModel.DataListener::class.java)\n    private val commentFilterOnBeforeSide: ((ContentRevision, Comment) -> Boolean) = { revision, comment ->\n        val position = comment.position\n        if (null === position || null === position.oldPath) {\n            false\n        } else {\n            RepositoryUtil.findAbsoluteCrossPlatformsPath(reviewContext.repository, position.oldPath!!) ==\n                RepositoryUtil.transformToCrossPlatformsPath(revision.file.path)\n        }\n    }\n    private val commentFilterOnAfterSide: ((ContentRevision, Comment) -> Boolean) = { revision, comment ->\n        val position = comment.position\n        if (null === position || null === position.newPath) {\n            false\n        } else {\n            RepositoryUtil.findAbsoluteCrossPlatformsPath(reviewContext.repository, position.newPath!!) ==\n                RepositoryUtil.transformToCrossPlatformsPath(revision.file.path)\n        }\n    }\n    private val commentFactoryOnBeforeSide: ((CommentPosition, Comment) -> CommentPoint) = { position, comment ->\n        CommentPoint(position.oldLine!!, comment)\n    }\n    private val commentFactoryOnAfterSide: ((CommentPosition, Comment) -> CommentPoint) = { position, comment ->\n        CommentPoint(position.newLine!!, comment)\n    }\n\n    override val messageBusConnection: MessageBusConnection = reviewContext.messageBusConnection\n    private val myMergeRequestDataNotifier = object : MergeRequestDataNotifier {\n        override fun fetchCommentsRequested(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) {\n        }\n\n        override fun onCommentsUpdated(\n            providerData: ProviderData,\n            mergeRequestInfo: MergeRequestInfo,\n            comments: List<Comment>\n        ) {\n            if (providerData.id != reviewContext.providerData.id ||\n                mergeRequestInfo.id != reviewContext.mergeRequestInfo.id) {\n                return\n            }\n            buildCommentsOnBeforeSide(comments)\n            buildCommentsOnAfterSide(comments)\n            buildDraftComments(comments)\n            dispatcher.multicaster.onCommentsUpdated(DataChangedSource.NOTIFIER)\n        }\n    }\n\n    override val providerData = reviewContext.providerData\n    override val mergeRequestInfo = reviewContext.mergeRequestInfo\n    override val diffReference: DiffReference?\n        get() = reviewContext.diffReference\n    override val commits\n        get() = reviewContext.commits\n\n    override val draftComments = mutableListOf<Comment>()\n\n    override var commentsOnBeforeSide = mutableListOf<CommentPoint>()\n        private set\n\n    override var commentsOnAfterSide = mutableListOf<CommentPoint>()\n        private set\n\n    init {\n        messageBusConnection.subscribe(MergeRequestDataNotifier.TOPIC, myMergeRequestDataNotifier)\n        Disposer.register(messageBusConnection, this)\n        buildCommentsOnBeforeSide(null)\n        buildCommentsOnAfterSide(null)\n    }\n\n    override fun dispose() {\n        dispatcher.listeners.clear()\n        commentsOnBeforeSide.clear()\n        commentsOnAfterSide.clear()\n    }\n\n    override fun rebuildCommentsWhenShowResolvedChanged(showResolved: Boolean) {\n        displayResolvedComments = showResolved\n        buildCommentsOnBeforeSide(null)\n        buildCommentsOnAfterSide(null)\n        dispatcher.multicaster.onCommentsUpdated(DataChangedSource.UI)\n    }\n\n    override fun rebuildCommentsWhenOnlyShowDraftChanged(onlyShowDraft: Boolean) {\n        onlyShowDraftComments = onlyShowDraft\n        buildCommentsOnBeforeSide(null)\n        buildCommentsOnAfterSide(null)\n        dispatcher.multicaster.onCommentsUpdated(DataChangedSource.UI)\n    }\n\n    private fun buildCommentPoints(\n        result: MutableList<CommentPoint>,\n        revision: ContentRevision?,\n        comments: List<Comment>?,\n        filter: ((ContentRevision, Comment) -> Boolean),\n        factory: ((CommentPosition, Comment) -> CommentPoint),\n        matcher: ((CommentPosition, ContentRevision, List<String>) -> Boolean)\n    ) {\n        result.clear()\n        if (null === revision) {\n            return\n        }\n        val commitIds = reviewContext.commits.map { it.id }\n\n        val list = if (null !== comments) {\n            comments.filter {\n                if (onlyShowDraftComments) {\n                    it.isDraft\n                } else {\n                    if (!displayResolvedComments && it.resolved) {\n                        false\n                    } else {\n                        filter.invoke(revision, it)\n                    }\n                }\n            }\n        } else {\n            reviewContext.getCommentsByPath(revision.file.path).filter {\n                if (onlyShowDraftComments) {\n                    it.isDraft\n                } else {\n                    displayResolvedComments || !it.resolved\n                }\n            }\n        }\n        for (comment in list) {\n            val position = comment.position\n            if (null === position) {\n                continue\n            }\n            if (matcher.invoke(position, revision, commitIds)) {\n                result.add(factory(position, comment))\n                continue\n            }\n        }\n    }\n\n    private fun buildDraftComments(comments: List<Comment>) {\n        draftComments.clear()\n        val items = comments.filter { it.isDraft }\n        draftComments.addAll(items)\n    }\n\n    private fun buildCommentsOnBeforeSide(comments: List<Comment>?) = buildCommentPoints(\n        commentsOnBeforeSide,\n        change.beforeRevision,\n        comments,\n        commentFilterOnBeforeSide,\n        commentFactoryOnBeforeSide\n    ) { position, revision, commits ->\n        val hash = revision.revisionNumber.asString()\n        null !== position.oldLine && (position.startHash == hash || position.baseHash == hash || commits.contains(hash))\n    }\n\n    private fun buildCommentsOnAfterSide(comments: List<Comment>?) = buildCommentPoints(\n        commentsOnAfterSide,\n        change.afterRevision,\n        comments,\n        commentFilterOnAfterSide,\n        commentFactoryOnAfterSide\n    ) { position, revision, commits ->\n        val hash = revision.revisionNumber.asString()\n        null !== position.newLine && (position.headHash == hash || position.baseHash == hash || commits.contains(hash))\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffPresenter.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.diff\n\nimport com.intellij.openapi.Disposable\nimport net.ntworld.mergeRequestIntegrationIde.SimplePresenter\n\ninterface DiffPresenter : SimplePresenter, Disposable {\n    val model: DiffModel\n    val view: DiffView<*>\n\n    fun publishAllDraftComments()\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffPresenterImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.diff\n\nimport com.intellij.diff.util.Side\nimport com.intellij.notification.NotificationType\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.openapi.util.Disposer\nimport com.intellij.openapi.vcs.changes.Change\nimport com.intellij.util.EventDispatcher\nimport git4idea.repo.GitRepository\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequest.CommentPositionChangeType\nimport net.ntworld.mergeRequest.CommentPositionSource\nimport net.ntworld.mergeRequest.command.DeleteCommentCommand\nimport net.ntworld.mergeRequest.command.ResolveCommentCommand\nimport net.ntworld.mergeRequest.command.UnresolveCommentCommand\nimport net.ntworld.mergeRequest.request.CreateCommentRequest\nimport net.ntworld.mergeRequest.request.PublishCommentsRequest\nimport net.ntworld.mergeRequest.request.ReplyCommentRequest\nimport net.ntworld.mergeRequest.request.UpdateCommentRequest\nimport net.ntworld.mergeRequestIntegration.internal.CommentPositionImpl\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegration.provider.ProviderException\nimport net.ntworld.mergeRequestIntegrationIde.AbstractPresenter\nimport net.ntworld.mergeRequestIntegrationIde.DataChangedSource\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterActionType\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRenderer\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.DiffNotifier\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.MergeRequestDataNotifier\nimport net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil\nimport java.util.*\n\ninternal class DiffPresenterImpl(\n    private val projectServiceProvider: ProjectServiceProvider,\n    override val model: DiffModel,\n    override val view: DiffView<*>\n) : AbstractPresenter<EventListener>(),\n    DiffPresenter, DiffView.ActionListener, DiffModel.DataListener,\n    DiffNotifier {\n    override val dispatcher = EventDispatcher.create(EventListener::class.java)\n\n    init {\n        view.addActionListener(this)\n        model.addDataListener(this)\n        model.messageBusConnection.subscribe(DiffNotifier.TOPIC, this)\n        Disposer.register(model, this)\n    }\n\n    override fun dispose() {\n        view.dispose()\n    }\n\n    override fun onInit() {}\n    override fun onDispose() {}\n    override fun onBeforeRediff() {}\n\n    override fun onAfterRediff() {\n        view.createGutterIcons()\n\n        val before = groupCommentsByLine(model.commentsOnBeforeSide)\n        for (item in before) {\n            view.initializeLine(model.reviewContext, item.key, Side.LEFT, item.value)\n        }\n\n        val after = groupCommentsByLine(model.commentsOnAfterSide)\n        for (item in after) {\n            view.initializeLine(model.reviewContext, item.key, Side.RIGHT, item.value)\n        }\n\n        if (projectServiceProvider.applicationSettings.displayCommentsInDiffView) {\n            ApplicationManager.getApplication().invokeLater {\n                view.showAllComments()\n            }\n        }\n\n        val scrollPosition = model.reviewContext.getChangeData(model.change, DiffNotifier.ScrollPosition)\n        if (null !== scrollPosition) {\n            val showComments = model.reviewContext.getChangeData(model.change, DiffNotifier.ScrollShowComments)\n            scrollToLine(scrollPosition, showComments)\n            clearChangeDataOfScrollToLineInReviewContext()\n        }\n    }\n\n    override fun onRediffAborted() {}\n\n    override fun onCommentsUpdated(source: DataChangedSource) {\n        if (source == DataChangedSource.NOTIFIER) {\n            ApplicationManager.getApplication().invokeLater {\n                handleWhenCommentsGetUpdated(source)\n            }\n        } else {\n            handleWhenCommentsGetUpdated(source)\n        }\n    }\n\n    private fun handleWhenCommentsGetUpdated(source: DataChangedSource) {\n        view.resetGutterIcons()\n        val before = groupCommentsByLine(model.commentsOnBeforeSide)\n        view.destroyExistingComments(before.keys, Side.LEFT)\n        for (item in before) {\n            view.initializeLine(model.reviewContext, item.key, Side.LEFT, item.value)\n            view.updateComments(item.key, Side.LEFT, item.value)\n        }\n\n        val after = groupCommentsByLine(model.commentsOnAfterSide)\n        view.destroyExistingComments(after.keys, Side.RIGHT)\n        for (item in after) {\n            view.initializeLine(model.reviewContext, item.key, Side.RIGHT, item.value)\n            view.updateComments(item.key, Side.RIGHT, item.value)\n        }\n    }\n\n    override fun onGutterActionPerformed(\n        renderer: GutterIconRenderer, type: GutterActionType, mode: DiffView.DisplayCommentMode\n    ) {\n        when (type) {\n            GutterActionType.ADD -> {\n                view.prepareLine(model.reviewContext, renderer, collectCommentsOfGutterIconRenderer(renderer))\n                view.displayEditorOnLine(renderer.logicalLine, renderer.side)\n            }\n            GutterActionType.TOGGLE -> {\n                view.prepareLine(model.reviewContext, renderer, collectCommentsOfGutterIconRenderer(renderer))\n                view.displayComments(renderer, mode)\n            }\n        }\n    }\n\n    override fun onEditCommentRequested(comment: Comment, content: String) {\n        projectServiceProvider.infrastructure.serviceBus() process UpdateCommentRequest.make(\n            providerId = model.providerData.id,\n            mergeRequestId = model.mergeRequestInfo.id,\n            comment = comment,\n            body = content\n        ) ifError {\n            projectServiceProvider.notify(\n                \"There was an error from server. \\n\\n ${it.message}\",\n                NotificationType.ERROR\n            )\n            throw ProviderException(it)\n        }\n        fetchAndUpdateComments()\n    }\n\n    override fun onReplyCommentRequested(content: String, repliedComment: Comment, logicalLine: Int, side: Side) {\n        projectServiceProvider.infrastructure.serviceBus() process ReplyCommentRequest.make(\n            providerId = model.providerData.id,\n            mergeRequestId = model.mergeRequestInfo.id,\n            repliedComment = repliedComment,\n            body = content\n        ) ifError {\n            projectServiceProvider.notify(\n                \"There was an error from server. \\n\\n ${it.message}\",\n                NotificationType.ERROR\n            )\n            throw ProviderException(it)\n        }\n        fetchAndUpdateComments()\n        view.resetEditorOnLine(logicalLine, side, repliedComment)\n    }\n\n    override fun onCreateCommentRequested(\n        content: String,\n        position: GutterPosition,\n        logicalLine: Int,\n        side: Side,\n        isDraft: Boolean\n    ) {\n        val commentPosition = convertGutterPositionToCommentPosition(position)\n        projectServiceProvider.infrastructure.serviceBus() process CreateCommentRequest.make(\n            providerId = model.providerData.id,\n            mergeRequestId = model.mergeRequestInfo.id,\n            position = commentPosition,\n            body = content,\n            isDraft = isDraft\n        ) ifError {\n            projectServiceProvider.notify(\n                \"There was an error from server. \\n\\n ${it.message}\",\n                NotificationType.ERROR\n            )\n            throw ProviderException(it)\n        }\n        fetchAndUpdateComments()\n        view.resetEditorOnLine(logicalLine, side, null)\n    }\n\n    override fun publishAllDraftComments() {\n        val draftCommentIds = model.draftComments.filter { it.isDraft } .map { it.id }\n        projectServiceProvider.infrastructure.serviceBus() process PublishCommentsRequest.make(\n            providerId = model.providerData.id,\n            mergeRequestId = model.mergeRequestInfo.id,\n            draftCommentIds = draftCommentIds\n        ) ifError {\n            projectServiceProvider.notify(\n                \"There was an error from server. \\n\\n ${it.message}\",\n                NotificationType.ERROR\n            )\n            throw ProviderException(it)\n        }\n        fetchAndUpdateComments()\n    }\n\n    override fun onPublishDraftCommentRequested(comment: Comment) {\n        projectServiceProvider.infrastructure.serviceBus() process PublishCommentsRequest.make(\n            providerId = model.providerData.id,\n            mergeRequestId = model.mergeRequestInfo.id,\n            draftCommentIds = listOf(comment.id)\n        ) ifError {\n            projectServiceProvider.notify(\n                \"There was an error from server. \\n\\n ${it.message}\",\n                NotificationType.ERROR\n            )\n            throw ProviderException(it)\n        }\n        fetchAndUpdateComments()\n    }\n\n    override fun onDeleteCommentRequested(comment: Comment) {\n        projectServiceProvider.infrastructure.commandBus() process DeleteCommentCommand.make(\n            providerId = model.providerData.id,\n            mergeRequestId = model.mergeRequestInfo.id,\n            comment = comment\n        )\n        fetchAndUpdateComments()\n    }\n\n    override fun onResolveCommentRequested(comment: Comment) {\n        projectServiceProvider.infrastructure.commandBus() process ResolveCommentCommand.make(\n            providerId = model.providerData.id,\n            mergeRequestId = model.mergeRequestInfo.id,\n            comment = comment\n        )\n        fetchAndUpdateComments()\n    }\n\n    override fun onUnresolveCommentRequested(comment: Comment) {\n        projectServiceProvider.infrastructure.commandBus() process UnresolveCommentCommand.make(\n            providerId = model.providerData.id,\n            mergeRequestId = model.mergeRequestInfo.id,\n            comment = comment\n        )\n        fetchAndUpdateComments()\n    }\n\n    override fun scrollToPositionRequested(\n        reviewContext: ReviewContext,\n        change: Change,\n        position: CommentPosition,\n        showComments: Boolean?\n    ) {\n        if (model.reviewContext === reviewContext && model.change == change) {\n            scrollToLine(position, showComments)\n            clearChangeDataOfScrollToLineInReviewContext()\n        }\n    }\n\n    override fun hideAllCommentsRequested(reviewContext: ReviewContext, change: Change) {\n        if (model.reviewContext === reviewContext && model.change == change) {\n            view.hideAllComments()\n        }\n    }\n\n    private fun fetchAndUpdateComments() {\n        projectServiceProvider.messageBus.syncPublisher(MergeRequestDataNotifier.TOPIC).fetchCommentsRequested(\n            model.providerData, model.mergeRequestInfo\n        )\n    }\n\n    private fun collectCommentsOfGutterIconRenderer(renderer: GutterIconRenderer): List<Comment> {\n        val result = mutableMapOf<String, CommentPoint>()\n        model.commentsOnBeforeSide\n            .filter { it.line == renderer.visibleLineLeft }\n            .forEach { result[it.id] = it }\n        model.commentsOnAfterSide\n            .filter { it.line == renderer.visibleLineRight }\n            .forEach { result[it.id] = it }\n\n        return result.values.map { it.comment }\n    }\n\n    private fun groupCommentsByLine(commentPoints: List<CommentPoint>): Map<Int, List<Comment>> {\n        val result = mutableMapOf<Int, MutableList<Comment>>()\n        for (commentPoint in commentPoints) {\n            if (!result.containsKey(commentPoint.line)) {\n                result[commentPoint.line] = mutableListOf()\n            }\n\n            val list = result[commentPoint.line]!!\n            list.add(commentPoint.comment)\n        }\n        return result\n    }\n\n    private fun convertGutterPositionToCommentPosition(input: GutterPosition): CommentPosition {\n        val repository: GitRepository? = RepositoryUtil.findRepository(projectServiceProvider, model.providerData)\n\n        return CommentPositionImpl(\n            oldLine = input.oldLine,\n            oldPath = if (null === input.oldPath) null else RepositoryUtil.findRelativePath(repository, input.oldPath),\n            newLine = input.newLine,\n            newPath = if (null === input.newPath) null else RepositoryUtil.findRelativePath(repository, input.newPath),\n            baseHash = if (input.baseHash.isNullOrEmpty()) findBaseHash() else input.baseHash,\n            headHash = if (input.headHash.isNullOrEmpty()) findHeadHash() else input.headHash,\n            startHash = if (input.startHash.isNullOrEmpty()) findStartHash() else input.startHash,\n            source = when (input.editorType) {\n                DiffView.EditorType.SINGLE_SIDE -> CommentPositionSource.SINGLE_SIDE\n                DiffView.EditorType.TWO_SIDE_LEFT -> CommentPositionSource.SIDE_BY_SIDE_LEFT\n                DiffView.EditorType.TWO_SIDE_RIGHT -> CommentPositionSource.SIDE_BY_SIDE_RIGHT\n                DiffView.EditorType.UNIFIED -> CommentPositionSource.UNIFIED\n            },\n            changeType = when (input.changeType) {\n                DiffView.ChangeType.UNKNOWN -> CommentPositionChangeType.UNKNOWN\n                DiffView.ChangeType.INSERTED -> CommentPositionChangeType.INSERTED\n                DiffView.ChangeType.DELETED -> CommentPositionChangeType.DELETED\n                DiffView.ChangeType.MODIFIED -> CommentPositionChangeType.MODIFIED\n            }\n        )\n    }\n\n    private fun findBaseHash(): String {\n        if (model.commits.isNotEmpty()) {\n            return model.commits.last().id\n        }\n\n        val diff = model.diffReference\n        return if (null === diff) \"\" else diff.baseHash\n    }\n\n    private fun findStartHash(): String {\n        val diff = model.diffReference\n        return if (null === diff) \"\" else diff.startHash\n    }\n\n    private fun findHeadHash(): String {\n        if (model.commits.isNotEmpty()) {\n            return model.commits.first().id\n        }\n\n        val diff = model.diffReference\n        return if (null === diff) \"\" else diff.headHash\n    }\n\n    private fun scrollToLine(position: CommentPosition, showComments: Boolean?) {\n        view.scrollToPosition(position, null !== showComments && showComments)\n    }\n\n    private fun clearChangeDataOfScrollToLineInReviewContext() {\n        model.reviewContext.putChangeData(model.change, DiffNotifier.ScrollPosition, null)\n        model.reviewContext.putChangeData(model.change, DiffNotifier.ScrollShowComments, null)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffView.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.diff\n\nimport com.intellij.diff.FrameDiffTool\nimport com.intellij.diff.util.Side\nimport com.intellij.openapi.Disposable\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequestIntegrationIde.View\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterActionType\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRenderer\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.CommentEvent\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\n\ninterface DiffView<V : FrameDiffTool.DiffViewer> : View<DiffView.ActionListener>, Disposable {\n    val viewer: V\n\n    fun initializeLine(reviewContext: ReviewContext, visibleLine: Int, side: Side, comments: List<Comment>)\n\n    fun prepareLine(reviewContext: ReviewContext, renderer: GutterIconRenderer, comments: List<Comment>)\n\n    fun createGutterIcons()\n\n    fun resetGutterIcons()\n\n    fun destroyExistingComments(excludedVisibleLines: Set<Int>, side: Side)\n\n    fun showAllComments()\n\n    fun hideAllComments()\n\n    fun resetEditorOnLine(logicalLine: Int, side: Side, repliedComment: Comment?)\n\n    fun updateComments(visibleLine: Int, side: Side, comments: List<Comment>)\n\n    fun displayEditorOnLine(logicalLine: Int, side: Side)\n\n    fun displayComments(visibleLine: Int, side: Side, mode: DisplayCommentMode)\n\n    fun displayComments(renderer: GutterIconRenderer, mode: DisplayCommentMode)\n\n    fun scrollToPosition(position: CommentPosition, showComments: Boolean)\n\n    enum class EditorType {\n        SINGLE_SIDE,\n        TWO_SIDE_LEFT,\n        TWO_SIDE_RIGHT,\n        UNIFIED\n    }\n\n    enum class ChangeType {\n        UNKNOWN,\n        INSERTED,\n        DELETED,\n        MODIFIED\n    }\n\n    enum class DisplayCommentMode {\n        TOGGLE,\n        SHOW,\n        HIDE\n    }\n\n    interface ActionListener : CommentEvent {\n        fun onInit()\n\n        fun onDispose()\n\n        fun onBeforeRediff()\n\n        fun onAfterRediff()\n\n        fun onRediffAborted()\n\n        fun onGutterActionPerformed(renderer: GutterIconRenderer, type: GutterActionType, mode: DisplayCommentMode)\n\n        fun onEditCommentRequested(comment: Comment, content: String)\n\n        fun onReplyCommentRequested(content: String, repliedComment: Comment, logicalLine: Int, side: Side)\n\n        fun onCreateCommentRequested(content: String, position: GutterPosition, logicalLine: Int, side: Side, isDraft: Boolean)\n\n        fun onPublishDraftCommentRequested(comment: Comment)\n    }\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffViewAddCommentActionBase.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.diff\n\nimport com.intellij.openapi.actionSystem.DataContext\nimport com.intellij.openapi.editor.Caret\nimport com.intellij.openapi.editor.Editor\nimport com.intellij.openapi.editor.actionSystem.EditorAction\nimport com.intellij.openapi.editor.actionSystem.EditorActionHandler\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRendererFactory\n\nopen class DiffViewAddCommentActionBase : EditorAction(MyHandler()) {\n\n    private class MyHandler : EditorActionHandler() {\n        override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {\n            return null !== GutterIconRendererFactory.findGutterIconRenderer(editor)\n        }\n\n        override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {\n            super.doExecute(editor, caret, dataContext)\n            val gutterRenderer = GutterIconRendererFactory.findGutterIconRenderer(editor)\n            if (null !== gutterRenderer) {\n                gutterRenderer.triggerAddAction()\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/DiffViewToggleCommentsActionBase.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.diff\n\nimport com.intellij.openapi.actionSystem.DataContext\nimport com.intellij.openapi.editor.Caret\nimport com.intellij.openapi.editor.Editor\nimport com.intellij.openapi.editor.actionSystem.EditorAction\nimport com.intellij.openapi.editor.actionSystem.EditorActionHandler\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRendererFactory\n\nopen class DiffViewToggleCommentsActionBase : EditorAction(MyHandler()) {\n\n    private class MyHandler : EditorActionHandler() {\n        override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {\n            return null !== GutterIconRendererFactory.findGutterIconRenderer(editor)\n        }\n\n        override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {\n            super.doExecute(editor, caret, dataContext)\n            val gutterRenderer = GutterIconRendererFactory.findGutterIconRenderer(editor)\n            if (null !== gutterRenderer) {\n                gutterRenderer.triggerToggleAction()\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/SimpleOneSideDiffView.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.diff\n\nimport com.intellij.diff.tools.simple.SimpleOnesideDiffViewer\nimport com.intellij.diff.util.Side\nimport com.intellij.openapi.editor.LogicalPosition\nimport com.intellij.openapi.editor.ScrollType\nimport com.intellij.openapi.editor.markup.HighlighterLayer\nimport com.intellij.openapi.vcs.changes.Change\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRenderer\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRendererFactory\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\n\nclass SimpleOneSideDiffView(\n    private val projectServiceProvider: ProjectServiceProvider,\n    override val viewer: SimpleOnesideDiffViewer,\n    private val change: Change,\n    private val side: Side\n) : AbstractDiffView<SimpleOnesideDiffViewer>(projectServiceProvider, viewer) {\n    private var myGutterIconsCreated: Boolean = false\n\n    override fun convertVisibleLineToLogicalLine(visibleLine: Int, side: Side): Int {\n        return visibleLine - 1\n    }\n\n    private fun initializeByLogicalLine(reviewContext: ReviewContext, line: Int, side: Side, comments: List<Comment>) {\n        initializeThreadOnLineIfNotAvailable(\n            reviewContext.providerData, reviewContext.mergeRequestInfo,\n            viewer.editor,\n            calcPosition(line),\n            line,\n            side,\n            comments\n        )\n    }\n\n    override fun initializeLine(reviewContext: ReviewContext, visibleLine: Int, side: Side, comments: List<Comment>) {\n        val logicalLine = convertVisibleLineToLogicalLine(visibleLine, side)\n        initializeByLogicalLine(reviewContext, logicalLine, side, comments)\n        val renderer = findGutterIconRenderer(logicalLine, side)\n        if (null !== renderer) {\n            updateGutterIcon(renderer, comments)\n        }\n    }\n\n    override fun prepareLine(reviewContext: ReviewContext, renderer: GutterIconRenderer, comments: List<Comment>) {\n        initializeByLogicalLine(reviewContext, renderer.logicalLine, renderer.side, comments)\n    }\n\n    override fun createGutterIcons() {\n        if (myGutterIconsCreated) {\n            return\n        }\n        for (logicalLine in 0 until viewer.editor.document.lineCount) {\n            registerGutterIconRenderer(GutterIconRendererFactory.makeGutterIconRenderer(\n                viewer.editor.markupModel.addLineHighlighter(logicalLine, HighlighterLayer.LAST, null),\n                projectServiceProvider.applicationSettings.showAddCommentIconsInDiffViewGutter,\n                logicalLine,\n                visibleLineLeft = if (side == Side.LEFT) logicalLine + 1 else null,\n                visibleLineRight = if (side == Side.RIGHT) logicalLine + 1 else null,\n                side = side,\n                actionListener = myGutterIconRendererActionListener\n            ))\n        }\n        myGutterIconsCreated = true\n    }\n\n    override fun updateComments(visibleLine: Int, side: Side, comments: List<Comment>) {\n        val logicalLine = convertVisibleLineToLogicalLine(visibleLine, side)\n        val renderer = findGutterIconRenderer(logicalLine, side)\n        if (null !== renderer) {\n            updateComments(renderer, comments)\n        }\n    }\n\n    override fun scrollToPosition(position: CommentPosition, showComments: Boolean) {\n        if (viewer.editor.isDisposed) {\n            return\n        }\n\n        val oldLine = position.oldLine\n        if (side == Side.LEFT && null !== oldLine) {\n            val logicalLine = convertVisibleLineToLogicalLine(oldLine, Side.LEFT)\n            viewer.editor.scrollingModel.scrollTo(LogicalPosition(logicalLine, 0), ScrollType.MAKE_VISIBLE)\n            if (showComments) {\n                displayComments(oldLine, Side.LEFT, DiffView.DisplayCommentMode.SHOW)\n            }\n            return\n        }\n\n        val newLine = position.newLine\n        if (side == Side.RIGHT && null !== newLine) {\n            val logicalLine = convertVisibleLineToLogicalLine(newLine, Side.RIGHT)\n            viewer.editor.scrollingModel.scrollTo(LogicalPosition(logicalLine, 0), ScrollType.MAKE_VISIBLE)\n            if (showComments) {\n                displayComments(newLine, Side.RIGHT, DiffView.DisplayCommentMode.SHOW)\n            }\n            return\n        }\n    }\n\n    private fun calcPosition(logicalLine: Int): GutterPosition {\n        return if (side == Side.LEFT) {\n            GutterPosition(\n                editorType = DiffView.EditorType.SINGLE_SIDE,\n                changeType = findChangeType(viewer.editor, logicalLine),\n                oldLine = logicalLine + 1,\n                oldPath = change.beforeRevision!!.file.toString(),\n                newLine = null,\n                newPath = null,\n                baseHash = change.beforeRevision!!.revisionNumber.asString()\n            )\n        } else {\n            GutterPosition(\n                editorType = DiffView.EditorType.SINGLE_SIDE,\n                changeType = findChangeType(viewer.editor, logicalLine),\n                newLine = logicalLine + 1,\n                newPath = change.afterRevision!!.file.toString(),\n                oldLine = null,\n                oldPath = null,\n                headHash = change.afterRevision!!.revisionNumber.asString()\n            )\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/TwoSideTextDiffView.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.diff\n\nimport com.intellij.diff.tools.util.side.TwosideTextDiffViewer\nimport com.intellij.diff.util.Side\nimport com.intellij.openapi.editor.LogicalPosition\nimport com.intellij.openapi.editor.ScrollType\nimport com.intellij.openapi.editor.markup.HighlighterLayer\nimport com.intellij.openapi.vcs.changes.Change\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRenderer\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRendererFactory\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\n\nclass TwoSideTextDiffView(\n    private val projectServiceProvider: ProjectServiceProvider,\n    override val viewer: TwosideTextDiffViewer,\n    private val change: Change\n) : AbstractDiffView<TwosideTextDiffViewer>(projectServiceProvider, viewer) {\n    private var myGutterIconsCreated: Boolean = false\n\n    override fun convertVisibleLineToLogicalLine(visibleLine: Int, side: Side): Int {\n        return visibleLine - 1\n    }\n\n    private fun initializeByLogicalLine(reviewContext: ReviewContext, line: Int, side: Side, comments: List<Comment>) {\n        if (side == Side.LEFT) {\n            initializeThreadOnLineIfNotAvailable(\n                reviewContext.providerData, reviewContext.mergeRequestInfo,\n                viewer.editor1,\n                calcPositionEditor1(line),\n                line, side,\n                comments\n            )\n        } else {\n            initializeThreadOnLineIfNotAvailable(\n                reviewContext.providerData, reviewContext.mergeRequestInfo,\n                viewer.editor2,\n                calcPositionEditor2(line),\n                line, side,\n                comments\n            )\n        }\n    }\n\n    override fun initializeLine(reviewContext: ReviewContext, visibleLine: Int, side: Side, comments: List<Comment>) {\n        val logicalLine = convertVisibleLineToLogicalLine(visibleLine, side)\n        initializeByLogicalLine(reviewContext, logicalLine, side, comments)\n        val renderer = findGutterIconRenderer(logicalLine, side)\n        if (null !== renderer) {\n            updateGutterIcon(renderer, comments)\n        }\n    }\n\n    override fun prepareLine(reviewContext: ReviewContext, renderer: GutterIconRenderer, comments: List<Comment>) {\n        initializeByLogicalLine(reviewContext, renderer.logicalLine, renderer.side, comments)\n    }\n\n    override fun createGutterIcons() {\n        if (myGutterIconsCreated) {\n            return\n        }\n\n        for (logicalLine in 0 until viewer.editor1.document.lineCount) {\n            registerGutterIconRenderer(\n                GutterIconRendererFactory.makeGutterIconRenderer(\n                    viewer.editor1.markupModel.addLineHighlighter(logicalLine, HighlighterLayer.LAST, null),\n                    projectServiceProvider.applicationSettings.showAddCommentIconsInDiffViewGutter,\n                    logicalLine,\n                    visibleLineLeft = logicalLine + 1,\n                    visibleLineRight = null,\n                    side = Side.LEFT,\n                    actionListener = myGutterIconRendererActionListener\n                )\n            )\n        }\n        for (logicalLine in 0 until viewer.editor2.document.lineCount) {\n            registerGutterIconRenderer(\n                GutterIconRendererFactory.makeGutterIconRenderer(\n                    viewer.editor2.markupModel.addLineHighlighter(logicalLine, HighlighterLayer.LAST, null),\n                    projectServiceProvider.applicationSettings.showAddCommentIconsInDiffViewGutter,\n                    logicalLine,\n                    visibleLineLeft = null,\n                    visibleLineRight = logicalLine + 1,\n                    side = Side.RIGHT,\n                    actionListener = myGutterIconRendererActionListener\n                )\n            )\n        }\n\n        myGutterIconsCreated = true\n    }\n\n    override fun updateComments(visibleLine: Int, side: Side, comments: List<Comment>) {\n        val renderer = findGutterIconRenderer(visibleLine - 1, side)\n        if (null !== renderer) {\n            updateComments(renderer, comments)\n        }\n    }\n\n    override fun scrollToPosition(position: CommentPosition, showComments: Boolean) {\n        val oldLine = position.oldLine\n        if (oldLine !== null && !viewer.editor1.isDisposed) {\n            val logicalLine = convertVisibleLineToLogicalLine(oldLine, Side.LEFT)\n            viewer.editor1.scrollingModel.scrollTo(LogicalPosition(logicalLine, 0), ScrollType.MAKE_VISIBLE)\n            if (showComments) {\n                displayComments(oldLine, Side.LEFT, DiffView.DisplayCommentMode.SHOW)\n            }\n        }\n\n        val newLine = position.newLine\n        if (newLine !== null && !viewer.editor1.isDisposed) {\n            val logicalLine = convertVisibleLineToLogicalLine(newLine, Side.RIGHT)\n            viewer.editor2.scrollingModel.scrollTo(LogicalPosition(logicalLine, 0), ScrollType.MAKE_VISIBLE)\n            if (showComments) {\n                displayComments(newLine, Side.RIGHT, DiffView.DisplayCommentMode.SHOW)\n            }\n        }\n    }\n\n    private fun calcPositionEditor1(logicalLine: Int): GutterPosition {\n        val newLine = viewer.syncScrollSupport!!.scrollable.transfer(Side.LEFT, logicalLine + 1)\n        return GutterPosition(\n            editorType = DiffView.EditorType.TWO_SIDE_LEFT,\n            changeType = findChangeType(viewer.editor1, logicalLine),\n            oldLine = logicalLine + 1,\n            oldPath = change.beforeRevision!!.file.toString(),\n            newLine = newLine,\n            newPath = change.afterRevision!!.file.toString(),\n            baseHash = change.beforeRevision!!.revisionNumber.asString(),\n            headHash = change.afterRevision!!.revisionNumber.asString()\n        )\n    }\n\n    private fun calcPositionEditor2(logicalLine: Int): GutterPosition {\n        val oldLine = viewer.syncScrollSupport!!.scrollable.transfer(Side.RIGHT, logicalLine + 1)\n        return GutterPosition(\n            editorType = DiffView.EditorType.TWO_SIDE_RIGHT,\n            changeType = findChangeType(viewer.editor2, logicalLine),\n            oldLine = oldLine,\n            oldPath = change.beforeRevision!!.file.toString(),\n            newLine = logicalLine + 1,\n            newPath = change.afterRevision!!.file.toString(),\n            baseHash = change.beforeRevision!!.revisionNumber.asString(),\n            headHash = change.afterRevision!!.revisionNumber.asString()\n        )\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/diff/UnifiedDiffView.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.diff\n\nimport com.intellij.diff.tools.fragmented.UnifiedDiffViewer\nimport com.intellij.diff.util.Side\nimport com.intellij.openapi.editor.LogicalPosition\nimport com.intellij.openapi.editor.ScrollType\nimport com.intellij.openapi.editor.markup.HighlighterLayer\nimport com.intellij.openapi.vcs.changes.Change\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRenderer\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterIconRendererFactory\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\n\nclass UnifiedDiffView(\n    private val projectServiceProvider: ProjectServiceProvider,\n    override val viewer: UnifiedDiffViewer,\n    private val change: Change\n) : AbstractDiffView<UnifiedDiffViewer>(projectServiceProvider, viewer) {\n    private var myGutterIconsCreated: Boolean = false\n    private val myLeftLineNumberConverter by lazy {\n        projectServiceProvider.intellijIdeApi.findLeftLineNumberConverter(viewer.editor)\n    }\n    private val myRightLineNumberConverter by lazy {\n        projectServiceProvider.intellijIdeApi.findRightLineNumberConverter(viewer.editor)\n    }\n    private val myCachedLeftLineNumbers = mutableMapOf<Int, Int>()\n    private val myCachedRightLineNumbers = mutableMapOf<Int, Int>()\n\n    override fun convertVisibleLineToLogicalLine(visibleLine: Int, side: Side): Int {\n        val map = if (side == Side.LEFT) myCachedLeftLineNumbers else myCachedRightLineNumbers\n        val logicalLine = map[visibleLine - 1]\n        if (null === logicalLine) {\n            return -1\n        }\n        return logicalLine\n    }\n\n    private fun initializeByLogicalLine(reviewContext: ReviewContext, line: Int, side: Side, comments: List<Comment>) {\n        initializeThreadOnLineIfNotAvailable(\n            reviewContext.providerData, reviewContext.mergeRequestInfo,\n            viewer.editor,\n            calcPosition(line),\n            line, side,\n            comments\n        )\n    }\n\n    override fun initializeLine(reviewContext: ReviewContext, visibleLine: Int, side: Side, comments: List<Comment>) {\n        val logicalLine = convertVisibleLineToLogicalLine(visibleLine, side)\n        if (-1 != logicalLine) {\n            initializeByLogicalLine(reviewContext, logicalLine, side, comments)\n            val renderer = findGutterIconRenderer(logicalLine, Side.LEFT)\n            if (null !== renderer) {\n                updateGutterIcon(renderer, comments)\n            }\n        }\n    }\n\n    override fun prepareLine(reviewContext: ReviewContext, renderer: GutterIconRenderer, comments: List<Comment>) {\n        initializeByLogicalLine(reviewContext, renderer.logicalLine, renderer.side, comments)\n    }\n\n    override fun createGutterIcons() {\n        if (myGutterIconsCreated) {\n            return\n        }\n        for (logicalLine in 0 until viewer.editor.document.lineCount) {\n            val left = myLeftLineNumberConverter.execute(logicalLine)\n            val right = myRightLineNumberConverter.execute(logicalLine)\n            myCachedLeftLineNumbers[left] = logicalLine\n            myCachedRightLineNumbers[right] = logicalLine\n\n            registerGutterIconRenderer(\n                GutterIconRendererFactory.makeGutterIconRenderer(\n                    viewer.editor.markupModel.addLineHighlighter(logicalLine, HighlighterLayer.LAST, null),\n                    projectServiceProvider.applicationSettings.showAddCommentIconsInDiffViewGutter && (-1 != left || -1 != right),\n                    logicalLine,\n                    visibleLineLeft = left + 1,\n                    visibleLineRight = right + 1,\n                    // Doesn't matter, unified view only have 1 side\n                    side = Side.LEFT,\n                    actionListener = myGutterIconRendererActionListener\n                )\n            )\n        }\n        myGutterIconsCreated = true\n    }\n\n    private fun findGutterIconRenderer(visibleLine: Int, side: Side, invoker: ((Int, GutterIconRenderer) -> Unit)) {\n        val logicalLine = convertVisibleLineToLogicalLine(visibleLine, side)\n        if (-1 == logicalLine) {\n            return\n        }\n\n        // Doesn't matter, unified view only have 1 side\n        // see exact comment above\n        val renderer = findGutterIconRenderer(logicalLine, Side.LEFT)\n        if (null !== renderer) {\n            invoker.invoke(logicalLine, renderer)\n        }\n    }\n\n    override fun updateComments(visibleLine: Int, side: Side, comments: List<Comment>) {\n        findGutterIconRenderer(visibleLine, side) { _, renderer ->\n            updateComments(renderer, comments)\n        }\n    }\n\n    override fun displayComments(renderer: GutterIconRenderer, mode: DiffView.DisplayCommentMode) {\n        // for unified view the line distributed either on left/right, so we have to find both of them\n        val thread = findThreadPresenter(renderer.logicalLine, renderer.side)\n        if (null !== thread) {\n            displayComments(thread.model, renderer.logicalLine, renderer.side, mode)\n        } else {\n            val oppositeSide = if (renderer.side == Side.LEFT) Side.RIGHT else Side.LEFT\n            val oppositeThread = findThreadPresenter(renderer.logicalLine, oppositeSide)\n            if (null !== oppositeThread) {\n                displayComments(oppositeThread.model, renderer.logicalLine, oppositeSide, mode)\n            }\n        }\n    }\n\n    override fun scrollToPosition(position: CommentPosition, showComments: Boolean) {\n        if (viewer.editor.isDisposed) {\n            return\n        }\n\n        val oldLine = position.oldLine\n        val newLine = position.newLine\n        if (null !== newLine && null === oldLine) {\n            return findGutterIconRenderer(newLine, Side.RIGHT) { logicalLine, renderer ->\n                viewer.editor.scrollingModel.scrollTo(LogicalPosition(logicalLine, 0), ScrollType.MAKE_VISIBLE)\n                if (showComments) {\n                    displayComments(renderer, DiffView.DisplayCommentMode.SHOW)\n                }\n            }\n        }\n\n        return findGutterIconRenderer(oldLine!!, Side.LEFT) { logicalLine, renderer ->\n            viewer.editor.scrollingModel.scrollTo(LogicalPosition(logicalLine, 0), ScrollType.MAKE_VISIBLE)\n            if (showComments) {\n                displayComments(renderer, DiffView.DisplayCommentMode.SHOW)\n            }\n        }\n    }\n\n    private fun calcPosition(logicalLine: Int): GutterPosition {\n        val left = myLeftLineNumberConverter.execute(logicalLine) + 1\n        val right = myRightLineNumberConverter.execute(logicalLine) + 1\n        return GutterPosition(\n            editorType = DiffView.EditorType.UNIFIED,\n            changeType = findChangeType(viewer.editor, logicalLine),\n            oldLine = if (left > 0) left else -1,\n            oldPath = change.beforeRevision!!.file.toString(),\n            newLine = if (right > 0) right else -1,\n            newPath = change.afterRevision!!.file.toString(),\n            baseHash = change.beforeRevision!!.revisionNumber.asString(),\n            headHash = change.afterRevision!!.revisionNumber.asString()\n        )\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/exception/InvalidConnectionException.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.exception\n\nclass InvalidConnectionException(message: String) : Exception(message)\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/AbstractApplicationServiceProvider.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure\n\nimport com.intellij.ide.AppLifecycleListener\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.openapi.project.Project\nimport com.intellij.openapi.project.impl.ProjectLifecycleListener\nimport net.ntworld.mergeRequest.ProjectVisibility\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.ProviderInfo\nimport net.ntworld.mergeRequest.ProviderStatus\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegrationIde.compatibility.IntellijIdeApi\nimport net.ntworld.mergeRequestIntegrationIde.compatibility.*\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ProviderSettingsImpl\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ServiceBase\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettings\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsManager\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsManagerImpl\nimport net.ntworld.mergeRequestIntegrationIde.watcher.WatcherManager\nimport net.ntworld.mergeRequestIntegrationIde.watcher.WatcherManagerImpl\nimport org.jdom.Element\nimport java.net.URL\n\nabstract class AbstractApplicationServiceProvider : ApplicationServiceProvider, ServiceBase() {\n    final override val watcherManager: WatcherManager = WatcherManagerImpl()\n    private val myProjectServiceProviders = mutableSetOf<ProjectServiceProvider>()\n\n    private val publicLegalGrantedDomains = listOf(\n        \"gitlab.com\",\n        \"www.gitlab.com\"\n    )\n    private val legalGrantedDomains = listOf(\n        \"gitlab.personio-internal.de\"\n    )\n    private val myAppLifecycleListener = object : AppLifecycleListener {\n        override fun appStarting(projectFromCommandLine: Project?) {\n            if (null !== projectFromCommandLine) {\n                findProjectServiceProvider(projectFromCommandLine).initialize()\n            }\n        }\n\n        override fun appClosing() {\n            watcherManager.dispose()\n        }\n    }\n    private val myProjectLifecycleListener = object: ProjectLifecycleListener {\n        override fun projectComponentsInitialized(project: Project) {\n            findProjectServiceProvider(project).initialize()\n        }\n    }\n\n    init {\n        val connection = ApplicationManager.getApplication().messageBus.connect()\n        connection.subscribe(AppLifecycleListener.TOPIC, myAppLifecycleListener)\n        connection.subscribe(ProjectLifecycleListener.TOPIC, myProjectLifecycleListener)\n    }\n\n    protected fun registerProjectServiceProvider(projectServiceProvider: ProjectServiceProvider)\n    {\n        myProjectServiceProviders.add(projectServiceProvider)\n        projectServiceProvider.providerStorage.updateApiOptions(settingsManager.toApiOptions())\n    }\n\n    override val intellijIdeApi: IntellijIdeApi = Version203Adapter()\n\n    override val settingsManager: ApplicationSettingsManager = ApplicationSettingsManagerImpl(::onSettingsChanged)\n\n    override fun getAllProjectServiceProviders(): List<ProjectServiceProvider> {\n        return myProjectServiceProviders.toList()\n    }\n\n    override fun getState(): Element? {\n        val element = super.getState()\n        if (null === element) {\n            return element\n        }\n        settingsManager.writeTo(element)\n        return element\n    }\n\n    override fun loadState(state: Element) {\n        super.loadState(state)\n        settingsManager.readFrom(state.children)\n    }\n\n    override fun addProviderConfiguration(id: String, info: ProviderInfo, credentials: ApiCredentials) {\n        providerSettingsData[id] = ProviderSettingsImpl(\n            id = id,\n            info = info,\n            credentials = encryptCredentials(info, credentials),\n            repository = \"\"\n        )\n    }\n\n    override fun removeAllProviderConfigurations() {\n        providerSettingsData.clear()\n        this.state\n    }\n\n    override fun getProviderConfigurations(): List<ProviderSettings> {\n        return providerSettingsData.values.map {\n            ProviderSettingsImpl(\n                id = it.id,\n                info = it.info,\n                credentials = decryptCredentials(it.info, it.credentials),\n                repository = \"\"\n            )\n        }\n    }\n\n    override fun isLegal(providerData: ProviderData): Boolean {\n        if (providerData.status == ProviderStatus.ERROR || providerData.project.url.isEmpty()) {\n            return false\n        }\n        val url = URL(providerData.project.url)\n        if (publicLegalGrantedDomains.contains(url.host) &&\n            providerData.project.visibility == ProjectVisibility.PUBLIC) {\n            return true\n        }\n        return legalGrantedDomains.contains(url.host)\n    }\n\n    private fun onSettingsChanged(old: ApplicationSettings, new: ApplicationSettings) {\n        getAllProjectServiceProviders().forEach {\n            it.onApplicationSettingsChanged(old, new)\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/AbstractProjectServiceProvider.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure\n\nimport com.intellij.notification.NotificationDisplayType\nimport com.intellij.notification.NotificationGroup\nimport com.intellij.notification.NotificationType\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.openapi.fileEditor.FileEditorManagerEvent\nimport com.intellij.openapi.fileEditor.FileEditorManagerListener\nimport com.intellij.openapi.fileEditor.TextEditor\nimport com.intellij.openapi.vcs.BranchChangeListener\nimport com.intellij.openapi.wm.ToolWindowManager\nimport com.intellij.util.messages.MessageBus\nimport net.ntworld.foundation.Infrastructure\nimport net.ntworld.foundation.MemorizedInfrastructure\nimport net.ntworld.foundation.util.UUIDGenerator\nimport net.ntworld.mergeRequest.MergeRequest\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.ProviderInfo\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequest.request.PublishAllCommentsRequest\nimport net.ntworld.mergeRequest.request.PublishCommentsRequest\nimport net.ntworld.mergeRequestIntegration.DefaultProviderStorage\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegration.provider.MemoryCache\nimport net.ntworld.mergeRequestIntegration.provider.ProviderException\nimport net.ntworld.mergeRequestIntegration.provider.github.Github\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.Gitlab\nimport net.ntworld.mergeRequestIntegrationIde.ComponentFactory\nimport net.ntworld.mergeRequestIntegrationIde.DefaultComponentFactory\nimport net.ntworld.mergeRequestIntegrationIde.IdeInfrastructure\nimport net.ntworld.mergeRequestIntegrationIde.compatibility.IntellijIdeApi\nimport net.ntworld.mergeRequestIntegrationIde.debug\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ProviderSettingsImpl\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ReviewContextManagerImpl\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ServiceBase\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.MergeRequestDataNotifier\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifier\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ReworkEditorNotifier\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.SingleMRToolWindowNotifier\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.provider.MergeRequestDataProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.service.FiltersStorageService\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.service.RepositoryFileService\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.service.internal.FiltersStorageServiceImpl\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.service.repositoryFile.CachedRepositoryFile\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.service.repositoryFile.LocalRepositoryFileService\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettings\nimport net.ntworld.mergeRequestIntegrationIde.rework.ReworkEditorManager\nimport net.ntworld.mergeRequestIntegrationIde.rework.ReworkManager\nimport net.ntworld.mergeRequestIntegrationIde.rework.internal.ReworkEditorManagerImpl\nimport net.ntworld.mergeRequestIntegrationIde.rework.internal.ReworkManagerImpl\nimport net.ntworld.mergeRequestIntegrationIde.task.RegisterProviderTask\nimport net.ntworld.mergeRequestIntegrationIde.ui.configuration.GithubConnectionsConfigurableBase\nimport net.ntworld.mergeRequestIntegrationIde.ui.configuration.GitlabConnectionsConfigurableBase\nimport org.jdom.Element\nimport com.intellij.openapi.project.Project as IdeaProject\n\nabstract class AbstractProjectServiceProvider(\n    final override val project: IdeaProject\n) : ProjectServiceProvider, ServiceBase() {\n    private val myNotification: NotificationGroup = NotificationGroup(\n        \"Merge Request Integration\", NotificationDisplayType.BALLOON, true\n    )\n    private var myIsInitialized = false\n\n    final override val providerStorage: ProviderStorage = DefaultProviderStorage()\n\n    override val infrastructure: Infrastructure = MemorizedInfrastructure(IdeInfrastructure(providerStorage))\n\n    override val applicationSettings: ApplicationSettings\n        get() = applicationServiceProvider.settingsManager\n\n    override val intellijIdeApi: IntellijIdeApi\n        get() = applicationServiceProvider.intellijIdeApi\n\n    final override val messageBus: MessageBus by lazy { project.messageBus }\n\n    override val repositoryFile: RepositoryFileService by lazy {\n        CachedRepositoryFile(\n            LocalRepositoryFileService(this),\n            MemoryCache()\n        )\n    }\n\n    override val filtersStorage: FiltersStorageService = FiltersStorageServiceImpl(this)\n\n    override val reviewContextManager: ReviewContextManager = ReviewContextManagerImpl(this)\n\n    override val projectNotifierTopic: ProjectNotifier = messageBus.syncPublisher(ProjectNotifier.TOPIC)\n\n    override val singleMRToolWindowNotifierTopic = messageBus.syncPublisher(SingleMRToolWindowNotifier.TOPIC)\n\n    override val reworkManager: ReworkManager = ReworkManagerImpl(this)\n\n    final override val reworkEditorManager: ReworkEditorManager = ReworkEditorManagerImpl(this)\n\n    override val componentFactory: ComponentFactory = DefaultComponentFactory(this)\n\n    private val myBranchChangeListener = object: BranchChangeListener {\n        override fun branchWillChange(branchName: String) {\n        }\n\n        override fun branchHasChanged(branchName: String) {\n            debug(\"BranchChangeListener triggered, request create ReworkWatcher for $branchName\")\n            reworkManager.requestCreateReworkWatcher(providerStorage.registeredProviders, branchName)\n        }\n    }\n\n    private val myFileEditorManagerListener = object: FileEditorManagerListener {\n        override fun selectionChanged(event: FileEditorManagerEvent) {\n            val editor = event.newEditor\n            if (editor !is TextEditor) {\n                return\n            }\n            val providers = providerStorage.registeredProviders\n            for (provider in providers) {\n                val reworkWatcher = reworkManager.findActiveReworkWatcher(provider)\n                if (null !== reworkWatcher) {\n                    ApplicationManager.getApplication().invokeLater {\n                        reworkEditorManager.bootstrap(editor, reworkWatcher)\n                    }\n                }\n            }\n        }\n    }\n\n    init {\n        messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, myFileEditorManagerListener)\n        messageBus.connect().subscribe(ReworkEditorNotifier.TOPIC, reworkEditorManager)\n    }\n\n    protected fun initWithApplicationServiceProvider(applicationSP: ApplicationServiceProvider) {\n        applicationSP.watcherManager.addWatcher(reviewContextManager)\n\n        val connection = messageBus.connect(project)\n        connection.subscribe(MergeRequestDataNotifier.TOPIC, MergeRequestDataProvider(this, messageBus))\n        connection.subscribe(BranchChangeListener.VCS_BRANCH_CHANGED, myBranchChangeListener)\n    }\n\n    override fun readStateItem(item: Element, id: String, settings: ProviderSettings) {\n        super.readStateItem(item, id, settings)\n        filtersStorage.readFrom(item, id)\n    }\n\n    override fun writeStateItem(item: Element, id: String, settings: ProviderSettings) {\n        super.writeStateItem(item, id, settings)\n        filtersStorage.writeTo(item, id)\n    }\n\n    override fun addProviderConfiguration(\n        id: String,\n        info: ProviderInfo,\n        credentials: ApiCredentials,\n        repository: String\n    ) {\n        providerSettingsData[id] = ProviderSettingsImpl(\n            id = id,\n            info = info,\n            credentials = encryptCredentials(info, credentials),\n            repository = repository\n        )\n    }\n\n    override fun removeProviderConfiguration(id: String) {\n        providerSettingsData.remove(id)\n    }\n\n    override fun getProviderConfigurations(): List<ProviderSettings> {\n        return providerSettingsData.values.map {\n            ProviderSettingsImpl(\n                id = it.id,\n                info = it.info,\n                credentials = decryptCredentials(it.info, it.credentials),\n                repository = it.repository\n            )\n        }\n    }\n\n    override fun initialize() {\n        myIsInitialized = false\n        providerStorage.clear()\n        reworkManager.clear()\n        projectNotifierTopic.starting()\n\n        getProviderConfigurations().forEach { registerProviderSettings(it) }\n        projectNotifierTopic.initialized()\n        myIsInitialized = true\n    }\n\n    override fun isInitialized(): Boolean {\n        return myIsInitialized\n    }\n\n    override fun openSingleMRToolWindow(invoker: (() -> Unit)?) {\n        val toolWindow = ToolWindowManager.getInstance(project).getToolWindow(\n            applicationServiceProvider.singleMRToolWindowName\n        )\n        if (null !== toolWindow) {\n            toolWindow.show(invoker)\n        }\n    }\n\n    override fun hideSingleMRToolWindow(invoker: (() -> Unit)?) {\n        val toolWindow = ToolWindowManager.getInstance(project).getToolWindow(\n            applicationServiceProvider.singleMRToolWindowName\n        )\n        if (null !== toolWindow) {\n            toolWindow.hide(invoker)\n        }\n    }\n\n    override fun isDoingCodeReview(): Boolean = null !== reviewContextManager.findDoingCodeReviewContext()\n\n    override fun onApplicationSettingsChanged(old: ApplicationSettings, new: ApplicationSettings) {\n        if (!old.enableReworkProcess && new.enableReworkProcess) {\n            for (provider in providerStorage.registeredProviders) {\n                reworkManager.createBranchWatcher(provider)\n            }\n        }\n    }\n\n    override fun startCodeReview(providerData: ProviderData, mergeRequest: MergeRequest) {\n        reviewContextManager.setContextToDoingCodeReview(providerData.id, mergeRequest.id)\n        val reviewContext = reviewContextManager.findDoingCodeReviewContext()\n        if (null !== reviewContext) {\n            projectNotifierTopic.startCodeReview(reviewContext)\n            openSingleMRToolWindow {\n                singleMRToolWindowNotifierTopic.showChangesWhenDoingCodeReview(\n                    reviewContext.providerData, reviewContext.changes\n                )\n            }\n        }\n    }\n\n    override fun stopCodeReview() {\n        val reviewContext = reviewContextManager.findDoingCodeReviewContext()\n        if (null !== reviewContext) {\n            infrastructure.serviceBus() process PublishAllCommentsRequest.make(\n                providerId = reviewContext.providerData.id,\n                mergeRequestId = reviewContext.mergeRequestInfo.id\n            ) ifError {\n                notify(\n                    \"There was an error from server. \\n\\n ${it.message}\",\n                    NotificationType.ERROR\n                )\n                throw ProviderException(it)\n            }\n\n            projectNotifierTopic.stopCodeReview(reviewContext)\n            reviewContext.closeAllChanges()\n            hideSingleMRToolWindow {\n                singleMRToolWindowNotifierTopic.hideChangesAfterDoingCodeReview()\n                singleMRToolWindowNotifierTopic.clearReworkTabs()\n            }\n        }\n        println(\"After doing code review\")\n        providerStorage.registeredProviders.forEach {\n            reworkManager.createBranchWatcher(it)\n        }\n        reviewContextManager.clearContextDoingCodeReview()\n    }\n\n    override fun notify(message: String) {\n        notify(message, NotificationType.INFORMATION)\n    }\n\n    override fun notify(message: String, type: NotificationType) {\n        val notification = myNotification.createNotification(message, type)\n        notification.notify(project)\n    }\n\n    private fun registerProviderSettings(settings: ProviderSettings) {\n        var name = \"\"\n        if (settings.info.id == Gitlab.id) {\n            name = GitlabConnectionsConfigurableBase.findNameFromId(settings.id)\n        }\n        if (settings.info.id == Github.id) {\n            name = GithubConnectionsConfigurableBase.findNameFromId(settings.id)\n        }\n\n        val task = RegisterProviderTask(\n            this,\n            id = UUIDGenerator.generate(),\n            name = name,\n            settings = settings,\n            listener = object : RegisterProviderTask.Listener {\n                override fun providerRegistered(providerData: ProviderData) {\n                    projectNotifierTopic.providerRegistered(providerData)\n                    reworkManager.createBranchWatcher(providerData)\n                }\n            }\n        )\n        task.start()\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/ApplicationServiceProvider.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure\n\nimport com.intellij.openapi.project.Project\nimport net.ntworld.foundation.Infrastructure\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.ProviderInfo\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegrationIde.compatibility.IntellijIdeApi\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsManager\nimport net.ntworld.mergeRequestIntegrationIde.watcher.WatcherManager\n\ninterface ApplicationServiceProvider {\n\n    val intellijIdeApi: IntellijIdeApi\n\n    val settingsManager: ApplicationSettingsManager\n\n    val watcherManager: WatcherManager\n\n    val singleMRToolWindowName: String\n\n    fun findProjectServiceProvider(project: Project): ProjectServiceProvider\n\n    fun addProviderConfiguration(id: String, info: ProviderInfo, credentials: ApiCredentials)\n\n    fun removeAllProviderConfigurations()\n\n    fun getProviderConfigurations(): List<ProviderSettings>\n\n    fun isLegal(providerData: ProviderData): Boolean\n\n    fun getAllProjectServiceProviders(): List<ProjectServiceProvider>\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/ProjectServiceProvider.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure\n\nimport com.intellij.notification.NotificationType\nimport com.intellij.util.messages.MessageBus\nimport net.ntworld.foundation.Infrastructure\nimport net.ntworld.mergeRequest.MergeRequest\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.ProviderInfo\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.ProviderStorage\nimport net.ntworld.mergeRequestIntegrationIde.ComponentFactory\nimport net.ntworld.mergeRequestIntegrationIde.compatibility.IntellijIdeApi\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.SingleMRToolWindowNotifier\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifier\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.service.FiltersStorageService\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.service.RepositoryFileService\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettings\nimport net.ntworld.mergeRequestIntegrationIde.rework.ReworkEditorManager\nimport net.ntworld.mergeRequestIntegrationIde.rework.ReworkManager\nimport com.intellij.openapi.project.Project as IdeaProject\n\ninterface ProjectServiceProvider {\n    val applicationServiceProvider: ApplicationServiceProvider\n\n    val infrastructure: Infrastructure\n\n    val providerStorage: ProviderStorage\n\n    val applicationSettings: ApplicationSettings\n\n    val intellijIdeApi: IntellijIdeApi\n\n    val project: IdeaProject\n\n    val messageBus: MessageBus\n\n    val repositoryFile: RepositoryFileService\n\n    val reworkManager: ReworkManager\n\n    val reworkEditorManager: ReworkEditorManager\n\n    val filtersStorage: FiltersStorageService\n\n    val reviewContextManager: ReviewContextManager\n\n    val projectNotifierTopic: ProjectNotifier\n\n    val singleMRToolWindowNotifierTopic: SingleMRToolWindowNotifier\n\n    val componentFactory: ComponentFactory\n\n    fun addProviderConfiguration(id: String, info: ProviderInfo, credentials: ApiCredentials, repository: String)\n\n    fun removeProviderConfiguration(id: String)\n\n    fun getProviderConfigurations(): List<ProviderSettings>\n\n    fun openSingleMRToolWindow(invoker: (() -> Unit)?)\n\n    fun hideSingleMRToolWindow(invoker: (() -> Unit)?)\n\n    fun initialize()\n\n    fun isInitialized(): Boolean\n\n    fun startCodeReview(providerData: ProviderData, mergeRequest: MergeRequest)\n\n    fun stopCodeReview()\n\n    fun isDoingCodeReview(): Boolean\n\n    fun onApplicationSettingsChanged(old: ApplicationSettings, new: ApplicationSettings)\n\n    fun notify(message: String)\n\n    fun notify(message: String, type: NotificationType)\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/ProviderSettings.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure\n\nimport net.ntworld.mergeRequest.ProviderInfo\nimport net.ntworld.mergeRequest.api.ApiCredentials\n\ninterface ProviderSettings {\n    val id: String\n\n    val info: ProviderInfo\n\n    val credentials: ApiCredentials\n    \n    val repository: String\n\n    val sharable: Boolean\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/ReviewContext.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure\n\nimport com.intellij.openapi.project.Project\nimport com.intellij.openapi.util.Key\nimport com.intellij.openapi.vcs.changes.Change\nimport com.intellij.util.messages.MessageBusConnection\nimport git4idea.repo.GitRepository\nimport net.ntworld.mergeRequest.*\n\ninterface ReviewContext {\n    val project: Project\n\n    val providerData: ProviderData\n\n    val mergeRequestInfo: MergeRequestInfo\n\n    val messageBusConnection: MessageBusConnection\n\n    val diffReference: DiffReference?\n\n    val repository: GitRepository?\n\n    val commits: List<Commit>\n\n    val comments: List<Comment>\n\n    val changes: List<Change>\n\n    val reviewingCommits: List<Commit>\n\n    val reviewingChanges: List<Change>\n\n    fun findChangeByPath(path: String): Change?\n\n    fun getCommentsByPath(path: String): List<Comment>\n\n    fun openChange(change: Change, focus: Boolean, displayMergeRequestId: Boolean)\n\n    fun hasAnyChangeOpened(): Boolean\n\n    fun closeAllChanges()\n\n    fun <T> getChangeData(change: Change, key: Key<T>): T?\n\n    fun <T> putChangeData(change: Change, key: Key<T>, value: T?)\n\n    companion object {\n        val KEY = Key.create<ReviewContext>(\"mri.ReviewContext\")\n    }\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/ReviewContextManager.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure\n\nimport com.intellij.openapi.vcs.changes.Change\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequestIntegrationIde.watcher.Watcher\n\ninterface ReviewContextManager : Watcher {\n    fun initContext(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, selected: Boolean)\n\n    fun isDoingCodeReview(providerId: String, mergeRequestId: String): Boolean\n\n    fun findDoingCodeReviewContext(): ReviewContext?\n\n    fun findSelectedContext(): ReviewContext?\n\n    fun findContext(providerId: String, mergeRequestId: String): ReviewContext?\n\n    fun setSelected(providerId: String, mergeRequestId: String)\n\n    fun clearContextDoingCodeReview()\n\n    fun setContextToDoingCodeReview(providerId: String, mergeRequestId: String)\n\n    fun getDraftCommentsCount(providerId: String, mergeRequestId: String): Int\n\n    fun updateComments(providerId: String, mergeRequestId: String, comments: List<Comment>)\n\n    fun updateCommits(providerId: String, mergeRequestId: String, commits: List<Commit>)\n\n    fun updateChanges(providerId: String, mergeRequestId: String, changes: List<Change>)\n\n    fun updateReviewingCommits(providerId: String, mergeRequestId: String, commits: List<Commit>)\n\n    fun updateReviewingChanges(providerId: String, mergeRequestId: String, changes: List<Change>)\n\n    fun updateMergeRequest(providerId: String, mergeRequest: MergeRequest)\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/internal/ApiCredentialsImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.internal\n\nimport net.ntworld.mergeRequest.api.ApiCredentials\n\ndata class ApiCredentialsImpl(\n    override val url: String,\n    override val login: String,\n    override val token: String,\n    override val projectId: String,\n    override val version: String,\n    override val info: String,\n    override val ignoreSSLCertificateErrors: Boolean\n): ApiCredentials\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/internal/DiffPreviewProviderImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.internal\n\nimport com.intellij.diff.impl.DiffRequestProcessor\nimport com.intellij.openapi.project.Project\nimport com.intellij.openapi.vcs.changes.Change\nimport com.intellij.openapi.vcs.changes.ChangesUtil\nimport com.intellij.openapi.vcs.changes.DiffPreviewProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\n\nclass DiffPreviewProviderImpl(\n    private val project: Project,\n    val change: Change,\n    val reviewContext: ReviewContext? = null,\n    private val showMergeRequestId: Boolean\n) : DiffPreviewProvider {\n\n    override fun getOwner(): Any {\n        return this\n    }\n\n    override fun createDiffRequestProcessor(): DiffRequestProcessor {\n        return DiffRequestProcessorImpl(project, change, reviewContext)\n    }\n\n    override fun getEditorTabName(): String {\n        if (null !== reviewContext && showMergeRequestId) {\n            return \"!${reviewContext.mergeRequestInfo.id}: ${ChangesUtil.getFilePath(change).name}\"\n        }\n        return ChangesUtil.getFilePath(change).name\n    }\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/internal/DiffRequestProcessorImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.internal\n\nimport com.intellij.diff.chains.DiffRequestProducer\nimport com.intellij.diff.impl.CacheDiffRequestProcessor\nimport com.intellij.diff.requests.NoDiffRequest\nimport com.intellij.diff.util.DiffPlaces\nimport com.intellij.openapi.project.Project\nimport com.intellij.openapi.util.Key\nimport com.intellij.openapi.vcs.changes.Change\nimport com.intellij.openapi.vcs.changes.DiffPreviewUpdateProcessor\nimport com.intellij.vcs.log.ui.frame.VcsLogChangesBrowser\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\n\nclass DiffRequestProcessorImpl(\n    project: Project,\n    private val change: Change,\n    val reviewContext: ReviewContext? = null\n) : CacheDiffRequestProcessor.Simple(project, DiffPlaces.DEFAULT), DiffPreviewUpdateProcessor {\n    override fun clear() {\n        applyRequest(NoDiffRequest.INSTANCE, false, null)\n    }\n\n    override fun getFastLoadingTimeMillis(): Int {\n        return 10\n    }\n\n    override fun refresh(fromModelRefresh: Boolean) {\n        updateRequest()\n    }\n\n    override fun getCurrentRequestProvider(): DiffRequestProducer? {\n        val context = if (null === reviewContext) {\n            HashMap()\n        } else {\n            mutableMapOf<Key<*>, Any>(\n                ReviewContext.KEY to reviewContext\n            )\n        }\n        return VcsLogChangesBrowser.createDiffRequestProducer(project!!, change, context, true)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/internal/ProviderSettingsImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.internal\n\nimport net.ntworld.mergeRequest.ProviderInfo\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProviderSettings\n\ndata class ProviderSettingsImpl(\n    override val id: String,\n    override val info: ProviderInfo,\n    override val credentials: ApiCredentials,\n    override val repository: String,\n    override val sharable: Boolean = false\n) : ProviderSettings"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/internal/ReviewContextImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.internal\n\nimport com.intellij.openapi.diagnostic.Logger\nimport com.intellij.openapi.fileEditor.ex.FileEditorManagerEx\nimport com.intellij.openapi.project.Project\nimport com.intellij.openapi.util.Key\nimport com.intellij.openapi.util.UserDataHolderBase\nimport com.intellij.openapi.vcs.changes.Change\nimport com.intellij.openapi.vcs.changes.ChangesUtil\nimport com.intellij.openapi.vcs.changes.PreviewDiffVirtualFile\nimport com.intellij.util.messages.MessageBusConnection\nimport git4idea.repo.GitRepository\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\nimport net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil\n\nclass ReviewContextImpl(\n    val projectServiceProvider: ProjectServiceProvider,\n    override val providerData: ProviderData,\n    override val mergeRequestInfo: MergeRequestInfo,\n    override val messageBusConnection: MessageBusConnection\n) : ReviewContext {\n    private val myLogger = Logger.getInstance(this.javaClass)\n    // private val myPreviewDiffVirtualFileMap = mutableMapOf<Change, PreviewDiffVirtualFile>()\n    private val myPreviewDiffVirtualFileMap = mutableMapOf<Int, PreviewDiffVirtualFile>()\n    private val myCommentsMap = mutableMapOf<String, MutableList<Comment>>()\n    private val myChangesMap = mutableMapOf<String, MutableList<Change>>()\n    private val myChangesDataMap = mutableMapOf<Change, UserDataHolderBase>()\n\n    override val project: Project = projectServiceProvider.project\n\n    override var diffReference: DiffReference? = null\n\n    override val repository: GitRepository? = RepositoryUtil.findRepository(projectServiceProvider, providerData)\n\n    override var commits: List<Commit> = listOf()\n\n    override var comments: List<Comment> = listOf()\n        set(value) {\n            field = value\n            buildCommentsMap(value)\n        }\n\n    override var changes: List<Change> = listOf()\n        set(value) {\n            field = value\n            buildChangesMap(value)\n        }\n\n    override var reviewingCommits: List<Commit> = listOf()\n\n    override var reviewingChanges: List<Change> = listOf()\n\n    override fun findChangeByPath(path: String): Change? {\n        val absolutePath = RepositoryUtil.findAbsoluteCrossPlatformsPath(repository, path)\n        val changes = myChangesMap[absolutePath]\n        if (null === changes) {\n            return null\n        }\n\n        // TODO: do something when has more than 1 change\n        if (changes.size > 1) {\n            myLogger.info(\"There is more than 1 changes for $path\")\n        }\n        return changes.first()\n    }\n\n    override fun getCommentsByPath(path: String): List<Comment> {\n        val crossPlatformsPath = RepositoryUtil.transformToCrossPlatformsPath(path)\n        val comments = myCommentsMap[crossPlatformsPath]\n        if (null !== comments) {\n            return comments\n        }\n        myLogger.info(\"There is no comments for $crossPlatformsPath\")\n        return listOf()\n    }\n\n    override fun openChange(change: Change, focus: Boolean, displayMergeRequestId: Boolean) {\n        val diffFile = myPreviewDiffVirtualFileMap[change.hashCode()]\n        if (null === diffFile) {\n            val provider = DiffPreviewProviderImpl(project, change, this, displayMergeRequestId)\n            val created = PreviewDiffVirtualFile(provider)\n            myPreviewDiffVirtualFileMap[change.hashCode()] = created\n            FileEditorManagerEx.getInstanceEx(project).openFile(created, focus)\n        } else {\n            FileEditorManagerEx.getInstanceEx(project).openFile(diffFile, focus)\n        }\n    }\n\n    override fun hasAnyChangeOpened(): Boolean {\n        return myPreviewDiffVirtualFileMap.isNotEmpty()\n    }\n\n    override fun closeAllChanges() {\n        val fileEditorManagerEx = FileEditorManagerEx.getInstanceEx(project)\n        myPreviewDiffVirtualFileMap.forEach { (_, diffFile) ->\n            fileEditorManagerEx.closeFile(diffFile)\n        }\n        myPreviewDiffVirtualFileMap.clear()\n    }\n\n    override fun <T> getChangeData(change: Change, key: Key<T>): T? {\n        val userDataHolder = myChangesDataMap[change]\n        return if (null !== userDataHolder) {\n            userDataHolder.getUserData(key)\n        } else null\n    }\n\n    override fun <T> putChangeData(change: Change, key: Key<T>, value: T?) {\n        val userDataHolder = myChangesDataMap[change]\n        if (null === userDataHolder) {\n            val dataHolder = UserDataHolderBase()\n            dataHolder.putUserData(key, value)\n            myChangesDataMap[change] = dataHolder\n        } else {\n            userDataHolder.putUserData(key, value)\n        }\n    }\n\n    private fun buildCommentsMap(value: Collection<Comment>) {\n        if (null === repository) {\n            return\n        }\n        myCommentsMap.clear()\n        for (comment in value) {\n            val position = comment.position\n            if (null === position) {\n                continue\n            }\n            if (null !== position.newPath) {\n                doHashComment(repository, position.newPath!!, comment)\n            }\n            if (null !== position.oldPath) {\n                doHashComment(repository, position.oldPath!!, comment)\n            }\n        }\n        myLogger.info(\"myCommentsMap was built successfully\")\n        myCommentsMap.forEach { (path, comments) ->\n            val commentIds = comments.map { it.id }\n            myLogger.info(\"$path contains ${commentIds.joinToString(\",\")}\")\n        }\n    }\n\n    private fun doHashComment(repository: GitRepository, path: String, comment: Comment) {\n        val fullPath = RepositoryUtil.findAbsoluteCrossPlatformsPath(repository, path)\n        val list = myCommentsMap[fullPath]\n        if (null === list) {\n            myCommentsMap[fullPath] = mutableListOf(comment)\n        } else {\n            if (!list.contains(comment)) {\n                list.add(comment)\n            }\n        }\n    }\n\n    private fun buildChangesMap(value: Collection<Change>) {\n        myChangesMap.clear()\n        for (change in value) {\n            val filePaths = ChangesUtil.getPathsCaseSensitive(change)\n            for (filePath in filePaths) {\n                val path = filePath.path\n                val list = myChangesMap.get(path)\n                if (null === list) {\n                    myChangesMap[path] = mutableListOf(change)\n                } else {\n                    if (!list.contains(change)) {\n                        list.add(change)\n                    }\n                }\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/internal/ReviewContextManagerImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.internal\n\nimport com.intellij.openapi.diagnostic.Logger\nimport com.intellij.openapi.vcs.changes.Change\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContextManager\n\nclass ReviewContextManagerImpl(\n    private val projectServiceProvider: ProjectServiceProvider\n) : ReviewContextManager {\n    private val myLogger = Logger.getInstance(this.javaClass)\n    private val myContexts = mutableMapOf<String, ReviewContextImpl>()\n    private var mySelected: String? = null\n    private var myDoingCodeReviewContext: String? = null\n    override val interval: Long = 60000\n\n    private fun keyOf(providerId: String, mergeRequestId: String) = \"$providerId:$mergeRequestId\"\n\n    override fun initContext(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, selected: Boolean) {\n        val key = keyOf(providerData.id, mergeRequestInfo.id)\n        myLogger.info(\"Init context $key\")\n        if (!myContexts.contains(key)) {\n            myContexts[key] = ReviewContextImpl(\n                projectServiceProvider, providerData, mergeRequestInfo, projectServiceProvider.messageBus.connect()\n            )\n        }\n        if (selected) {\n            mySelected = key\n        }\n    }\n\n    override fun isDoingCodeReview(providerId: String, mergeRequestId: String): Boolean {\n        val key = myDoingCodeReviewContext\n        return if (null === key) false else keyOf(providerId, mergeRequestId) == key\n    }\n\n    override fun findSelectedContext() : ReviewContext? {\n        val key = mySelected\n        if (null === key) {\n            return null\n        }\n        return myContexts[key]\n    }\n\n    override fun findDoingCodeReviewContext(): ReviewContext? {\n        val key = myDoingCodeReviewContext\n        if (null === key) {\n            return null\n        }\n        return myContexts[key]\n    }\n\n    override fun findContext(providerId: String, mergeRequestId: String): ReviewContext? {\n        return myContexts[keyOf(providerId, mergeRequestId)]\n    }\n\n    override fun setSelected(providerId: String, mergeRequestId: String) {\n        mySelected = keyOf(providerId, mergeRequestId)\n    }\n\n    override fun clearContextDoingCodeReview() {\n        myDoingCodeReviewContext = null\n    }\n\n    override fun setContextToDoingCodeReview(providerId: String, mergeRequestId: String) {\n        myDoingCodeReviewContext = keyOf(providerId, mergeRequestId)\n    }\n\n    override fun getDraftCommentsCount(providerId: String, mergeRequestId: String): Int {\n        val key = keyOf(providerId, mergeRequestId)\n        val context = myContexts[key]\n        if (null !== context) {\n            return context.comments.filter { it.isDraft }.count()\n        }\n        return 0\n    }\n\n    override fun updateComments(providerId: String, mergeRequestId: String, comments: List<Comment>) {\n        val key = keyOf(providerId, mergeRequestId)\n        val context = myContexts[key]\n        if (null !== context) {\n            myLogger.info(\"Update comments for $key\")\n            context.comments = comments\n        }\n    }\n\n    override fun updateCommits(providerId: String, mergeRequestId: String, commits: List<Commit>) {\n        val key = keyOf(providerId, mergeRequestId)\n        val context = myContexts[key]\n        if (null !== context) {\n            myLogger.info(\"Update commits for $key\")\n            context.commits = commits\n        }\n    }\n\n    override fun updateChanges(providerId: String, mergeRequestId: String, changes: List<Change>) {\n        val key = keyOf(providerId, mergeRequestId)\n        val context = myContexts[key]\n        if (null !== context) {\n            myLogger.info(\"Update changes for $key\")\n            context.changes = changes\n        }\n    }\n\n    override fun updateReviewingCommits(providerId: String, mergeRequestId: String, commits: List<Commit>) {\n        val key = keyOf(providerId, mergeRequestId)\n        val context = myContexts[key]\n        if (null !== context) {\n            myLogger.info(\"Update reviewingCommits for $key\")\n            context.reviewingCommits = commits\n        }\n    }\n\n    override fun updateReviewingChanges(providerId: String, mergeRequestId: String, changes: List<Change>) {\n        val key = keyOf(providerId, mergeRequestId)\n        val context = myContexts[key]\n        if (null !== context) {\n            myLogger.info(\"Update reviewingChanges for $key\")\n            context.reviewingChanges = changes\n        }\n    }\n\n    override fun updateMergeRequest(providerId: String, mergeRequest: MergeRequest) {\n        val key = keyOf(providerId, mergeRequest.id)\n        val context = myContexts[key]\n        if (null !== context) {\n            myLogger.info(\"Update diffReference for $key\")\n            context.diffReference = mergeRequest.diffReference\n        }\n    }\n\n    override fun canExecute(): Boolean {\n        return myContexts.isNotEmpty()\n    }\n\n    override fun shouldTerminate(): Boolean {\n        return false\n    }\n\n    override fun execute() {\n        val ids = myContexts\n            .filter { it.key != mySelected && it.key != myDoingCodeReviewContext && !it.value.hasAnyChangeOpened() }\n            .map { it.key }\n\n        if (ids.isNotEmpty()) {\n            ids.forEach {\n                val context = myContexts.remove(it)\n                if (null !== context) {\n                    context.messageBusConnection.disconnect()\n                }\n            }\n            myLogger.info(\"Clear inactive contexts Id: ${ids.joinToString(\", \")}\")\n        }\n    }\n\n    override fun terminate() {\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/internal/ServiceBase.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.internal\n\nimport com.intellij.credentialStore.CredentialAttributes\nimport com.intellij.ide.passwordSafe.PasswordSafe\nimport com.intellij.openapi.components.PersistentStateComponent\nimport net.ntworld.mergeRequest.ProviderInfo\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.Gitlab\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProviderSettings\nimport org.jdom.Element\n\nopen class ServiceBase : PersistentStateComponent<Element> {\n    protected val providerSettingsData = mutableMapOf<String, ProviderSettings>()\n    private val supportedProviders: List<ProviderInfo> = listOf(\n        Gitlab\n        // Gitlab, Github\n    )\n\n    override fun getState(): Element? {\n        val element = Element(\"Provider\")\n        providerSettingsData.values.map {\n            val item = Element(\"Item\")\n            item.setAttribute(\"id\", it.id)\n            writeStateItem(item, it.id, it)\n            element.addContent(item)\n        }\n        return element\n    }\n\n    protected open fun writeStateItem(item: Element, id: String, settings: ProviderSettings) {\n        item.setAttribute(\"providerId\", settings.info.id)\n        item.setAttribute(\"url\", settings.credentials.url)\n        item.setAttribute(\"login\", settings.credentials.login)\n        item.setAttribute(\"projectId\", settings.credentials.projectId)\n        item.setAttribute(\"version\", settings.credentials.version)\n        item.setAttribute(\"info\", settings.credentials.info)\n        item.setAttribute(\"ignoreSSLCertificateErrors\", if (settings.credentials.ignoreSSLCertificateErrors) \"1\" else \"0\")\n        item.setAttribute(\"repository\", settings.repository)\n    }\n\n    override fun loadState(state: Element) {\n        for (item in state.children) {\n            if (item.name != \"Item\") {\n                continue\n            }\n\n            val info = supportedProviders.firstOrNull { it.id == item.getAttribute(\"providerId\").value }\n            if (null === info) {\n                continue\n            }\n            val credentials = ApiCredentialsImpl(\n                url = item.getAttribute(\"url\").value,\n                login = item.getAttribute(\"login\").value,\n                token = \"\",\n                projectId = item.getAttribute(\"projectId\").value,\n                version = item.getAttribute(\"version\").value,\n                info = item.getAttribute(\"info\").value,\n                ignoreSSLCertificateErrors = shouldIgnoreSSLCertificateErrors(item)\n            )\n            val id = item.getAttribute(\"id\").value\n            val settings = ProviderSettingsImpl(\n                id = id.trim(),\n                info = info,\n                credentials = decryptCredentials(info, credentials),\n                repository = item.getAttribute(\"repository\").value\n            )\n            readStateItem(item, id, settings)\n        }\n    }\n\n    protected open fun readStateItem(item: Element, id: String, settings: ProviderSettings) {\n        providerSettingsData[id] = settings\n    }\n\n    private fun shouldIgnoreSSLCertificateErrors(item: Element): Boolean {\n        val attribute = item.getAttribute(\"ignoreSSLCertificateErrors\")\n        if (null === attribute) {\n            return false\n        }\n        return attribute.value == \"1\" || attribute.value.toLowerCase() == \"true\"\n    }\n\n    protected fun encryptCredentials(info: ProviderInfo, credentials: ApiCredentials): ApiCredentials {\n        encryptPassword(info, credentials, credentials.token)\n        return ApiCredentialsImpl(\n            url = credentials.url,\n            // -----------------------------------------------------------------\n            // Always bind login and token because if we don't the token will be\n            // empty if the state not stored to the storage yet.\n            // It's safe because the secret is stored on memory only.\n            login = credentials.login,\n            token = credentials.token,\n            // -----------------------------------------------------------------\n            projectId = credentials.projectId,\n            version = credentials.version,\n            info = credentials.info,\n            ignoreSSLCertificateErrors = credentials.ignoreSSLCertificateErrors\n        )\n    }\n\n    protected fun decryptCredentials(info: ProviderInfo, credentials: ApiCredentials): ApiCredentials {\n        return ApiCredentialsImpl(\n            url = credentials.url,\n            // -----------------------------------------------------------------\n            // Always bind login and token because if we don't the token will be\n            // empty if the state not stored to the storage yet.\n            // It's safe because the secret is stored on memory only.\n            login = credentials.login,\n            token = decryptPassword(info, credentials) ?: credentials.token,\n            // -----------------------------------------------------------------\n            projectId = credentials.projectId,\n            version = credentials.version,\n            info = credentials.info,\n            ignoreSSLCertificateErrors = credentials.ignoreSSLCertificateErrors\n        )\n    }\n\n    private fun encryptPassword(info: ProviderInfo, credentials: ApiCredentials, password: String) {\n        PasswordSafe.instance.setPassword(makeCredentialAttribute(info, credentials), password)\n    }\n\n    private fun decryptPassword(info: ProviderInfo, credentials: ApiCredentials): String? {\n        val password = PasswordSafe.instance.getPassword(makeCredentialAttribute(info, credentials))\n        if (null === password || password.isEmpty()) {\n            // Handle legacy CredentialAttribute\n            return PasswordSafe.instance.getPassword(makeLegacyCredentialAttribute(info, credentials))\n        }\n        return password\n    }\n\n    /**\n     * For Windows, Intellij is using KeePass which have a 36 chars limitation on the group name, therefore I have\n     * to shorten the group name since v2019.3.3\n     */\n    private fun makeCredentialAttribute(info: ProviderInfo, credentials: ApiCredentials): CredentialAttributes {\n        if (credentials.url == credentials.login) {\n            return CredentialAttributes(\"MRI:${info.id}\", credentials.url)\n        }\n        return CredentialAttributes(\"MRI:${info.id}\", \"${credentials.login}:${credentials.url}\")\n    }\n\n    /**\n     * I have to keep legacy credential attribute otherwise current users have to input the token again\n     * which is not available anymore. I meant can't see the token again after refreshing Gitlab's page.\n     */\n    private fun makeLegacyCredentialAttribute(info: ProviderInfo, credentials: ApiCredentials): CredentialAttributes {\n        return CredentialAttributes(\n            \"MRI - ${info.id} - ${credentials.url} - ${credentials.login}\"\n        )\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/notifier/DiffNotifier.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier\n\nimport com.intellij.openapi.util.Key\nimport com.intellij.openapi.vcs.changes.Change\nimport com.intellij.util.messages.Topic\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\n\ninterface DiffNotifier {\n    companion object {\n        val TOPIC = Topic.create(\"MRI:DiffNotifier\", DiffNotifier::class.java)\n\n        val ScrollPosition = Key.create<CommentPosition>(\"DiffNotifier.ScrollPosition\")\n        val ScrollShowComments = Key.create<Boolean>(\"DiffNotifier.ScrollShowComments\")\n    }\n\n    fun scrollToPositionRequested(\n        reviewContext: ReviewContext,\n        change: Change,\n        position: CommentPosition,\n        showComments: Boolean?\n    )\n\n    fun hideAllCommentsRequested(reviewContext: ReviewContext, change: Change)\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/notifier/MergeRequestDataNotifier.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier\n\nimport com.intellij.util.messages.Topic\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\n\ninterface MergeRequestDataNotifier {\n    companion object {\n        val TOPIC = Topic.create(\"MRI:MergeRequestDataNotifier\", MergeRequestDataNotifier::class.java)\n    }\n\n    fun fetchCommentsRequested(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo)\n\n    fun onCommentsUpdated(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comments: List<Comment>)\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/notifier/ProjectNotifier.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier\n\nimport com.intellij.util.messages.Topic\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettings\n\ninterface ProjectNotifier {\n    companion object {\n        val TOPIC = Topic.create(\"MRI:ProjectNotifier\", ProjectNotifier::class.java)\n    }\n\n    fun starting()\n\n    fun initialized()\n\n    fun providerRegistered(providerData: ProviderData)\n\n    fun startCodeReview(reviewContext: ReviewContext)\n\n    fun stopCodeReview(reviewContext: ReviewContext)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/notifier/ProjectNotifierAdapter.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier\n\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\n\nopen class ProjectNotifierAdapter : ProjectNotifier {\n    override fun starting() {\n    }\n\n    override fun initialized() {\n    }\n\n    override fun providerRegistered(providerData: ProviderData) {\n    }\n\n    override fun startCodeReview(reviewContext: ReviewContext) {\n    }\n\n    override fun stopCodeReview(reviewContext: ReviewContext) {\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/notifier/ReworkEditorNotifier.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier\n\nimport com.intellij.openapi.fileEditor.TextEditor\nimport com.intellij.util.messages.Topic\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.rework.ReworkWatcher\n\ninterface ReworkEditorNotifier {\n    companion object {\n        val TOPIC = Topic.create(\"MRI:ReworkEditorNotifier\", ReworkEditorNotifier::class.java)\n    }\n\n    fun bootstrap(reworkWatcher: ReworkWatcher)\n\n    fun bootstrap(editor: TextEditor, reworkWatcher: ReworkWatcher)\n\n    fun open(providerData: ProviderData, path: String, line: Int? = null)\n\n    fun commentsUpdated(providerData: ProviderData)\n\n    fun shutdown(providerData: ProviderData)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/notifier/ReworkWatcherNotifier.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier\n\nimport com.intellij.util.messages.Topic\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreeView\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node.Node\n\ninterface ReworkWatcherNotifier {\n    companion object {\n        val TOPIC = Topic.create(\"MRI:ReworkWatcherNotifier\", ReworkWatcherNotifier::class.java)\n    }\n\n    fun requestFetchComment(providerData: ProviderData)\n\n    fun changeDisplayResolvedComments(providerData: ProviderData, value: Boolean)\n\n    fun changeOnlyShowDraftComments(providerData: ProviderData, value: Boolean)\n\n    fun commentTreeNodeSelected(providerData: ProviderData, node: Node, type: CommentTreeView.TreeSelectType)\n\n    fun openCreateGeneralCommentForm(providerData: ProviderData)\n\n    fun requestReplyComment(providerData: ProviderData, content: String, repliedComment: Comment)\n\n    fun requestCreateComment(providerData: ProviderData, content: String, position: GutterPosition?, isDraft: Boolean)\n\n    fun requestEditComment(providerData: ProviderData, comment: Comment, content: String)\n\n    fun requestPublishComment(providerData: ProviderData, comment: Comment)\n\n    fun requestDeleteComment(providerData: ProviderData, comment: Comment)\n\n    fun requestResolveComment(providerData: ProviderData, comment: Comment)\n\n    fun requestUnresolveComment(providerData: ProviderData, comment: Comment)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/notifier/SingleMRToolWindowNotifier.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier\n\nimport com.intellij.openapi.vcs.changes.Change\nimport com.intellij.util.messages.Topic\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.rework.ReworkWatcher\n\ninterface SingleMRToolWindowNotifier {\n    companion object {\n        val TOPIC = Topic.create(\"MRI:SingleMRToolWindowNotifier\", SingleMRToolWindowNotifier::class.java)\n    }\n\n    fun registerReworkWatcher(reworkWatcher: ReworkWatcher)\n\n    fun removeReworkWatcher(reworkWatcher: ReworkWatcher)\n\n    fun hideChangesAfterDoingCodeReview()\n\n    fun clearReworkTabs()\n\n    fun showChangesWhenDoingCodeReview(providerData: ProviderData, changes: List<Change>)\n\n    fun showReworkChanges(reworkWatcher: ReworkWatcher, changes: List<Change>)\n\n    fun showReworkComments(reworkWatcher: ReworkWatcher, comments: List<Comment>, displayResolvedComments: Boolean)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/notifier/provider/MergeRequestDataProvider.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.provider\n\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.util.messages.MessageBus\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.MergeRequestDataNotifier\nimport net.ntworld.mergeRequestIntegrationIde.task.GetCommentsTask\n\nclass MergeRequestDataProvider(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val messageBus: MessageBus\n) : MergeRequestDataNotifier {\n    private val getCommentsTaskListener = object : GetCommentsTask.Listener {\n        override fun dataReceived(\n            providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comments: List<Comment>\n        ) {\n            ApplicationManager.getApplication().invokeLater {\n                messageBus.syncPublisher(MergeRequestDataNotifier.TOPIC)\n                    .onCommentsUpdated(providerData, mergeRequestInfo, comments)\n            }\n        }\n    }\n\n    override fun fetchCommentsRequested(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) {\n        val task = GetCommentsTask(\n            projectServiceProvider,\n            providerData,\n            mergeRequestInfo,\n            getCommentsTaskListener\n        )\n        task.start()\n    }\n\n    override fun onCommentsUpdated(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comments: List<Comment>) {\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/service/FiltersStorageService.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.service\n\nimport net.ntworld.mergeRequest.api.MergeRequestOrdering\nimport net.ntworld.mergeRequest.query.GetMergeRequestFilter\nimport org.jdom.Element\n\ninterface FiltersStorageService {\n\n    fun find(providerDataKey: String): Pair<GetMergeRequestFilter, MergeRequestOrdering>\n\n    fun save(providerDataKey: String, filters: GetMergeRequestFilter, ordering: MergeRequestOrdering)\n\n    fun writeTo(element: Element, providerDataKey: String)\n\n    fun readFrom(element: Element, providerDataKey: String)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/service/RepositoryFileService.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.service\n\nimport com.intellij.openapi.vcs.changes.Change\nimport net.ntworld.mergeRequest.ProviderData\nimport javax.swing.Icon\n\ninterface RepositoryFileService {\n\n    fun findChanges(providerData: ProviderData, hashes: List<String>): List<Change>\n\n    fun findIcon(providerData: ProviderData, path: String) : Icon\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/service/internal/FiltersStorageServiceImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.service.internal\n\nimport net.ntworld.mergeRequest.MergeRequestState\nimport net.ntworld.mergeRequest.api.MergeRequestOrdering\nimport net.ntworld.mergeRequest.query.GetMergeRequestFilter\nimport net.ntworld.mergeRequest.query.generated.GetMergeRequestFilterImpl\nimport net.ntworld.mergeRequestIntegration.util.SavedFiltersUtil\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.service.FiltersStorageService\nimport org.jdom.Element\n\nclass FiltersStorageServiceImpl(\n    private val serviceProvider: ProjectServiceProvider\n) : FiltersStorageService {\n    private val myFiltersData: MutableMap<String, Pair<GetMergeRequestFilter, MergeRequestOrdering>> = mutableMapOf()\n\n    override fun find(providerDataKey: String): Pair<GetMergeRequestFilter, MergeRequestOrdering> {\n        val data = myFiltersData[providerDataKey]\n        return if (null !== data && serviceProvider.applicationSettings.saveMRFilterState) {\n            data\n        } else {\n            Pair(\n                GetMergeRequestFilterImpl(\n                    id = null,\n                    state = MergeRequestState.OPENED,\n                    search = \"\",\n                    authorId = \"\",\n                    assigneeId = \"\",\n                    approverIds = listOf(\"\"),\n                    sourceBranch = \"\"\n                ),\n                MergeRequestOrdering.RECENTLY_UPDATED\n            )\n        }\n    }\n\n    override fun save(providerDataKey: String, filters: GetMergeRequestFilter, ordering: MergeRequestOrdering) {\n        if (serviceProvider.applicationSettings.saveMRFilterState) {\n            myFiltersData[providerDataKey] = Pair(filters, ordering)\n        }\n    }\n\n    override fun readFrom(element: Element, providerDataKey: String) {\n        val attribute = element.getAttribute(\"savedFilters\")\n        if (null !== attribute) {\n            val data = SavedFiltersUtil.parse(attribute.value)\n            if (null !== data) {\n                myFiltersData[providerDataKey] = data\n            }\n        }\n    }\n\n    override fun writeTo(element: Element, providerDataKey: String) {\n        val data = myFiltersData[providerDataKey]\n        if (null !== data) {\n            element.setAttribute(\"savedFilters\", SavedFiltersUtil.stringify(data.first, data.second))\n        }\n    }\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/service/repositoryFile/CachedRepositoryFile.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.service.repositoryFile\n\nimport com.intellij.openapi.diagnostic.Logger\nimport com.intellij.openapi.vcs.changes.Change\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.api.Cache\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.service.RepositoryFileService\nimport javax.swing.Icon\n\nclass CachedRepositoryFile(\n    service: RepositoryFileService,\n    private val cache: Cache\n) : RepositoryFileDecorator(service) {\n    private val myLogger = Logger.getInstance(this.javaClass)\n\n    override fun findChanges(providerData: ProviderData, hashes: List<String>): List<Change> {\n        val key = \"${providerData.id}:${hashes.joinToString(\",\")}\"\n        return cache.getOrRun(key) {\n            myLogger.info(\"Cache $key not found\")\n            val result = super.findChanges(providerData, hashes)\n\n            cache.set(key, result, FIND_CHANGES_TTL)\n            result\n        }\n    }\n\n    override fun findIcon(providerData: ProviderData, path: String): Icon {\n        val key = \"${providerData.id}:icon:${path}\"\n        return cache.getOrRun(key) {\n            myLogger.info(\"Cache $key not found\")\n            val result = super.findIcon(providerData, path)\n\n            cache.set(key, result, FIND_ICON_TTL)\n            result\n        }\n    }\n\n    companion object {\n        const val FIND_CHANGES_TTL = 30000\n        const val FIND_ICON_TTL = 60000\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/service/repositoryFile/LocalRepositoryFileService.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.service.repositoryFile\n\nimport com.intellij.icons.AllIcons\nimport com.intellij.openapi.diagnostic.Logger\nimport com.intellij.openapi.util.Iconable\nimport com.intellij.openapi.vcs.changes.Change\nimport com.intellij.openapi.vfs.LocalFileSystem\nimport com.intellij.openapi.vfs.VirtualFile\nimport com.intellij.psi.PsiFile\nimport com.intellij.psi.PsiManager\nimport com.intellij.vcs.log.impl.VcsLogContentUtil\nimport com.intellij.vcs.log.util.VcsLogUtil\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.service.RepositoryFileService\nimport net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil\nimport javax.swing.Icon\n\nclass LocalRepositoryFileService(\n    private val projectServiceProvider: ProjectServiceProvider\n) : RepositoryFileService {\n    private val myLogger = Logger.getInstance(this.javaClass)\n\n    override fun findChanges(providerData: ProviderData, hashes: List<String>): List<Change> {\n        try {\n            val repository = RepositoryUtil.findRepository(projectServiceProvider, providerData)\n            if (null === repository) {\n                return listOf()\n            }\n            val log = VcsLogContentUtil.getOrCreateLog(projectServiceProvider.project)\n            if (null === log) {\n                return listOf()\n            }\n\n            val details = VcsLogUtil.getDetails(\n                log.dataManager.getLogProvider(repository.root),\n                repository.root,\n                hashes\n            )\n            return VcsLogUtil.collectChanges(details) { it.changes }\n        } catch (exception: Exception) {\n            myLogger.info(\"Cannot findChanges for ${providerData.repository}, hashes: ${hashes.joinToString(\",\")}\")\n            throw exception\n        }\n    }\n\n    override fun findIcon(providerData: ProviderData, path: String): Icon {\n        val repository = RepositoryUtil.findRepository(projectServiceProvider, providerData)\n        val psiFile = findPsiFile(RepositoryUtil.findAbsoluteCrossPlatformsPath(repository, path))\n        if (null === psiFile) {\n            return AllIcons.FileTypes.Any_type\n        }\n\n        return try {\n            psiFile.getIcon(Iconable.ICON_FLAG_READ_STATUS)\n        } catch (exception: Exception) {\n            AllIcons.FileTypes.Any_type\n        }\n    }\n\n    private fun findPsiFile(path: String): PsiFile? {\n        val file = findVirtualFileByPath(path)\n        if (null === file) {\n            return null\n        }\n        return PsiManager.getInstance(projectServiceProvider.project).findFile(file)\n    }\n\n    fun findVirtualFileByPath(path: String): VirtualFile? {\n        val file = LocalFileSystem.getInstance().findFileByPath(path)\n        if (null !== file) {\n            return file\n        }\n        return null\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/service/repositoryFile/RepositoryFileDecorator.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.service.repositoryFile\n\nimport com.intellij.openapi.vcs.changes.Change\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.service.RepositoryFileService\nimport javax.swing.Icon\n\nopen class RepositoryFileDecorator(\n    private val service: RepositoryFileService\n) : RepositoryFileService {\n\n    override fun findChanges(providerData: ProviderData, hashes: List<String>): List<Change> {\n        return service.findChanges(providerData, hashes)\n    }\n\n    override fun findIcon(providerData: ProviderData, path: String): Icon {\n        return service.findIcon(providerData, path)\n    }\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/ApplicationSettings.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting\n\nimport net.ntworld.mergeRequest.api.ApiOptions\nimport net.ntworld.mergeRequestIntegration.internal.ApiOptionsImpl\n\ninterface ApplicationSettings {\n    val enableRequestCache: Boolean\n\n    val saveMRFilterState: Boolean\n\n    val displayCommentsInDiffView: Boolean\n\n    val showAddCommentIconsInDiffViewGutter: Boolean\n\n    val checkoutTargetBranch: Boolean\n\n    val maxDiffChangesOpenedAutomatically: Int\n\n    val displayUpVotesAndDownVotes: Boolean\n\n    val displayMergeRequestState: Boolean\n\n    val enableReworkProcess: Boolean\n\n    fun toApiOptions(): ApiOptions {\n        return ApiOptionsImpl(\n            enableRequestCache = this.enableRequestCache\n        )\n    }\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/ApplicationSettingsImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting\n\ndata class ApplicationSettingsImpl(\n    override val enableRequestCache: Boolean,\n    override val saveMRFilterState: Boolean,\n    override val displayCommentsInDiffView: Boolean,\n    override val showAddCommentIconsInDiffViewGutter: Boolean,\n    override val checkoutTargetBranch: Boolean,\n    override val maxDiffChangesOpenedAutomatically: Int,\n    override val displayUpVotesAndDownVotes: Boolean,\n    override val displayMergeRequestState: Boolean,\n    override val enableReworkProcess: Boolean\n) : ApplicationSettings {\n    companion object {\n        val DEFAULT = ApplicationSettingsImpl(\n            enableRequestCache = true,\n            saveMRFilterState = true,\n            displayCommentsInDiffView = false,\n            showAddCommentIconsInDiffViewGutter = true,\n            checkoutTargetBranch = false,\n            maxDiffChangesOpenedAutomatically = 10,\n            displayUpVotesAndDownVotes = false,\n            displayMergeRequestState = true,\n            enableReworkProcess = true\n        )\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/ApplicationSettingsManager.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting\n\nimport org.jdom.Element\n\ninterface ApplicationSettingsManager : ApplicationSettings {\n    fun writeTo(element: Element)\n\n    fun readFrom(elements: List<Element>): ApplicationSettings\n\n    fun update(settings: ApplicationSettings)\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/ApplicationSettingsManagerImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting\n\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option.*\nimport org.jdom.Element\n\nclass ApplicationSettingsManagerImpl(\n    private val changedInvoker: ((ApplicationSettings, ApplicationSettings) -> Unit)\n) : ApplicationSettingsManager {\n    private var mySettings: ApplicationSettings = ApplicationSettingsImpl.DEFAULT\n    private val myOptionEnableRequestCache = EnableRequestCacheOption()\n    private val myOptionSaveMRFilterState = SaveMRFilterStateOption()\n    private val myOptionDisplayCommentsInDiffView = DisplayCommentsInDiffViewOption()\n    private val myOptionShowAddCommentIconsInDiffViewGutter = ShowAddCommentIconsInDiffViewGutterOption()\n    private val myOptionCheckoutTargetBranch = CheckoutTargetBranchOption()\n    private val myOptionMaxDiffChangesOpenedAutomatically = MaxDiffChangesOpenedAutomaticallyOption()\n    private val myOptionDisplayUpVotesAndDownVotes = DisplayUpVotesAndDownVotesOption()\n    private val myOptionDisplayMergeRequestState = DisplayMergeRequestStateOption()\n    private val myOptionEnableReworkProcess = EnableReworkProcessOption()\n    private val myAllSettingOptions = listOf<SettingOption<*>>(\n        myOptionEnableRequestCache,\n        myOptionSaveMRFilterState,\n        myOptionDisplayCommentsInDiffView,\n        myOptionShowAddCommentIconsInDiffViewGutter,\n        myOptionCheckoutTargetBranch,\n        myOptionMaxDiffChangesOpenedAutomatically,\n        myOptionDisplayUpVotesAndDownVotes,\n        myOptionDisplayMergeRequestState,\n        myOptionEnableReworkProcess\n    )\n\n    val settings: ApplicationSettings\n        get() = mySettings\n\n    override val enableRequestCache: Boolean\n        get() = mySettings.enableRequestCache\n\n    override val saveMRFilterState: Boolean\n        get() = mySettings.saveMRFilterState\n\n    override val displayCommentsInDiffView: Boolean\n        get() = mySettings.displayCommentsInDiffView\n\n    override val showAddCommentIconsInDiffViewGutter: Boolean\n        get() = mySettings.showAddCommentIconsInDiffViewGutter\n\n    override val checkoutTargetBranch: Boolean\n        get() = mySettings.checkoutTargetBranch\n\n    override val maxDiffChangesOpenedAutomatically: Int\n        get() = mySettings.maxDiffChangesOpenedAutomatically\n\n    override val displayUpVotesAndDownVotes: Boolean\n        get() = mySettings.displayUpVotesAndDownVotes\n\n    override val displayMergeRequestState: Boolean\n        get() = mySettings.displayMergeRequestState\n\n    override val enableReworkProcess: Boolean\n        get() = mySettings.enableReworkProcess\n\n    override fun readFrom(elements: List<Element>): ApplicationSettings {\n        var settings = ApplicationSettingsImpl.DEFAULT\n        for (item in elements) {\n            if (item.name != \"Setting\") {\n                continue\n            }\n\n            val nameAttribute = item.getAttribute(\"name\")\n            if (null === nameAttribute) {\n                continue\n            }\n\n            val valueAttribute = item.getAttribute(\"value\")\n            if (null === valueAttribute) {\n                continue\n            }\n\n            for (option in myAllSettingOptions) {\n                if (option.name == nameAttribute.value.trim()) {\n                    settings = option.readValue(valueAttribute.value, settings)\n                }\n            }\n        }\n        mySettings = settings\n        return settings\n    }\n\n    override fun writeTo(element: Element) {\n        writeOption(element, myOptionEnableRequestCache, mySettings.enableRequestCache)\n        writeOption(element, myOptionSaveMRFilterState, mySettings.saveMRFilterState)\n        writeOption(element, myOptionDisplayCommentsInDiffView, mySettings.displayCommentsInDiffView)\n        writeOption(element, myOptionShowAddCommentIconsInDiffViewGutter, mySettings.showAddCommentIconsInDiffViewGutter)\n        writeOption(element, myOptionCheckoutTargetBranch, mySettings.checkoutTargetBranch)\n        writeOption(element, myOptionMaxDiffChangesOpenedAutomatically, mySettings.maxDiffChangesOpenedAutomatically)\n        writeOption(element, myOptionDisplayUpVotesAndDownVotes, mySettings.displayUpVotesAndDownVotes)\n        writeOption(element, myOptionDisplayMergeRequestState, mySettings.displayMergeRequestState)\n        writeOption(element, myOptionEnableReworkProcess, mySettings.enableReworkProcess)\n    }\n\n    override fun update(settings: ApplicationSettings) {\n        val oldSettings = mySettings\n        mySettings = settings\n        changedInvoker.invoke(oldSettings, settings)\n    }\n\n    private fun<T> writeOption(root: Element, option: SettingOption<T>, value: T) {\n        val item = Element(\"Setting\")\n        item.setAttribute(\"name\", option.name)\n        item.setAttribute(\"value\", option.writeValue(value))\n        root.addContent(item)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/BooleanOption.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option\n\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl\n\nabstract class BooleanOption : SettingOption<Boolean> {\n    protected abstract fun getOptionValueFromSettings(settings: ApplicationSettingsImpl) : Boolean\n\n    protected abstract fun copySettings(settings: ApplicationSettingsImpl, value: Boolean) : ApplicationSettingsImpl\n\n    final override fun writeValue(value: Boolean): String {\n        return if (value) \"1\" else \"0\"\n    }\n\n    final override fun readValue(input: String, currentSettings: ApplicationSettingsImpl): ApplicationSettingsImpl {\n        val value = input.trim() == \"1\"\n        if (getOptionValueFromSettings(currentSettings) != value) {\n            return copySettings(currentSettings, value)\n        }\n        return currentSettings\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/CheckoutTargetBranchOption.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option\n\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl\n\nclass CheckoutTargetBranchOption : BooleanOption() {\n    override val name: String = \"code-review:checkout-target-branch\"\n\n    override fun getOptionValueFromSettings(settings: ApplicationSettingsImpl): Boolean {\n        return settings.checkoutTargetBranch\n    }\n\n    override fun copySettings(settings: ApplicationSettingsImpl, value: Boolean): ApplicationSettingsImpl {\n        return settings.copy(checkoutTargetBranch = value)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/DisplayCommentsInDiffViewOption.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option\n\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl\n\nclass DisplayCommentsInDiffViewOption : BooleanOption() {\n    override val name: String = \"comments:display-comments-in-diff-view\"\n\n    override fun getOptionValueFromSettings(settings: ApplicationSettingsImpl): Boolean {\n        return settings.displayCommentsInDiffView\n    }\n\n    override fun copySettings(settings: ApplicationSettingsImpl, value: Boolean): ApplicationSettingsImpl {\n        return settings.copy(displayCommentsInDiffView = value)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/DisplayMergeRequestStateOption.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option\n\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl\n\nclass DisplayMergeRequestStateOption : BooleanOption() {\n    override val name: String = \"merge-request:display-state\"\n\n    override fun getOptionValueFromSettings(settings: ApplicationSettingsImpl): Boolean {\n        return settings.displayMergeRequestState\n    }\n\n    override fun copySettings(settings: ApplicationSettingsImpl, value: Boolean): ApplicationSettingsImpl {\n        return settings.copy(displayMergeRequestState = value)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/DisplayUpVotesAndDownVotesOption.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option\n\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl\n\nclass DisplayUpVotesAndDownVotesOption : BooleanOption() {\n    override val name: String = \"merge-request:display-upvotes-downvotes\"\n\n    override fun getOptionValueFromSettings(settings: ApplicationSettingsImpl): Boolean {\n        return settings.displayUpVotesAndDownVotes\n    }\n\n    override fun copySettings(settings: ApplicationSettingsImpl, value: Boolean): ApplicationSettingsImpl {\n        return settings.copy(displayUpVotesAndDownVotes = value)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/EnableRequestCacheOption.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option\n\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl\n\nclass EnableRequestCacheOption : BooleanOption() {\n    override val name: String = \"enable-request-cache\"\n\n    override fun getOptionValueFromSettings(settings: ApplicationSettingsImpl): Boolean {\n        return settings.enableRequestCache\n    }\n\n    override fun copySettings(settings: ApplicationSettingsImpl, value: Boolean): ApplicationSettingsImpl {\n        return settings.copy(enableRequestCache = value)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/EnableReworkProcessOption.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option\n\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl\n\nclass EnableReworkProcessOption : BooleanOption() {\n    override val name: String = \"rework:enabled\"\n\n    override fun getOptionValueFromSettings(settings: ApplicationSettingsImpl): Boolean {\n        return settings.enableReworkProcess\n    }\n\n    override fun copySettings(settings: ApplicationSettingsImpl, value: Boolean): ApplicationSettingsImpl {\n        return settings.copy(enableReworkProcess = value)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/MaxDiffChangesOpenedAutomaticallyOption.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option\n\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl\n\nclass MaxDiffChangesOpenedAutomaticallyOption : SettingOption<Int> {\n    override val name: String = \"code-review:max-diff-change-opened-automatically\"\n\n    override fun writeValue(value: Int): String {\n        return if (!isValid(value)) DEFAULT_VALUE.toString() else value.toString()\n    }\n\n    override fun readValue(input: String, currentSettings: ApplicationSettingsImpl): ApplicationSettingsImpl {\n        val value = parse(input)\n        if (currentSettings.maxDiffChangesOpenedAutomatically != value) {\n            return currentSettings.copy(maxDiffChangesOpenedAutomatically = value)\n        }\n        return currentSettings\n    }\n\n    companion object {\n        const val DEFAULT_VALUE = 10\n\n        fun parse(input: String) : Int {\n            val rawValue = input.trim().toIntOrNull()\n            return if (null === rawValue || !isValid(rawValue)) DEFAULT_VALUE else rawValue\n        }\n\n        private fun isValid(value: Int) = value in 0..1000\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/SaveMRFilterStateOption.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option\n\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl\n\nclass SaveMRFilterStateOption : BooleanOption() {\n    override val name: String = \"save-mr-filter-state\"\n\n    override fun getOptionValueFromSettings(settings: ApplicationSettingsImpl): Boolean {\n        return settings.saveMRFilterState\n    }\n\n    override fun copySettings(settings: ApplicationSettingsImpl, value: Boolean): ApplicationSettingsImpl {\n        return settings.copy(saveMRFilterState = value)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/SettingOption.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option\n\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl\n\ninterface SettingOption<T> {\n    val name: String\n\n    fun writeValue(value: T): String\n\n    fun readValue(input: String, currentSettings: ApplicationSettingsImpl): ApplicationSettingsImpl\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/setting/option/ShowAddCommentIconsInDiffViewGutterOption.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option\n\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl\n\nclass ShowAddCommentIconsInDiffViewGutterOption : BooleanOption() {\n    override val name: String = \"comments:show-add-comment-icons-in-diff-view-gutter\"\n\n    override fun getOptionValueFromSettings(settings: ApplicationSettingsImpl): Boolean {\n        return settings.showAddCommentIconsInDiffViewGutter\n    }\n\n    override fun copySettings(settings: ApplicationSettingsImpl, value: Boolean): ApplicationSettingsImpl {\n        return settings.copy(showAddCommentIconsInDiffViewGutter = value)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/CommentsTabFactory.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments\n\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\n\nobject CommentsTabFactory {\n    fun makeCommentsTabView(\n        projectServiceProvider: ProjectServiceProvider,\n        providerData: ProviderData\n    ): CommentsTabView {\n        return CommentsTabViewImpl(projectServiceProvider, providerData)\n    }\n\n    fun makeCommentsTabModel(\n        projectServiceProvider: ProjectServiceProvider,\n        providerData: ProviderData\n    ): CommentsTabModel {\n        return CommentsTabModelImpl(projectServiceProvider, providerData)\n    }\n\n    fun makeCommentsTabPresenter(\n        projectServiceProvider: ProjectServiceProvider,\n        model: CommentsTabModel,\n        view: CommentsTabView\n    ): CommentsTabPresenter {\n        return CommentsTabPresenterImpl(projectServiceProvider, model, view)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/CommentsTabModel.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments\n\nimport com.intellij.openapi.Disposable\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.DataChangedSource\nimport net.ntworld.mergeRequestIntegrationIde.Model\nimport java.util.*\n\ninterface CommentsTabModel : Model<CommentsTabModel.DataListener>, Disposable {\n    val providerData: ProviderData\n\n    var mergeRequestInfo: MergeRequestInfo\n\n    val comments: List<Comment>\n\n    var displayResolvedComments: Boolean\n\n    var onlyShowDraftComments: Boolean\n\n    interface DataListener : EventListener {\n        fun onMergeRequestInfoChanged()\n\n        fun onCommentsUpdated(source: DataChangedSource)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/CommentsTabModelImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments\n\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequestIntegrationIde.AbstractModel\nimport net.ntworld.mergeRequestIntegrationIde.DataChangedSource\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.MergeRequestDataNotifier\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.Empty\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\n\nclass CommentsTabModelImpl(\n    private val projectServiceProvider: ProjectServiceProvider,\n    override val providerData: ProviderData\n) : AbstractModel<CommentsTabModel.DataListener>(), CommentsTabModel {\n    override val dispatcher = EventDispatcher.create(CommentsTabModel.DataListener::class.java)\n\n    private val myComments: MutableList<Comment> = mutableListOf()\n\n    override var comments: MutableList<Comment> = mutableListOf()\n        private set\n\n    override var mergeRequestInfo: MergeRequestInfo = MergeRequestInfo.Empty\n        set(value) {\n            field = value\n            dispatcher.multicaster.onMergeRequestInfoChanged()\n        }\n\n    override var displayResolvedComments: Boolean = false\n        set(value) {\n            if (field != value) {\n                field = value\n                buildComments()\n                dispatcher.multicaster.onCommentsUpdated(DataChangedSource.UI)\n            }\n        }\n\n    override var onlyShowDraftComments: Boolean = false\n        set(value) {\n            if (field != value) {\n                field = value\n                buildComments()\n                dispatcher.multicaster.onCommentsUpdated(DataChangedSource.UI)\n            }\n        }\n\n    private val myMessageBusConnection = projectServiceProvider.messageBus.connect()\n    private val myMergeRequestDataNotifier = object : MergeRequestDataNotifier {\n        override fun fetchCommentsRequested(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) {\n        }\n\n        override fun onCommentsUpdated(\n            providerData: ProviderData,\n            mergeRequestInfo: MergeRequestInfo,\n            comments: List<Comment>\n        ) {\n            val currentProviderData = this@CommentsTabModelImpl.providerData\n            val currentMergeRequestInfo = this@CommentsTabModelImpl.mergeRequestInfo\n            if (providerData.id != currentProviderData.id ||\n                mergeRequestInfo.id != currentMergeRequestInfo.id) {\n                return\n            }\n            myComments.clear()\n            myComments.addAll(comments)\n            buildComments()\n            dispatcher.multicaster.onCommentsUpdated(DataChangedSource.NOTIFIER)\n        }\n    }\n\n    init {\n        myMessageBusConnection.subscribe(MergeRequestDataNotifier.TOPIC, myMergeRequestDataNotifier)\n    }\n\n    override fun dispose() {\n        myMessageBusConnection.disconnect()\n    }\n\n    private fun buildComments() {\n        comments.clear()\n        if (onlyShowDraftComments) {\n            comments.addAll(myComments.filter { it.isDraft })\n        } else {\n            if (displayResolvedComments) {\n                comments.addAll(myComments)\n            } else {\n                comments.addAll(myComments.filter { !it.resolved })\n            }\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/CommentsTabPresenter.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments\n\nimport com.intellij.openapi.Disposable\nimport com.intellij.ui.tabs.TabInfo\nimport net.ntworld.mergeRequestIntegrationIde.SimplePresenter\n\ninterface CommentsTabPresenter : SimplePresenter, Disposable {\n    val model: CommentsTabModel\n\n    val view: CommentsTabView\n\n    val tabInfo: TabInfo\n        get() = view.tabInfo\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/CommentsTabPresenterImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments\n\nimport com.intellij.notification.NotificationType\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.command.DeleteCommentCommand\nimport net.ntworld.mergeRequest.command.ResolveCommentCommand\nimport net.ntworld.mergeRequest.command.UnresolveCommentCommand\nimport net.ntworld.mergeRequest.request.CreateCommentRequest\nimport net.ntworld.mergeRequest.request.PublishCommentsRequest\nimport net.ntworld.mergeRequest.request.ReplyCommentRequest\nimport net.ntworld.mergeRequest.request.UpdateCommentRequest\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegration.provider.ProviderException\nimport net.ntworld.mergeRequestIntegrationIde.AbstractPresenter\nimport net.ntworld.mergeRequestIntegrationIde.DataChangedSource\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.DiffNotifier\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.MergeRequestDataNotifier\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node.*\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.isEmpty\nimport java.util.*\n\nclass CommentsTabPresenterImpl(\n    private val projectServiceProvider: ProjectServiceProvider,\n    override val model: CommentsTabModel,\n    override val view: CommentsTabView\n) : AbstractPresenter<EventListener>(),\n    CommentsTabPresenter, CommentsTabModel.DataListener, CommentsTabView.ActionListener {\n    override val dispatcher = EventDispatcher.create(EventListener::class.java)\n    private val myDiffPublisher = projectServiceProvider.messageBus.syncPublisher(DiffNotifier.TOPIC)\n\n    init {\n        model.addDataListener(this)\n        view.addActionListener(this)\n    }\n\n    override fun onMergeRequestInfoChanged() {\n        if (model.mergeRequestInfo.isEmpty()) {\n            view.hideThread()\n        } else {\n            requestFetchComments()\n        }\n    }\n\n    override fun onCommentsUpdated(source: DataChangedSource) {\n        if (source == DataChangedSource.NOTIFIER) {\n            ApplicationManager.getApplication().invokeLater {\n                handleWhenCommentsGetUpdated()\n            }\n        } else {\n            handleWhenCommentsGetUpdated()\n        }\n    }\n\n    override fun dispose() {\n        view.dispose()\n        model.dispose()\n    }\n\n    /**\n     * Node tree structure:\n     *\n     * + Root (hidden)\n     *   - GeneralCommentNode\n     *     - ThreadNode\n     *       - CommentNode\n     *   - FileNode\n     *     - FileLineNode\n     *       - ThreadNode\n     *         - CommentNode\n     */\n    override fun onTreeNodeSelected(node: Node) = assertMergeRequestInfoIsAvailable {\n        if (node is GeneralCommentsNode) {\n            collectAndDisplayCommentThread(it, node)\n            return@assertMergeRequestInfoIsAvailable\n        }\n\n        if (node is ThreadNode) {\n            collectAndDisplayCommentThread(it, node.parent!!)\n            return@assertMergeRequestInfoIsAvailable\n        }\n\n        if (node is CommentNode) {\n            collectAndDisplayCommentThread(it, node.parent!!.parent!!)\n            return@assertMergeRequestInfoIsAvailable\n        }\n\n        if (node is FileNode) {\n            val reviewContext = projectServiceProvider.reviewContextManager.findContext(model.providerData.id, it.id)\n            if (null !== reviewContext) {\n                val change = reviewContext.findChangeByPath(node.path)\n                if (null !== change) {\n                    reviewContext.openChange(change, focus = false, displayMergeRequestId = !projectServiceProvider.isDoingCodeReview())\n                    view.hideThread()\n                }\n            }\n            return@assertMergeRequestInfoIsAvailable\n        }\n\n        if (node is FileLineNode) {\n            collectAndDisplayCommentThread(it, node)\n            val reviewContext = projectServiceProvider.reviewContextManager.findContext(model.providerData.id, it.id)\n            if (null !== reviewContext) {\n                val change = reviewContext.findChangeByPath(node.path)\n                if (null !== change) {\n                    reviewContext.putChangeData(change, DiffNotifier.ScrollPosition, node.position)\n                    reviewContext.putChangeData(change, DiffNotifier.ScrollShowComments, true)\n                    reviewContext.openChange(change, focus = false, displayMergeRequestId = !projectServiceProvider.isDoingCodeReview())\n                    myDiffPublisher.hideAllCommentsRequested(reviewContext, change)\n                    myDiffPublisher.scrollToPositionRequested(reviewContext, change, node.position, true)\n                }\n            }\n        }\n    }\n\n\n    override fun onShowResolvedCommentsToggled(displayResolvedComments: Boolean) {\n        model.displayResolvedComments = displayResolvedComments\n    }\n\n    override fun onShowDraftCommentsOnlyToggled(onlyShowDraftComments: Boolean) {\n        model.onlyShowDraftComments = onlyShowDraftComments\n    }\n\n    override fun onCreateGeneralCommentClicked() {\n        assertMergeRequestInfoIsAvailable {\n            if (view.hasGeneralCommentsTreeNode()) {\n                view.selectGeneralCommentsTreeNode()\n            } else {\n                view.renderThread(it, mapOf())\n            }\n            view.focusToMainEditor()\n        }\n    }\n\n    override fun onRefreshButtonClicked() = requestFetchComments()\n\n    override fun onDeleteCommentRequested(comment: Comment) = assertMergeRequestInfoIsAvailable {\n        projectServiceProvider.infrastructure.commandBus() process DeleteCommentCommand.make(\n            providerId = model.providerData.id,\n            mergeRequestId = it.id,\n            comment = comment\n        )\n        requestFetchComments()\n    }\n\n    override fun onResolveCommentRequested(comment: Comment) = assertMergeRequestInfoIsAvailable {\n        projectServiceProvider.infrastructure.commandBus() process ResolveCommentCommand.make(\n            providerId = model.providerData.id,\n            mergeRequestId = it.id,\n            comment = comment\n        )\n        requestFetchComments()\n    }\n\n    override fun onUnresolveCommentRequested(comment: Comment) = assertMergeRequestInfoIsAvailable {\n        projectServiceProvider.infrastructure.commandBus() process UnresolveCommentCommand.make(\n            providerId = model.providerData.id,\n            mergeRequestId = it.id,\n            comment = comment\n        )\n        requestFetchComments()\n    }\n\n    override fun onReplyCommentRequested(repliedComment: Comment, content: String) = assertMergeRequestInfoIsAvailable {\n        projectServiceProvider.infrastructure.serviceBus() process ReplyCommentRequest.make(\n            providerId = model.providerData.id,\n            mergeRequestId = it.id,\n            repliedComment = repliedComment,\n            body = content\n        ) ifError { exception ->\n            projectServiceProvider.notify(\n                \"There was an error from server. \\n\\n ${exception.message}\",\n                NotificationType.ERROR\n            )\n            throw ProviderException(exception)\n        }\n        view.clearMainEditorText()\n        requestFetchComments()\n    }\n\n    override fun onEditCommentRequested(comment: Comment, content: String) = assertMergeRequestInfoIsAvailable {\n        projectServiceProvider.infrastructure.serviceBus() process UpdateCommentRequest.make(\n            providerId = model.providerData.id,\n            mergeRequestId = it.id,\n            comment = comment,\n            body = content\n        ) ifError { exception ->\n            projectServiceProvider.notify(\n                \"There was an error from server. \\n\\n ${exception.message}\",\n                NotificationType.ERROR\n            )\n            throw ProviderException(exception)\n        }\n        requestFetchComments()\n    }\n\n    override fun onPublishDraftCommentRequested(comment: Comment) {\n        projectServiceProvider.infrastructure.serviceBus() process PublishCommentsRequest.make(\n            providerId = model.providerData.id,\n            mergeRequestId = model.mergeRequestInfo.id,\n            draftCommentIds = listOf(comment.id)\n        ) ifError {\n            projectServiceProvider.notify(\n                \"There was an error from server. \\n\\n ${it.message}\",\n                NotificationType.ERROR\n            )\n            throw ProviderException(it)\n        }\n        requestFetchComments()\n    }\n\n    override fun onCreateCommentRequested(content: String, position: CommentPosition?, isDraft: Boolean) = assertMergeRequestInfoIsAvailable {\n        projectServiceProvider.infrastructure.serviceBus() process CreateCommentRequest.make(\n            providerId = model.providerData.id,\n            mergeRequestId = model.mergeRequestInfo.id,\n            position = position,\n            body = content,\n            isDraft = isDraft\n        ) ifError {\n            projectServiceProvider.notify(\n                \"There was an error from server. \\n\\n ${it.message}\",\n                NotificationType.ERROR\n            )\n            throw ProviderException(it)\n        }\n        view.clearMainEditorText()\n        requestFetchComments()\n    }\n\n    private fun requestFetchComments() = assertMergeRequestInfoIsAvailable {\n        projectServiceProvider.messageBus.syncPublisher(MergeRequestDataNotifier.TOPIC).fetchCommentsRequested(\n            model.providerData, it\n        )\n    }\n\n    private fun handleWhenCommentsGetUpdated() {\n        view.displayCommentCount(model.comments.size)\n        view.hideThread()\n        view.renderTree(model.mergeRequestInfo, model.comments, model.displayResolvedComments)\n    }\n\n    private fun assertMergeRequestInfoIsAvailable(invoker: ((MergeRequestInfo) -> Unit)) {\n        val mergeRequestInfo = model.mergeRequestInfo\n        if (!mergeRequestInfo.isEmpty()) {\n            invoker.invoke(mergeRequestInfo)\n        }\n    }\n\n    private fun collectAndDisplayCommentThread(mergeRequestInfo: MergeRequestInfo, parent: Node) {\n        view.renderThread(mergeRequestInfo, parent.groupComments())\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/CommentsTabView.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments\n\nimport com.intellij.openapi.Disposable\nimport com.intellij.ui.tabs.TabInfo\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequestIntegrationIde.Component\nimport net.ntworld.mergeRequestIntegrationIde.View\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.CommentEvent\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node.Node\nimport java.util.*\n\ninterface CommentsTabView : View<CommentsTabView.ActionListener>, Component, Disposable {\n    val tabInfo: TabInfo\n\n    fun displayCommentCount(count: Int)\n\n    fun renderTree(mergeRequestInfo: MergeRequestInfo, comments: List<Comment>, displayResolvedComments: Boolean)\n\n    fun hideThread()\n\n    fun renderThread(mergeRequestInfo: MergeRequestInfo, groupedComments: Map<String, List<Comment>>)\n\n    fun hasGeneralCommentsTreeNode(): Boolean\n\n    fun selectGeneralCommentsTreeNode()\n\n    fun focusToMainEditor()\n\n    fun clearMainEditorText()\n\n    interface ActionListener : EventListener, CommentEvent {\n        fun onTreeNodeSelected(node: Node)\n\n        fun onShowResolvedCommentsToggled(displayResolvedComments: Boolean)\n\n        fun onShowDraftCommentsOnlyToggled(onlyShowDraftComments: Boolean)\n\n        fun onCreateGeneralCommentClicked()\n\n        fun onRefreshButtonClicked()\n\n        fun onReplyCommentRequested(repliedComment: Comment, content: String)\n\n        fun onEditCommentRequested(comment: Comment, content: String)\n\n        fun onPublishDraftCommentRequested(comment: Comment)\n\n        fun onCreateCommentRequested(content: String, position: CommentPosition?, isDraft: Boolean)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/CommentsTabViewImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments\n\nimport com.intellij.openapi.ui.Messages\nimport com.intellij.ui.JBColor\nimport com.intellij.ui.OnePixelSplitter\nimport com.intellij.ui.ScrollPaneFactory\nimport com.intellij.ui.components.Panel\nimport com.intellij.ui.tabs.TabInfo\nimport com.intellij.util.EventDispatcher\nimport com.intellij.util.ui.JBUI\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.AbstractView\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreeFactory\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreePresenter\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.component.Icons\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.EditorComponent\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.GroupComponent\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.Options\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreeView\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node.Node\nimport javax.swing.BoxLayout\nimport javax.swing.JComponent\nimport javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER\n\nclass CommentsTabViewImpl(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData\n) : AbstractView<CommentsTabView.ActionListener>(), CommentsTabView {\n    override val dispatcher = EventDispatcher.create(CommentsTabView.ActionListener::class.java)\n\n    private val mySplitter = OnePixelSplitter(\n        this::class.java.canonicalName,\n        0.5f\n    )\n    private val myTreePresenter: CommentTreePresenter = CommentTreeFactory.makePresenter(\n        CommentTreeFactory.makeModel(providerData),\n        CommentTreeFactory.makeView(projectServiceProvider, providerData, showOpenDiffViewDescription = false)\n    )\n    private val myTreeListener = object : CommentTreePresenter.Listener {\n        override fun onTreeNodeSelected(node: Node, type: CommentTreeView.TreeSelectType) {\n            dispatcher.multicaster.onTreeNodeSelected(node)\n        }\n\n        override fun onShowResolvedCommentsToggled(displayResolvedComments: Boolean) {\n            dispatcher.multicaster.onShowResolvedCommentsToggled(displayResolvedComments)\n        }\n\n        override fun onShowDraftCommentsOnlyToggled(onlyShowDraftComments: Boolean) {\n            dispatcher.multicaster.onShowDraftCommentsOnlyToggled(onlyShowDraftComments)\n        }\n\n        override fun onCreateGeneralCommentClicked() {\n            dispatcher.multicaster.onCreateGeneralCommentClicked()\n        }\n\n        override fun onRefreshButtonClicked() {\n            dispatcher.multicaster.onRefreshButtonClicked()\n        }\n    }\n    private var myCommentPosition: CommentPosition? = null\n    private val myGroupCommentCollection = mutableListOf<GroupComponent>()\n    private val myThreadPanel = Panel()\n    private val myThreadBoxLayout = JBUI.Panels.simplePanel()\n    private val myThreadWrapper = ScrollPaneFactory.createScrollPane(myThreadBoxLayout, true)\n    private val myMainEditorEventListener = object : EditorComponent.EventListener {\n        override fun onEditorFocused(editor: EditorComponent) {\n            myThreadWrapper.verticalScrollBar.value = myThreadWrapper.verticalScrollBar.maximum\n        }\n\n        override fun onEditorResized(editor: EditorComponent) {}\n\n        override fun onCancelClicked(editor: EditorComponent) {\n            if (editor.text.isNotBlank()) {\n                val result = Messages.showYesNoDialog(\n                    \"Do you want to delete the whole content?\", \"Are you sure\", Messages.getQuestionIcon()\n                )\n                result == Messages.YES\n            }\n            editor.text = \"\"\n        }\n\n        override fun onSubmitClicked(editor: EditorComponent, isDraft: Boolean) {\n            val text = editor.text.trim()\n            val position = myCommentPosition\n            if (text.isNotEmpty()) {\n                dispatcher.multicaster.onCreateCommentRequested(editor.text, position, isDraft)\n            }\n        }\n    }\n    private val myMainEditor by lazy {\n        val editor = projectServiceProvider.componentFactory.commentComponents.makeEditor(\n            projectServiceProvider.project, EditorComponent.Type.NEW_DISCUSSION, 0,\n            borderLeftRight = 0,\n            showCancelAction = false,\n            isDoingCodeReview = projectServiceProvider.isDoingCodeReview()\n        )\n        editor.isVisible = true\n        editor.addListener(myMainEditorEventListener)\n\n        editor\n    }\n    private val myGroupComponentEventListener = object : GroupComponent.EventListener {\n        override fun onResized() {}\n\n        override fun onOpenDialogClicked() {\n        }\n\n        override fun onEditorCreated(groupId: String, editor: EditorComponent) {\n        }\n\n        override fun onEditorDestroyed(groupId: String, editor: EditorComponent) {\n        }\n\n        override fun onReplyCommentRequested(comment: Comment, content: String) {\n            if (content.trim().isNotEmpty()) {\n                dispatcher.multicaster.onReplyCommentRequested(comment, content)\n            }\n        }\n\n        override fun onEditCommentRequested(comment: Comment, content: String) {\n            dispatcher.multicaster.onEditCommentRequested(comment, content)\n        }\n\n        override fun onPublishDraftCommentRequested(comment: Comment) {\n            dispatcher.multicaster.onPublishDraftCommentRequested(comment)\n        }\n\n        override fun onDeleteCommentRequested(comment: Comment) {\n            dispatcher.multicaster.onDeleteCommentRequested(comment)\n        }\n\n        override fun onResolveCommentRequested(comment: Comment) {\n            dispatcher.multicaster.onResolveCommentRequested(comment)\n        }\n\n        override fun onUnresolveCommentRequested(comment: Comment) {\n            dispatcher.multicaster.onUnresolveCommentRequested(comment)\n        }\n    }\n\n    override val component: JComponent = mySplitter\n\n    override val tabInfo: TabInfo by lazy {\n        val tabInfo = TabInfo(component)\n        tabInfo.text = \"Comments\"\n        tabInfo.icon = Icons.Comments\n        tabInfo\n    }\n\n    init {\n        myThreadPanel.background = JBColor.border()\n        myThreadBoxLayout.background = JBColor.border()\n        myThreadWrapper.background = JBColor.border()\n        myThreadWrapper.horizontalScrollBarPolicy = HORIZONTAL_SCROLLBAR_NEVER\n\n        myThreadBoxLayout.addToCenter(myThreadPanel)\n        myThreadPanel.layout = BoxLayout(myThreadPanel, BoxLayout.Y_AXIS)\n        myThreadBoxLayout.addToBottom(myMainEditor.component)\n\n        mySplitter.firstComponent = myTreePresenter.component\n        mySplitter.secondComponent = null\n\n        myTreePresenter.addListener(myTreeListener)\n    }\n\n    override fun displayCommentCount(count: Int) {\n        tabInfo.text = if (0 == count) \"Comments\" else \"Comments · $count\"\n    }\n\n    override fun dispose() {\n    }\n\n    override fun renderTree(\n        mergeRequestInfo: MergeRequestInfo, comments: List<Comment>, displayResolvedComments: Boolean\n    ) {\n        myTreePresenter.model.mergeRequestInfo = mergeRequestInfo\n        myTreePresenter.model.comments = comments\n        myTreePresenter.model.displayResolvedComments = displayResolvedComments\n    }\n\n    override fun hideThread() {\n        mySplitter.secondComponent = null\n    }\n\n    override fun renderThread(mergeRequestInfo: MergeRequestInfo, groupedComments: Map<String, List<Comment>>) {\n        myThreadPanel.removeAll()\n        myGroupCommentCollection.forEach { it.dispose() }\n        myGroupCommentCollection.clear()\n        myCommentPosition = null\n\n        groupedComments.forEach { (groupId, comments) ->\n            if (comments.isEmpty()) {\n                return@forEach\n            }\n\n            val group = projectServiceProvider.componentFactory.commentComponents.makeGroup(\n                providerData, mergeRequestInfo, projectServiceProvider.project, false, groupId, comments,\n                Options(borderLeftRight = 0, showMoveToDialog = false)\n            )\n            myCommentPosition = comments.first().position\n\n            group.addListener(myGroupComponentEventListener)\n            myThreadPanel.add(group.component)\n            myGroupCommentCollection.add(group)\n        }\n\n        if (null !== myCommentPosition) {\n            myMainEditor.addCommentNowButtonText = \"Add comment\"\n            myMainEditor.addCommentNowButtonDesc = \"Add comment to current line\"\n        } else {\n            myMainEditor.addCommentNowButtonText = \"Add general comment\"\n            myMainEditor.addCommentNowButtonDesc = \"Create new thread of general comment\"\n        }\n        mySplitter.secondComponent = myThreadWrapper\n    }\n\n    override fun hasGeneralCommentsTreeNode(): Boolean = myTreePresenter.hasGeneralCommentsTreeNode()\n\n    override fun selectGeneralCommentsTreeNode() = myTreePresenter.selectGeneralCommentsTreeNode()\n\n    override fun focusToMainEditor() = myMainEditor.focus()\n\n    override fun clearMainEditorText() {\n        myMainEditor.text = \"\"\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/CommentTreeFactory.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree\n\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\n\nobject CommentTreeFactory {\n    fun makeModel(providerData: ProviderData): CommentTreeModel {\n        return CommentTreeModelImpl(providerData)\n    }\n\n    fun makeView(\n        projectServiceProvider: ProjectServiceProvider,\n        providerData: ProviderData,\n        showOpenDiffViewDescription: Boolean\n    ): CommentTreeView {\n        return CommentTreeViewImpl(projectServiceProvider, providerData, showOpenDiffViewDescription)\n    }\n\n    fun makePresenter(\n        model: CommentTreeModel,\n        view: CommentTreeView\n    ): CommentTreePresenter {\n        return CommentTreePresenterImpl(model, view)\n    }\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/CommentTreeModel.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.Model\nimport java.util.*\n\ninterface CommentTreeModel : Model<CommentTreeModel.DataListener> {\n    val providerData: ProviderData\n\n    var mergeRequestInfo: MergeRequestInfo\n\n    var comments: List<Comment>\n\n    var displayResolvedComments: Boolean\n\n    interface DataListener : EventListener {\n        fun onMergeRequestInfoChanged()\n\n        fun onCommentsUpdated()\n\n        fun onDisplayResolvedCommentsChanged()\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/CommentTreeModelImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree\n\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.AbstractModel\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.Empty\n\nclass CommentTreeModelImpl(\n    override val providerData: ProviderData\n) : AbstractModel<CommentTreeModel.DataListener>(), CommentTreeModel {\n    override val dispatcher = EventDispatcher.create(CommentTreeModel.DataListener::class.java)\n\n    override var mergeRequestInfo: MergeRequestInfo = MergeRequestInfo.Empty\n        set(value) {\n            field = value\n            dispatcher.multicaster.onMergeRequestInfoChanged()\n        }\n\n    override var comments: List<Comment> = listOf()\n        set(value) {\n            field = value\n            dispatcher.multicaster.onCommentsUpdated()\n        }\n\n    override var displayResolvedComments: Boolean = false\n        set(value) {\n            field = value\n            dispatcher.multicaster.onDisplayResolvedCommentsChanged()\n        }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/CommentTreePresenter.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree\n\nimport net.ntworld.mergeRequestIntegrationIde.Component\nimport net.ntworld.mergeRequestIntegrationIde.Presenter\nimport javax.swing.JComponent\n\ninterface CommentTreePresenter : Presenter<CommentTreePresenter.Listener>, Component {\n    val model: CommentTreeModel\n\n    val view: CommentTreeView\n\n    override val component: JComponent\n        get() = view.component\n\n    fun setToolbarMode(mode: CommentTreeView.ToolbarMode) = view.setToolbarMode(mode)\n\n    fun hasGeneralCommentsTreeNode(): Boolean\n\n    fun selectGeneralCommentsTreeNode()\n\n    interface Listener : CommentTreeView.ActionListener\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/CommentTreePresenterImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree\n\nclass CommentTreePresenterImpl(\n    override val model: CommentTreeModel,\n    override val view: CommentTreeView\n) : CommentTreePresenter, CommentTreeModel.DataListener {\n\n    init {\n        model.addDataListener(this)\n    }\n\n    override fun onMergeRequestInfoChanged() {\n    }\n\n    override fun onCommentsUpdated() {\n        view.renderTree(model.mergeRequestInfo, model.comments)\n    }\n\n    override fun onDisplayResolvedCommentsChanged() {\n        view.setShowResolvedCommentState(model.displayResolvedComments)\n    }\n\n    override fun hasGeneralCommentsTreeNode(): Boolean {\n        return view.hasGeneralCommentsTreeNode()\n    }\n\n    override fun selectGeneralCommentsTreeNode() {\n        view.selectGeneralCommentsTreeNode()\n    }\n\n    override fun addListener(listener: CommentTreePresenter.Listener) = view.addActionListener(listener)\n    override fun removeListener(listener: CommentTreePresenter.Listener) = view.removeActionListener(listener)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/CommentTreeView.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequestIntegrationIde.Component\nimport net.ntworld.mergeRequestIntegrationIde.View\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node.Node\nimport java.util.*\n\ninterface CommentTreeView : View<CommentTreeView.ActionListener>, Component {\n\n    fun renderTree(mergeRequestInfo: MergeRequestInfo, comments: List<Comment>)\n\n    fun setShowResolvedCommentState(selected: Boolean)\n\n    fun hasGeneralCommentsTreeNode(): Boolean\n\n    fun selectGeneralCommentsTreeNode()\n\n    fun setToolbarMode(mode: ToolbarMode)\n\n    interface ActionListener : EventListener {\n        fun onTreeNodeSelected(node: Node, type: TreeSelectType)\n\n        fun onShowResolvedCommentsToggled(displayResolvedComments: Boolean)\n\n        fun onShowDraftCommentsOnlyToggled(onlyShowDraftComments: Boolean)\n\n        fun onCreateGeneralCommentClicked()\n\n        fun onRefreshButtonClicked()\n    }\n\n    enum class ToolbarMode {\n        FULL,\n        MINI\n    }\n\n    enum class TreeSelectType {\n        NORMAL,\n        DOUBLE_CLICK,\n        PRESS_ENTER\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/CommentTreeViewImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree\n\nimport com.intellij.ide.util.treeView.NodeRenderer\nimport com.intellij.ide.util.treeView.PresentableNodeDescriptor\nimport com.intellij.ui.ScrollPaneFactory\nimport com.intellij.ui.treeStructure.Tree\nimport com.intellij.util.EventDispatcher\nimport com.intellij.util.ui.tree.TreeUtil\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.AbstractView\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node.*\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.ui.util.CustomSimpleToolWindowPanel\nimport java.awt.event.KeyAdapter\nimport java.awt.event.KeyEvent\nimport java.awt.event.MouseAdapter\nimport java.awt.event.MouseEvent\nimport javax.swing.JComponent\nimport javax.swing.event.TreeSelectionListener\nimport javax.swing.tree.*\n\nclass CommentTreeViewImpl(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData,\n    private val showOpenDiffViewDescription: Boolean\n) : AbstractView<CommentTreeView.ActionListener>(), CommentTreeView {\n    override val dispatcher = EventDispatcher.create(CommentTreeView.ActionListener::class.java)\n\n    private val myComponent = CustomSimpleToolWindowPanel(vertical = true, borderless = true)\n\n    private val myTree = Tree()\n    private val myRoot = DefaultMutableTreeNode()\n    private val myModel = DefaultTreeModel(myRoot)\n    private val myRenderer = NodeRenderer()\n    private var myIsTreeRendering = false\n    private val myTreeCellRenderer = TreeCellRenderer { tree, value, selected, expanded, leaf, row, hasFocus ->\n        myRenderer.getTreeCellRendererComponent(\n            tree,\n            value,\n            selected,\n            expanded,\n            leaf,\n            row,\n            hasFocus\n        )\n    }\n    private val myTreeSelectionListener = TreeSelectionListener {\n        if (null !== it && !myIsTreeRendering) {\n            handleOnTreeNodeSelectedEvent(it.path, CommentTreeView.TreeSelectType.NORMAL)\n        }\n    }\n    private val myTreeMouseListener = object : MouseAdapter() {\n        override fun mousePressed(e: MouseEvent?) {\n            if (null === e) {\n                return\n            }\n\n            if (e.clickCount == 2) {\n                handleOnTreeNodeSelectedEvent(myTree.selectionPath, CommentTreeView.TreeSelectType.DOUBLE_CLICK)\n            }\n        }\n    }\n    private val myKeyListener = object: KeyAdapter() {\n        override fun keyPressed(e: KeyEvent?) {\n            if (null === e) {\n                return\n            }\n            if (e.keyCode == KeyEvent.VK_ENTER) {\n                handleOnTreeNodeSelectedEvent(myTree.selectionPath, CommentTreeView.TreeSelectType.PRESS_ENTER)\n            }\n        }\n    }\n\n    private val myToolbar = CommentTreeViewToolbar(myTree, dispatcher)\n    private val nodeSyncManager: NodeSyncManager by lazy {\n        NodeSyncManagerImpl(NodeDescriptorServiceImpl(projectServiceProvider, providerData))\n    }\n    private val mySyncedTree: SyncedTree by lazy {\n        nodeSyncManager.makeSyncedTree(myTree, myModel, myRoot)\n    }\n\n    init {\n        val treeSelectionModel = DefaultTreeSelectionModel()\n        treeSelectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION\n\n        myTree.model = myModel\n        myTree.cellRenderer = myTreeCellRenderer\n        myTree.isRootVisible = false\n        myTree.selectionModel = treeSelectionModel\n\n        myTree.addTreeSelectionListener(myTreeSelectionListener)\n        myTree.addMouseListener(myTreeMouseListener)\n        myTree.addKeyListener(myKeyListener)\n\n        myComponent.setContent(ScrollPaneFactory.createScrollPane(myTree, true))\n        myComponent.toolbar = myToolbar.component\n    }\n\n    override fun renderTree(mergeRequestInfo: MergeRequestInfo, comments: List<Comment>) {\n        myIsTreeRendering = true\n        val builder = RootNodeBuilder(comments, showOpenDiffViewDescription)\n        val root = builder.build()\n        nodeSyncManager.sync(mergeRequestInfo, root, mySyncedTree)\n        handleOnTreeNodeSelectedEvent(myTree.selectionPath, CommentTreeView.TreeSelectType.NORMAL)\n\n        myIsTreeRendering = false\n    }\n\n    override fun setShowResolvedCommentState(selected: Boolean) {\n        myToolbar.showResolved = selected\n    }\n\n    override fun hasGeneralCommentsTreeNode(): Boolean {\n        val children = myRoot.children()\n        for (child in children) {\n            if (isGeneralCommentsTreeNode(child as TreeNode)) {\n                return true\n            }\n        }\n        return false\n    }\n\n    override fun selectGeneralCommentsTreeNode() {\n        val children = myRoot.children()\n        for (child in children) {\n            if (isGeneralCommentsTreeNode(child as TreeNode)) {\n                myTree.selectionPath = TreeUtil.getPath(myRoot, child as @org.jetbrains.annotations.NotNull TreeNode)\n                break\n            }\n        }\n    }\n\n    override fun setToolbarMode(mode: CommentTreeView.ToolbarMode) {\n        myToolbar.setMode(mode)\n    }\n\n    private fun isGeneralCommentsTreeNode(node: TreeNode) : Boolean {\n        val treeNode = node as? DefaultMutableTreeNode ?: return false\n        val descriptor = treeNode.userObject as? PresentableNodeDescriptor<*> ?: return false\n        return descriptor.element is GeneralCommentsNode\n    }\n\n    private fun handleOnTreeNodeSelectedEvent(selectedPath: TreePath?, type: CommentTreeView.TreeSelectType) {\n        if (null === selectedPath) {\n            return\n        }\n\n        val lastPath = selectedPath.lastPathComponent as? DefaultMutableTreeNode ?: return\n        val descriptor = lastPath.userObject as? PresentableNodeDescriptor<*> ?: return\n        val element = descriptor.element as? Node ?: return\n        dispatcher.multicaster.onTreeNodeSelected(element, type)\n    }\n\n    override val component: JComponent = myComponent\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/CommentTreeViewToolbar.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree\n\nimport com.intellij.icons.AllIcons\nimport com.intellij.openapi.actionSystem.*\nimport com.intellij.ui.treeStructure.actions.CollapseAllAction\nimport com.intellij.ui.treeStructure.actions.ExpandAllAction\nimport com.intellij.util.EventDispatcher\nimport net.miginfocom.swing.MigLayout\nimport net.ntworld.mergeRequestIntegrationIde.Component\nimport net.ntworld.mergeRequestIntegrationIde.component.Icons\nimport javax.swing.JComponent\nimport javax.swing.JPanel\nimport javax.swing.JTree\n\ninternal class CommentTreeViewToolbar(\n    private val myTree: JTree,\n    private val dispatcher: EventDispatcher<CommentTreeView.ActionListener>\n) : Component {\n    var showResolved: Boolean = false\n    var onlyShowDraftComments: Boolean = false\n    private var myMode = CommentTreeView.ToolbarMode.FULL\n    private val mySkipResolvedButton = MySkipResolvedButton(this)\n    private val myToggleDraftsButton = MyToggleDraftsButton(this)\n    private val myRefreshButton = MyRefreshButton(this)\n    private val myAddGeneralComment = MyAddGeneralComment(this)\n\n    private val myPanel by lazy {\n        val panel = JPanel(MigLayout(\"ins 0, fill\", \"[left]push[right]\", \"center\"))\n        val mainActionGroup = DefaultActionGroup()\n        mainActionGroup.add(mySkipResolvedButton)\n        mainActionGroup.addSeparator()\n        mainActionGroup.add(myToggleDraftsButton)\n        val mainToolbar = ActionManager.getInstance().createActionToolbar(\n            \"${this::class.java.canonicalName}/toolbar\",\n            mainActionGroup,\n            true\n        )\n\n        val rightActionGroup = DefaultActionGroup()\n        rightActionGroup.add(myRefreshButton)\n        rightActionGroup.addSeparator()\n        rightActionGroup.add(ExpandAllAction(myTree))\n        rightActionGroup.add(CollapseAllAction(myTree))\n        rightActionGroup.addSeparator()\n        rightActionGroup.add(myAddGeneralComment)\n        val rightToolbar = ActionManager.getInstance().createActionToolbar(\n            \"${this::class.java.canonicalName}/toolbar-right\",\n            rightActionGroup,\n            true\n        )\n\n        panel.add(mainToolbar.component)\n        panel.add(rightToolbar.component)\n        panel\n    }\n\n    override val component: JComponent = myPanel\n\n    fun setMode(mode: CommentTreeView.ToolbarMode) {\n        if (mode != myMode) {\n            myMode = mode\n        }\n    }\n\n    private class MySkipResolvedButton(private val self: CommentTreeViewToolbar) :\n        ToggleAction(\"Resolved Comments\", \"Toggle resolved comments\", null) {\n        override fun isSelected(e: AnActionEvent): Boolean {\n            return self.showResolved\n        }\n\n        override fun setSelected(e: AnActionEvent, state: Boolean) {\n            self.showResolved = state\n            self.dispatcher.multicaster.onShowResolvedCommentsToggled(self.showResolved)\n        }\n\n        override fun update(e: AnActionEvent) {\n            super.update(e)\n            e.presentation.isEnabled = !self.onlyShowDraftComments\n            if (self.myMode == CommentTreeView.ToolbarMode.FULL) {\n                e.presentation.icon = null\n            } else {\n                e.presentation.icon = Icons.Resolved\n            }\n        }\n\n        override fun displayTextInToolbar(): Boolean {\n            return self.myMode == CommentTreeView.ToolbarMode.FULL\n        }\n        override fun useSmallerFontForTextInToolbar() = true\n    }\n\n    private class MyToggleDraftsButton(private val self: CommentTreeViewToolbar) :\n        ToggleAction(\"Only Draft\", \"Only show draft comments\", null) {\n        override fun isSelected(e: AnActionEvent): Boolean {\n            return self.onlyShowDraftComments\n        }\n\n        override fun setSelected(e: AnActionEvent, state: Boolean) {\n            self.onlyShowDraftComments = state\n            self.dispatcher.multicaster.onShowDraftCommentsOnlyToggled(self.onlyShowDraftComments)\n        }\n\n        override fun displayTextInToolbar(): Boolean = true\n        override fun useSmallerFontForTextInToolbar() = true\n    }\n\n    private class MyRefreshButton(private val self: CommentTreeViewToolbar) :\n        AnAction(\"Refresh\", \"Refresh comment list\", AllIcons.Actions.Refresh) {\n        override fun actionPerformed(e: AnActionEvent) {\n            self.dispatcher.multicaster.onRefreshButtonClicked()\n        }\n    }\n\n    private class MyAddGeneralComment(private val self: CommentTreeViewToolbar) : AnAction(\n        \"General Comment\", \"Add a general comment\", AllIcons.General.Add\n    ) {\n        override fun actionPerformed(e: AnActionEvent) {\n            self.dispatcher.multicaster.onCreateGeneralCommentClicked()\n        }\n\n        override fun update(e: AnActionEvent) {\n            super.update(e)\n            if (self.myMode == CommentTreeView.ToolbarMode.FULL) {\n                e.presentation.text = \"General Comment\"\n            } else {\n                e.presentation.text = \"Add a general comment\"\n            }\n        }\n\n        override fun displayTextInToolbar(): Boolean {\n            return self.myMode == CommentTreeView.ToolbarMode.FULL\n        }\n        override fun useSmallerFontForTextInToolbar() = true\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/AbstractNode.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node\n\nabstract class AbstractNode : Node {\n    override var parent: Node? = null\n\n    override val children: MutableList<Node> = mutableListOf()\n\n    override fun add(node: Node) {\n        node.parent = this\n\n        children.add(node)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/CommentNode.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node\n\nimport com.intellij.ide.projectView.PresentationData\nimport com.intellij.ui.SimpleTextAttributes\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequestIntegration.util.DateTimeUtil\nimport net.ntworld.mergeRequestIntegrationIde.component.Icons\n\nopen class CommentNode(\n    val comment: Comment,\n    val position: CommentPosition?\n) : AbstractNode() {\n    override val id: String = \"comment[${comment.id}]\"\n\n    override fun updatePresentation(presentation: PresentationData) {\n        presentation.setIcon(if (comment.resolved) Icons.TreeNode.ResolvedComment else Icons.TreeNode.UnresolvedComment)\n        presentation.addText(\"${comment.author.name} \", SimpleTextAttributes.REGULAR_ATTRIBUTES)\n        presentation.addText(\"@${comment.author.username} · \", SimpleTextAttributes.GRAYED_ATTRIBUTES)\n        if (comment.isDraft) {\n            presentation.addText(\"draft\", SimpleTextAttributes.REGULAR_ATTRIBUTES)\n        } else {\n            presentation.addText(DateTimeUtil.toPretty(comment.createdAt), SimpleTextAttributes.REGULAR_ATTRIBUTES)\n        }\n    }\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/FileLineNode.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node\n\nimport com.intellij.ide.projectView.PresentationData\nimport com.intellij.ui.SimpleTextAttributes\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequestIntegrationIde.util.TextChoiceUtil\n\nclass FileLineNode(\n    val path: String,\n    val line: Int,\n    val position: CommentPosition,\n    private val totalCount: Int,\n    private val draftCount: Int,\n    private val showOpenDiffViewDescription: Boolean\n) : AbstractNode() {\n    override val id: String = \"line[$path:$line]\"\n    private val openDiffViewDescription = if (null !== position.newLine) \"\" else \" (open diff view)\"\n\n    override fun updatePresentation(presentation: PresentationData) {\n        presentation.addText(\"Line $line\", SimpleTextAttributes.REGULAR_ATTRIBUTES)\n\n        presentation.addText(\" · \" + TextChoiceUtil.commentWithDraft(totalCount, draftCount), SimpleTextAttributes.GRAY_ATTRIBUTES)\n\n        if (openDiffViewDescription.isNotEmpty() && showOpenDiffViewDescription) {\n            presentation.addText(openDiffViewDescription, SimpleTextAttributes.GRAY_ATTRIBUTES)\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/FileNode.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node\n\nimport com.intellij.ide.projectView.PresentationData\nimport com.intellij.ui.SimpleTextAttributes\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.util.TextChoiceUtil\n\nclass FileNode(\n    val path: String,\n    private val draftCount: Int\n) : AbstractNode() {\n    override fun updatePresentation(\n        projectServiceProvider: ProjectServiceProvider,\n        providerData: ProviderData,\n        presentation: PresentationData\n    ) {\n        super.updatePresentation(projectServiceProvider, providerData, presentation)\n        presentation.setIcon(projectServiceProvider.repositoryFile.findIcon(providerData, path))\n    }\n\n    override val id: String = \"file[$path]\"\n\n    override fun updatePresentation(presentation: PresentationData) {\n        val fileName = findFileName(path)\n        presentation.addText(fileName, SimpleTextAttributes.REGULAR_ATTRIBUTES)\n\n        if (draftCount > 0) {\n            presentation.addText(\" · \" + TextChoiceUtil.draftComment(draftCount), SimpleTextAttributes.GRAYED_ATTRIBUTES)\n        }\n\n        if (fileName != path) {\n            presentation.addText(\" · $path\", SimpleTextAttributes.GRAYED_ATTRIBUTES)\n        }\n    }\n\n    private fun findFileName(path: String): String {\n        return path.split(\"/\").last()\n    }\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/GeneralCommentsNode.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node\n\nimport com.intellij.ide.projectView.PresentationData\nimport com.intellij.ui.SimpleTextAttributes\nimport net.ntworld.mergeRequestIntegrationIde.util.TextChoiceUtil\n\nclass GeneralCommentsNode(private val totalCount: Int, private val draftCount: Int) : AbstractNode() {\n    override val id: String = \"general-comments\"\n\n    override fun updatePresentation(presentation: PresentationData) {\n        presentation.addText(\"General \", SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES)\n        presentation.addText(\n            \" · \" + TextChoiceUtil.commentWithDraft(totalCount, draftCount),\n            SimpleTextAttributes.GRAY_ATTRIBUTES\n        )\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/Node.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node\n\nimport com.intellij.ide.projectView.PresentationData\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\n\ninterface Node {\n    val id: String\n\n    var parent: Node?\n\n    val children: List<Node>\n\n    val childCount\n        get() = children.size\n\n    fun add(node: Node)\n\n    fun updatePresentation(presentation: PresentationData)\n\n    fun updatePresentation(projectServiceProvider: ProjectServiceProvider, providerData: ProviderData, presentation: PresentationData) {\n        updatePresentation(presentation)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/NodeDescriptorService.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node\n\nimport com.intellij.ide.util.treeView.PresentableNodeDescriptor\n\ninterface NodeDescriptorService {\n    fun make(node: Node): PresentableNodeDescriptor<Node>\n\n    fun findNode(input: Any?): Node?\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/NodeDescriptorServiceImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node\n\nimport com.intellij.ide.projectView.PresentationData\nimport com.intellij.ide.util.treeView.PresentableNodeDescriptor\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\n\nclass NodeDescriptorServiceImpl(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData\n) : NodeDescriptorService {\n\n    override fun make(node: Node): PresentableNodeDescriptor<Node> {\n        val presentation = MyPresentableNodeDescriptor(projectServiceProvider, providerData, node)\n        presentation.update()\n        return presentation\n    }\n\n    override fun findNode(input: Any?): Node? {\n        return if (null !== input && input is MyPresentableNodeDescriptor) {\n            input.element\n        } else null\n    }\n\n    private class MyPresentableNodeDescriptor(\n        private val projectServiceProvider: ProjectServiceProvider,\n        private val providerData: ProviderData,\n        private val element: Node\n    ) : PresentableNodeDescriptor<Node>(projectServiceProvider.project, null) {\n        override fun update(presentation: PresentationData) {\n            element.updatePresentation(projectServiceProvider, providerData, presentation)\n        }\n\n        override fun getElement(): Node = element\n    }\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/NodeFactory.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPosition\n\nobject NodeFactory {\n    fun makeRoot(): RootNode = RootNode()\n\n    fun makeGeneralComments(root: RootNode, totalCount: Int, draftCount: Int): GeneralCommentsNode {\n        val node = GeneralCommentsNode(totalCount, draftCount)\n        root.add(node)\n\n        return node\n    }\n\n    fun makeThread(parent: Node, threadId: String, repliedCount: Int, draftCount: Int, comment: Comment): ThreadNode {\n        val node = ThreadNode(threadId, repliedCount, draftCount, comment, comment.position)\n        parent.add(node)\n        return node\n    }\n\n    fun makeComment(parent: ThreadNode, comment: Comment): CommentNode {\n        val node = CommentNode(comment, parent.position)\n        parent.add(node)\n        return node\n    }\n\n    fun makeFile(parent: RootNode, path: String, draftCount: Int): FileNode {\n        val node = FileNode(path, draftCount)\n        parent.add(node)\n        return node\n    }\n\n    fun makeFileLine(\n        parent: FileNode,\n        path: String,\n        line: Int,\n        totalCount: Int,\n        draftCount: Int,\n        position: CommentPosition,\n        showOpenDiffViewDescription: Boolean\n    ): FileLineNode {\n        val node = FileLineNode(path, line, position, totalCount, draftCount, showOpenDiffViewDescription)\n        parent.add(node)\n        return node\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/NodeSyncManager.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node\n\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport javax.swing.JTree\nimport javax.swing.tree.DefaultMutableTreeNode\nimport javax.swing.tree.DefaultTreeModel\n\ninterface NodeSyncManager {\n    fun sync(mergeRequestInfo: MergeRequestInfo, root: RootNode, tree: SyncedTree)\n\n    fun makeSyncedTree(tree: JTree, treeModel: DefaultTreeModel, treeRoot: DefaultMutableTreeNode): SyncedTree\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/NodeSyncManagerImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node\n\nimport com.intellij.ide.util.treeView.PresentableNodeDescriptor\nimport com.intellij.util.ui.tree.TreeUtil\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport java.lang.NullPointerException\nimport javax.swing.JTree\nimport javax.swing.tree.*\nimport kotlin.math.min\n\nclass NodeSyncManagerImpl(\n    private val nodeDescriptorService: NodeDescriptorService\n) : NodeSyncManager {\n    override fun sync(mergeRequestInfo: MergeRequestInfo, root: RootNode, tree: SyncedTree) {\n        val expandingNodes = mutableSetOf<String>()\n        val selectedNodeToTopIds = mutableSetOf<String>()\n        val selectedTreeNode = tree.selectedTreeNode()\n        if (null !== selectedTreeNode) {\n            val node = nodeDescriptorService.findNode(selectedTreeNode.userObject)\n            var currentNode: Node? = node\n            while (null !== currentNode) {\n                selectedNodeToTopIds.add(currentNode.id)\n                currentNode = currentNode.parent\n            }\n        }\n\n        syncStructure(root, tree.treeRoot) { node, treeNode ->\n            if (tree.isExpand(treeNode)) {\n                expandingNodes.add(node.id)\n            }\n        }\n\n        tree.nodeStructureChanged(tree.treeRoot)\n        loopStructure(tree.treeRoot) { node, treeNode ->\n            if (expandingNodes.contains(node.id)) {\n                tree.expand(treeNode)\n            }\n            if (selectedNodeToTopIds.contains(node.id)) {\n                tree.select(treeNode)\n            }\n        }\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    private fun loopStructure(treeNode: DefaultMutableTreeNode, visitor: (Node, DefaultMutableTreeNode) -> Unit) {\n        visitor((treeNode.userObject as PresentableNodeDescriptor<Node>).element, treeNode)\n        val children = treeNode.children();\n        if (children != null) {\n            while (children.hasMoreElements()) {\n                val child = children.nextElement()\n                loopStructure(child as DefaultMutableTreeNode, visitor)\n            }\n        }\n    }\n\n    override fun makeSyncedTree(\n        tree: JTree, treeModel: DefaultTreeModel, treeRoot: DefaultMutableTreeNode\n    ): SyncedTree {\n        return MySyncedTree(tree, treeModel, treeRoot)\n    }\n\n    fun syncStructure(\n        parent: Node,\n        treeNode: DefaultMutableTreeNode,\n        visitor: ((Node, DefaultMutableTreeNode) -> Unit)\n    ) {\n        visitor(parent, treeNode)\n        treeNode.userObject = nodeDescriptorService.make(parent)\n\n        val treeNodeChildren = treeNode.children().toList()\n        val treeNodeChildCount = treeNodeChildren.size\n        val index = min(treeNodeChildCount, parent.childCount)\n        for (i in 0 until index) {\n            val treeNodeChild = treeNodeChildren[i]\n            syncStructure(parent.children[i], treeNodeChild as DefaultMutableTreeNode, visitor)\n        }\n\n        if (treeNodeChildCount < parent.childCount && index < parent.childCount) {\n            for (i in index until parent.childCount) {\n                val child = parent.children[i]\n                val userObject = nodeDescriptorService.make(child)\n                val childTreeNode = DefaultMutableTreeNode(userObject)\n                syncStructure(child, childTreeNode, visitor)\n                treeNode.add(childTreeNode)\n            }\n            return\n        }\n\n        if (treeNodeChildCount > parent.childCount && index < treeNode.childCount) {\n            for (i in index until treeNodeChildCount) {\n                // Always remove index because after removing 1 item the list is has 1 item less, then index\n                // is the same :D\n                treeNode.remove(index)\n            }\n            return\n        }\n    }\n\n    private class MySyncedTree(\n        private val tree: JTree,\n        private val treeModel: DefaultTreeModel,\n        override val treeRoot: DefaultMutableTreeNode\n    ) : SyncedTree {\n        private fun findPath(treeNode: TreeNode): TreePath? {\n            return try {\n                TreeUtil.getPath(treeRoot, treeNode)\n            } catch (exception: NullPointerException) {\n                null\n            }\n        }\n\n        override fun isExpand(treeNode: TreeNode): Boolean {\n            val path = findPath(treeNode)\n            return if (null !== path) tree.isExpanded(path) else false\n        }\n\n        override fun selectedTreeNode(): DefaultMutableTreeNode? {\n            return if (null === tree.selectionPath) {\n                null\n            } else {\n                tree.selectionPath!!.lastPathComponent as DefaultMutableTreeNode?\n            }\n        }\n\n        override fun select(treeNode: TreeNode) {\n            val path = findPath(treeNode)\n            if (null !== path) {\n                tree.selectionPath = path\n            }\n        }\n\n        override fun expand(treeNode: TreeNode) {\n            val path = findPath(treeNode)\n            if (null !== path) {\n                tree.expandPath(path)\n            }\n        }\n\n        override fun nodeStructureChanged(treeNode: TreeNode) {\n            treeModel.nodeStructureChanged(treeNode)\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/RootNode.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node\n\nimport com.intellij.ide.projectView.PresentationData\n\nclass RootNode : AbstractNode() {\n    override val id: String = \"root\"\n\n    override fun updatePresentation(presentation: PresentationData) {}\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/RootNodeBuilder.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequestIntegrationIde.util.CommentUtil\n\nclass RootNodeBuilder(\n    comments: List<Comment>,\n    private val showOpenDiffViewDescription: Boolean\n) {\n    private val generalComments = comments.filter {\n        null === it.position\n    }\n    private val positionComments = comments.filter {\n        null !== it.position\n    }\n\n    fun build(): RootNode {\n        val root = NodeFactory.makeRoot()\n\n        if (generalComments.isNotEmpty()) {\n            buildGeneralComments(root)\n        }\n\n        if (positionComments.isNotEmpty()) {\n            buildPositionComments(root)\n        }\n\n        return root\n    }\n\n    private fun buildGeneralComments(root: RootNode) {\n        val generalCommentsNode = NodeFactory.makeGeneralComments(\n            root,\n            generalComments.count(),\n            generalComments.filter { it.isDraft }.count()\n        )\n        buildThreadComments(generalCommentsNode, generalComments)\n    }\n\n    private fun buildThreadComments(parent: Node, comments: List<Comment>) {\n        val groups = CommentUtil.groupCommentsByThreadId(comments)\n        groups.forEach { (id, items) ->\n            if (items.isEmpty()) {\n                return@forEach\n            }\n\n            val draftCount = items.filter { it.isDraft }.count() - 1\n            val threadNode = NodeFactory.makeThread(parent, id, items.count() - 1, draftCount, items.first())\n            for (i in 1..items.lastIndex) {\n                NodeFactory.makeComment(threadNode, items[i])\n            }\n        }\n    }\n\n    private fun buildPositionComments(root: RootNode) {\n        val groupedByPath = CommentUtil.groupCommentsByPositionPath(positionComments)\n        groupedByPath.forEach { (path, items) ->\n            if (items.isEmpty()) {\n                return@forEach\n            }\n\n            val draftCountOfFile = items.filter { it.isDraft }.count()\n            val fileNode = NodeFactory.makeFile(root, path, draftCountOfFile)\n            val groupedByLine = CommentUtil.groupCommentsByPositionLine(items)\n            groupedByLine.forEach { (line, comments) ->\n                if (comments.isNotEmpty()) {\n                    val draftCountOfFileLine = comments.filter { it.isDraft }.count()\n                    val fileLine = NodeFactory.makeFileLine(\n                        fileNode, path, line, comments.count(), draftCountOfFileLine, comments.last().position!!, showOpenDiffViewDescription\n                    )\n                    buildThreadComments(fileLine, comments)\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/SyncedTree.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node\n\nimport javax.swing.tree.DefaultMutableTreeNode\nimport javax.swing.tree.TreeNode\n\ninterface SyncedTree {\n    val treeRoot: DefaultMutableTreeNode\n\n    fun isExpand(treeNode: TreeNode): Boolean\n\n    fun selectedTreeNode(): DefaultMutableTreeNode?\n\n    fun expand(treeNode: TreeNode)\n\n    fun select(treeNode: TreeNode)\n\n    fun nodeStructureChanged(treeNode: TreeNode)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/ThreadNode.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node\n\nimport com.intellij.ide.projectView.PresentationData\nimport com.intellij.ui.SimpleTextAttributes\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.CommentPosition\nimport net.ntworld.mergeRequestIntegrationIde.util.TextChoiceUtil\n\nclass ThreadNode(\n    val threadId: String,\n    private val repliedCount: Int,\n    private val draftCount: Int,\n    comment: Comment,\n    position: CommentPosition?\n) : CommentNode(comment, position) {\n    override val id: String = \"thread[${comment.id}]\"\n\n    override fun updatePresentation(presentation: PresentationData) {\n        super.updatePresentation(presentation)\n        val text = buildReplyAndDraftText()\n        if (null !== text) {\n            presentation.addText(\n                \" · $text\",\n                SimpleTextAttributes.GRAYED_ATTRIBUTES\n            )\n        }\n    }\n\n    private fun buildReplyAndDraftText(): String? {\n        if (repliedCount > 0 && draftCount > 0) {\n            return TextChoiceUtil.replyWithDraft(repliedCount, draftCount)\n        }\n\n        if (repliedCount > 0) {\n            return TextChoiceUtil.reply(repliedCount)\n        }\n\n        if (draftCount > 0) {\n            return TextChoiceUtil.draft(draftCount)\n        }\n\n        return null\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/_fn.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node\n\nimport net.ntworld.mergeRequest.Comment\n\nfun Node.groupComments(): Map<String, List<Comment>> {\n    val groupedComments = mutableMapOf<String, MutableList<Comment>>()\n    this.children.forEach {\n        if (it !is ThreadNode) {\n            return@forEach\n        }\n        groupedComments[it.threadId] = mutableListOf(it.comment)\n        it.children.forEach { node ->\n            if (node is CommentNode) {\n                groupedComments[it.threadId]!!.add(node.comment)\n            }\n        }\n    }\n    return groupedComments\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/fn.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest\n\nimport net.ntworld.mergeRequest.DateTime\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.MergeRequestState\n\nprivate class MergeRequestInfoEmpty : MergeRequestInfo {\n    override val id: String = \"\"\n    override val provider: String = \"\"\n    override val projectId: String = \"\"\n    override val title: String = \"\"\n    override val description: String = \"\"\n    override val url: String = \"\"\n    override val state: MergeRequestState = MergeRequestState.CLOSED\n    override val createdAt: DateTime = \"\"\n    override val updatedAt: DateTime = \"\"\n}\n\nfun MergeRequestInfo.isEmpty(): Boolean {\n    return this.id.isEmpty()\n}\n\nval MergeRequestInfo.Companion.Empty: MergeRequestInfo\n    get() = MergeRequestInfoEmpty()\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/BranchWatcher.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.rework\n\nimport git4idea.repo.GitRepository\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.watcher.Watcher\n\ninterface BranchWatcher : Watcher {\n    val providerData: ProviderData\n\n    val repository: GitRepository\n\n    val currentBranchName: String?\n\n    fun shutdown()\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/ReworkEditorController.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.rework\n\nimport com.intellij.openapi.Disposable\nimport com.intellij.openapi.editor.ex.EditorEx\nimport com.intellij.openapi.fileEditor.TextEditor\nimport com.intellij.openapi.vcs.changes.Change\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\n\ninterface ReworkEditorController : Disposable {\n    val textEditor: TextEditor\n\n    val editor: EditorEx\n\n    val providerData: ProviderData\n\n    val mergeRequestInfo: MergeRequestInfo\n\n    val path: String\n\n    val revisionNumber: String\n\n    fun updateComments()\n\n    fun hideAllComments()\n\n    fun scrollToLine(visibleLine: Int)\n\n    fun displayCommentsOnLine(visibleLine: Int)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/ReworkEditorManager.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.rework\n\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ReworkEditorNotifier\n\ninterface ReworkEditorManager: ReworkEditorNotifier {\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/ReworkManager.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.rework\n\nimport com.intellij.openapi.vcs.changes.Change\nimport git4idea.repo.GitRepository\nimport net.ntworld.mergeRequest.ProviderData\n\ninterface ReworkManager {\n\n    fun clear()\n\n    fun markBranchWatcherTerminated(branchWatcher: BranchWatcher)\n\n    fun markReworkWatcherTerminated(reworkWatcher: ReworkWatcher)\n\n    fun createBranchWatcher(providerData: ProviderData)\n\n    fun requestCreateReworkWatcher(providers: List<ProviderData>, branchName: String)\n\n    fun requestCreateReworkWatcher(providerData: ProviderData, repository: GitRepository, branchName: String)\n\n    fun findReworkWatcherByChange(providerData: ProviderData, change: Change): ReworkWatcher?\n\n    fun findActiveReworkWatcher(providerData: ProviderData): ReworkWatcher?\n\n    fun getActiveReworkWatchers(): List<ReworkWatcher>\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/ReworkWatcher.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.rework\n\nimport com.intellij.openapi.vcs.changes.Change\nimport git4idea.repo.GitRepository\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.watcher.Watcher\n\ninterface ReworkWatcher : Watcher {\n\n    val projectServiceProvider: ProjectServiceProvider\n\n    val repository: GitRepository\n\n    val branchName: String\n\n    val providerData: ProviderData\n\n    val mergeRequestInfo: MergeRequestInfo\n\n    val commits: List<Commit>\n\n    val changes: List<Change>\n\n    val comments: List<Comment>\n\n    val onlyShowDraftComments: Boolean\n\n    val displayResolvedComments: Boolean\n\n    fun isChangesBuilt(): Boolean\n\n    fun isFetchedComments(): Boolean\n\n    fun shutdown()\n\n    fun openChange(change: Change)\n\n    fun findChangeByPath(absolutePath: String): Change?\n\n    fun findCommentsByPath(absolutePath: String): List<Comment>\n\n    fun fetchComments()\n\n    fun key(): String {\n        return keyOf(providerData, branchName)\n    }\n\n    companion object {\n        fun keyOf(providerData: ProviderData, branchName: String): String {\n            return \"${providerData.id}:${branchName}\"\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/internal/BranchWatcherImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.rework.internal\n\nimport git4idea.repo.GitRepository\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.debug\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.rework.BranchWatcher\nimport net.ntworld.mergeRequestIntegrationIde.rework.ReworkManager\n\nclass BranchWatcherImpl(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val reworkManager: ReworkManager,\n    override val providerData: ProviderData,\n    override val repository: GitRepository\n) : BranchWatcher {\n    private var myTerminate = false\n    private var myPrevBranchName: String? = null\n\n    override val currentBranchName: String?\n        get() = repository.currentBranchName\n\n    override val interval: Long = 3000\n\n    override fun canExecute(): Boolean {\n        return !projectServiceProvider.project.isDisposed\n    }\n\n    override fun shouldTerminate(): Boolean {\n        return myTerminate ||\n            projectServiceProvider.project.isDisposed ||\n            projectServiceProvider.isDoingCodeReview() ||\n            !projectServiceProvider.applicationSettings.enableReworkProcess\n    }\n\n    override fun execute() {\n        val branchName = currentBranchName\n        if (null !== branchName && myPrevBranchName != branchName) {\n            myPrevBranchName = branchName\n            debug(\"${providerData.id}: BranchWatcher found branch changed, request create watcher\")\n            reworkManager.requestCreateReworkWatcher(providerData, repository, branchName)\n        }\n    }\n\n    override fun terminate() {\n        debug(\"${providerData.id}: BranchWatcher is terminated\")\n        reworkManager.markBranchWatcherTerminated(this)\n    }\n\n    override fun shutdown() {\n        debug(\"${providerData.id}: terminate BranchWatcher\")\n        myTerminate = true\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/internal/ReworkEditorControllerImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.rework.internal\n\nimport com.intellij.diff.util.Side\nimport com.intellij.openapi.editor.LogicalPosition\nimport com.intellij.openapi.editor.ScrollType\nimport com.intellij.openapi.editor.ex.EditorEx\nimport com.intellij.openapi.editor.markup.HighlighterLayer\nimport com.intellij.openapi.fileEditor.TextEditor\nimport com.intellij.openapi.util.Disposer\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.*\nimport net.ntworld.mergeRequestIntegrationIde.component.thread.ThreadFactory\nimport net.ntworld.mergeRequestIntegrationIde.component.thread.ThreadModel\nimport net.ntworld.mergeRequestIntegrationIde.component.thread.ThreadPresenter\nimport net.ntworld.mergeRequestIntegrationIde.diff.DiffView\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ReworkWatcherNotifier\nimport net.ntworld.mergeRequestIntegrationIde.rework.ReworkEditorController\nimport net.ntworld.mergeRequestIntegrationIde.util.CommentUtil\n\nclass ReworkEditorControllerImpl(\n    private val projectServiceProvider: ProjectServiceProvider,\n    override val textEditor: TextEditor,\n    override val editor: EditorEx,\n    override val providerData: ProviderData,\n    override val mergeRequestInfo: MergeRequestInfo,\n    override val path: String,\n    override val revisionNumber: String\n) : ReworkEditorController {\n    private val shouldDisplayAddIcon = false\n    private val myReworkRequester = projectServiceProvider.messageBus.syncPublisher(ReworkWatcherNotifier.TOPIC)\n\n    private val myGutterIconRenderers = mutableMapOf<Int, GutterIconRenderer>()\n    private val myThreadPresenters = mutableMapOf<Int, ThreadPresenter>()\n\n    init {\n        registerGutterIconRenderers()\n\n        val reworkWatcher = projectServiceProvider.reworkManager.findActiveReworkWatcher(providerData)\n        if (null !== reworkWatcher) {\n            val commentsByLines = CommentUtil.groupCommentsByPositionNewLine(\n                reworkWatcher.findCommentsByPath(path)\n            )\n            commentsByLines.forEach { (visibleLine, comments) ->\n                initializeLine(visibleLine, comments)\n            }\n        }\n\n        Disposer.register(textEditor, this)\n    }\n\n    override fun dispose() {\n        val makeupModel = textEditor.editor.markupModel\n        val highlighters = makeupModel.allHighlighters\n        for (highlighter in highlighters) {\n            if (highlighter.gutterIconRenderer is GutterIconRenderer) {\n                makeupModel.removeHighlighter(highlighter)\n                highlighter.dispose()\n            }\n        }\n\n        myGutterIconRenderers.clear()\n        myThreadPresenters.forEach { it.value.dispose() }\n        myThreadPresenters.clear()\n    }\n\n    override fun updateComments() {\n        resetGutterIcons()\n\n        val reworkWatcher = projectServiceProvider.reworkManager.findActiveReworkWatcher(providerData)\n        if (null !== reworkWatcher) {\n            val commentsByLines = CommentUtil.groupCommentsByPositionNewLine(\n                reworkWatcher.findCommentsByPath(path)\n            )\n            destroyExistingComments(commentsByLines.keys)\n            commentsByLines.forEach { (visibleLine, comments) ->\n                initializeLine(visibleLine, comments)\n                assertThreadPresenterAvailable(visibleLine - 1) {\n                    it.model.comments = comments\n                }\n            }\n        }\n    }\n\n    override fun hideAllComments() {\n        for (presenter in myThreadPresenters) {\n            presenter.value.model.visible = false\n        }\n    }\n\n    override fun displayCommentsOnLine(visibleLine: Int) {\n        this.assertThreadPresenterAvailable(visibleLine - 1) {\n            this.displayComments(it.model, visibleLine - 1, DiffView.DisplayCommentMode.SHOW)\n        }\n    }\n\n    override fun scrollToLine(visibleLine: Int) {\n        editor.scrollingModel.scrollTo(LogicalPosition(visibleLine - 1, 0), ScrollType.MAKE_VISIBLE)\n    }\n\n    private fun initializeLine(visibleLine: Int, comments: List<Comment>) {\n        val logicalLine = visibleLine - 1\n        initializeThreadPresenterOnLineIfNotAvailable(logicalLine, comments)\n        val renderer = myGutterIconRenderers[logicalLine]\n        if (null !== renderer) {\n            updateGutterIcon(renderer, comments)\n        }\n    }\n\n    private fun resetGutterIcons() {\n        for (renderer in myGutterIconRenderers.values) {\n            renderer.setState(GutterState.NO_COMMENT)\n        }\n    }\n\n    private fun destroyExistingComments(excludedVisibleLines: Set<Int>) {\n        val excludedLogicalLines = excludedVisibleLines\n            .map { it - 1 }\n            .filter { it >= 0 }\n\n        val removedKeys = mutableListOf<Int>()\n        for (entry in myThreadPresenters) {\n            if (excludedLogicalLines.contains(entry.key)) {\n                continue\n            }\n            entry.value.model.comments = listOf()\n            removedKeys.add(entry.key)\n        }\n        removedKeys.forEach { myThreadPresenters.remove(it) }\n    }\n\n    private fun resetEditorOnLine(logicalLine: Int, repliedComment: Comment?) {\n        val thread = myThreadPresenters[logicalLine] ?: return\n        thread.model.resetEditor(repliedComment)\n    }\n\n    // TODO: Duplicate AbstractDiffView.updateGutterIcon maybe move to Util class\n    private fun updateGutterIcon(renderer: GutterIconRenderer, comments: List<Comment>) {\n        val state = if (comments.isEmpty()) {\n            GutterState.NO_COMMENT\n        } else {\n            if (comments.filter { it.isDraft }.isNotEmpty()) {\n                GutterState.HAS_DRAFT\n            } else {\n                if (comments.size == 1) GutterState.THREAD_HAS_SINGLE_COMMENT else GutterState.THREAD_HAS_MULTI_COMMENTS\n            }\n        }\n\n        renderer.setState(state)\n    }\n\n    private fun registerGutterIconRenderers() {\n        val lineCount = editor.document.lineCount\n        for (logicalLine in 0 until lineCount) {\n            myGutterIconRenderers[logicalLine] = GutterIconRendererFactory.makeGutterIconRenderer(\n                editor.markupModel.addLineHighlighter(logicalLine, HighlighterLayer.LAST, null),\n                shouldDisplayAddIcon,\n                logicalLine,\n                visibleLineLeft = null,\n                visibleLineRight = logicalLine + 1,\n                side = Side.RIGHT,\n                actionListener = MyGutterIconRendererActionListener(this)\n            )\n        }\n    }\n\n    private fun initializeThreadPresenterOnLineIfNotAvailable(logicalLine: Int, comments: List<Comment>) {\n        if (!myThreadPresenters.containsKey(logicalLine)) {\n            val model = ThreadFactory.makeModel(comments)\n            val view = ThreadFactory.makeView(\n                projectServiceProvider, editor, providerData, mergeRequestInfo, logicalLine, Side.RIGHT,\n                GutterPosition(\n                    editorType = DiffView.EditorType.SINGLE_SIDE,\n                    changeType = DiffView.ChangeType.UNKNOWN,\n                    newLine = logicalLine + 1,\n                    newPath = path,\n                    oldLine = null,\n                    oldPath = null,\n                    headHash = revisionNumber\n                ),\n                replyInDialog = true\n            )\n            val presenter = ThreadFactory.makePresenter(model, view)\n\n            presenter.addListener(MyThreadPresenterEventListener(this))\n            Disposer.register(textEditor, presenter)\n\n            myThreadPresenters[logicalLine] = presenter\n        }\n    }\n\n    private fun assertThreadPresenterAvailable(logicalLine: Int, invoker: ((ThreadPresenter) -> Unit)) {\n        val thread = myThreadPresenters[logicalLine] ?: return\n\n        invoker.invoke(thread)\n    }\n\n    private fun toggleComments(logicalLine: Int) {\n        this.assertThreadPresenterAvailable(logicalLine) {\n            this.displayComments(it.model, logicalLine, DiffView.DisplayCommentMode.TOGGLE)\n        }\n    }\n\n    private fun displayComments(model: ThreadModel, logicalLine: Int, mode: DiffView.DisplayCommentMode) {\n        when (mode) {\n            DiffView.DisplayCommentMode.TOGGLE -> model.visible = !model.visible\n            DiffView.DisplayCommentMode.SHOW -> model.visible = true\n            DiffView.DisplayCommentMode.HIDE -> model.visible = false\n        }\n        setWritingStateOfGutterIconRenderer(model, logicalLine)\n    }\n\n    private fun setWritingStateOfGutterIconRenderer(model: ThreadModel, logicalLine: Int) {\n        val renderer = myGutterIconRenderers[logicalLine]\n        if (null !== renderer && model.showEditor) {\n            renderer.setState(GutterState.WRITING)\n        }\n    }\n\n    private fun collectCommentsOfLine(logicalLine: Int): List<Comment> {\n        val visibleLine = logicalLine + 1\n        val reworkWatcher = projectServiceProvider.reworkManager.findActiveReworkWatcher(providerData)\n        if (null !== reworkWatcher) {\n            val comments = reworkWatcher.findCommentsByPath(path)\n            return comments.filter {\n                val position = it.position ?: return@filter false\n\n                position.newLine == visibleLine\n            }\n        }\n        return listOf()\n    }\n\n    private class MyGutterIconRendererActionListener(\n        private val self: ReworkEditorControllerImpl\n    ) : GutterIconRendererActionListener {\n\n        override fun performGutterIconRendererAction(gutterIconRenderer: GutterIconRenderer, type: GutterActionType) {\n            when (type) {\n                GutterActionType.ADD -> {\n                }\n                GutterActionType.TOGGLE -> {\n                    self.initializeThreadPresenterOnLineIfNotAvailable(\n                        gutterIconRenderer.logicalLine, self.collectCommentsOfLine(gutterIconRenderer.logicalLine)\n                    )\n                    self.toggleComments(gutterIconRenderer.logicalLine)\n                }\n            }\n        }\n\n    }\n\n    private class MyThreadPresenterEventListener(\n        private val self: ReworkEditorControllerImpl\n    ) : ThreadPresenter.EventListener {\n        override fun onMainEditorClosed(threadPresenter: ThreadPresenter) {\n        }\n\n        override fun onEditCommentRequested(comment: Comment, content: String) {\n            self.myReworkRequester.requestEditComment(self.providerData, comment, content)\n        }\n\n        override fun onReplyCommentRequested(content: String, repliedComment: Comment, logicalLine: Int, side: Side) {\n            self.myReworkRequester.requestReplyComment(self.providerData, content, repliedComment)\n            self.resetEditorOnLine(logicalLine, repliedComment)\n        }\n\n        override fun onCreateCommentRequested(content: String, position: GutterPosition, logicalLine: Int, side: Side, isDraft: Boolean) {\n            self.myReworkRequester.requestCreateComment(self.providerData, content, position, isDraft)\n            self.resetEditorOnLine(logicalLine, null)\n        }\n\n        override fun onPublishDraftCommentRequested(comment: Comment) {\n            self.myReworkRequester.requestPublishComment(self.providerData, comment)\n        }\n\n        override fun onDeleteCommentRequested(comment: Comment) {\n            self.myReworkRequester.requestDeleteComment(self.providerData, comment)\n        }\n\n        override fun onResolveCommentRequested(comment: Comment) {\n            self.myReworkRequester.requestResolveComment(self.providerData, comment)\n        }\n\n        override fun onUnresolveCommentRequested(comment: Comment) {\n            self.myReworkRequester.requestUnresolveComment(self.providerData, comment)\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/internal/ReworkEditorManagerImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.rework.internal\n\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.openapi.editor.ex.EditorEx\nimport com.intellij.openapi.fileEditor.TextEditor\nimport com.intellij.openapi.fileEditor.ex.FileEditorManagerEx\nimport com.intellij.openapi.vfs.LocalFileSystem\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.rework.ReworkEditorController\nimport net.ntworld.mergeRequestIntegrationIde.rework.ReworkEditorManager\nimport net.ntworld.mergeRequestIntegrationIde.rework.ReworkWatcher\n\nclass ReworkEditorManagerImpl(\n    private val projectServiceProvider: ProjectServiceProvider\n) : ReworkEditorManager {\n    private val myFileEditorManagerEx = FileEditorManagerEx.getInstanceEx(projectServiceProvider.project)\n    private val myControllerMap = mutableMapOf<TextEditor, ReworkEditorController>()\n\n    override fun bootstrap(reworkWatcher: ReworkWatcher) {\n        val editors = FileEditorManagerEx.getInstance(projectServiceProvider.project).allEditors\n        for (editor in editors) {\n            if (editor is TextEditor) {\n                bootstrap(editor, reworkWatcher)\n            }\n        }\n    }\n\n    override fun bootstrap(editor: TextEditor, reworkWatcher: ReworkWatcher) {\n        bindControllerToTextEditor(editor, reworkWatcher)\n    }\n\n    override fun open(providerData: ProviderData, path: String, line: Int?) {\n        val file = LocalFileSystem.getInstance().findFileByPath(path) ?: return\n\n        ApplicationManager.getApplication().invokeLater {\n            val editors = myFileEditorManagerEx.openFile(file, true)\n            for (fileEditor in editors) {\n                if (fileEditor is TextEditor) {\n                    val controller = bindControllerToTextEditor(fileEditor, providerData)\n                    if (null !== line && null !== controller) {\n                        scrollAndDisplayCommentsOnLine(controller, line)\n                    }\n                }\n            }\n        }\n    }\n\n    private fun scrollAndDisplayCommentsOnLine(controller: ReworkEditorController, line: Int) {\n        controller.hideAllComments()\n        controller.scrollToLine(line)\n        controller.displayCommentsOnLine(line)\n    }\n\n    override fun commentsUpdated(providerData: ProviderData) {\n        ApplicationManager.getApplication().invokeLater {\n            val editors = myFileEditorManagerEx.allEditors\n            for (editor in editors) {\n                if (editor is TextEditor) {\n                    val controller = myControllerMap[editor] ?: continue\n                    controller.updateComments()\n                }\n            }\n        }\n    }\n\n    override fun shutdown(providerData: ProviderData) {\n        val editors = myFileEditorManagerEx.allEditors\n        for (editor in editors) {\n            if (editor is TextEditor) {\n                val controller = myControllerMap[editor] ?: continue\n                controller.dispose()\n            }\n        }\n    }\n\n    private fun bindControllerToTextEditor(\n        textEditor: TextEditor,\n        providerData: ProviderData\n    ): ReworkEditorController? {\n        if (myControllerMap.contains(textEditor)) {\n            return myControllerMap[textEditor]\n        }\n\n        val reworkWatcher = projectServiceProvider.reworkManager.findActiveReworkWatcher(providerData)\n        if (null === reworkWatcher) {\n            return null\n        }\n        return bindControllerToTextEditor(textEditor, reworkWatcher)\n    }\n\n    private fun bindControllerToTextEditor(\n        textEditor: TextEditor,\n        reworkWatcher: ReworkWatcher\n    ): ReworkEditorController? {\n        if (myControllerMap.contains(textEditor)) {\n            return myControllerMap[textEditor]\n        }\n\n        val editor = textEditor.editor as? EditorEx ?: return null\n        val virtualFile = textEditor.file ?: return null\n        val change = reworkWatcher.findChangeByPath(virtualFile.path) ?: return null\n        val afterRevision = change.afterRevision ?: return null\n\n        val instance = ReworkEditorControllerImpl(\n            projectServiceProvider,\n            textEditor,\n            editor,\n            reworkWatcher.providerData,\n            reworkWatcher.mergeRequestInfo,\n            virtualFile.path,\n            afterRevision.revisionNumber.toString()\n        )\n        myControllerMap[textEditor] = instance\n        return instance\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/internal/ReworkGeneralCommentsView.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.rework.internal\n\nimport com.intellij.openapi.ui.DialogBuilder\nimport com.intellij.openapi.ui.Messages\nimport com.intellij.ui.JBColor\nimport com.intellij.ui.ScrollPaneFactory\nimport com.intellij.ui.components.Panel\nimport com.intellij.util.ui.JBUI\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.EditorComponent\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.GroupComponent\nimport net.ntworld.mergeRequestIntegrationIde.component.comment.Options\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ReworkWatcherNotifier\nimport net.ntworld.mergeRequestIntegrationIde.rework.ReworkWatcher\nimport java.awt.Dimension\nimport javax.swing.BorderFactory\nimport javax.swing.BoxLayout\nimport javax.swing.ScrollPaneConstants\n\nclass ReworkGeneralCommentsView(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData,\n    private val reworkWatcher: ReworkWatcher\n) {\n    private val myReworkRequester = projectServiceProvider.messageBus.syncPublisher(ReworkWatcherNotifier.TOPIC)\n    private val myGroupCommentCollection = mutableListOf<GroupComponent>()\n    private val myThreadPanel = Panel()\n    private val myThreadBoxLayout = JBUI.Panels.simplePanel()\n    private val myThreadWrapper = ScrollPaneFactory.createScrollPane(myThreadBoxLayout, true)\n\n    private val myMainEditorEventListener = object : EditorComponent.EventListener {\n        override fun onEditorFocused(editor: EditorComponent) {\n            myThreadWrapper.verticalScrollBar.value = myThreadWrapper.verticalScrollBar.maximum\n        }\n\n        override fun onEditorResized(editor: EditorComponent) {}\n\n        override fun onCancelClicked(editor: EditorComponent) {\n            if (editor.text.isNotBlank()) {\n                val result = Messages.showYesNoDialog(\n                    \"Do you want to delete the whole content?\", \"Are you sure\", Messages.getQuestionIcon()\n                )\n                result == Messages.YES\n            }\n            editor.text = \"\"\n        }\n\n        override fun onSubmitClicked(editor: EditorComponent, isDraft: Boolean) {\n            val text = editor.text.trim()\n            if (text.isNotEmpty()) {\n                myReworkRequester.requestCreateComment(providerData, text, null, isDraft)\n            }\n        }\n    }\n\n    private val myMainEditor by lazy {\n        val editor = projectServiceProvider.componentFactory.commentComponents.makeEditor(\n            projectServiceProvider.project, EditorComponent.Type.NEW_DISCUSSION, 0,\n            borderLeftRight = 0, showCancelAction = false,\n            isDoingCodeReview = projectServiceProvider.isDoingCodeReview()\n        )\n        editor.isVisible = true\n        editor.addListener(myMainEditorEventListener)\n\n        editor\n    }\n\n    private val myGroupComponentEventListener = object : GroupComponent.EventListener {\n        override fun onResized() {}\n\n        override fun onOpenDialogClicked() {\n        }\n\n        override fun onEditorCreated(groupId: String, editor: EditorComponent) {\n        }\n\n        override fun onEditorDestroyed(groupId: String, editor: EditorComponent) {\n        }\n\n        override fun onReplyCommentRequested(comment: Comment, content: String) {\n            if (content.trim().isNotEmpty()) {\n                myReworkRequester.requestReplyComment(providerData, content, comment)\n            }\n        }\n\n        override fun onEditCommentRequested(comment: Comment, content: String) {\n            myReworkRequester.requestEditComment(providerData, comment, content)\n        }\n\n        override fun onPublishDraftCommentRequested(comment: Comment) {\n            myReworkRequester.requestPublishComment(providerData, comment)\n        }\n\n        override fun onDeleteCommentRequested(comment: Comment) {\n            myReworkRequester.requestDeleteComment(providerData, comment)\n        }\n\n        override fun onResolveCommentRequested(comment: Comment) {\n            myReworkRequester.requestResolveComment(providerData, comment)\n        }\n\n        override fun onUnresolveCommentRequested(comment: Comment) {\n            myReworkRequester.requestUnresolveComment(providerData, comment)\n        }\n    }\n\n    init {\n        myMainEditor.addCommentNowButtonText = \"Add general comment\"\n        myMainEditor.addCommentNowButtonDesc = \"Create new thread of general comment\"\n\n        myThreadPanel.background = JBColor.border()\n        myThreadBoxLayout.background = JBColor.border()\n        myThreadWrapper.background = JBColor.border()\n        myThreadWrapper.border = BorderFactory.createMatteBorder(1, 1, 0, 1, JBColor.border())\n        myThreadWrapper.minimumSize = Dimension(600, 100)\n        myThreadWrapper.maximumSize = Dimension(1200, -1)\n        myThreadWrapper.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER\n\n        myThreadBoxLayout.addToCenter(myThreadPanel)\n        myThreadPanel.layout = BoxLayout(myThreadPanel, BoxLayout.Y_AXIS)\n        myThreadBoxLayout.addToBottom(myMainEditor.component)\n    }\n\n    fun focusMainEditor() {\n        myMainEditor.focus()\n    }\n\n    fun render(groupedComments: Map<String, List<Comment>>) {\n        myThreadPanel.removeAll()\n        myGroupCommentCollection.forEach { it.dispose() }\n        myGroupCommentCollection.clear()\n\n        groupedComments.forEach { (groupId, comments) ->\n            if (comments.isEmpty()) {\n                return@forEach\n            }\n\n            val group = projectServiceProvider.componentFactory.commentComponents.makeGroup(\n                reworkWatcher.providerData,\n                reworkWatcher.mergeRequestInfo,\n                projectServiceProvider.project,\n                false, groupId, comments,\n                Options(borderLeftRight = 0, showMoveToDialog = false)\n            )\n\n            group.addListener(myGroupComponentEventListener)\n            myThreadPanel.add(group.component)\n            myGroupCommentCollection.add(group)\n        }\n\n        val builder = DialogBuilder()\n        builder.setCenterPanel(myThreadWrapper)\n        builder.removeAllActions()\n        builder.addOkAction()\n        builder.resizable(true)\n        builder.show()\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/internal/ReworkManagerImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.rework.internal\n\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.openapi.vcs.changes.Change\nimport git4idea.repo.GitRepository\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.MergeRequestState\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.ProviderStatus\nimport net.ntworld.mergeRequest.api.MergeRequestOrdering\nimport net.ntworld.mergeRequest.query.GetMergeRequestFilter\nimport net.ntworld.mergeRequestIntegration.exception.ProviderNotFoundException\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegrationIde.debug\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.rework.BranchWatcher\nimport net.ntworld.mergeRequestIntegrationIde.rework.ReworkManager\nimport net.ntworld.mergeRequestIntegrationIde.rework.ReworkWatcher\nimport net.ntworld.mergeRequestIntegrationIde.task.SearchMergeRequestTask\nimport net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil\nimport java.util.*\n\ninternal class ReworkManagerImpl(\n    private val projectServiceProvider: ProjectServiceProvider\n) : ReworkManager {\n    private val myBranchWatchers = Collections.synchronizedMap(mutableMapOf<String, BranchWatcher>())\n    private val myReworkWatchers = Collections.synchronizedMap(mutableMapOf<String, ReworkWatcher>())\n\n    override fun clear() {\n        myBranchWatchers.forEach { entry -> entry.value.shutdown() }\n        myBranchWatchers.clear()\n\n        myReworkWatchers.forEach { entry -> entry.value.shutdown() }\n        myReworkWatchers.clear()\n    }\n\n    override fun markBranchWatcherTerminated(branchWatcher: BranchWatcher) {\n        myBranchWatchers.remove(branchWatcher.providerData.id)\n\n        debug(\"${branchWatcher.providerData.id}: clear BranchWatcher\")\n    }\n\n    override fun markReworkWatcherTerminated(reworkWatcher: ReworkWatcher) {\n        val key = reworkWatcher.key()\n        myReworkWatchers.remove(key)\n\n        debug(\"$key: clear ReworkManager\")\n    }\n\n    override fun createBranchWatcher(providerData: ProviderData) {\n        if (!projectServiceProvider.applicationSettings.enableReworkProcess) {\n            return\n        }\n\n        if (providerData.status != ProviderStatus.ACTIVE || myBranchWatchers.containsKey(providerData.id)) {\n            return\n        }\n\n        var gitRepository = RepositoryUtil.findRepository(projectServiceProvider, providerData)\n        var count = 0\n        while (gitRepository === null && count < 100) {\n            debug(\"${providerData.id}: cannot find repository, retry in 10s\")\n            count++\n            Thread.sleep(10000)\n            gitRepository = RepositoryUtil.findRepository(projectServiceProvider, providerData)\n        }\n\n        val repository = gitRepository\n        if (null !== repository) {\n            val branchWatcher = BranchWatcherImpl(\n                projectServiceProvider, this, providerData, repository\n            )\n            myBranchWatchers[providerData.id] = branchWatcher\n\n            debug(\"${providerData.id}: create BranchWatcher\")\n            projectServiceProvider.applicationServiceProvider.watcherManager.addWatcher(branchWatcher)\n        }\n    }\n\n    override fun requestCreateReworkWatcher(providers: List<ProviderData>, branchName: String) {\n        if (!projectServiceProvider.applicationSettings.enableReworkProcess) {\n            return\n        }\n\n        val pair = findProviderData(providers, branchName)\n        val providerData = pair.first\n        val repository = pair.second\n        if (null !== providerData && null !== repository) {\n            requestCreateReworkWatcher(providerData, repository, branchName)\n        }\n    }\n\n    override fun requestCreateReworkWatcher(providerData: ProviderData, repository: GitRepository, branchName: String) {\n        if (!projectServiceProvider.applicationSettings.enableReworkProcess) {\n            return\n        }\n\n        val key = ReworkWatcher.keyOf(providerData, branchName)\n        if (myReworkWatchers.contains(key)) {\n            return\n        }\n\n        val task = SearchMergeRequestTask(\n            projectServiceProvider,\n            providerData,\n            GetMergeRequestFilter.make(\n                state = MergeRequestState.OPENED,\n                id = null,\n                search = \"\",\n                authorId = \"\",\n                assigneeId = \"\",\n                approverIds = listOf(),\n                sourceBranch = branchName\n            ),\n            MergeRequestOrdering.RECENTLY_UPDATED,\n            object : SearchMergeRequestTask.Listener {\n                override fun onError(exception: Exception) {\n                    if (exception !is ProviderNotFoundException) {\n                        throw exception\n                    }\n                }\n\n                override fun dataReceived(list: List<MergeRequestInfo>, page: Int, totalPages: Int, totalItems: Int) {\n                    if (myReworkWatchers.contains(key)) {\n                        return\n                    }\n\n                    ApplicationManager.getApplication().invokeLater {\n                        if (list.isNotEmpty()) {\n                            val mergeRequestInfo = list.first()\n                            debug(\"${providerData.id}:${branchName}: create watcher\")\n                            val reworkWatcher = ReworkWatcherImpl(\n                                projectServiceProvider,\n                                repository,\n                                branchName,\n                                providerData,\n                                mergeRequestInfo\n                            )\n                            myReworkWatchers[key] = reworkWatcher\n                            projectServiceProvider.applicationServiceProvider.watcherManager.addWatcher(reworkWatcher)\n                        }\n                    }\n                }\n            }\n        )\n\n        task.start()\n    }\n\n    override fun findReworkWatcherByChange(providerData: ProviderData, change: Change): ReworkWatcher? {\n        for (entry in myReworkWatchers) {\n            if (entry.value.providerData.id != providerData.id) {\n                continue\n            }\n\n            if (entry.value.changes.contains(change)) {\n                return entry.value\n            }\n        }\n        return null\n    }\n\n    override fun findActiveReworkWatcher(providerData: ProviderData): ReworkWatcher? {\n        for (entry in myReworkWatchers) {\n            if (entry.value.providerData.id != providerData.id) {\n                continue\n            }\n            return entry.value\n        }\n        return null\n    }\n\n    override fun getActiveReworkWatchers(): List<ReworkWatcher> {\n        return myReworkWatchers.values.toList()\n    }\n\n    private fun findProviderData(\n        providers: List<ProviderData>,\n        branchName: String\n    ): Pair<ProviderData?, GitRepository?> {\n        for (provider in providers) {\n            val repository = RepositoryUtil.findRepository(projectServiceProvider, provider)\n            if (null === repository) {\n                continue\n            }\n\n            if (repository.currentBranchName != branchName) {\n                continue\n            }\n\n            return Pair(provider, repository)\n        }\n        return Pair(null, null)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/rework/internal/ReworkWatcherImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.rework.internal\n\nimport com.intellij.notification.NotificationType\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.openapi.fileEditor.ex.FileEditorManagerEx\nimport com.intellij.openapi.vcs.changes.Change\nimport com.intellij.openapi.vcs.changes.ChangesUtil\nimport com.intellij.openapi.vcs.changes.PreviewDiffVirtualFile\nimport git4idea.repo.GitRepository\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequest.command.DeleteCommentCommand\nimport net.ntworld.mergeRequest.command.ResolveCommentCommand\nimport net.ntworld.mergeRequest.command.UnresolveCommentCommand\nimport net.ntworld.mergeRequest.request.CreateCommentRequest\nimport net.ntworld.mergeRequest.request.PublishCommentsRequest\nimport net.ntworld.mergeRequest.request.ReplyCommentRequest\nimport net.ntworld.mergeRequest.request.UpdateCommentRequest\nimport net.ntworld.mergeRequestIntegration.internal.CommentPositionImpl\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegration.provider.ProviderException\nimport net.ntworld.mergeRequestIntegrationIde.component.gutter.GutterPosition\nimport net.ntworld.mergeRequestIntegrationIde.debug\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.DiffPreviewProviderImpl\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ReviewContextImpl\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.DiffNotifier\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ReworkEditorNotifier\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ReworkWatcherNotifier\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreeView\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node.*\nimport net.ntworld.mergeRequestIntegrationIde.rework.ReworkWatcher\nimport net.ntworld.mergeRequestIntegrationIde.task.FindMergeRequestTask\nimport net.ntworld.mergeRequestIntegrationIde.task.GetCommentsTask\nimport net.ntworld.mergeRequestIntegrationIde.task.GetCommitsTask\nimport net.ntworld.mergeRequestIntegrationIde.util.CommentUtil\nimport net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil\nimport java.util.*\n\nclass ReworkWatcherImpl(\n    override val projectServiceProvider: ProjectServiceProvider,\n    override val repository: GitRepository,\n    override val branchName: String,\n    override val providerData: ProviderData,\n    override val mergeRequestInfo: MergeRequestInfo\n) : ReworkWatcher, ReworkWatcherNotifier {\n    private val myConnection = projectServiceProvider.messageBus.connect()\n    private val myEditorManager = projectServiceProvider.messageBus.syncPublisher(\n        ReworkEditorNotifier.TOPIC\n    )\n    private val myDiffPublisher = projectServiceProvider.messageBus.syncPublisher(DiffNotifier.TOPIC)\n\n    private val myPreviewDiffVirtualFileMap = mutableMapOf<Change, PreviewDiffVirtualFile>()\n    private val myCommentsMap = Collections.synchronizedMap(mutableMapOf<String, List<Comment>>())\n    private val myChangesMap = Collections.synchronizedMap(mutableMapOf<String, MutableList<Change>>())\n    private val myComments = mutableListOf<Comment>()\n    private val myReworkGeneralCommentsView = ReworkGeneralCommentsView(projectServiceProvider, providerData, this)\n\n    @Volatile\n    private var myTerminate = false\n    private var myIsChangesBuilt = false\n    private var myIsFetchedComments = false\n    private var myRunCount = 0\n    private var myMergeRequest: MergeRequest? = null\n    override var commits: List<Commit> = listOf()\n    override var changes: List<Change> = listOf()\n    override var comments: MutableList<Comment> = mutableListOf()\n        private set\n\n    override var onlyShowDraftComments: Boolean = false\n        private set\n    override var displayResolvedComments: Boolean = false\n        private set\n\n    override val interval: Long = 10000\n    private val myGetCommitsTaskListener = object : GetCommitsTask.Listener {\n        override fun dataReceived(mergeRequestInfo: MergeRequestInfo, commits: List<Commit>) {\n            this@ReworkWatcherImpl.commits = commits\n            changes = projectServiceProvider.repositoryFile.findChanges(providerData, commits.map { it.id })\n            buildChangesMap()\n            myIsChangesBuilt = true\n            ApplicationManager.getApplication().invokeLater {\n                projectServiceProvider.openSingleMRToolWindow {\n                    projectServiceProvider.singleMRToolWindowNotifierTopic.showReworkChanges(\n                        this@ReworkWatcherImpl, changes\n                    )\n                }\n\n                myEditorManager.bootstrap(this@ReworkWatcherImpl)\n            }\n        }\n    }\n    private val myGetCommentsTaskListener = object : GetCommentsTask.Listener {\n        override fun dataReceived(\n            providerData: ProviderData,\n            mergeRequestInfo: MergeRequestInfo,\n            comments: List<Comment>\n        ) {\n            myComments.clear()\n            myComments.addAll(comments)\n            buildCommentsAndSendCommentsUpdatedSignal()\n            myIsFetchedComments = true\n        }\n    }\n    private val myFindMergeRequestTaskListener = object : FindMergeRequestTask.Listener {\n        override fun dataReceived(mergeRequest: MergeRequest) {\n            myMergeRequest = mergeRequest\n        }\n    }\n\n    private val reviewContext = ReviewContextImpl(\n        projectServiceProvider, providerData, mergeRequestInfo, projectServiceProvider.messageBus.connect()\n    )\n\n    init {\n        myConnection.subscribe(ReworkWatcherNotifier.TOPIC, this)\n        projectServiceProvider.singleMRToolWindowNotifierTopic.registerReworkWatcher(this)\n        fetchCommits()\n        fetchMergeRequest()\n    }\n\n    override fun isChangesBuilt(): Boolean {\n        return myIsChangesBuilt\n    }\n\n    override fun isFetchedComments(): Boolean {\n        return myIsFetchedComments\n    }\n\n    override fun canExecute(): Boolean {\n        return !myTerminate && !projectServiceProvider.project.isDisposed\n    }\n\n    override fun shouldTerminate(): Boolean {\n        return myTerminate ||\n            projectServiceProvider.project.isDisposed ||\n            repository.currentBranchName != branchName ||\n            projectServiceProvider.isDoingCodeReview() ||\n            !projectServiceProvider.applicationSettings.enableReworkProcess\n    }\n\n    override fun execute() {\n        // the watcher runs every 10 seconds, so 18x10s = 3 minutes\n        if (myRunCount % 18 == 0) {\n            fetchComments()\n        }\n        myRunCount += 1\n    }\n\n    override fun terminate() {\n        debug(\"${providerData.id}:$branchName: ReworkWatcher is terminated\")\n        if (!projectServiceProvider.messageBus.isDisposed) {\n            projectServiceProvider.singleMRToolWindowNotifierTopic.removeReworkWatcher(this)\n        }\n        projectServiceProvider.reworkManager.markReworkWatcherTerminated(this)\n        myConnection.disconnect()\n    }\n\n    override fun shutdown() {\n        debug(\"${providerData.id}:$branchName: terminate ReworkWatcher\")\n        if (!projectServiceProvider.messageBus.isDisposed) {\n            projectServiceProvider.singleMRToolWindowNotifierTopic.removeReworkWatcher(this)\n        }\n        ApplicationManager.getApplication().invokeLater {\n            val fileEditorManagerEx = FileEditorManagerEx.getInstanceEx(projectServiceProvider.project)\n            myPreviewDiffVirtualFileMap.forEach { (_, diffFile) ->\n                fileEditorManagerEx.closeFile(diffFile)\n            }\n            myPreviewDiffVirtualFileMap.clear()\n\n            myEditorManager.shutdown(providerData)\n        }\n        myTerminate = true\n    }\n\n    override fun openChange(change: Change) {\n        val afterRevision = change.afterRevision\n        if (null === afterRevision) {\n            updateDataToReviewContext()\n            return openChangeAsDiffView(change)\n        }\n        myEditorManager.open(providerData, afterRevision.file.path)\n    }\n\n    override fun findChangeByPath(absolutePath: String): Change? {\n        return if (myTerminate) null else {\n            val changes = myChangesMap[absolutePath]\n            if (null === changes) {\n                return null\n            }\n            return changes.first()\n        }\n    }\n\n    override fun findCommentsByPath(absolutePath: String): List<Comment> {\n        return myCommentsMap[absolutePath] ?: listOf()\n    }\n\n    override fun fetchComments() {\n        debug(\"${providerData.id}:$branchName: fetching comments\")\n        val task = GetCommentsTask(\n            projectServiceProvider = projectServiceProvider,\n            providerData = providerData,\n            mergeRequestInfo = mergeRequestInfo,\n            listener = myGetCommentsTaskListener\n        )\n        task.start()\n    }\n\n    override fun requestFetchComment(providerData: ProviderData) = assertHasSameProvider(providerData) {\n        fetchComments()\n    }\n\n    override fun changeDisplayResolvedComments(\n        providerData: ProviderData, value: Boolean\n    ) = assertHasSameProvider(providerData) {\n        if (value != displayResolvedComments) {\n            displayResolvedComments = value\n            buildCommentsAndSendCommentsUpdatedSignal()\n        }\n    }\n\n    override fun changeOnlyShowDraftComments(providerData: ProviderData, value: Boolean) = assertHasSameProvider(providerData) {\n        if (value != onlyShowDraftComments) {\n            onlyShowDraftComments = value\n            buildCommentsAndSendCommentsUpdatedSignal()\n        }\n    }\n\n    override fun commentTreeNodeSelected(\n        providerData: ProviderData,\n        node: Node,\n        type: CommentTreeView.TreeSelectType\n    ) = assertHasSameProvider(providerData) {\n        if (type == CommentTreeView.TreeSelectType.NORMAL) {\n            return@assertHasSameProvider\n        }\n        when (node) {\n            is GeneralCommentsNode -> displayGeneralComments(node.groupComments(), false)\n            is ThreadNode -> when (val parent = node.parent) {\n                is GeneralCommentsNode -> displayGeneralComments(parent.groupComments(), false)\n                is FileLineNode -> openFileAndDisplayThreadByLineNode(parent)\n            }\n            is CommentNode -> when (val parent = node.parent!!.parent) {\n                is GeneralCommentsNode -> displayGeneralComments(parent.groupComments(), false)\n                is FileLineNode -> openFileAndDisplayThreadByLineNode(parent)\n            }\n            is FileNode -> openFileByNode(node)\n            is FileLineNode -> openFileAndDisplayThreadByLineNode(node)\n        }\n    }\n\n    override fun openCreateGeneralCommentForm(providerData: ProviderData) = assertHasSameProvider(providerData) {\n        val groupedComments = mutableMapOf<String, MutableList<Comment>>()\n        comments.forEach {\n            if (null !== it.position) {\n                return@forEach\n            }\n            if (!groupedComments.containsKey(it.parentId)) {\n                groupedComments[it.parentId] = mutableListOf()\n            }\n            groupedComments[it.parentId]!!.add(it)\n        }\n        displayGeneralComments(groupedComments, true)\n    }\n\n    override fun requestReplyComment(\n        providerData: ProviderData,\n        content: String,\n        repliedComment: Comment\n    ) = assertHasSameProvider(providerData) {\n        projectServiceProvider.infrastructure.serviceBus() process ReplyCommentRequest.make(\n            providerId = providerData.id,\n            mergeRequestId = mergeRequestInfo.id,\n            repliedComment = repliedComment,\n            body = content\n        ) ifError {\n            projectServiceProvider.notify(\n                \"There was an error from server. \\n\\n ${it.message}\",\n                NotificationType.ERROR\n            )\n            throw ProviderException(it)\n        }\n        fetchComments()\n    }\n\n    override fun requestCreateComment(\n        providerData: ProviderData,\n        content: String,\n        position: GutterPosition?,\n        isDraft: Boolean\n    ) = assertHasSameProvider(providerData) {\n        val commentPosition = convertGutterPositionToCommentPosition(position)\n        projectServiceProvider.infrastructure.serviceBus() process CreateCommentRequest.make(\n            providerId = providerData.id,\n            mergeRequestId = mergeRequestInfo.id,\n            position = commentPosition,\n            body = content,\n            isDraft = false\n        ) ifError {\n            projectServiceProvider.notify(\n                \"There was an error from server. \\n\\n ${it.message}\",\n                NotificationType.ERROR\n            )\n            throw ProviderException(it)\n        }\n        fetchComments()\n    }\n\n    override fun requestEditComment(providerData: ProviderData, comment: Comment, content: String) {\n        projectServiceProvider.infrastructure.serviceBus() process UpdateCommentRequest.make(\n            providerId = providerData.id,\n            mergeRequestId = mergeRequestInfo.id,\n            comment = comment,\n            body = content\n        ) ifError {\n            projectServiceProvider.notify(\n                \"There was an error from server. \\n\\n ${it.message}\",\n                NotificationType.ERROR\n            )\n            throw ProviderException(it)\n        }\n        fetchComments()\n    }\n\n    override fun requestPublishComment(providerData: ProviderData, comment: Comment) {\n        projectServiceProvider.infrastructure.serviceBus() process PublishCommentsRequest.make(\n            providerId = providerData.id,\n            mergeRequestId = mergeRequestInfo.id,\n            draftCommentIds = listOf(comment.id)\n        ) ifError {\n            projectServiceProvider.notify(\n                \"There was an error from server. \\n\\n ${it.message}\",\n                NotificationType.ERROR\n            )\n            throw ProviderException(it)\n        }\n        fetchComments()\n    }\n\n    override fun requestDeleteComment(\n        providerData: ProviderData,\n        comment: Comment\n    ) = assertHasSameProvider(providerData) {\n        projectServiceProvider.infrastructure.commandBus() process DeleteCommentCommand.make(\n            providerId = providerData.id,\n            mergeRequestId = mergeRequestInfo.id,\n            comment = comment\n        )\n        fetchComments()\n    }\n\n    override fun requestResolveComment(\n        providerData: ProviderData,\n        comment: Comment\n    ) = assertHasSameProvider(providerData) {\n        projectServiceProvider.infrastructure.commandBus() process ResolveCommentCommand.make(\n            providerId = providerData.id,\n            mergeRequestId = mergeRequestInfo.id,\n            comment = comment\n        )\n        fetchComments()\n    }\n\n    override fun requestUnresolveComment(\n        providerData: ProviderData,\n        comment: Comment\n    ) = assertHasSameProvider(providerData) {\n        projectServiceProvider.infrastructure.commandBus() process UnresolveCommentCommand.make(\n            providerId = providerData.id,\n            mergeRequestId = mergeRequestInfo.id,\n            comment = comment\n        )\n        fetchComments()\n    }\n\n    private fun convertGutterPositionToCommentPosition(input: GutterPosition?): CommentPosition? {\n        if (null === input) return null\n\n        return CommentPositionImpl(\n            oldLine = input.oldLine,\n            oldPath = if (null === input.oldPath) null else RepositoryUtil.findRelativePath(repository, input.oldPath),\n            newLine = input.newLine,\n            newPath = if (null === input.newPath) null else RepositoryUtil.findRelativePath(repository, input.newPath),\n            baseHash = if (input.baseHash.isNullOrEmpty()) findBaseHash() else input.baseHash,\n            headHash = if (input.headHash.isNullOrEmpty()) findHeadHash() else input.headHash,\n            startHash = if (input.startHash.isNullOrEmpty()) findStartHash() else input.startHash,\n            source = CommentPositionSource.UNKNOWN,\n            changeType = CommentPositionChangeType.UNKNOWN\n        )\n    }\n\n    private fun findBaseHash(): String {\n        if (commits.isNotEmpty()) {\n            return commits.last().id\n        }\n\n        val mr = myMergeRequest ?: return \"\"\n        val diff = mr.diffReference\n        return if (null === diff) \"\" else diff.baseHash\n    }\n\n    private fun findStartHash(): String {\n        val mr = myMergeRequest ?: return \"\"\n        val diff = mr.diffReference\n        return if (null === diff) \"\" else diff.startHash\n    }\n\n    private fun findHeadHash(): String {\n        if (commits.isNotEmpty()) {\n            return commits.first().id\n        }\n\n        val mr = myMergeRequest ?: return \"\"\n        val diff = mr.diffReference\n        return if (null === diff) \"\" else diff.headHash\n    }\n\n    private fun openFileByNode(node: FileNode) {\n        val fullPath = RepositoryUtil.findAbsoluteCrossPlatformsPath(repository, node.path)\n        openFileAndDisplayThread(absolutePath = fullPath, line = null, position = null)\n    }\n\n    private fun openFileAndDisplayThreadByLineNode(node: FileLineNode) {\n        val fullPath = RepositoryUtil.findAbsoluteCrossPlatformsPath(repository, node.path)\n        if (null === node.position.newLine) {\n            // edge case, open diff view instead of editor view if there is no newLine\n            val change = findChangeByPath(fullPath)\n            if (null !== change) {\n                updateDataToReviewContext()\n                openChangeAsDiffView(change)\n                reviewContext.putChangeData(change, DiffNotifier.ScrollPosition, node.position)\n                reviewContext.putChangeData(change, DiffNotifier.ScrollShowComments, true)\n                myDiffPublisher.hideAllCommentsRequested(reviewContext, change)\n                myDiffPublisher.scrollToPositionRequested(reviewContext, change, node.position, true)\n            }\n        } else {\n            openFileAndDisplayThread(absolutePath = fullPath, line = node.line, position = node.position)\n        }\n    }\n\n    private fun openFileAndDisplayThread(absolutePath: String, line: Int?, position: CommentPosition?) {\n        val change = findChangeByPath(absolutePath)\n        if (null === change) {\n            return myEditorManager.open(providerData, absolutePath, line)\n        }\n\n        val afterRevision = change.afterRevision\n        if (null === afterRevision) {\n            updateDataToReviewContext()\n            openChangeAsDiffView(change)\n            if (null !== position) {\n                reviewContext.putChangeData(change, DiffNotifier.ScrollPosition, position)\n                reviewContext.putChangeData(change, DiffNotifier.ScrollShowComments, true)\n                myDiffPublisher.hideAllCommentsRequested(reviewContext, change)\n                myDiffPublisher.scrollToPositionRequested(reviewContext, change, position, true)\n            }\n            return\n        }\n        return myEditorManager.open(providerData, absolutePath, line)\n    }\n\n    private fun displayGeneralComments(groupedComments: Map<String, List<Comment>>, focusToEditor: Boolean) {\n        myReworkGeneralCommentsView.render(groupedComments)\n        if (focusToEditor) {\n            myReworkGeneralCommentsView.focusMainEditor()\n        }\n    }\n\n    private fun buildCommentsMap() {\n        myCommentsMap.clear()\n        val grouped = CommentUtil.groupCommentsByNewPath(comments)\n        grouped.forEach { (relativePath, list) ->\n            val fullPath = RepositoryUtil.findAbsoluteCrossPlatformsPath(repository, relativePath)\n            myCommentsMap[fullPath] = list\n        }\n    }\n\n    private fun buildChangesMap() {\n        myChangesMap.clear()\n        for (change in changes) {\n            val filePaths = ChangesUtil.getPathsCaseSensitive(change)\n            for (filePath in filePaths) {\n                val path = filePath.path\n                val list = myChangesMap.get(path)\n                if (null === list) {\n                    myChangesMap[path] = mutableListOf(change)\n                } else {\n                    if (!list.contains(change)) {\n                        list.add(change)\n                    }\n                }\n            }\n        }\n    }\n\n    private fun openChangeAsDiffView(change: Change) {\n        val project = projectServiceProvider.project\n        val diffFile = myPreviewDiffVirtualFileMap[change]\n        if (null === diffFile) {\n            val provider = DiffPreviewProviderImpl(project, change, reviewContext, false)\n            val created = PreviewDiffVirtualFile(provider)\n            myPreviewDiffVirtualFileMap[change] = created\n            FileEditorManagerEx.getInstanceEx(project).openFile(created, true)\n        } else {\n            FileEditorManagerEx.getInstanceEx(project).openFile(diffFile, true)\n        }\n    }\n\n    private fun updateDataToReviewContext() {\n        val mr = myMergeRequest\n        if (null !== mr) {\n            reviewContext.diffReference = mr.diffReference\n        }\n        reviewContext.commits = commits\n        reviewContext.reviewingCommits = commits\n        reviewContext.changes = changes\n        reviewContext.reviewingChanges = changes\n        reviewContext.comments = comments\n    }\n\n    private fun fetchCommits() {\n        debug(\"${providerData.id}:$branchName: fetching commits\")\n        val task = GetCommitsTask(\n            projectServiceProvider = projectServiceProvider,\n            providerData = providerData,\n            mergeRequestInfo = mergeRequestInfo,\n            listener = myGetCommitsTaskListener\n        )\n        task.start()\n    }\n\n    private fun fetchMergeRequest() {\n        debug(\"${providerData.id}:$branchName: fetching mergeRequest\")\n        val task = FindMergeRequestTask(\n            projectServiceProvider = projectServiceProvider,\n            providerData = providerData,\n            mergeRequestInfo = mergeRequestInfo,\n            listener = myFindMergeRequestTaskListener\n        )\n        task.start()\n    }\n\n    private fun assertHasSameProvider(providerData: ProviderData, invoker: (() -> Unit)) {\n        if (this.providerData.id == providerData.id) {\n            invoker()\n        }\n    }\n\n    private fun buildCommentsAndSendCommentsUpdatedSignal() {\n        comments.clear()\n        if (onlyShowDraftComments) {\n            comments.addAll(myComments.filter { it.isDraft })\n        } else {\n            if (displayResolvedComments) {\n                comments.addAll(myComments)\n            } else {\n                comments.addAll(myComments.filter { !it.resolved })\n            }\n        }\n        buildCommentsMap()\n\n        myEditorManager.commentsUpdated(providerData)\n        ApplicationManager.getApplication().invokeLater {\n            projectServiceProvider.singleMRToolWindowNotifierTopic.showReworkComments(\n                this, comments, displayResolvedComments\n            )\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/FetchProjectMembersTask.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.task\n\nimport com.intellij.openapi.progress.ProgressIndicator\nimport com.intellij.openapi.progress.ProgressManager\nimport com.intellij.openapi.progress.Task\nimport com.intellij.openapi.progress.impl.BackgroundableProcessIndicator\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.UserInfo\nimport net.ntworld.mergeRequest.UserStatus\nimport net.ntworld.mergeRequest.query.GetProjectMembersQuery\nimport net.ntworld.mergeRequestIntegration.internal.UserInfoImpl\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport com.intellij.openapi.project.Project as IdeaProject\n\nclass FetchProjectMembersTask(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData,\n    private val addEmptyMember: Boolean,\n    private val listener: Listener\n) : Task.Backgroundable(projectServiceProvider.project, \"Fetching project members...\", true) {\n    fun start() {\n        ProgressManager.getInstance().runProcessWithProgressAsynchronously(\n            this,\n            Indicator(this)\n        )\n    }\n\n    override fun run(indicator: ProgressIndicator) {\n        try {\n            listener.taskStarted()\n            val query = GetProjectMembersQuery.make(providerData.id)\n            val result = projectServiceProvider.infrastructure.queryBus() process query\n            val data = result.members\n                .filter { it.status == UserStatus.ACTIVE }\n                .sortedBy { it.name }\n            if (addEmptyMember) {\n                val dataWithEmptyMember = mutableListOf<UserInfo>(UserInfoImpl.None)\n                listener.dataReceived(dataWithEmptyMember + data)\n            } else {\n                listener.dataReceived(data)\n            }\n            listener.taskEnded()\n        } catch (exception: Exception) {\n            listener.onError(exception)\n        }\n    }\n\n    private class Indicator(private val task: FetchProjectMembersTask) : BackgroundableProcessIndicator(task)\n\n    interface Listener {\n        fun onError(exception: Exception)\n\n        fun taskStarted()\n\n        fun dataReceived(collection: List<UserInfo>)\n\n        fun taskEnded()\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/FindApprovalTask.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.task\n\nimport com.intellij.openapi.progress.ProgressIndicator\nimport com.intellij.openapi.progress.ProgressManager\nimport com.intellij.openapi.progress.Task\nimport com.intellij.openapi.progress.impl.BackgroundableProcessIndicator\nimport net.ntworld.mergeRequest.Approval\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.query.FindApprovalQuery\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\n\nclass FindApprovalTask(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData,\n    private val mergeRequestInfo: MergeRequestInfo,\n    private val listener: Listener\n) : Task.Backgroundable(projectServiceProvider.project, \"Fetching approval data...\", true) {\n    fun start() {\n        ProgressManager.getInstance().runProcessWithProgressAsynchronously(\n            this,\n            Indicator(this)\n        )\n    }\n\n    private class Indicator(private val task: FindApprovalTask) : BackgroundableProcessIndicator(task)\n\n    override fun run(indicator: ProgressIndicator) {\n        try {\n            listener.taskStarted()\n            val result = projectServiceProvider.infrastructure.queryBus() process FindApprovalQuery.make(\n                providerId = providerData.id,\n                mergeRequestId = mergeRequestInfo.id\n            )\n            listener.dataReceived(mergeRequestInfo, result.approval)\n            listener.taskEnded()\n        } catch (exception: Exception) {\n            listener.onError(exception)\n        }\n    }\n\n    interface Listener {\n        fun onError(exception: Exception) {\n            throw exception\n        }\n\n        fun taskStarted() {}\n\n        fun dataReceived(mergeRequestInfo: MergeRequestInfo, approval: Approval)\n\n        fun taskEnded() {}\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/FindMergeRequestTask.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.task\n\nimport com.intellij.openapi.progress.ProgressIndicator\nimport com.intellij.openapi.progress.ProgressManager\nimport com.intellij.openapi.progress.Task\nimport com.intellij.openapi.progress.impl.BackgroundableProcessIndicator\nimport net.ntworld.mergeRequest.MergeRequest\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.query.FindMergeRequestQuery\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\n\nclass FindMergeRequestTask(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData,\n    private val mergeRequestInfo: MergeRequestInfo,\n    private val listener: Listener\n) : Task.Backgroundable(projectServiceProvider.project, \"Fetching merge request...\", true) {\n    fun start() {\n        ProgressManager.getInstance().runProcessWithProgressAsynchronously(\n            this,\n            Indicator(this)\n        )\n    }\n\n    private class Indicator(private val task: FindMergeRequestTask) : BackgroundableProcessIndicator(task)\n\n    override fun run(indicator: ProgressIndicator) {\n        try {\n            listener.taskStarted()\n            val result = projectServiceProvider.infrastructure.queryBus() process FindMergeRequestQuery.make(\n                providerId = providerData.id,\n                mergeRequestId = mergeRequestInfo.id\n            )\n            listener.dataReceived(result.mergeRequest)\n            projectServiceProvider.reviewContextManager.updateMergeRequest(providerData.id, result.mergeRequest)\n            listener.taskEnded()\n        } catch (exception: Exception) {\n            listener.onError(exception)\n        }\n    }\n\n    interface Listener {\n        fun onError(exception: Exception) {\n            throw exception\n        }\n\n        fun taskStarted() {}\n\n        fun dataReceived(mergeRequest: MergeRequest)\n\n        fun taskEnded() {}\n    }\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/GetAvailableUpdatesTask.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.task\n\nimport com.intellij.openapi.progress.ProgressIndicator\nimport com.intellij.openapi.progress.ProgressManager\nimport com.intellij.openapi.progress.Task\nimport com.intellij.openapi.progress.impl.BackgroundableProcessIndicator\nimport com.intellij.openapi.project.Project\nimport net.ntworld.mergeRequestIntegration.update.UpdateManager\n\nclass GetAvailableUpdatesTask(\n    ideaProject: Project,\n    private val listener: Listener\n) : Task.Backgroundable(ideaProject, \"Check available updates...\", true) {\n    fun start() {\n        ProgressManager.getInstance().runProcessWithProgressAsynchronously(\n            this,\n            Indicator(this)\n        )\n    }\n\n    private class Indicator(private val task: GetAvailableUpdatesTask) : BackgroundableProcessIndicator(task)\n\n    override fun run(indicator: ProgressIndicator) {\n        try {\n            listener.dataReceived(UpdateManager.getAvailableUpdates())\n        } catch (exception: Exception) {\n            listener.onError(exception)\n        }\n    }\n\n    interface Listener {\n        fun onError(exception: Exception) {\n            throw exception\n        }\n\n        fun dataReceived(updates: List<String>)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/GetCommentsTask.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.task\n\nimport com.intellij.openapi.progress.ProgressIndicator\nimport com.intellij.openapi.progress.ProgressManager\nimport com.intellij.openapi.progress.Task\nimport com.intellij.openapi.progress.impl.BackgroundableProcessIndicator\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.query.GetCommentsQuery\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\n\nclass GetCommentsTask(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData,\n    private val mergeRequestInfo: MergeRequestInfo,\n    private val listener: Listener\n): Task.Backgroundable(projectServiceProvider.project, \"Fetching comment data...\", true)  {\n    fun start() {\n        ProgressManager.getInstance().runProcessWithProgressAsynchronously(\n            this,\n            Indicator(this)\n        )\n    }\n\n    private class Indicator(private val task: GetCommentsTask) : BackgroundableProcessIndicator(task)\n\n    override fun run(indicator: ProgressIndicator) {\n        try {\n            listener.taskStarted()\n            val result = projectServiceProvider.infrastructure.queryBus() process GetCommentsQuery.make(\n                providerId = providerData.id,\n                mergeRequestId = mergeRequestInfo.id\n            )\n            listener.dataReceived(providerData, mergeRequestInfo, result.comments)\n            projectServiceProvider.reviewContextManager.updateComments(\n                providerData.id, mergeRequestInfo.id, result.comments\n            )\n            listener.taskEnded()\n        } catch (exception: Exception) {\n            listener.onError(exception)\n        }\n    }\n\n    interface Listener {\n        fun onError(exception: Exception) {\n            throw exception\n        }\n\n        fun taskStarted() {}\n\n        fun dataReceived(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comments: List<Comment>)\n\n        fun taskEnded() {}\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/GetCommitsTask.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.task\n\nimport com.intellij.openapi.progress.ProgressIndicator\nimport com.intellij.openapi.progress.ProgressManager\nimport com.intellij.openapi.progress.Task\nimport com.intellij.openapi.progress.impl.BackgroundableProcessIndicator\nimport net.ntworld.mergeRequest.Commit\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.query.GetCommitsQuery\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\n\nclass GetCommitsTask(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData,\n    private val mergeRequestInfo: MergeRequestInfo,\n    private val listener: Listener\n) : Task.Backgroundable(projectServiceProvider.project, \"Fetching commit data...\", true) {\n    fun start() {\n        ProgressManager.getInstance().runProcessWithProgressAsynchronously(\n            this,\n            Indicator(this)\n        )\n    }\n\n    private class Indicator(private val task: GetCommitsTask) : BackgroundableProcessIndicator(task)\n\n    override fun run(indicator: ProgressIndicator) {\n        try {\n            listener.taskStarted()\n            val result = projectServiceProvider.infrastructure.queryBus() process GetCommitsQuery.make(\n                providerId = providerData.id,\n                mergeRequestId = mergeRequestInfo.id\n            )\n            listener.dataReceived(mergeRequestInfo, result.commits)\n            projectServiceProvider.reviewContextManager.updateCommits(\n                providerData.id, mergeRequestInfo.id, result.commits\n            )\n            listener.taskEnded()\n        } catch (exception: Exception) {\n            listener.onError(exception)\n        }\n    }\n\n    interface Listener {\n        fun onError(exception: Exception) {\n            throw exception\n        }\n\n        fun taskStarted() {}\n\n        fun dataReceived(mergeRequestInfo: MergeRequestInfo, commits: List<Commit>)\n\n        fun taskEnded() {}\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/GetPipelinesTask.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.task\n\nimport com.intellij.openapi.progress.ProgressIndicator\nimport com.intellij.openapi.progress.ProgressManager\nimport com.intellij.openapi.progress.Task\nimport com.intellij.openapi.progress.impl.BackgroundableProcessIndicator\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.Pipeline\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.query.GetPipelinesQuery\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\n\nclass GetPipelinesTask(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData,\n    private val mergeRequestInfo: MergeRequestInfo,\n    private val listener: Listener\n) : Task.Backgroundable(projectServiceProvider.project, \"Fetching pipeline data...\", true) {\n    fun start() {\n        ProgressManager.getInstance().runProcessWithProgressAsynchronously(\n            this,\n            Indicator(this)\n        )\n    }\n\n    private class Indicator(private val task: GetPipelinesTask) : BackgroundableProcessIndicator(task)\n\n    override fun run(indicator: ProgressIndicator) {\n        try {\n            listener.taskStarted()\n            val result = projectServiceProvider.infrastructure.queryBus() process GetPipelinesQuery.make(\n                providerId = providerData.id,\n                mergeRequestId = mergeRequestInfo.id\n            )\n            listener.dataReceived(mergeRequestInfo, result.pipelines)\n            listener.taskEnded()\n        } catch (exception: Exception) {\n            listener.onError(exception)\n        }\n    }\n\n    interface Listener {\n        fun onError(exception: Exception) {\n            throw exception\n        }\n\n        fun taskStarted() {}\n\n        fun dataReceived(mergeRequestInfo: MergeRequestInfo, pipelines: List<Pipeline>)\n\n        fun taskEnded() {}\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/RegisterProviderTask.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.task\n\nimport com.intellij.notification.NotificationType\nimport com.intellij.openapi.progress.ProgressIndicator\nimport com.intellij.openapi.progress.ProgressManager\nimport com.intellij.openapi.progress.Task\nimport com.intellij.openapi.progress.impl.BackgroundableProcessIndicator\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.launch\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.ProviderStatus\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProviderSettings\n\nclass RegisterProviderTask(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val id: String,\n    private val name: String,\n    private val settings: ProviderSettings,\n    private val listener: Listener\n) : Task.Backgroundable(projectServiceProvider.project, \"Fetching provider information...\", true) {\n\n    fun start() {\n        ProgressManager.getInstance().runProcessWithProgressAsynchronously(\n            this,\n            Indicator(this)\n        )\n    }\n\n    override fun run(indicator: ProgressIndicator) {\n        val job = GlobalScope.launch {\n            val pair = projectServiceProvider.providerStorage.register(\n                infrastructure = projectServiceProvider.infrastructure,\n                id = id,\n                key = settings.id,\n                name = name,\n                info = settings.info,\n                credentials = settings.credentials,\n                repository = settings.repository\n            )\n            if (!indicator.isCanceled) {\n                listener.providerRegistered(pair.first)\n                if (pair.first.status == ProviderStatus.ERROR) {\n                    projectServiceProvider.notify(pair.first.errorMessage ?: \"\", NotificationType.ERROR)\n                }\n                if (null !== pair.second) {\n                    throw pair.second!!\n                }\n            }\n        }\n        job.start()\n        while (job.isActive && !indicator.isCanceled) {\n            Thread.sleep(100);\n        }\n        if (job.isActive) {\n            job.cancel()\n        }\n        indicator.checkCanceled()\n    }\n\n    private class Indicator(private val task: RegisterProviderTask) : BackgroundableProcessIndicator(task)\n\n    interface Listener {\n        fun providerRegistered(providerData: ProviderData)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/RepositoryFetchAllRemotesTask.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.task\n\nimport com.intellij.openapi.components.ServiceManager\nimport com.intellij.openapi.progress.ProgressIndicator\nimport com.intellij.openapi.progress.Task\nimport git4idea.fetch.GitFetchSupport\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil\n\nclass RepositoryFetchAllRemotesTask(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData\n) : Task.Backgroundable(projectServiceProvider.project, \"Fetching...\", true) {\n    override fun run(indicator: ProgressIndicator) {\n        val repository = RepositoryUtil.findRepository(projectServiceProvider, providerData)\n        if (null !== repository) {\n            val service = ServiceManager.getService(projectServiceProvider.project, GitFetchSupport::class.java)\n            service.fetchAllRemotes(listOf(repository))\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/task/SearchMergeRequestTask.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.task\n\nimport com.intellij.openapi.progress.ProgressIndicator\nimport com.intellij.openapi.progress.ProgressManager\nimport com.intellij.openapi.progress.Task\nimport com.intellij.openapi.progress.impl.BackgroundableProcessIndicator\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.api.MergeRequestOrdering\nimport net.ntworld.mergeRequest.query.GetMergeRequestFilter\nimport net.ntworld.mergeRequest.query.GetMergeRequestsQuery\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegrationIde.SEARCH_MERGE_REQUEST_ITEMS_PER_PAGE\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\n\nclass SearchMergeRequestTask(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData,\n    private val filtering: GetMergeRequestFilter,\n    private val ordering: MergeRequestOrdering,\n    private val listener: Listener\n) : Task.Backgroundable(projectServiceProvider.project, \"Fetching merge requests...\", true) {\n    var page: Int = 1\n    fun start() = start(1)\n\n    fun start(page: Int) {\n        this.page = page\n        ProgressManager.getInstance().runProcessWithProgressAsynchronously(\n            this,\n            Indicator(this)\n        )\n    }\n\n    override fun run(indicator: ProgressIndicator) {\n        try {\n            val currentPage = page\n            listener.taskStarted()\n            val query = GetMergeRequestsQuery.make(\n                providerId = providerData.id,\n                filterBy = filtering,\n                orderBy = ordering,\n                page = currentPage,\n                itemsPerPage = SEARCH_MERGE_REQUEST_ITEMS_PER_PAGE\n            )\n            val result = projectServiceProvider.infrastructure.queryBus() process query\n            listener.dataReceived(result.mergeRequests, currentPage, result.totalPages, result.totalItems)\n            listener.taskEnded()\n        } catch (exception: Exception) {\n            listener.onError(exception)\n        }\n    }\n\n    private class Indicator(private val task: SearchMergeRequestTask) : BackgroundableProcessIndicator(task)\n\n    interface Listener {\n        fun onError(exception: Exception) {\n            throw exception\n        }\n\n        fun taskStarted() {}\n\n        fun dataReceived(list: List<MergeRequestInfo>, page: Int, totalPages: Int, totalItems: Int)\n\n        fun taskEnded() {}\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/toolWindow/CommentsToolWindowTab.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.toolWindow\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\n\ninterface CommentsToolWindowTab: ReworkToolWindowTab {\n    fun setMergeRequestInfo(mergeRequestInfo: MergeRequestInfo)\n\n    fun setComments(comments: List<Comment>)\n\n    fun setDisplayResolvedComments(value: Boolean)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/toolWindow/FilesToolWindowTab.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.toolWindow\n\nimport com.intellij.openapi.vcs.changes.Change\nimport net.ntworld.mergeRequest.ProviderData\n\ninterface FilesToolWindowTab: ReworkToolWindowTab {\n    val isCodeReviewChanges: Boolean\n\n    fun setChanges(providerData: ProviderData, changes: List<Change>)\n\n    fun hide()\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/toolWindow/ReworkToolWindowTab.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.toolWindow\n\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.Component\n\ninterface ReworkToolWindowTab: Component {\n    val providerData: ProviderData\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/toolWindow/SingleMRToolWindowFactoryBase.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.toolWindow\n\nimport com.intellij.openapi.project.Project\nimport com.intellij.openapi.wm.ToolWindow\nimport com.intellij.openapi.wm.ToolWindowFactory\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider\n\nabstract class SingleMRToolWindowFactoryBase(\n    private val applicationServiceProvider: ApplicationServiceProvider\n) : ToolWindowFactory {\n    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {\n        val projectServiceProvider = applicationServiceProvider.findProjectServiceProvider(project)\n        if (!projectServiceProvider.isInitialized()) {\n            projectServiceProvider.initialize()\n        }\n        SingleMRToolWindowManager(projectServiceProvider, toolWindow)\n    }\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/toolWindow/SingleMRToolWindowManager.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.toolWindow\n\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.openapi.vcs.changes.Change\nimport com.intellij.openapi.wm.ToolWindow\nimport com.intellij.ui.content.Content\nimport com.intellij.ui.content.ContentFactory\nimport com.intellij.util.ContentUtilEx\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.SINGLE_MR_CHANGES_WHEN_DOING_CODE_REVIEW_NAME\nimport net.ntworld.mergeRequestIntegrationIde.SINGLE_MR_REWORK_CHANGES_PREFIX\nimport net.ntworld.mergeRequestIntegrationIde.SINGLE_MR_REWORK_COMMENTS_PREFIX\nimport net.ntworld.mergeRequestIntegrationIde.debug\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.SingleMRToolWindowNotifier\nimport net.ntworld.mergeRequestIntegrationIde.rework.ReworkWatcher\n\nclass SingleMRToolWindowManager(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val toolWindow: ToolWindow\n) : SingleMRToolWindowNotifier {\n    private var myReviewChangesTab: FilesToolWindowTab? = null\n    private val myReworkChangesTabs = mutableSetOf<FilesToolWindowTab>()\n    private val myReworkCommentsTabs = mutableSetOf<CommentsToolWindowTab>()\n    private val mySupportedProviders = mutableMapOf<String, String>()\n\n    init {\n        projectServiceProvider.messageBus.connect().subscribe(SingleMRToolWindowNotifier.TOPIC, this)\n        val activeReworkWatchers = projectServiceProvider.reworkManager.getActiveReworkWatchers()\n        activeReworkWatchers.forEach {\n            registerReworkWatcher(it)\n            if (it.isChangesBuilt()) {\n                showReworkChanges(it, it.changes)\n            }\n            if (it.isFetchedComments()) {\n                showReworkComments(it, it.comments, it.displayResolvedComments)\n            }\n        }\n    }\n\n    override fun registerReworkWatcher(reworkWatcher: ReworkWatcher) {\n        val key = reworkWatcher.key()\n        debug(\"$key: add watcher to SupportedProviders\")\n        mySupportedProviders[reworkWatcher.providerData.id] = reworkWatcher.branchName\n    }\n\n    override fun removeReworkWatcher(reworkWatcher: ReworkWatcher) {\n        val key = reworkWatcher.key()\n        val currentBranch = mySupportedProviders[reworkWatcher.providerData.id]\n        if (currentBranch == reworkWatcher.branchName) {\n            debug(\"$key: remove ReworkWatcher out SupportedWatchers\")\n            mySupportedProviders.remove(reworkWatcher.providerData.id)\n            ApplicationManager.getApplication().invokeLater {\n                debug(\"$key: remove tab out of tool window\")\n\n                val tab = myReworkChangesTabs.firstOrNull { it.providerData.id == reworkWatcher.providerData.id }\n                if (null !== tab) {\n                    removeTabOutOfToolWindow(tab, myReworkChangesTabs, SINGLE_MR_REWORK_CHANGES_PREFIX)\n                }\n\n                val commentsTab = myReworkCommentsTabs.firstOrNull { it.providerData.id == reworkWatcher.providerData.id }\n                if (null !== commentsTab) {\n                    removeTabOutOfToolWindow(commentsTab, myReworkCommentsTabs, SINGLE_MR_REWORK_COMMENTS_PREFIX)\n                }\n\n                if (myReworkChangesTabs.isEmpty() && myReworkCommentsTabs.isEmpty()) {\n                    projectServiceProvider.hideSingleMRToolWindow {\n                        debug(\"$key: hide tool window because there is nothing left\")\n                    }\n                }\n            }\n        } else {\n            debug(\"$key: current provider have another branch, do nothing\")\n        }\n    }\n\n    override fun hideChangesAfterDoingCodeReview() {\n        val reviewTab = myReviewChangesTab\n        if (null !== reviewTab) {\n            reviewTab.hide()\n        }\n    }\n\n    override fun clearReworkTabs() {\n        removeAllReworkToolWindowTabs()\n    }\n\n    override fun showChangesWhenDoingCodeReview(providerData: ProviderData, changes: List<Change>) {\n        removeAllReworkToolWindowTabs()\n\n        val currentTab = myReviewChangesTab\n        if (null !== currentTab) {\n            currentTab.setChanges(providerData, changes)\n            return\n        }\n\n        val newTab = makeFilesToolWindowTab(providerData, true)\n        newTab.setChanges(providerData, changes)\n        toolWindow.contentManager.addContent(\n            ContentFactory.SERVICE.getInstance().createContent(\n                newTab.component,\n                SINGLE_MR_CHANGES_WHEN_DOING_CODE_REVIEW_NAME,\n                true\n            )\n        )\n        myReviewChangesTab = newTab\n    }\n\n    override fun showReworkChanges(\n        reworkWatcher: ReworkWatcher, changes: List<Change>\n    ) = fillingDataForReworkTab(reworkWatcher) {\n        val currentTab = myReworkChangesTabs.firstOrNull { it.providerData.id == reworkWatcher.providerData.id }\n        if (null !== currentTab) {\n            debug(\"${reworkWatcher.key()}: update current Files tab's data\")\n            currentTab.setChanges(reworkWatcher.providerData, changes)\n            return@fillingDataForReworkTab\n        }\n\n        val newTab = makeFilesToolWindowTab(reworkWatcher.providerData, false)\n        newTab.setChanges(reworkWatcher.providerData, changes)\n        debug(\"${reworkWatcher.key()}: add new Files tab of  to tool window\")\n        addTabToToolWindow(newTab, myReworkChangesTabs, SINGLE_MR_REWORK_CHANGES_PREFIX)\n    }\n\n    override fun showReworkComments(\n        reworkWatcher: ReworkWatcher,\n        comments: List<Comment>,\n        displayResolvedComments: Boolean\n    ) = fillingDataForReworkTab(reworkWatcher) {\n        val currentTab = myReworkCommentsTabs.firstOrNull { it.providerData.id == reworkWatcher.providerData.id }\n        if (null !== currentTab) {\n            debug(\"${reworkWatcher.key()}: update current Comments tab's data\")\n            currentTab.setMergeRequestInfo(reworkWatcher.mergeRequestInfo)\n            currentTab.setDisplayResolvedComments(displayResolvedComments)\n            currentTab.setComments(comments)\n            return@fillingDataForReworkTab\n        }\n\n        val newTab = makeCommentsToolWindowTab(reworkWatcher.providerData, reworkWatcher.mergeRequestInfo, comments)\n        debug(\"${reworkWatcher.key()}: add new Comments tab to tool window\")\n        addTabToToolWindow(newTab, myReworkCommentsTabs, SINGLE_MR_REWORK_COMMENTS_PREFIX)\n    }\n\n    private fun removeToolWindowTabsForCodeReview() {\n        val content = toolWindow.contentManager.findContent(SINGLE_MR_CHANGES_WHEN_DOING_CODE_REVIEW_NAME)\n        if (null !== content) {\n            toolWindow.contentManager.removeContent(content, true)\n            myReviewChangesTab = null\n        }\n    }\n\n    private fun fillingDataForReworkTab(reworkWatcher: ReworkWatcher, invoker: (() -> Unit)) {\n        val key = reworkWatcher.key()\n        if (mySupportedProviders.contains(reworkWatcher.providerData.id)) {\n            removeToolWindowTabsForCodeReview()\n            debug(\"$key: ReworkWatcher is supported\")\n            invoker()\n        } else {\n            debug(\"$key: ReworkWatcher is NOT supported\")\n        }\n    }\n\n    private fun <T> addTabToToolWindow(\n        tab: T,\n        bucket: MutableSet<T>,\n        prefix: String\n    ): Boolean where T : ReworkToolWindowTab {\n        // There are 3 scenarios when removing a tab:\n        //  A) 0 -> 1: we should add content\n        //  B) 1 -> 2: we should remove content, add old tab to tabbed content\n        //  C) 2 -> n: just add tabbed content\n        if (bucket.isEmpty()) {\n            toolWindow.contentManager.addContent(\n                ContentFactory.SERVICE.getInstance().createContent(tab.component, prefix, true)\n            )\n            return bucket.add(tab)\n        }\n\n        if (bucket.size == 1) {\n            val oldTab = bucket.first()\n            val oldContent = toolWindow.contentManager.findContent(prefix)\n            toolWindow.contentManager.removeContent(oldContent, false)\n\n            ContentUtilEx.addTabbedContent(\n                toolWindow.contentManager,\n                oldContent.component,\n                prefix,\n                oldTab.providerData.name,\n                true,\n                null\n            )\n        }\n\n        ContentUtilEx.addTabbedContent(\n            toolWindow.contentManager,\n            tab.component,\n            prefix,\n            tab.providerData.name,\n            true,\n            null\n        )\n        return bucket.add(tab)\n    }\n\n    private fun <T> removeTabOutOfToolWindow(\n        tab: T,\n        bucket: MutableSet<T>,\n        prefix: String\n    ) where T : ReworkToolWindowTab {\n        // There are 3 scenarios when removing a tab:\n        //  A) 1 -> 0: we should remove content\n        //  C) 2 -> 1: we should remove tabbed content, create new content with remaining tab\n        //  B) n -> 2: just remove tab of tabbed content\n        if (!bucket.contains(tab)) {\n            return\n        }\n\n        if (bucket.size == 1) {\n            val content = toolWindow.contentManager.findContent(prefix)\n            toolWindow.contentManager.removeContent(content, true)\n            return bucket.clear()\n        }\n\n        val tabbedContent = ContentUtilEx.findTabbedContent(toolWindow.contentManager, prefix)\n        if (null === tabbedContent) {\n            return\n        }\n\n        if (bucket.size == 2) {\n            toolWindow.contentManager.removeContent(tabbedContent, false)\n            bucket.remove(tab)\n\n            val remainingTab = bucket.first()\n            toolWindow.contentManager.addContent(\n                ContentFactory.SERVICE.getInstance().createContent(remainingTab.component, prefix, true)\n            )\n            return\n        }\n\n        val tabInTabbedContent = tabbedContent.tabs.firstOrNull { it.second === tab.component }\n        if (null !== tabInTabbedContent) {\n            bucket.remove(tab)\n            tabbedContent.tabs.remove(tabInTabbedContent)\n        }\n    }\n\n    private fun removeAllReworkToolWindowTabs() {\n        // There are 2 scenarios when removing all tab:\n        //  A) bucket > 1: we should remove tabbed content\n        //  B) bucket = 1: we should remove content\n        val changes = if (myReworkChangesTabs.size > 1) {\n            ContentUtilEx.findTabbedContent(toolWindow.contentManager, SINGLE_MR_REWORK_CHANGES_PREFIX)\n        } else {\n            toolWindow.contentManager.findContent(SINGLE_MR_REWORK_CHANGES_PREFIX)\n        }\n        if (null !== changes) {\n            toolWindow.contentManager.removeContent(changes, true)\n        }\n        myReworkChangesTabs.clear()\n\n        val comments = if (myReworkCommentsTabs.size > 1) {\n            ContentUtilEx.findTabbedContent(toolWindow.contentManager, SINGLE_MR_REWORK_COMMENTS_PREFIX)\n        } else {\n            toolWindow.contentManager.findContent(SINGLE_MR_REWORK_COMMENTS_PREFIX)\n        }\n        if (null !== comments) {\n            toolWindow.contentManager.removeContent(comments, true)\n        }\n        myReworkCommentsTabs.clear()\n    }\n\n    private fun makeFilesToolWindowTab(providerData: ProviderData, isCodeReviewChange: Boolean): FilesToolWindowTab {\n        return projectServiceProvider.componentFactory.toolWindowTabs.makeFilesToolWindowTab(\n            providerData, isCodeReviewChange\n        )\n    }\n\n    private fun makeCommentsToolWindowTab(\n        providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, comments: List<Comment>\n    ): CommentsToolWindowTab {\n        return projectServiceProvider.componentFactory.toolWindowTabs.makeCommentsToolWindowTab(\n            providerData, mergeRequestInfo, comments\n        )\n    }\n\n    private data class ComponentContainer(\n        val content: Content,\n        val isTabbed: Boolean\n    )\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/toolWindow/internal/CommentsToolWindowTabImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.toolWindow.internal\n\nimport net.ntworld.mergeRequest.Comment\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ReworkWatcherNotifier\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreeFactory\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreePresenter\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreeView\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node.Node\nimport net.ntworld.mergeRequestIntegrationIde.toolWindow.CommentsToolWindowTab\nimport javax.swing.JComponent\n\nclass CommentsToolWindowTabImpl(\n    private val projectServiceProvider: ProjectServiceProvider,\n    override val providerData: ProviderData,\n    mergeRequestInfo: MergeRequestInfo,\n    comments: List<Comment>\n): CommentsToolWindowTab {\n    private val myPublisher = projectServiceProvider.messageBus.syncPublisher(\n        ReworkWatcherNotifier.TOPIC\n    )\n    private val myTreePresenter: CommentTreePresenter = CommentTreeFactory.makePresenter(\n        CommentTreeFactory.makeModel(providerData),\n        CommentTreeFactory.makeView(projectServiceProvider, providerData, showOpenDiffViewDescription = true)\n    )\n    private val myTreePresenterListener = object: CommentTreePresenter.Listener {\n        override fun onTreeNodeSelected(node: Node, type: CommentTreeView.TreeSelectType) {\n            myPublisher.commentTreeNodeSelected(providerData, node, type)\n        }\n\n        override fun onShowResolvedCommentsToggled(displayResolvedComments: Boolean) {\n            myPublisher.changeDisplayResolvedComments(providerData, displayResolvedComments)\n        }\n\n        override fun onShowDraftCommentsOnlyToggled(onlyShowDraftComments: Boolean) {\n            myPublisher.changeOnlyShowDraftComments(providerData, onlyShowDraftComments)\n        }\n\n        override fun onCreateGeneralCommentClicked() {\n            myPublisher.openCreateGeneralCommentForm(providerData)\n        }\n\n        override fun onRefreshButtonClicked() {\n            myPublisher.requestFetchComment(providerData)\n        }\n    }\n\n    init {\n        myTreePresenter.setToolbarMode(CommentTreeView.ToolbarMode.MINI)\n        myTreePresenter.model.displayResolvedComments = false\n        myTreePresenter.model.comments = comments\n        myTreePresenter.model.mergeRequestInfo = mergeRequestInfo\n        myTreePresenter.addListener(myTreePresenterListener)\n    }\n\n    override val component: JComponent = myTreePresenter.component\n\n    override fun setMergeRequestInfo(mergeRequestInfo: MergeRequestInfo) {\n        myTreePresenter.model.mergeRequestInfo = mergeRequestInfo\n    }\n\n    override fun setComments(comments: List<Comment>) {\n        myTreePresenter.model.comments = comments\n    }\n\n    override fun setDisplayResolvedComments(value: Boolean) {\n        myTreePresenter.model.displayResolvedComments = value\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/toolWindow/internal/FilesToolWindowTabImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.toolWindow.internal\n\nimport com.intellij.openapi.actionSystem.ActionManager\nimport com.intellij.openapi.actionSystem.DefaultActionGroup\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.openapi.project.Project\nimport com.intellij.openapi.ui.SimpleToolWindowPanel\nimport com.intellij.openapi.vcs.changes.Change\nimport com.intellij.openapi.vcs.changes.ui.ChangesTreeImpl\nimport com.intellij.openapi.vcs.changes.ui.TreeModelBuilder\nimport com.intellij.ui.JBColor\nimport com.intellij.ui.ScrollPaneFactory\nimport net.miginfocom.swing.MigLayout\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.CommentTreeView\nimport net.ntworld.mergeRequestIntegrationIde.toolWindow.FilesToolWindowTab\nimport net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit.CommitChanges\nimport net.ntworld.mergeRequestIntegrationIde.ui.util.CustomSimpleToolWindowPanel\nimport net.ntworld.mergeRequestIntegrationIde.ui.util.ToolbarUtil\nimport java.awt.GridBagLayout\nimport java.awt.event.KeyAdapter\nimport java.awt.event.KeyEvent\nimport java.awt.event.MouseAdapter\nimport java.awt.event.MouseEvent\nimport javax.swing.JComponent\nimport javax.swing.JLabel\nimport javax.swing.JPanel\nimport javax.swing.SwingConstants\nimport javax.swing.event.TreeSelectionListener\nimport javax.swing.tree.DefaultMutableTreeNode\nimport javax.swing.tree.DefaultTreeModel\nimport javax.swing.tree.TreePath\n\nclass FilesToolWindowTabImpl(\n    private val projectServiceProvider: ProjectServiceProvider,\n    override val providerData: ProviderData,\n    override val isCodeReviewChanges: Boolean\n) : FilesToolWindowTab {\n    private var myProviderData: ProviderData? = null\n    private val myComponentEmpty = JPanel()\n    private val myLabelEmpty = JLabel(\"\", SwingConstants.CENTER)\n    private val myComponent = CustomSimpleToolWindowPanel(vertical = true, borderless = true)\n    private val myTree = MyTree(projectServiceProvider.project)\n    private val myTreeWrapper = ScrollPaneFactory.createScrollPane(myTree, true)\n    private val myToolbar by lazy {\n        val panel = JPanel(MigLayout(\"ins 0, fill\", \"[left]0[left, fill]push[right]\", \"center\"))\n\n        val leftActionGroup = DefaultActionGroup()\n        val leftToolbar = ActionManager.getInstance().createActionToolbar(\n            \"${CommitChanges::class.java.canonicalName}/toolbar-left\",\n            leftActionGroup,\n            true\n        )\n\n        panel.add(JPanel())\n        panel.add(leftToolbar.component)\n        panel.add(\n            ToolbarUtil.createExpandAndCollapseToolbar(\n                \"${this::class.java.canonicalName}/toolbar-right\",\n                myTree\n            )\n        )\n        panel\n    }\n    private val myTreeMouseListener = object : MouseAdapter() {\n        override fun mousePressed(e: MouseEvent?) {\n            if (null === e) {\n                return\n            }\n\n            if (e.clickCount == 2) {\n                handleTreeItemSelected(myTree.selectionPath)\n            }\n        }\n    }\n    private val myKeyListener = object: KeyAdapter() {\n        override fun keyPressed(e: KeyEvent?) {\n            if (null === e) {\n                return\n            }\n            if (e.keyCode == KeyEvent.VK_ENTER) {\n                handleTreeItemSelected(myTree.selectionPath)\n            }\n        }\n    }\n\n    init {\n        myLabelEmpty.text = \"<html>Files' changes will be displayed when you do Code Review<br/>or<br/>open a branch which has an opened Merge Request</html>\"\n        myComponentEmpty.background = JBColor.background()\n        myComponentEmpty.layout = GridBagLayout()\n        myComponentEmpty.add(myLabelEmpty)\n        myComponent.toolbar = myToolbar\n        myTree.addMouseListener(myTreeMouseListener)\n        myTree.addKeyListener(myKeyListener)\n\n        val reviewContext = projectServiceProvider.reviewContextManager.findDoingCodeReviewContext()\n        if (null !== reviewContext) {\n            setChanges(reviewContext.providerData, reviewContext.reviewingChanges)\n        } else {\n            hide()\n        }\n    }\n\n    override val component: JComponent = myComponent\n\n    private fun handleTreeItemSelected(path: TreePath?) {\n        if (null === path) {\n            return\n        }\n\n        val node = path.lastPathComponent as? DefaultMutableTreeNode ?: return\n        val change = node.userObject as? Change ?: return\n        val reviewContext = projectServiceProvider.reviewContextManager.findDoingCodeReviewContext()\n        if (null !== reviewContext) {\n            reviewContext.openChange(change, focus = true, displayMergeRequestId = false)\n            return\n        }\n\n        val providerData = myProviderData\n        if (null !== providerData) {\n            val reworkWatcher = projectServiceProvider.reworkManager.findReworkWatcherByChange(\n                providerData,\n                change\n            )\n            if (null !== reworkWatcher) {\n                reworkWatcher.openChange(change)\n            }\n        }\n    }\n\n    override fun setChanges(providerData: ProviderData, changes: List<Change>) {\n        myProviderData = providerData\n        ApplicationManager.getApplication().invokeLater {\n            myTree.setChangesToDisplay(changes)\n        }\n        myToolbar.isVisible = true\n        myComponent.setContent(myTreeWrapper)\n    }\n\n    override fun hide() {\n        myTree.setChangesToDisplay(listOf())\n        myToolbar.isVisible = false\n        myComponent.setContent(myComponentEmpty)\n    }\n\n    private class MyTree(ideaProject: Project) : ChangesTreeImpl<Change>(\n        ideaProject, false, false, Change::class.java\n    ) {\n        override fun buildTreeModel(changes: MutableList<out Change>): DefaultTreeModel {\n            return TreeModelBuilder.buildFromChanges(myProject, grouping, changes, null)\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/Component.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui\n\nimport javax.swing.JComponent\n\ninterface Component {\n    fun createComponent(): JComponent\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/MainToolWindowFactoryBase.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui\n\nimport com.intellij.openapi.project.Project\nimport com.intellij.openapi.wm.ToolWindow\nimport com.intellij.openapi.wm.ToolWindowFactory\nimport com.intellij.ui.content.ContentFactory\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.ui.toolWindowTab.HomeToolWindowTab\n\nopen class MainToolWindowFactoryBase(\n    private val applicationServiceProvider: ApplicationServiceProvider\n) : ToolWindowFactory {\n    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {\n        val home = ContentFactory.SERVICE.getInstance().createContent(\n            HomeToolWindowTab(\n                applicationServiceProvider.findProjectServiceProvider(project), toolWindow\n            ).createComponent(),\n            \"Home\",\n            true\n        )\n        home.isCloseable = false\n        toolWindow.contentManager.addContent(home)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/README.md",
    "content": "\nAll classes in this folder is LEGACY, will be moved to outside and distributed based on domain."
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/AbstractConnectionsConfigurable.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.configuration\n\nimport com.intellij.icons.AllIcons\nimport com.intellij.openapi.Disposable\nimport com.intellij.openapi.actionSystem.AnAction\nimport com.intellij.openapi.actionSystem.AnActionEvent\nimport com.intellij.openapi.actionSystem.DefaultActionGroup\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.openapi.diagnostic.Logger\nimport com.intellij.openapi.options.SearchableConfigurable\nimport com.intellij.ui.tabs.TabInfo\nimport git4idea.repo.GitRepository\nimport net.ntworld.mergeRequest.Project\nimport com.intellij.openapi.project.Project as IdeaProject\nimport net.ntworld.mergeRequest.ProviderInfo\nimport net.ntworld.mergeRequest.api.ApiConnection\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegrationIde.configuration.vos.GitRemotePathInfo\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ApiCredentialsImpl\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProviderSettings\nimport net.ntworld.mergeRequestIntegrationIde.ui.util.Tabs\nimport net.ntworld.mergeRequestIntegrationIde.ui.util.TabsUI\nimport javax.swing.JComponent\n\nabstract class AbstractConnectionsConfigurable(\n    private val projectServiceProvider: ProjectServiceProvider\n) : SearchableConfigurable, Disposable {\n    private val logger = Logger.getInstance(AbstractConnectionsConfigurable::class.java)\n    private val myData = mutableMapOf<String, MyProviderSettings>()\n    private val myTabInfos = mutableMapOf<String, TabInfo>()\n    private val myInitializedData = mutableMapOf<String, MyProviderSettings>()\n    private var myIsInitialized = false\n    private val myTabs: TabsUI by lazy {\n        val tabs = Tabs(projectServiceProvider.project, projectServiceProvider.project)\n\n        tabs.setCommonCenterActionGroupFactory {\n            val actionGroup = DefaultActionGroup()\n            actionGroup.add(myAddTabAction)\n            actionGroup\n        }\n\n        tabs\n    }\n\n    private class MyAddTabAction(private val self: AbstractConnectionsConfigurable) : AnAction(\n        \"New Connection\", \"Add new connection\", AllIcons.Actions.OpenNewTab\n    ) {\n        override fun actionPerformed(e: AnActionEvent) {\n            perform()\n        }\n\n        fun perform() {\n            var count = self.myTabs.getTabs().tabCount + 1\n            var name = \"Connection $count\"\n            while (self.myData.containsKey(self.findIdFromName(name))) {\n                count++\n                name = \"Connection $count\"\n            }\n            self.addNewConnection(name)\n        }\n    }\n    private val myAddTabAction = MyAddTabAction(this)\n\n    private val myConnectionListener = object : ConnectionUI.Listener {\n        override fun test(connectionUI: ConnectionUI, name: String, connection: ApiConnection, shared: Boolean) {\n            logger.debug(\"Start testing connection $name\")\n            try {\n                assertConnectionIsValid(connection)\n                logger.debug(\"Connection $name tested\")\n                connectionUI.onConnectionTested(name, connection, shared)\n            } catch (exception: Exception) {\n                logger.debug(\"Connection $name has error ${exception.message}\")\n                connectionUI.onConnectionError(name, connection, shared, exception)\n            }\n        }\n\n        override fun verify(connectionUI: ConnectionUI, name: String, credentials: ApiCredentials, repository: String) {\n            logger.debug(\"Start verifying credentials $name\")\n            try {\n                val project = findProject(credentials)\n                if (null !== project) {\n                    connectionUI.onCredentialsVerified(name, credentials, repository, project)\n                } else {\n                    connectionUI.onCredentialsInvalid(name, credentials, repository)\n                }\n            } catch (exception: Exception) {\n                logger.debug(\"Connection $name has error ${exception.message}\")\n                connectionUI.onCredentialsInvalid(name, credentials, repository)\n            }\n        }\n\n        override fun delete(connectionUI: ConnectionUI, name: String) {\n            logger.debug(\"Delete connection $name\")\n            val id = findIdFromName(name).trim()\n            val settings = myData[id]\n            if (null !== settings) {\n                logger.debug(\"Update 'deleted' of connection $name to true\")\n                myData[id] = settings.copy(deleted = true)\n            }\n\n            val tabs = myTabs.getTabs()\n            val tabInfo = myTabInfos[id]\n            if (null !== tabInfo) {\n                tabs.removeTab(tabInfo)\n            }\n\n            if (tabs.tabCount == 0) {\n                myAddTabAction.perform()\n            } else {\n                tabs.select(tabs.getTabAt(0), true)\n            }\n        }\n\n        override fun update(\n            connectionUI: ConnectionUI,\n            name: String,\n            credentials: ApiCredentials,\n            shared: Boolean,\n            repository: String\n        ) {\n            val id = findIdFromName(name)\n            val settings = MyProviderSettings(\n                id = id,\n                info = makeProviderInfo(),\n                credentials = credentials,\n                repository = repository,\n                sharable = shared,\n                deleted = false\n            )\n            if (validateProviderSettings(settings)) {\n                myData[id] = settings\n            }\n        }\n\n        override fun changeName(connectionUI: ConnectionUI, oldName: String, newName: String) {\n            logger.debug(\"Name of connection change from $oldName to $newName\")\n            val oldId = findIdFromName(oldName).trim()\n            val newId = findIdFromName(newName).trim()\n\n            val oldData = myData[oldId]\n            if (null !== oldData) {\n                myData[newId] = oldData.copy(id = newId, deleted = false)\n                myData[oldId] = oldData.copy(deleted = true)\n            }\n\n            val tabInfo = myTabInfos[oldId]\n            if (null !== tabInfo) {\n                tabInfo.text = newName\n                myTabInfos[newId] = tabInfo\n                myTabInfos.remove(oldId)\n            }\n        }\n\n        override fun guessProject(connectionUI: ConnectionUI, credentials: ApiCredentials, repository: GitRepository) {\n            val remotes = mutableSetOf<String>()\n            repository.remotes.forEach { remote ->\n                remote.urls.forEach {\n                    val path = parseRemotePath(it)\n                    if (null !== path) {\n                        remotes.add(path)\n                    }\n                }\n                remote.pushUrls.forEach {\n                    val path = parseRemotePath(it)\n                    if (null !== path) {\n                        remotes.add(path)\n                    }\n                }\n            }\n\n            remotes.forEach {\n                val project = findProjectByPath(credentials, it)\n                connectionUI.onProjectGuessed(repository, project)\n            }\n        }\n\n        private fun parseRemotePath(input: String): String? {\n            val info = GitRemotePathInfo(input)\n            return if (info.isValid) info.toString() else null\n        }\n    }\n\n    abstract fun makeProviderInfo(): ProviderInfo\n\n    abstract fun findNameFromId(id: String): String\n\n    abstract fun findIdFromName(name: String): String\n\n    abstract fun makeConnection(): ConnectionUI\n\n    abstract fun validateConnection(connection: ApiConnection): Boolean\n\n    abstract fun findProject(credentials: ApiCredentials): Project?\n\n    abstract fun findProjectByPath(credentials: ApiCredentials, path: String): Project?\n\n    abstract fun assertConnectionIsValid(connection: ApiConnection)\n\n    private fun buildInitializedData(initUI: Boolean = true, initDataForCheckModification: Boolean = true) {\n        if (myIsInitialized) {\n            return\n        }\n\n        val currentProviderInfo = makeProviderInfo()\n        myInitializedData.clear()\n        val applicationServiceProvider = projectServiceProvider.applicationServiceProvider\n        val appConnections = applicationServiceProvider.getProviderConfigurations()\n        projectServiceProvider.getProviderConfigurations().forEach {\n            if (it.info.id == currentProviderInfo.id) {\n                myInitializedData[it.id] = MyProviderSettings(\n                    id = it.id,\n                    info = it.info,\n                    credentials = it.credentials,\n                    repository = it.repository,\n                    sharable = it.sharable,\n                    deleted = false\n                )\n            }\n        }\n        for (appConnection in appConnections) {\n            if (appConnection.info.id != currentProviderInfo.id) {\n                continue\n            }\n            if (myInitializedData.containsKey(appConnection.id)) {\n                myInitializedData[appConnection.id] = MyProviderSettings(\n                    id = myInitializedData[appConnection.id]!!.id,\n                    info = myInitializedData[appConnection.id]!!.info,\n                    credentials = appConnection.credentials,\n                    repository = myInitializedData[appConnection.id]!!.repository,\n                    sharable = true,\n                    deleted = false\n                )\n            } else {\n                myInitializedData[appConnection.id] = MyProviderSettings(\n                    id = appConnection.id,\n                    info = appConnection.info,\n                    credentials = MyProviderSettings.makeCredentialsFromApplicationLevel(\n                        appConnection.credentials\n                    ),\n                    repository = \"\",\n                    sharable = true,\n                    deleted = false\n                )\n            }\n        }\n        myIsInitialized = true\n        if (initUI) {\n            if (myInitializedData.isEmpty()) {\n                addNewConnection(\"Default\")\n            }\n            myInitializedData.forEach { (key, value) ->\n                val connectionUI = initConnection(value)\n                addConnectionToTabPane(value, connectionUI)\n                myData[key] = value\n            }\n        }\n        if (initDataForCheckModification) {\n            myInitializedData.forEach { (key, value) ->\n                myData[key] = value\n            }\n        }\n    }\n\n    private fun makeConnectionWithEventListener(): ConnectionUI {\n        val connection = makeConnection()\n        connection.dispatcher.addListener(myConnectionListener)\n\n        return connection\n    }\n\n    private fun initConnection(data: ProviderSettings): ConnectionUI {\n        val ui = makeConnectionWithEventListener()\n        ApplicationManager.getApplication().invokeLater {\n            ui.initialize(findNameFromId(data.id), data.credentials, data.sharable, data.repository)\n        }\n        return ui\n    }\n\n    private fun addConnectionToTabPane(data: ProviderSettings, connectionUI: ConnectionUI, selected: Boolean = false) {\n        val tabInfo = TabInfo(connectionUI.createComponent())\n        tabInfo.text = findNameFromId(data.id)\n\n        myTabInfos[data.id.trim()] = tabInfo\n        myTabs.addTab(tabInfo)\n        if (selected) {\n            myTabs.getTabs().select(tabInfo, true)\n        }\n    }\n\n    private fun addNewConnection(name: String) {\n        val id = findIdFromName(name)\n        val data = MyProviderSettings.makeDefault(id, makeProviderInfo())\n        val connection = makeConnectionWithEventListener()\n        connection.setName(name)\n\n        myData[id] = data\n        addConnectionToTabPane(data, connection, true)\n    }\n\n    override fun isModified(): Boolean {\n        buildInitializedData()\n        val initializedData = myInitializedData.filter { validateProviderSettings(it.value) }\n        val validData = myData.filter { validateProviderSettings(it.value) }\n        if (validData.size != initializedData.size) {\n            logger.info(\"Size of valid data & initialized data is not match\")\n            return true\n        }\n\n        for (entry in validData) {\n            if (!initializedData.containsKey(entry.key)) {\n                logger.info(\"Initialized data does not contains ${entry.key}\")\n                return true\n            }\n            val initializedItem = initializedData[entry.key]\n            if (null === initializedItem) {\n                logger.info(\"Initialized data does not contains ${entry.key}\")\n                return true\n            }\n            if (!initializedItem.isEquals(entry.value)) {\n                logger.info(\"Initialized data and data of ${entry.key} is not equals\")\n                return true\n            }\n        }\n\n        logger.info(\"All data are matched, not modified\")\n        return false\n    }\n\n    override fun apply() {\n        logger.info(\"Delete global connections\")\n        val applicationServiceProvider = projectServiceProvider.applicationServiceProvider\n        applicationServiceProvider.removeAllProviderConfigurations()\n\n        for (entry in myData) {\n            if (entry.value.deleted) {\n                logger.info(\"Delete connection ${entry.key}\")\n                projectServiceProvider.removeProviderConfiguration(entry.value.id.trim())\n                continue\n            }\n\n            if (entry.value.sharable) {\n                logger.info(\"Save connection ${entry.key} to global\")\n                applicationServiceProvider.addProviderConfiguration(\n                    id = entry.value.id.trim(),\n                    info = entry.value.info,\n                    credentials = entry.value.credentials\n                )\n            }\n\n            if (validateProviderSettings(entry.value)) {\n                logger.info(\"Save connection ${entry.key}\")\n                projectServiceProvider.addProviderConfiguration(\n                    id = entry.value.id.trim(),\n                    info = entry.value.info,\n                    credentials = entry.value.credentials,\n                    repository = entry.value.repository\n                )\n            }\n        }\n\n        myData.clear()\n        myIsInitialized = false\n        projectServiceProvider.initialize()\n        buildInitializedData(initUI = false, initDataForCheckModification = true)\n    }\n\n    override fun reset() {\n        myData.clear()\n        myTabInfos.clear()\n        myTabs.getTabs().removeAllTabs()\n        myIsInitialized = false\n        buildInitializedData()\n    }\n\n    override fun dispose() {\n        myData.clear()\n        myTabInfos.clear()\n    }\n\n    override fun createComponent(): JComponent? {\n        val component = myTabs.component\n        return component\n    }\n\n    private fun validateProviderSettings(providerSettings: ProviderSettings): Boolean {\n        return validateConnection(providerSettings.credentials) &&\n            providerSettings.repository.isNotEmpty() &&\n            providerSettings.credentials.projectId.isNotEmpty()\n    }\n\n    private data class MyProviderSettings(\n        override val id: String,\n        override val info: ProviderInfo,\n        override val credentials: ApiCredentials,\n        override val repository: String,\n        override val sharable: Boolean,\n        val deleted: Boolean\n    ) : ProviderSettings {\n\n        fun isEquals(other: MyProviderSettings): Boolean {\n            return id == other.id &&\n                info.id == other.info.id &&\n                isCredentialsEquals(other.credentials) &&\n                repository == other.repository &&\n                sharable == other.sharable &&\n                deleted == other.deleted\n        }\n\n        private fun isCredentialsEquals(other: ApiCredentials): Boolean {\n            return credentials.projectId == other.projectId &&\n                credentials.version == other.version &&\n                credentials.info == other.info &&\n                credentials.url == other.url &&\n                credentials.ignoreSSLCertificateErrors == other.ignoreSSLCertificateErrors &&\n                credentials.login == other.login &&\n                credentials.token == other.token\n        }\n\n        companion object {\n            fun makeCredentialsFromApplicationLevel(credentials: ApiCredentials): ApiCredentials {\n                return ApiCredentialsImpl(\n                    url = credentials.url,\n                    login = credentials.login,\n                    token = credentials.token,\n                    projectId = \"\",\n                    version = credentials.version,\n                    info = \"\",\n                    ignoreSSLCertificateErrors = credentials.ignoreSSLCertificateErrors\n                )\n            }\n\n            fun makeDefault(id: String, info: ProviderInfo): MyProviderSettings {\n                return MyProviderSettings(\n                    id = id,\n                    info = info,\n                    credentials = ApiCredentialsImpl(\n                        url = \"\",\n                        login = \"\",\n                        token = \"\",\n                        projectId = \"\",\n                        version = \"\",\n                        info = \"\",\n                        ignoreSSLCertificateErrors = false\n                    ),\n                    repository = \"\",\n                    sharable = false,\n                    deleted = false\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/ConfigurationBase.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.configuration\n\nimport com.intellij.openapi.options.SearchableConfigurable\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettings\nimport javax.swing.JComponent\n\nabstract class ConfigurationBase(\n    private val applicationServiceProvider: ApplicationServiceProvider\n) : SearchableConfigurable {\n    private var myInitializedSettings: ApplicationSettings = applicationServiceProvider.settingsManager\n    private var myCurrentSettings: ApplicationSettings = applicationServiceProvider.settingsManager\n    private val mySettingsUI: SettingsUI = SettingsConfiguration()\n    private val mySettingsListener = object : SettingsUI.Listener {\n        override fun change(settings: ApplicationSettings) {\n            myCurrentSettings = settings\n        }\n    }\n\n    init {\n        mySettingsUI.initialize(myInitializedSettings)\n\n        mySettingsUI.dispatcher.addListener(mySettingsListener)\n    }\n\n    override fun isModified(): Boolean {\n        return myCurrentSettings != myInitializedSettings\n    }\n\n    override fun apply() {\n        applicationServiceProvider.settingsManager.update(myCurrentSettings)\n        myInitializedSettings = myCurrentSettings\n        applicationServiceProvider.getAllProjectServiceProviders().forEach {\n            it.providerStorage.updateApiOptions(myCurrentSettings.toApiOptions())\n        }\n    }\n\n    override fun reset() {\n        myCurrentSettings = myInitializedSettings\n        mySettingsUI.initialize(myInitializedSettings)\n    }\n\n    override fun createComponent(): JComponent? = mySettingsUI.createComponent()\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/ConnectionUI.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.configuration\n\nimport com.intellij.util.EventDispatcher\nimport git4idea.repo.GitRepository\nimport net.ntworld.mergeRequest.Project\nimport net.ntworld.mergeRequest.api.ApiConnection\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport java.util.*\n\ninterface ConnectionUI : Component {\n    val dispatcher: EventDispatcher<Listener>\n\n    fun initialize(name: String, credentials: ApiCredentials, shared: Boolean, repository: String)\n\n    fun setName(name: String)\n\n    fun onConnectionTested(name: String, connection: ApiConnection, shared: Boolean)\n\n    fun onProjectGuessed(repository: GitRepository, project: Project?)\n\n    fun onConnectionError(name: String, connection: ApiConnection, shared: Boolean, exception: Exception)\n\n    fun onCredentialsVerified(name: String, credentials: ApiCredentials, repository: String, project: Project)\n\n    fun onCredentialsInvalid(name: String, credentials: ApiCredentials, repository: String)\n\n    interface Listener : EventListener {\n        fun test(connectionUI: ConnectionUI, name: String, connection: ApiConnection, shared: Boolean)\n\n        fun verify(connectionUI: ConnectionUI, name: String, credentials: ApiCredentials, repository: String)\n\n        fun delete(connectionUI: ConnectionUI, name: String)\n\n        fun update(\n            connectionUI: ConnectionUI,\n            name: String,\n            credentials: ApiCredentials,\n            shared: Boolean,\n            repository: String\n        )\n\n        fun changeName(connectionUI: ConnectionUI, oldName: String, newName: String)\n\n        fun guessProject(connectionUI: ConnectionUI, credentials: ApiCredentials, repository: GitRepository)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/GithubConnection.form",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<form xmlns=\"http://www.intellij.com/uidesigner/form/\" version=\"1\" bind-to-class=\"net.ntworld.mergeRequestIntegrationIde.ui.configuration.GithubConnection\">\n  <grid id=\"27dc6\" binding=\"myWholePanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n    <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n    <constraints>\n      <xy x=\"20\" y=\"20\" width=\"687\" height=\"400\"/>\n    </constraints>\n    <properties/>\n    <border type=\"none\"/>\n    <children>\n      <grid id=\"6e520\" layout-manager=\"GridLayoutManager\" row-count=\"3\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n        <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n        <constraints>\n          <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n        </constraints>\n        <properties/>\n        <border type=\"none\"/>\n        <children>\n          <grid id=\"63432\" binding=\"mySettingsPanel\" layout-manager=\"GridLayoutManager\" row-count=\"9\" column-count=\"2\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"d447a\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"2\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Server URL\"/>\n                </properties>\n              </component>\n              <component id=\"94305\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"4\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Private token\"/>\n                </properties>\n              </component>\n              <grid id=\"d542c\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"3\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n                <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n                <constraints>\n                  <grid row=\"5\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n                <border type=\"none\"/>\n                <children>\n                  <component id=\"f0571\" class=\"javax.swing.JButton\" binding=\"myTestBtn\">\n                    <constraints>\n                      <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Test Connection\"/>\n                    </properties>\n                  </component>\n                  <hspacer id=\"77127\">\n                    <constraints>\n                      <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                  </hspacer>\n                  <component id=\"3f61a\" class=\"javax.swing.JCheckBox\" binding=\"myShared\">\n                    <constraints>\n                      <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Share credentials with other projects\"/>\n                    </properties>\n                  </component>\n                </children>\n              </grid>\n              <component id=\"7e6e8\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"6\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Local repository\"/>\n                </properties>\n              </component>\n              <component id=\"5ff2d\" class=\"javax.swing.JComboBox\" binding=\"myRepository\">\n                <constraints>\n                  <grid row=\"6\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"2\" anchor=\"8\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n              </component>\n              <component id=\"ecbe4\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"7\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Project\"/>\n                </properties>\n              </component>\n              <component id=\"2f60c\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"\"/>\n                </properties>\n              </component>\n              <component id=\"201b4\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Name\"/>\n                </properties>\n              </component>\n              <component id=\"ce341\" class=\"javax.swing.JTextField\" binding=\"myName\">\n                <constraints>\n                  <grid row=\"1\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\">\n                    <preferred-size width=\"150\" height=\"-1\"/>\n                  </grid>\n                </constraints>\n                <properties/>\n              </component>\n              <component id=\"253c0\" class=\"javax.swing.JList\" binding=\"myProjectList\">\n                <constraints>\n                  <grid row=\"8\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"2\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\">\n                    <preferred-size width=\"150\" height=\"50\"/>\n                  </grid>\n                </constraints>\n                <properties/>\n              </component>\n              <component id=\"b1948\" class=\"javax.swing.JPasswordField\" binding=\"myToken\">\n                <constraints>\n                  <grid row=\"4\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\">\n                    <preferred-size width=\"150\" height=\"-1\"/>\n                  </grid>\n                </constraints>\n                <properties/>\n              </component>\n              <grid id=\"55da0\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"3\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n                <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n                <constraints>\n                  <grid row=\"7\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n                <border type=\"none\"/>\n                <children>\n                  <component id=\"6759\" class=\"javax.swing.JTextField\" binding=\"myTerm\">\n                    <constraints>\n                      <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\">\n                        <preferred-size width=\"150\" height=\"-1\"/>\n                      </grid>\n                    </constraints>\n                    <properties/>\n                  </component>\n                  <component id=\"d0ee7\" class=\"javax.swing.JLabel\">\n                    <constraints>\n                      <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Tip: use &quot;user:username term&quot; to search projects of the user\"/>\n                    </properties>\n                  </component>\n                </children>\n              </grid>\n              <grid id=\"3cc46\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"3\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n                <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n                <constraints>\n                  <grid row=\"2\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n                <border type=\"none\"/>\n                <children>\n                  <component id=\"d5121\" class=\"javax.swing.JTextField\" binding=\"myUrl\">\n                    <constraints>\n                      <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\">\n                        <preferred-size width=\"150\" height=\"-1\"/>\n                      </grid>\n                    </constraints>\n                    <properties/>\n                  </component>\n                  <component id=\"41993\" class=\"javax.swing.JCheckBox\" binding=\"myIgnoreSSLError\">\n                    <constraints>\n                      <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Ingore any SSL certificate errors\"/>\n                    </properties>\n                  </component>\n                </children>\n              </grid>\n              <component id=\"a6ec\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"3\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Username\"/>\n                </properties>\n              </component>\n              <component id=\"84892\" class=\"javax.swing.JTextField\" binding=\"myUsername\">\n                <constraints>\n                  <grid row=\"3\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\">\n                    <preferred-size width=\"150\" height=\"-1\"/>\n                  </grid>\n                </constraints>\n                <properties/>\n              </component>\n            </children>\n          </grid>\n          <vspacer id=\"e66ce\">\n            <constraints>\n              <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n          </vspacer>\n          <grid id=\"492bb\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"2\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"2\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <hspacer id=\"33197\">\n                <constraints>\n                  <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </hspacer>\n              <component id=\"6723a\" class=\"javax.swing.JButton\" binding=\"myDeleteBtn\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Delete this connection\"/>\n                </properties>\n              </component>\n            </children>\n          </grid>\n        </children>\n      </grid>\n    </children>\n  </grid>\n</form>\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/GithubConnection.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.configuration\n\nimport com.intellij.openapi.project.Project as IdeaProject\nimport com.intellij.openapi.ui.Messages\nimport com.intellij.util.EventDispatcher\nimport git4idea.repo.GitRepository\nimport net.ntworld.mergeRequest.Project\nimport net.ntworld.mergeRequest.api.ApiConnection\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ApiCredentialsImpl\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil\nimport javax.swing.*\nimport javax.swing.event.DocumentEvent\nimport javax.swing.event.DocumentListener\n\nclass GithubConnection(\n    private val projectServiceProvider: ProjectServiceProvider\n) : ConnectionUI {\n    var myWholePanel: JPanel? = null\n    var mySettingsPanel: JPanel? = null\n    var myName: JTextField? = null\n    var myUrl: JTextField? = null\n    var myUsername: JTextField? = null\n    var myToken: JPasswordField? = null\n    var myTestBtn: JButton? = null\n    var myDeleteBtn: JButton? = null\n    var myRepository: JComboBox<String>? = null\n    var myTerm: JTextField? = null\n    var myProjectList: JList<Project>? = null\n    var myShared: JCheckBox? = null\n    var myIgnoreSSLError: JCheckBox? = null\n\n    private var myCurrentName: String = \"\"\n    private var mySelectedRepository: String? = null\n    private var mySelectedProject: Project? = null\n    private var myIsTested: Boolean = false\n    private val myProjectFinder: ProjectFinderUI by lazy {\n        GithubProjectFinder(projectServiceProvider, myTerm!!, myProjectList!!)\n    }\n    private val myNameFieldListener = object : DocumentListener {\n        override fun changedUpdate(e: DocumentEvent?) {\n            val newName = myName!!.text\n            if (newName.trim() != myCurrentName) {\n                dispatcher.multicaster.changeName(this@GithubConnection, myCurrentName, newName)\n                myCurrentName = newName.trim()\n            }\n        }\n\n        override fun insertUpdate(e: DocumentEvent?) = changedUpdate(e)\n\n        override fun removeUpdate(e: DocumentEvent?) = changedUpdate(e)\n    }\n    private val myConnectionFieldsListener = object : DocumentListener {\n        override fun changedUpdate(e: DocumentEvent?) {\n            myTestBtn!!.isEnabled = myName!!.text.isNotEmpty() &&\n                myUrl!!.text.isNotEmpty() &&\n                myUsername!!.text.isNotEmpty() &&\n                getToken().isNotEmpty()\n            updateConnectionData()\n        }\n\n        override fun insertUpdate(e: DocumentEvent?) = changedUpdate(e)\n\n        override fun removeUpdate(e: DocumentEvent?) = changedUpdate(e)\n    }\n\n    override val dispatcher = EventDispatcher.create(ConnectionUI.Listener::class.java)\n\n    init {\n        myIgnoreSSLError!!.isVisible = false\n\n        val repositories = RepositoryUtil.getRepositoriesByProject(projectServiceProvider.project)\n        repositories.forEach { myRepository!!.addItem(it) }\n        if (repositories.isNotEmpty()) {\n            myRepository!!.selectedItem = repositories.first()\n        }\n\n        myTestBtn!!.addActionListener { onTestClicked() }\n        myDeleteBtn!!.addActionListener { onDeleteClicked() }\n\n        myName!!.document.addDocumentListener(myNameFieldListener)\n        myName!!.document.addDocumentListener(myConnectionFieldsListener)\n        myUrl!!.document.addDocumentListener(myConnectionFieldsListener)\n        myUsername!!.document.addDocumentListener(myConnectionFieldsListener)\n        myToken!!.document.addDocumentListener(myConnectionFieldsListener)\n\n        myIgnoreSSLError!!.addChangeListener { updateConnectionData() }\n        myRepository!!.addActionListener {\n            updateConnectionData()\n        }\n        myProjectFinder.addProjectChangedListener(object: ProjectFinderUI.ProjectChangedListener {\n            override fun projectChanged(projectId: String) {\n                updateConnectionData()\n            }\n        })\n    }\n\n    override fun initialize(name: String, credentials: ApiCredentials, shared: Boolean, repository: String) {\n        myIsTested = true\n        myCurrentName = name.trim()\n        myName!!.text = name.trim()\n        myUrl!!.text = if (credentials.url.isNotEmpty()) credentials.url else \"https://api.github.com\"\n        myUsername!!.text = credentials.login\n        myToken!!.text = credentials.token\n        myIgnoreSSLError!!.isSelected = credentials.ignoreSSLCertificateErrors\n        myShared!!.isSelected = shared\n        mySelectedRepository = repository\n        myTestBtn!!.isEnabled = false\n\n        if (credentials.projectId.isNotEmpty()) {\n            dispatcher.multicaster.verify(this, name, credentials, repository)\n        }\n        updateFieldsState()\n    }\n\n    override fun setName(name: String) {\n        myName!!.text = name.trim()\n        myUrl!!.text = \"https://api.github.com\"\n        myCurrentName = name.trim()\n    }\n\n    override fun onConnectionTested(name: String, connection: ApiConnection, shared: Boolean) {\n        myIsTested = true\n        myTestBtn!!.isEnabled = false\n        Messages.showInfoMessage(\"Successfully connected!\", \"Info\")\n        updateFieldsState()\n    }\n\n    override fun onProjectGuessed(repository: GitRepository, project: Project?) {\n    }\n\n    override fun onConnectionError(name: String, connection: ApiConnection, shared: Boolean, exception: Exception) {\n        myIsTested = false\n        Messages.showErrorDialog(\"Cannot connect, error: ${exception.message}\", \"Error\")\n        updateFieldsState()\n    }\n\n    override fun onCredentialsVerified(\n        name: String, credentials: ApiCredentials, repository: String, project: Project\n    ) {\n        mySelectedProject = project\n        mySelectedRepository = repository\n        updateFieldsState()\n    }\n\n    override fun onCredentialsInvalid(name: String, credentials: ApiCredentials, repository: String) {\n        updateFieldsState()\n    }\n\n    override fun createComponent(): JComponent = myWholePanel!!\n\n    private fun updateConnectionData() {\n        dispatcher.multicaster.update(\n            this,\n            myName!!.text.trim(),\n            ApiCredentialsImpl(\n                url = myUrl!!.text.trim(),\n                login = myUsername!!.text.trim(),\n                token = getToken().trim(),\n                projectId = myProjectFinder.getSelectedProjectId(),\n                version = \"v3\",\n                info = \"\",\n                ignoreSSLCertificateErrors = myIgnoreSSLError!!.isSelected\n            ),\n            myShared!!.isSelected,\n            myRepository!!.selectedItem as String? ?: \"\"\n        )\n    }\n\n    private fun getToken(): String {\n        return String(myToken!!.password)\n    }\n\n    private fun updateFieldsState() {\n        myRepository!!.isEnabled = myIsTested\n        myProjectFinder.setEnabled(\n            myIsTested,\n            ApiCredentialsImpl(\n                url = myUrl!!.text.trim(),\n                login = myUsername!!.text.trim(),\n                token = getToken().trim(),\n                projectId = \"\",\n                version = \"v3\",\n                info = \"\",\n                ignoreSSLCertificateErrors = myIgnoreSSLError!!.isSelected\n            )\n        )\n        if (null !== mySelectedRepository) {\n            myRepository!!.selectedItem = mySelectedRepository\n        }\n        if (null !== mySelectedProject) {\n            myProjectFinder.setSelectedProject(mySelectedProject)\n        }\n    }\n\n    private fun onDeleteClicked() {\n        val name = myName!!.text\n        this.dispatcher.multicaster.delete(this, name)\n    }\n\n    private fun onTestClicked() {\n        val name = myName!!.text\n        val url = myUrl!!.text\n        val username = myUsername!!.text\n        val token = getToken()\n        val shared = myShared!!.isSelected\n        val ignoreSSLCertificateErrors = myIgnoreSSLError!!.isSelected\n        if (name.isEmpty() || username.isEmpty() || url.isEmpty() || token.isEmpty()) {\n            return\n        }\n        this.dispatcher.multicaster.test(\n            this,\n            name,\n            ApiCredentialsImpl(\n                url = url,\n                login = myUsername!!.text.trim(),\n                token = token,\n                ignoreSSLCertificateErrors = ignoreSSLCertificateErrors,\n                info = \"\",\n                projectId = \"\",\n                version = \"v3\"\n            ),\n            shared\n        )\n    }\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/GithubConnectionsConfigurableBase.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.configuration\n\nimport net.ntworld.mergeRequest.Project\nimport net.ntworld.mergeRequest.api.ApiConnection\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.github.Github\nimport net.ntworld.mergeRequestIntegration.provider.github.request.GithubFindCurrentUserRequest\nimport net.ntworld.mergeRequestIntegration.provider.github.request.GithubFindRepositoryRequest\nimport net.ntworld.mergeRequestIntegration.provider.github.transformer.GithubRepositoryTransformer\nimport net.ntworld.mergeRequestIntegration.provider.github.vo.GithubProjectId\nimport net.ntworld.mergeRequestIntegrationIde.exception.InvalidConnectionException\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ApiCredentialsImpl\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\n\nopen class GithubConnectionsConfigurableBase(\n    private val projectServiceProvider: ProjectServiceProvider\n) : AbstractConnectionsConfigurable(projectServiceProvider) {\n    override fun getId(): String = \"MRI:github\"\n\n    override fun getDisplayName(): String = \"Github\"\n\n    override fun makeProviderInfo() = Github\n\n    override fun findNameFromId(id: String): String = Companion.findNameFromId(id)\n\n    override fun findIdFromName(name: String): String = Companion.findIdFromName(name)\n\n    override fun makeConnection(): ConnectionUI {\n        return GithubConnection(projectServiceProvider)\n    }\n\n    override fun validateConnection(connection: ApiConnection): Boolean {\n        return connection.url.isNotEmpty() && connection.login.isNotEmpty() && connection.token.isNotEmpty()\n    }\n\n    override fun findProject(credentials: ApiCredentials): Project? {\n        val out = projectServiceProvider.infrastructure.serviceBus() process GithubFindRepositoryRequest(\n            credentials = credentials,\n            repositoryId = GithubProjectId.parseId(credentials.projectId).toString()\n        )\n        val response = out.getResponse()\n        return if (response.isSuccess) {\n            GithubRepositoryTransformer.transform(response.repository)\n        } else {\n            null\n        }\n    }\n\n    override fun findProjectByPath(credentials: ApiCredentials, path: String): Project? {\n        return null\n    }\n\n    override fun assertConnectionIsValid(connection: ApiConnection) {\n        val out = projectServiceProvider.infrastructure.serviceBus() process GithubFindCurrentUserRequest(\n            credentials = ApiCredentialsImpl(\n                url = connection.url,\n                login = connection.login,\n                token = connection.token,\n                ignoreSSLCertificateErrors = connection.ignoreSSLCertificateErrors,\n                info = \"\",\n                projectId = \"\",\n                version = \"\"\n            )\n        )\n\n        val error = out.getResponse().error\n        if (null !== error) {\n            throw InvalidConnectionException(error.message)\n        }\n    }\n\n    companion object {\n        private const val PREFIX = \"github:\"\n\n        fun findNameFromId(id: String): String {\n            if (id.startsWith(PREFIX)) {\n                return id.substring(PREFIX.length)\n            }\n            return id\n        }\n\n        fun findIdFromName(name: String): String {\n            return \"$PREFIX$name\"\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/GithubProjectFinder.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.configuration\n\nimport com.intellij.openapi.progress.ProgressIndicator\nimport com.intellij.openapi.progress.ProgressManager\nimport com.intellij.openapi.progress.Task\nimport com.intellij.openapi.progress.impl.BackgroundableProcessIndicator\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.Project\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.github.request.GithubSearchRepositoriesRequest\nimport net.ntworld.mergeRequestIntegration.provider.github.transformer.GithubRepositoryTransformer\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.ui.panel.ProjectPanel\nimport java.awt.Component\nimport java.awt.event.FocusEvent\nimport java.awt.event.FocusListener\nimport java.util.*\nimport javax.swing.*\nimport javax.swing.event.DocumentEvent\nimport javax.swing.event.DocumentListener\nimport com.intellij.openapi.project.Project as IdeaProject\n\nclass GithubProjectFinder(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val myTerm: JTextField,\n    private val myProjectList: JList<Project>\n) : ProjectFinderUI {\n    private var myIsTermTouched: Boolean = false\n    private var mySelectedProject: Project? = null\n    private var myCredentials: ApiCredentials? = null\n    private val mySearchDispatcher = EventDispatcher.create(MySearchListener::class.java)\n    private val myProjectChangedDispatcher = EventDispatcher.create(ProjectFinderUI.ProjectChangedListener::class.java)\n\n    init {\n        myTerm.text = \"Search project...\"\n        myTerm.addFocusListener(object : FocusListener {\n            override fun focusLost(e: FocusEvent?) {\n                if (myTerm.text.isEmpty()) {\n                    myTerm.text = \"Search project...\"\n                    myIsTermTouched = false\n                }\n            }\n\n            override fun focusGained(e: FocusEvent?) {\n                if (!myIsTermTouched) {\n                    myTerm.text = \"\"\n                }\n                myIsTermTouched = true\n            }\n        })\n\n        myProjectList.selectionMode = ListSelectionModel.SINGLE_SELECTION\n        myProjectList.cellRenderer = object : ListCellRenderer<Project> {\n            override fun getListCellRendererComponent(\n                list: JList<out Project>?,\n                value: Project?,\n                index: Int,\n                isSelected: Boolean,\n                cellHasFocus: Boolean\n            ): Component? {\n                if (null === value) {\n                    return JPanel()\n                }\n                return ProjectPanel(value, isSelected).getComponent()\n            }\n        }\n        myProjectList.addListSelectionListener {\n            myProjectChangedDispatcher.multicaster.projectChanged(getSelectedProjectId())\n        }\n\n        myTerm.document.addDocumentListener(object : DocumentListener {\n            override fun changedUpdate(e: DocumentEvent?) {\n                if (myTerm.text.isNotEmpty()) {\n                    triggerSearchTask(myTerm.text.trim())\n                }\n            }\n\n            override fun insertUpdate(e: DocumentEvent?) = changedUpdate(e)\n\n            override fun removeUpdate(e: DocumentEvent?) = changedUpdate(e)\n        })\n\n        mySearchDispatcher.addListener(object : MySearchListener {\n            override fun searchFinished(term: String, projects: List<Project>) {\n                if (myTerm.text == term) {\n                    myProjectList.setListData(projects.toTypedArray())\n                }\n            }\n        })\n    }\n\n    private fun triggerSearchTask(term: String) {\n        myProjectChangedDispatcher.multicaster.projectChanged(\"\")\n        MySearchTask(projectServiceProvider, term, this).start()\n    }\n\n    override fun addProjectChangedListener(listener: ProjectFinderUI.ProjectChangedListener) {\n        myProjectChangedDispatcher.addListener(listener)\n    }\n\n    override fun getSelectedProjectId(): String {\n        val value = myProjectList.selectedValue\n        return if (null === value) \"\" else value.id\n    }\n\n    override fun setSelectedProject(project: Project?) {\n        if (null !== project) {\n            val current = mySelectedProject\n            if (null !== current && current.id == project.id) {\n                return\n            }\n\n            myProjectList.setListData(arrayOf(project))\n            myProjectList.selectedIndex = 0\n            mySelectedProject = project\n        }\n    }\n\n    override fun setEnabled(value: Boolean, credentials: ApiCredentials) {\n        myTerm.isEnabled = value\n        myProjectList.isEnabled = value\n        myCredentials = credentials\n    }\n\n    private interface MySearchListener : EventListener {\n        fun searchFinished(term: String, projects: List<Project>)\n    }\n\n    private class MySearchTask(\n        private val projectServiceProvider: ProjectServiceProvider,\n        private val term: String,\n        private val self: GithubProjectFinder\n    ) : Task.Backgroundable(projectServiceProvider.project, \"Searching github projects...\", true) {\n        fun start() {\n            if (self.myIsTermTouched) {\n                ProgressManager.getInstance().runProcessWithProgressAsynchronously(this, Indicator(this))\n            }\n        }\n\n        override fun run(indicator: ProgressIndicator) {\n            val credentials = self.myCredentials ?: return\n            Thread.sleep(300)\n            if (term != self.myTerm.text) {\n                return\n            }\n\n            val out = projectServiceProvider.infrastructure.serviceBus() process GithubSearchRepositoriesRequest(\n                credentials = credentials,\n                term = term\n            )\n\n            if (!out.hasError()) {\n                self.mySearchDispatcher.multicaster.searchFinished(\n                    term, out.getResponse().repositories.map { GithubRepositoryTransformer.transform(it) }\n                )\n            }\n        }\n\n        private class Indicator(private val task: MySearchTask) : BackgroundableProcessIndicator(task)\n    }\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/GitlabConnection.form",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<form xmlns=\"http://www.intellij.com/uidesigner/form/\" version=\"1\" bind-to-class=\"net.ntworld.mergeRequestIntegrationIde.ui.configuration.GitlabConnection\">\n  <grid id=\"27dc6\" binding=\"myWholePanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n    <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n    <constraints>\n      <xy x=\"20\" y=\"20\" width=\"640\" height=\"400\"/>\n    </constraints>\n    <properties/>\n    <border type=\"none\"/>\n    <children>\n      <grid id=\"6e520\" layout-manager=\"GridLayoutManager\" row-count=\"3\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n        <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n        <constraints>\n          <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n        </constraints>\n        <properties/>\n        <border type=\"none\"/>\n        <children>\n          <grid id=\"63432\" binding=\"mySettingsPanel\" layout-manager=\"GridLayoutManager\" row-count=\"9\" column-count=\"2\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"d447a\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"2\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Server URL\"/>\n                </properties>\n              </component>\n              <component id=\"94305\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"3\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Private token\"/>\n                </properties>\n              </component>\n              <grid id=\"d542c\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"3\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n                <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n                <constraints>\n                  <grid row=\"4\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n                <border type=\"none\"/>\n                <children>\n                  <component id=\"f0571\" class=\"javax.swing.JButton\" binding=\"myTestBtn\">\n                    <constraints>\n                      <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Test Connection\"/>\n                    </properties>\n                  </component>\n                  <hspacer id=\"77127\">\n                    <constraints>\n                      <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                  </hspacer>\n                  <component id=\"3f61a\" class=\"javax.swing.JCheckBox\" binding=\"myShared\">\n                    <constraints>\n                      <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Share credentials with other projects\"/>\n                    </properties>\n                  </component>\n                </children>\n              </grid>\n              <component id=\"7e6e8\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"5\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Local repository\"/>\n                </properties>\n              </component>\n              <component id=\"5ff2d\" class=\"javax.swing.JComboBox\" binding=\"myRepository\">\n                <constraints>\n                  <grid row=\"5\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"2\" anchor=\"8\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n              </component>\n              <component id=\"ecbe4\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"6\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Project\"/>\n                </properties>\n              </component>\n              <component id=\"2f60c\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"\"/>\n                </properties>\n              </component>\n              <component id=\"201b4\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Name\"/>\n                </properties>\n              </component>\n              <component id=\"ce341\" class=\"javax.swing.JTextField\" binding=\"myName\">\n                <constraints>\n                  <grid row=\"1\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\">\n                    <preferred-size width=\"150\" height=\"-1\"/>\n                  </grid>\n                </constraints>\n                <properties/>\n              </component>\n              <component id=\"253c0\" class=\"javax.swing.JList\" binding=\"myProjectList\">\n                <constraints>\n                  <grid row=\"7\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"2\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\">\n                    <preferred-size width=\"150\" height=\"50\"/>\n                  </grid>\n                </constraints>\n                <properties/>\n              </component>\n              <component id=\"b1948\" class=\"javax.swing.JPasswordField\" binding=\"myToken\">\n                <constraints>\n                  <grid row=\"3\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\">\n                    <preferred-size width=\"150\" height=\"-1\"/>\n                  </grid>\n                </constraints>\n                <properties/>\n              </component>\n              <grid id=\"55da0\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"5\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n                <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n                <constraints>\n                  <grid row=\"6\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n                <border type=\"none\"/>\n                <children>\n                  <component id=\"6759\" class=\"javax.swing.JTextField\" binding=\"myTerm\">\n                    <constraints>\n                      <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\">\n                        <preferred-size width=\"150\" height=\"-1\"/>\n                      </grid>\n                    </constraints>\n                    <properties/>\n                  </component>\n                  <component id=\"56095\" class=\"javax.swing.JCheckBox\" binding=\"mySearchStarred\">\n                    <constraints>\n                      <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Starred\"/>\n                    </properties>\n                  </component>\n                  <component id=\"d367a\" class=\"javax.swing.JCheckBox\" binding=\"mySearchMembership\">\n                    <constraints>\n                      <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Membership\"/>\n                    </properties>\n                  </component>\n                  <component id=\"fbc88\" class=\"javax.swing.JCheckBox\" binding=\"mySearchOwn\">\n                    <constraints>\n                      <grid row=\"0\" column=\"3\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Own\"/>\n                    </properties>\n                  </component>\n                </children>\n              </grid>\n              <component id=\"a6ec\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"8\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Information\"/>\n                </properties>\n              </component>\n              <component id=\"6d406\" class=\"javax.swing.JCheckBox\" binding=\"myMergeApprovalsFeature\">\n                <constraints>\n                  <grid row=\"8\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <selected value=\"false\"/>\n                  <text value=\"I am using plan &quot;Bronze&quot; or above then I have Merge Approvals feature\"/>\n                </properties>\n              </component>\n              <grid id=\"3cc46\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"3\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n                <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n                <constraints>\n                  <grid row=\"2\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n                <border type=\"none\"/>\n                <children>\n                  <component id=\"d5121\" class=\"javax.swing.JTextField\" binding=\"myUrl\">\n                    <constraints>\n                      <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\">\n                        <preferred-size width=\"150\" height=\"-1\"/>\n                      </grid>\n                    </constraints>\n                    <properties/>\n                  </component>\n                  <component id=\"41993\" class=\"javax.swing.JCheckBox\" binding=\"myIgnoreSSLError\">\n                    <constraints>\n                      <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Ignore any SSL certificate errors\"/>\n                    </properties>\n                  </component>\n                </children>\n              </grid>\n            </children>\n          </grid>\n          <vspacer id=\"e66ce\">\n            <constraints>\n              <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n          </vspacer>\n          <grid id=\"492bb\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"2\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"2\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <hspacer id=\"33197\">\n                <constraints>\n                  <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </hspacer>\n              <component id=\"6723a\" class=\"javax.swing.JButton\" binding=\"myDeleteBtn\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Delete this connection\"/>\n                </properties>\n              </component>\n            </children>\n          </grid>\n        </children>\n      </grid>\n    </children>\n  </grid>\n</form>\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/GitlabConnection.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.configuration\n\nimport com.intellij.openapi.ui.Messages\nimport com.intellij.util.EventDispatcher\nimport git4idea.repo.GitRepository\nimport net.ntworld.mergeRequest.Project\nimport net.ntworld.mergeRequest.api.ApiConnection\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabUtil\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ApiCredentialsImpl\nimport net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil\nimport javax.swing.*\nimport javax.swing.event.DocumentEvent\nimport javax.swing.event.DocumentListener\n\nclass GitlabConnection(\n    private val projectServiceProvider: ProjectServiceProvider\n) : ConnectionUI {\n    var myWholePanel: JPanel? = null\n    var mySettingsPanel: JPanel? = null\n    var myName: JTextField? = null\n    var myUrl: JTextField? = null\n    var myToken: JPasswordField? = null\n    var myTestBtn: JButton? = null\n    var myDeleteBtn: JButton? = null\n    var myRepository: JComboBox<String>? = null\n    var myTerm: JTextField? = null\n    var myProjectList: JList<Project>? = null\n    var myShared: JCheckBox? = null\n    var mySearchStarred: JCheckBox? = null\n    var mySearchMembership: JCheckBox? = null\n    var mySearchOwn: JCheckBox? = null\n    var myMergeApprovalsFeature: JCheckBox? = null\n    var myIgnoreSSLError: JCheckBox? = null\n\n    private var myCurrentName: String = \"\"\n    private var mySelectedRepository: String? = null\n    private var mySelectedProject: Project? = null\n    private var myIsTested: Boolean = false\n    private val myProjectFinder: ProjectFinderUI by lazy {\n        GitlabProjectFinder(\n            projectServiceProvider,\n            myTerm!!,\n            myProjectList!!,\n            mySearchStarred!!,\n            mySearchMembership!!,\n            mySearchOwn!!,\n            myMergeApprovalsFeature!!\n        )\n    }\n    private val myNameFieldListener = object : DocumentListener {\n        override fun changedUpdate(e: DocumentEvent?) {\n            val newName = myName!!.text\n            if (newName.trim() != myCurrentName) {\n                dispatcher.multicaster.changeName(this@GitlabConnection, myCurrentName, newName)\n                myCurrentName = newName.trim()\n            }\n        }\n\n        override fun insertUpdate(e: DocumentEvent?) = changedUpdate(e)\n\n        override fun removeUpdate(e: DocumentEvent?) = changedUpdate(e)\n    }\n    private val myConnectionFieldsListener = object : DocumentListener {\n        override fun changedUpdate(e: DocumentEvent?) {\n            myTestBtn!!.isEnabled = myName!!.text.isNotEmpty() && myUrl!!.text.isNotEmpty() && getToken().isNotEmpty()\n            updateConnectionData()\n        }\n\n        override fun insertUpdate(e: DocumentEvent?) = changedUpdate(e)\n\n        override fun removeUpdate(e: DocumentEvent?) = changedUpdate(e)\n    }\n\n    override val dispatcher = EventDispatcher.create(ConnectionUI.Listener::class.java)\n\n    init {\n        val repositories = RepositoryUtil.getRepositoriesByProject(projectServiceProvider.project)\n        repositories.forEach { myRepository!!.addItem(it) }\n        if (repositories.isNotEmpty()) {\n            myRepository!!.selectedItem = repositories.first()\n        }\n\n        myTestBtn!!.addActionListener { onTestClicked() }\n        myDeleteBtn!!.addActionListener { onDeleteClicked() }\n\n        myName!!.document.addDocumentListener(myNameFieldListener)\n        myName!!.document.addDocumentListener(myConnectionFieldsListener)\n        myUrl!!.document.addDocumentListener(myConnectionFieldsListener)\n        myToken!!.document.addDocumentListener(myConnectionFieldsListener)\n\n        myIgnoreSSLError!!.addChangeListener { updateConnectionData() }\n        myRepository!!.addActionListener {\n            requestGuessProjectByCurrentRepository()\n            updateConnectionData()\n        }\n        myProjectFinder.addProjectChangedListener(object: ProjectFinderUI.ProjectChangedListener {\n            override fun projectChanged(projectId: String) {\n                updateConnectionData()\n            }\n        })\n        myMergeApprovalsFeature!!.addChangeListener { updateConnectionData() }\n    }\n\n    override fun initialize(name: String, credentials: ApiCredentials, shared: Boolean, repository: String) {\n        myIsTested = true\n        myCurrentName = name.trim()\n        myName!!.text = name.trim()\n        myUrl!!.text = if (credentials.url.isNotEmpty()) credentials.url else \"https://gitlab.com\"\n        myToken!!.text = credentials.token\n        myIgnoreSSLError!!.isSelected = credentials.ignoreSSLCertificateErrors\n        myShared!!.isSelected = shared\n        mySelectedRepository = repository\n        myTestBtn!!.isEnabled = false\n\n        myMergeApprovalsFeature!!.isSelected = GitlabUtil.hasMergeApprovalFeature(credentials)\n        if (credentials.projectId.isNotEmpty()) {\n            dispatcher.multicaster.verify(this, name, credentials, repository)\n        }\n        updateFieldsState()\n    }\n\n    override fun setName(name: String) {\n        myName!!.text = name.trim()\n        myUrl!!.text = \"https://gitlab.com\"\n        myCurrentName = name.trim()\n    }\n\n    override fun onConnectionTested(name: String, connection: ApiConnection, shared: Boolean) {\n        myIsTested = true\n        myTestBtn!!.isEnabled = false\n        updateFieldsState()\n        requestGuessProjectByCurrentRepository()\n        Messages.showInfoMessage(\"Successfully connected!\", \"Info\")\n    }\n\n    override fun onProjectGuessed(repository: GitRepository, project: Project?) {\n        if (null !== project && null === mySelectedProject) {\n            mySelectedProject = project\n            myProjectFinder.setSelectedProject(mySelectedProject)\n        }\n    }\n\n    override fun onConnectionError(name: String, connection: ApiConnection, shared: Boolean, exception: Exception) {\n        myIsTested = false\n        Messages.showErrorDialog(\"Cannot connect, error: ${exception.message}\", \"Error\")\n        updateFieldsState()\n    }\n\n    override fun onCredentialsVerified(\n        name: String, credentials: ApiCredentials, repository: String, project: Project\n    ) {\n        mySelectedProject = project\n        mySelectedRepository = repository\n        updateFieldsState()\n    }\n\n    override fun onCredentialsInvalid(name: String, credentials: ApiCredentials, repository: String) {\n        updateFieldsState()\n    }\n\n    override fun createComponent(): JComponent = myWholePanel!!\n\n    private fun requestGuessProjectByCurrentRepository() {\n        val repository = RepositoryUtil.findRepositoryByPath(\n            projectServiceProvider.project,\n            myRepository!!.selectedItem as String? ?: \"\"\n        )\n        if (null !== repository) {\n            this.dispatcher.multicaster.guessProject(this, buildApiCredentials(), repository)\n        }\n    }\n\n    private fun buildApiCredentials(): ApiCredentials {\n        return ApiCredentialsImpl(\n            url = myUrl!!.text.trim(),\n            login = \"\",\n            token = getToken().trim(),\n            projectId = myProjectFinder.getSelectedProjectId(),\n            version = \"v4\",\n            info = if (myMergeApprovalsFeature!!.isSelected) GitlabUtil.getMergeApprovalFeatureInfo() else \"\",\n            ignoreSSLCertificateErrors = myIgnoreSSLError!!.isSelected\n        )\n    }\n\n    private fun updateConnectionData() {\n        dispatcher.multicaster.update(\n            this,\n            myName!!.text.trim(),\n            buildApiCredentials(),\n            myShared!!.isSelected,\n            myRepository!!.selectedItem as String? ?: \"\"\n        )\n    }\n\n    private fun getToken(): String {\n        return String(myToken!!.password)\n    }\n\n    private fun updateFieldsState() {\n        myRepository!!.isEnabled = myIsTested\n        myProjectFinder.setEnabled(\n            myIsTested,\n            ApiCredentialsImpl(\n                url = myUrl!!.text.trim(),\n                login = \"\",\n                token = getToken().trim(),\n                projectId = \"\",\n                version = \"v4\",\n                info = if (myMergeApprovalsFeature!!.isSelected) GitlabUtil.getMergeApprovalFeatureInfo() else \"\",\n                ignoreSSLCertificateErrors = myIgnoreSSLError!!.isSelected\n            )\n        )\n        if (null !== mySelectedRepository) {\n            myRepository!!.selectedItem = mySelectedRepository\n        }\n        if (null !== mySelectedProject) {\n            myProjectFinder.setSelectedProject(mySelectedProject)\n        }\n    }\n\n    private fun onDeleteClicked() {\n        val name = myName!!.text\n        this.dispatcher.multicaster.delete(this, name)\n    }\n\n    private fun onTestClicked() {\n        val name = myName!!.text\n        val url = myUrl!!.text\n        val token = getToken()\n        val shared = myShared!!.isSelected\n        val ignoreSSLCertificateErrors = myIgnoreSSLError!!.isSelected\n        if (name.isEmpty() || url.isEmpty() || token.isEmpty()) {\n            return\n        }\n        this.dispatcher.multicaster.test(\n            this,\n            name,\n            ApiCredentialsImpl(\n                url = url,\n                login = \"\",\n                token = token,\n                ignoreSSLCertificateErrors = ignoreSSLCertificateErrors,\n                info = \"\",\n                projectId = \"\",\n                version = \"v4\"\n            ),\n            shared\n        )\n    }\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/GitlabConnectionsConfigurableBase.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.configuration\n\nimport net.ntworld.mergeRequest.ProviderInfo\nimport net.ntworld.mergeRequest.api.ApiConnection\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.Gitlab\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabFindProjectRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabSearchProjectsRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.GitlabProjectTransformer\nimport net.ntworld.mergeRequestIntegrationIde.exception.InvalidConnectionException\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.internal.ApiCredentialsImpl\n\nopen class GitlabConnectionsConfigurableBase(\n    private val projectServiceProvider: ProjectServiceProvider\n) : AbstractConnectionsConfigurable(projectServiceProvider) {\n    override fun makeProviderInfo(): ProviderInfo = Gitlab\n\n    override fun findNameFromId(id: String) = Companion.findNameFromId(id)\n\n    override fun findIdFromName(name: String) = Companion.findIdFromName(name)\n\n    override fun makeConnection(): ConnectionUI {\n        return GitlabConnection(projectServiceProvider)\n    }\n\n    override fun validateConnection(connection: ApiConnection): Boolean {\n        return connection.url.isNotEmpty() && connection.token.isNotEmpty()\n    }\n\n    override fun findProject(credentials: ApiCredentials): net.ntworld.mergeRequest.Project? {\n        val out = projectServiceProvider.infrastructure.serviceBus() process GitlabFindProjectRequest(\n            credentials = credentials,\n            projectId = credentials.projectId.toInt()\n        )\n        val response = out.getResponse()\n        return if (response.isSuccess) {\n            GitlabProjectTransformer.transform(response.project)\n        } else {\n            null\n        }\n    }\n\n    override fun findProjectByPath(credentials: ApiCredentials, path: String): net.ntworld.mergeRequest.Project? {\n        val out = projectServiceProvider.infrastructure.serviceBus() process GitlabFindProjectRequest(\n            credentials = credentials,\n            projectId = 0,\n            projectPath = path\n        )\n        val response = out.getResponse()\n        return if (response.isSuccess) {\n            GitlabProjectTransformer.transform(response.project)\n        } else {\n            null\n        }\n    }\n\n    override fun assertConnectionIsValid(connection: ApiConnection) {\n        val out = projectServiceProvider.infrastructure.serviceBus() process GitlabSearchProjectsRequest(\n            credentials = ApiCredentialsImpl(\n                url = connection.url,\n                login = connection.login,\n                token = connection.token,\n                ignoreSSLCertificateErrors = connection.ignoreSSLCertificateErrors,\n                info = \"\",\n                projectId = \"\",\n                version = \"\"\n            ),\n            term = \"\"\n        )\n\n        val error = out.getResponse().error\n        if (null !== error) {\n            throw InvalidConnectionException(error.message)\n        }\n    }\n\n    override fun getId(): String = \"MRI:gitlab\"\n\n    override fun getDisplayName(): String = \"Gitlab new\"\n\n    companion object {\n        private const val PREFIX = \"gitlab:\"\n\n        fun findNameFromId(id: String): String {\n            if (id.startsWith(PREFIX)) {\n                return id.substring(PREFIX.length)\n            }\n            return id\n        }\n\n        fun findIdFromName(name: String): String {\n            return \"$PREFIX$name\"\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/GitlabProjectFinder.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.configuration\n\nimport com.intellij.openapi.progress.ProgressIndicator\nimport com.intellij.openapi.progress.ProgressManager\nimport com.intellij.openapi.progress.Task\nimport com.intellij.openapi.progress.impl.BackgroundableProcessIndicator\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.Project\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.GitlabUtil\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.request.GitlabSearchProjectsRequest\nimport net.ntworld.mergeRequestIntegration.provider.gitlab.transformer.GitlabProjectTransformer\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.ui.panel.ProjectPanel\nimport java.awt.Component\nimport java.awt.event.FocusEvent\nimport java.awt.event.FocusListener\nimport java.util.*\nimport javax.swing.*\nimport javax.swing.event.DocumentEvent\nimport javax.swing.event.DocumentListener\n\nclass GitlabProjectFinder(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val myTerm: JTextField,\n    private val myProjectList: JList<Project>,\n    private val mySearchStarred: JCheckBox,\n    private val mySearchMembership: JCheckBox,\n    private val mySearchOwn: JCheckBox,\n    private val myMergeApprovalsFeature: JCheckBox\n) : ProjectFinderUI {\n    private var myIsTermTouched: Boolean = false\n    private var mySelectedProject: Project? = null\n    private var myCredentials: ApiCredentials? = null\n    private val mySearchDispatcher = EventDispatcher.create(MySearchListener::class.java)\n    private val myProjectChangedDispatcher = EventDispatcher.create(ProjectFinderUI.ProjectChangedListener::class.java)\n\n    init {\n        myTerm.text = \"Search project...\"\n        myTerm.addFocusListener(object : FocusListener {\n            override fun focusLost(e: FocusEvent?) {\n                if (myTerm.text.isEmpty()) {\n                    myTerm.text = \"Search project...\"\n                    myIsTermTouched = false\n                }\n            }\n\n            override fun focusGained(e: FocusEvent?) {\n                if (!myIsTermTouched) {\n                    myTerm.text = \"\"\n                }\n                myIsTermTouched = true\n            }\n        })\n\n        myProjectList.selectionMode = ListSelectionModel.SINGLE_SELECTION\n        myProjectList.cellRenderer = object : ListCellRenderer<Project> {\n            override fun getListCellRendererComponent(\n                list: JList<out Project>?,\n                value: Project?,\n                index: Int,\n                isSelected: Boolean,\n                cellHasFocus: Boolean\n            ): Component? {\n                if (null === value) {\n                    return JPanel()\n                }\n                return ProjectPanel(value, isSelected).getComponent()\n            }\n        }\n        myProjectList.addListSelectionListener {\n            myProjectChangedDispatcher.multicaster.projectChanged(getSelectedProjectId())\n        }\n\n        myTerm.document.addDocumentListener(object : DocumentListener {\n            override fun changedUpdate(e: DocumentEvent?) {\n                if (myTerm.text.isNotEmpty()) {\n                    triggerSearchTask(myTerm.text.trim())\n                }\n            }\n\n            override fun insertUpdate(e: DocumentEvent?) = changedUpdate(e)\n\n            override fun removeUpdate(e: DocumentEvent?) = changedUpdate(e)\n        })\n\n        mySearchDispatcher.addListener(object : MySearchListener {\n            override fun searchFinished(term: String, projects: List<Project>) {\n                if (myTerm.text == term) {\n                    myProjectList.setListData(projects.toTypedArray())\n                }\n            }\n        })\n\n        mySearchOwn.addChangeListener { triggerSearchTask(myTerm.text) }\n        mySearchMembership.addChangeListener { triggerSearchTask(myTerm.text) }\n        mySearchStarred.addChangeListener { triggerSearchTask(myTerm.text) }\n    }\n\n    private fun triggerSearchTask(term: String) {\n        myProjectChangedDispatcher.multicaster.projectChanged(\"\")\n        MySearchTask(projectServiceProvider, term, this).start()\n    }\n\n    override fun addProjectChangedListener(listener: ProjectFinderUI.ProjectChangedListener) {\n        myProjectChangedDispatcher.addListener(listener)\n    }\n\n    override fun getSelectedProjectId(): String {\n        val value = myProjectList.selectedValue\n        return if (null === value) \"\" else value.id\n    }\n\n    override fun setSelectedProject(project: Project?) {\n        if (null !== project) {\n            val current = mySelectedProject\n            if (null !== current && current.id == project.id) {\n                return\n            }\n\n            myProjectList.setListData(arrayOf(project))\n            myProjectList.selectedIndex = 0\n            mySelectedProject = project\n        }\n    }\n\n    override fun setEnabled(value: Boolean, credentials: ApiCredentials) {\n        myTerm.isEnabled = value\n        myProjectList.isEnabled = value\n        mySearchStarred.isEnabled = value\n        mySearchMembership.isEnabled = value\n        mySearchOwn.isEnabled = value\n        myMergeApprovalsFeature.isEnabled = value\n        myMergeApprovalsFeature.isSelected = GitlabUtil.hasMergeApprovalFeature(credentials)\n        myCredentials = credentials\n    }\n\n    private interface MySearchListener : EventListener {\n        fun searchFinished(term: String, projects: List<Project>)\n    }\n\n    private class MySearchTask(\n        private val projectServiceProvider: ProjectServiceProvider,\n        private val term: String,\n        private val self: GitlabProjectFinder\n    ) : Task.Backgroundable(projectServiceProvider.project, \"Searching gitlab projects...\", true) {\n        fun start() {\n            if (self.myIsTermTouched) {\n                ProgressManager.getInstance().runProcessWithProgressAsynchronously(this, Indicator(this))\n            }\n        }\n\n        override fun run(indicator: ProgressIndicator) {\n            val credentials = self.myCredentials ?: return\n            Thread.sleep(300)\n            if (term != self.myTerm.text) {\n                return\n            }\n\n            val out = projectServiceProvider.infrastructure.serviceBus() process GitlabSearchProjectsRequest(\n                credentials = credentials,\n                term = term,\n                owner = self.mySearchOwn.isSelected,\n                membership = self.mySearchMembership.isSelected,\n                starred = self.mySearchStarred.isSelected\n            )\n\n            if (!out.hasError()) {\n                self.mySearchDispatcher.multicaster.searchFinished(\n                    term, out.getResponse().projects.map { GitlabProjectTransformer.transform(it) }\n                )\n            }\n        }\n\n        private class Indicator(private val task: MySearchTask) : BackgroundableProcessIndicator(task)\n    }\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/ProjectFinderUI.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.configuration\n\nimport net.ntworld.mergeRequest.Project\nimport net.ntworld.mergeRequest.api.ApiCredentials\nimport java.util.*\n\ninterface ProjectFinderUI {\n    fun setEnabled(value: Boolean, credentials: ApiCredentials)\n\n    fun setSelectedProject(project: Project?)\n\n    fun getSelectedProjectId(): String\n\n    fun addProjectChangedListener(listener: ProjectChangedListener)\n\n    interface ProjectChangedListener : EventListener {\n        fun projectChanged(projectId: String)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/SettingsConfiguration.form",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<form xmlns=\"http://www.intellij.com/uidesigner/form/\" version=\"1\" bind-to-class=\"net.ntworld.mergeRequestIntegrationIde.ui.configuration.SettingsConfiguration\">\n  <grid id=\"27dc6\" binding=\"myWholePanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n    <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n    <constraints>\n      <xy x=\"20\" y=\"20\" width=\"759\" height=\"413\"/>\n    </constraints>\n    <properties/>\n    <border type=\"none\"/>\n    <children>\n      <tabbedpane id=\"6084e\" binding=\"myTabbedPane\">\n        <constraints>\n          <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\">\n            <preferred-size width=\"200\" height=\"200\"/>\n          </grid>\n        </constraints>\n        <properties/>\n        <border type=\"none\"/>\n        <children>\n          <grid id=\"659f3\" layout-manager=\"GridLayoutManager\" row-count=\"2\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"10\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <tabbedpane title=\"General\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <vspacer id=\"d58e9\">\n                <constraints>\n                  <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </vspacer>\n              <grid id=\"c8e64\" binding=\"myPerformancePanel\" layout-manager=\"GridLayoutManager\" row-count=\"3\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n                <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n                <border type=\"none\"/>\n                <children>\n                  <component id=\"d75b2\" class=\"javax.swing.JCheckBox\" binding=\"myEnableRequestCache\">\n                    <constraints>\n                      <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Enable requests cache to reduce unnecessary calls to server\"/>\n                    </properties>\n                  </component>\n                  <vspacer id=\"33034\">\n                    <constraints>\n                      <grid row=\"2\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                  </vspacer>\n                  <component id=\"49894\" class=\"javax.swing.JCheckBox\" binding=\"mySaveMRFilterState\">\n                    <constraints>\n                      <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Keep selected values of filters and order state in Merge Request list\"/>\n                    </properties>\n                  </component>\n                </children>\n              </grid>\n            </children>\n          </grid>\n          <grid id=\"9c734\" layout-manager=\"GridLayoutManager\" row-count=\"3\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"10\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <tabbedpane title=\"Code Review\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <grid id=\"3c482\" binding=\"myCodeReviewOptionsPanel\" layout-manager=\"GridLayoutManager\" row-count=\"3\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n                <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n                <border type=\"none\"/>\n                <children>\n                  <component id=\"2daf8\" class=\"javax.swing.JCheckBox\" binding=\"myCheckoutTargetBranch\">\n                    <constraints>\n                      <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Checkout target branch when doing Code Review\"/>\n                    </properties>\n                  </component>\n                  <vspacer id=\"d8770\">\n                    <constraints>\n                      <grid row=\"2\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                  </vspacer>\n                </children>\n              </grid>\n              <vspacer id=\"45fbd\">\n                <constraints>\n                  <grid row=\"2\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </vspacer>\n              <grid id=\"1e6a\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"4\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n                <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n                <constraints>\n                  <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n                <border type=\"none\"/>\n                <children>\n                  <component id=\"96606\" class=\"javax.swing.JLabel\">\n                    <constraints>\n                      <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Open diff changes automatically if number of changes less than:\"/>\n                    </properties>\n                  </component>\n                  <hspacer id=\"1f37c\">\n                    <constraints>\n                      <grid row=\"0\" column=\"3\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                  </hspacer>\n                  <component id=\"29b06\" class=\"javax.swing.JTextField\" binding=\"myMaxDiffChangesOpenedAutomatically\">\n                    <constraints>\n                      <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\">\n                        <preferred-size width=\"50\" height=\"-1\"/>\n                        <maximum-size width=\"50\" height=\"-1\"/>\n                      </grid>\n                    </constraints>\n                    <properties/>\n                  </component>\n                </children>\n              </grid>\n            </children>\n          </grid>\n          <grid id=\"5913\" layout-manager=\"GridLayoutManager\" row-count=\"2\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"10\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <tabbedpane title=\"Comments\"/>\n            </constraints>\n            <properties>\n              <enabled value=\"true\"/>\n            </properties>\n            <border type=\"none\"/>\n            <children>\n              <grid id=\"9365b\" binding=\"myCommentOptionsPanel\" layout-manager=\"GridLayoutManager\" row-count=\"3\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n                <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n                <border type=\"none\"/>\n                <children>\n                  <component id=\"50aa1\" class=\"javax.swing.JCheckBox\" binding=\"myDisplayCommentsInDiffView\">\n                    <constraints>\n                      <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Always display comments in diff view\"/>\n                    </properties>\n                  </component>\n                  <vspacer id=\"16567\">\n                    <constraints>\n                      <grid row=\"2\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                  </vspacer>\n                  <component id=\"38d38\" class=\"javax.swing.JCheckBox\" binding=\"myShowAddCommentIconsInDiffViewGutter\">\n                    <constraints>\n                      <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Show add comment icons in diff view gutter\"/>\n                    </properties>\n                  </component>\n                </children>\n              </grid>\n              <vspacer id=\"51c3e\">\n                <constraints>\n                  <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </vspacer>\n            </children>\n          </grid>\n          <grid id=\"8c73a\" layout-manager=\"GridLayoutManager\" row-count=\"2\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"10\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <tabbedpane title=\"Merge Request\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <grid id=\"c18e3\" binding=\"myMergeRequestOptionsPanel\" layout-manager=\"GridLayoutManager\" row-count=\"3\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n                <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n                <border type=\"none\"/>\n                <children>\n                  <component id=\"4156c\" class=\"javax.swing.JCheckBox\" binding=\"myDisplayUpVotesAndDownVotes\">\n                    <constraints>\n                      <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Display up votes and down votes in toolbar\"/>\n                    </properties>\n                  </component>\n                  <vspacer id=\"7c1e5\">\n                    <constraints>\n                      <grid row=\"2\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                  </vspacer>\n                  <component id=\"10591\" class=\"javax.swing.JCheckBox\" binding=\"myDisplayMergeRequestState\">\n                    <constraints>\n                      <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Display merge request state (open, merged, closed) in toolbar\"/>\n                    </properties>\n                  </component>\n                </children>\n              </grid>\n              <vspacer id=\"d85ee\">\n                <constraints>\n                  <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </vspacer>\n            </children>\n          </grid>\n          <grid id=\"824c0\" layout-manager=\"GridLayoutManager\" row-count=\"2\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"10\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <tabbedpane title=\"Rework Process\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <grid id=\"26163\" binding=\"myReworkProcessOptionsPanel\" layout-manager=\"GridLayoutManager\" row-count=\"2\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n                <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n                <border type=\"none\"/>\n                <children>\n                  <component id=\"21a2a\" class=\"javax.swing.JCheckBox\" binding=\"myEnableReworkProcess\">\n                    <constraints>\n                      <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Enable rework process which watch and display changes and comments based on current branch\"/>\n                    </properties>\n                  </component>\n                  <vspacer id=\"730e6\">\n                    <constraints>\n                      <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                  </vspacer>\n                </children>\n              </grid>\n              <vspacer id=\"73bb\">\n                <constraints>\n                  <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </vspacer>\n            </children>\n          </grid>\n        </children>\n      </tabbedpane>\n    </children>\n  </grid>\n</form>\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/SettingsConfiguration.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.configuration\n\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettingsImpl\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.option.MaxDiffChangesOpenedAutomaticallyOption\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettings\nimport javax.swing.*\nimport javax.swing.event.DocumentEvent\nimport javax.swing.event.DocumentListener\n\nclass SettingsConfiguration : SettingsUI {\n    var myTabbedPane: JTabbedPane? = null\n    var myWholePanel: JPanel? = null\n    var myPerformancePanel: JPanel? = null\n    var myEnableRequestCache: JCheckBox? = null\n    var mySaveMRFilterState: JCheckBox? = null\n\n    var myCommentOptionsPanel: JPanel? = null\n    var myDisplayCommentsInDiffView: JCheckBox? = null\n    var myShowAddCommentIconsInDiffViewGutter: JCheckBox? = null\n\n    var myCodeReviewOptionsPanel: JPanel? = null\n    var myCheckoutTargetBranch: JCheckBox? = null\n\n    var myMergeRequestOptionsPanel: JPanel? = null\n    var myDisplayUpVotesAndDownVotes: JCheckBox? = null\n    var myDisplayMergeRequestState: JCheckBox? = null\n\n    var myMaxDiffChangesOpenedAutomatically: JTextField? = null\n\n    var myReworkProcessOptionsPanel: JPanel? = null\n    var myEnableReworkProcess: JCheckBox? = null\n\n    override val dispatcher = EventDispatcher.create(SettingsUI.Listener::class.java)\n\n    init {\n        // myPerformancePanel!!.border = BorderFactory.createTitledBorder(\"Performance\")\n        myEnableRequestCache!!.addActionListener { dispatchSettingsUpdated() }\n        mySaveMRFilterState!!.addActionListener { dispatchSettingsUpdated() }\n        myDisplayCommentsInDiffView!!.addActionListener { dispatchSettingsUpdated() }\n        myShowAddCommentIconsInDiffViewGutter!!.addActionListener { dispatchSettingsUpdated() }\n        myDisplayUpVotesAndDownVotes!!.addActionListener { dispatchSettingsUpdated() }\n        myDisplayMergeRequestState!!.addActionListener { dispatchSettingsUpdated() }\n        myEnableReworkProcess!!.addActionListener { dispatchSettingsUpdated() }\n\n        myCheckoutTargetBranch!!.addActionListener { dispatchSettingsUpdated() }\n        myMaxDiffChangesOpenedAutomatically!!.document.addDocumentListener(object : DocumentListener {\n            override fun changedUpdate(e: DocumentEvent?) {\n                dispatchSettingsUpdated()\n            }\n\n            override fun insertUpdate(e: DocumentEvent?) = changedUpdate(e)\n            override fun removeUpdate(e: DocumentEvent?) = changedUpdate(e)\n        })\n    }\n\n    private fun dispatchSettingsUpdated() {\n        val settings =\n            ApplicationSettingsImpl(\n                enableRequestCache = myEnableRequestCache!!.isSelected,\n                saveMRFilterState = mySaveMRFilterState!!.isSelected,\n                displayCommentsInDiffView = myDisplayCommentsInDiffView!!.isSelected,\n                showAddCommentIconsInDiffViewGutter = myShowAddCommentIconsInDiffViewGutter!!.isSelected,\n                checkoutTargetBranch = myCheckoutTargetBranch!!.isSelected,\n                maxDiffChangesOpenedAutomatically = MaxDiffChangesOpenedAutomaticallyOption.parse(\n                    myMaxDiffChangesOpenedAutomatically!!.text\n                ),\n                displayUpVotesAndDownVotes = myDisplayUpVotesAndDownVotes!!.isSelected,\n                displayMergeRequestState = myDisplayMergeRequestState!!.isSelected,\n                enableReworkProcess = myEnableReworkProcess!!.isSelected\n            )\n        dispatcher.multicaster.change(settings)\n    }\n\n    override fun initialize(settings: ApplicationSettings) {\n        myEnableRequestCache!!.isSelected = settings.enableRequestCache\n        mySaveMRFilterState!!.isSelected = settings.saveMRFilterState\n        myDisplayCommentsInDiffView!!.isSelected = settings.displayCommentsInDiffView\n        myShowAddCommentIconsInDiffViewGutter!!.isSelected = settings.showAddCommentIconsInDiffViewGutter\n        myCheckoutTargetBranch!!.isSelected = settings.checkoutTargetBranch\n        myMaxDiffChangesOpenedAutomatically!!.text = settings.maxDiffChangesOpenedAutomatically.toString()\n        myDisplayUpVotesAndDownVotes!!.isSelected = settings.displayUpVotesAndDownVotes\n        myDisplayMergeRequestState!!.isSelected = settings.displayMergeRequestState\n        myEnableReworkProcess!!.isSelected = settings.enableReworkProcess\n    }\n\n    override fun createComponent(): JComponent = myWholePanel!!\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/configuration/SettingsUI.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.configuration\n\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.setting.ApplicationSettings\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport java.util.*\n\ninterface SettingsUI : Component {\n    val dispatcher: EventDispatcher<Listener>\n\n    fun initialize(settings: ApplicationSettings)\n\n    interface Listener: EventListener {\n        fun change(settings: ApplicationSettings)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/AbstractMergeRequestCollection.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest\n\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.MergeRequestState\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.ProviderStatus\nimport net.ntworld.mergeRequest.api.MergeRequestOrdering\nimport net.ntworld.mergeRequest.query.GetMergeRequestFilter\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegrationIde.component.PaginationToolbar\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.task.SearchMergeRequestTask\nimport net.ntworld.mergeRequestIntegrationIde.ui.util.CustomSimpleToolWindowPanel\nimport javax.swing.JComponent\n\nabstract class AbstractMergeRequestCollection(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData\n) : MergeRequestCollectionUI {\n    abstract fun makeContent(): JComponent\n\n    abstract fun fetchDataStarted()\n\n    abstract fun fetchDataStopped()\n\n    abstract fun dataReceived(collection: List<MergeRequestInfo>)\n\n    open fun fetchDataError() {\n    }\n\n    override val eventDispatcher = EventDispatcher.create(MergeRequestCollectionEventListener::class.java)\n    private val mySplitter = CustomSimpleToolWindowPanel(vertical = true, borderless = true)\n    private val myPaginator: PaginationToolbar = projectServiceProvider.componentFactory.makePaginationToolbar(\n        displayRefreshButton = true\n    )\n    private val myContentDelegate = lazy {\n        makeContent()\n    }\n    private val myContent: JComponent by myContentDelegate\n    private var myFilter = GetMergeRequestFilter.make(\n        state = MergeRequestState.OPENED,\n        id = null,\n        search = \"\",\n        authorId = \"\",\n        assigneeId = \"\",\n        approverIds = listOf(),\n        sourceBranch = \"\"\n    )\n    private var myOrdering = MergeRequestOrdering.RECENTLY_UPDATED\n    private val myListener = object : SearchMergeRequestTask.Listener {\n        override fun onError(exception: Exception) {\n            myPaginator.enable()\n            fetchDataError()\n            fetchDataStopped()\n        }\n\n        override fun taskStarted() {\n            myPaginator.disable()\n            fetchDataStarted()\n        }\n        override fun taskEnded() {\n            myPaginator.enable()\n            fetchDataStopped()\n        }\n\n        override fun dataReceived(list: List<MergeRequestInfo>, page: Int, totalPages: Int, totalItems: Int) {\n            ApplicationManager.getApplication().invokeLater {\n                dataReceived(list)\n                myPaginator.setData(page, totalPages, totalItems)\n            }\n        }\n    }\n    private val myPaginatorListener = object: PaginationToolbar.Listener {\n        override fun changePage(page: Int) {\n            fetchPage(page)\n        }\n    }\n\n    init {\n        myPaginator.addListener(myPaginatorListener)\n        mySplitter.toolbar = myPaginator.component\n    }\n\n    final override fun setFilter(filter: GetMergeRequestFilter) {\n        myFilter = filter\n    }\n\n    final override fun setOrder(ordering: MergeRequestOrdering) {\n        myOrdering = ordering\n    }\n\n    override fun fetchData() = fetchPage(1)\n\n    private fun fetchPage(page: Int) {\n        if (providerData.status == ProviderStatus.ACTIVE) {\n            val task = SearchMergeRequestTask(\n                projectServiceProvider = projectServiceProvider,\n                providerData = providerData,\n                filtering = myFilter,\n                ordering = myOrdering,\n                listener = myListener\n            )\n            task.start(page)\n        }\n    }\n\n    override fun createComponent(): JComponent {\n        if (!myContentDelegate.isInitialized()) {\n            mySplitter.setContent(myContent)\n        }\n        return mySplitter\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestCollection.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest\n\nimport com.intellij.openapi.Disposable\nimport com.intellij.openapi.ui.SimpleToolWindowPanel\nimport com.intellij.openapi.util.Disposer\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.api.MergeRequestOrdering\nimport net.ntworld.mergeRequest.query.GetMergeRequestFilter\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifier\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifierAdapter\nimport javax.swing.JComponent\n\nclass MergeRequestCollection(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData\n) : MergeRequestCollectionUI, Disposable {\n    private val myComponent = SimpleToolWindowPanel(true, true)\n    private val myFilter: MergeRequestCollectionFilterUI by lazy {\n        MergeRequestCollectionFilter(projectServiceProvider, providerData)\n    }\n    private val myTree: MergeRequestCollectionUI by lazy {\n        val tree = MergeRequestCollectionTree(projectServiceProvider, providerData)\n        if (projectServiceProvider.applicationSettings.saveMRFilterState) {\n            val pair = projectServiceProvider.filtersStorage.find(providerData.key)\n            tree.setFilter(pair.first)\n            tree.setOrder(pair.second)\n        }\n\n        tree.fetchData()\n        tree\n    }\n    private val myFilterListener = object: MergeRequestCollectionFilterEventListener {\n        override fun filterChanged(filter: GetMergeRequestFilter) {\n            myTree.setFilter(filter)\n            myTree.fetchData()\n        }\n\n        override fun orderChanged(order: MergeRequestOrdering) {\n            myTree.setOrder(order)\n            myTree.fetchData()\n        }\n    }\n    private val myProjectNotifier = object : ProjectNotifierAdapter() {\n        override fun startCodeReview(reviewContext: ReviewContext) {\n            myComponent.isVisible = false\n        }\n\n        override fun stopCodeReview(reviewContext: ReviewContext) {\n            myComponent.isVisible = true\n        }\n    }\n    private val myConnection = projectServiceProvider.messageBus.connect()\n\n    init {\n        myComponent.toolbar = myFilter.createComponent()\n        myComponent.setContent(myTree.createComponent())\n        myFilter.eventDispatcher.addListener(myFilterListener)\n        myTree.eventDispatcher.addListener(object: MergeRequestCollectionEventListener {\n            override fun mergeRequestUnselected() {\n            }\n\n            override fun mergeRequestSelected(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) {\n            }\n        })\n        myConnection.subscribe(ProjectNotifier.TOPIC, myProjectNotifier)\n        Disposer.register(projectServiceProvider.project, this)\n    }\n\n    override val eventDispatcher = myTree.eventDispatcher\n\n    override fun setFilter(filter: GetMergeRequestFilter) {\n        myTree.setFilter(filter)\n    }\n\n    override fun setOrder(ordering: MergeRequestOrdering) {\n        myTree.setOrder(ordering)\n    }\n\n    override fun fetchData() {\n        myTree.fetchData()\n    }\n\n    override fun createComponent(): JComponent = myComponent\n\n    override fun dispose() {\n        myConnection.disconnect()\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestCollectionEventListener.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest\n\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport java.util.*\n\ninterface MergeRequestCollectionEventListener : EventListener {\n    fun mergeRequestUnselected()\n\n    fun mergeRequestSelected(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestCollectionFilter.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest\n\nimport com.intellij.icons.AllIcons\nimport com.intellij.ide.HelpTooltip\nimport com.intellij.openapi.actionSystem.*\nimport com.intellij.openapi.ui.popup.JBPopupFactory\nimport com.intellij.ui.SearchTextField\nimport com.intellij.ui.components.panels.Wrapper\nimport com.intellij.ui.popup.AbstractPopup\nimport com.intellij.util.EventDispatcher\nimport com.intellij.util.ui.JBUI\nimport net.miginfocom.swing.MigLayout\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.api.MergeRequestOrdering\nimport net.ntworld.mergeRequest.query.GetMergeRequestFilter\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.ui.panel.MergeRequestFilterPropertiesPanel\nimport java.awt.Point\nimport java.awt.event.KeyEvent\nimport java.awt.event.KeyListener\nimport javax.swing.Icon\nimport javax.swing.JComponent\nimport javax.swing.JPanel\nimport javax.swing.SwingUtilities\n\nclass MergeRequestCollectionFilter(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData\n) : MergeRequestCollectionFilterUI {\n    override val eventDispatcher = EventDispatcher.create(MergeRequestCollectionFilterEventListener::class.java)\n    private var myOrdering = MergeRequestOrdering.RECENTLY_UPDATED\n\n    private val myFilterPropertiesChanged: (() -> Unit) = {\n        val filter = myAdvanceFilterButton.buildFilter(mySearchField.text)\n        eventDispatcher.multicaster.filterChanged(filter)\n        saveFilterAndOrdering(filter, null)\n    }\n    private val mySearchField = MySearchTextField(this)\n    private val myMainActionGroup = DefaultActionGroup()\n    private val myToolbar = ActionManager.getInstance().createActionToolbar(\n        \"${MergeRequestCollectionFilter::class.java.canonicalName}${providerData.id}/toolbar\",\n        myMainActionGroup,\n        true\n    )\n    private val myAdvanceFilterButton: AdvanceFilterButton = AdvanceFilterButton(\n        projectServiceProvider, providerData, myToolbar.component, myFilterPropertiesChanged\n    )\n\n    private val myPanel by lazy {\n        val panel = JPanel(MigLayout(\"ins 0, fill\", \"[left]0[left, fill]push[right]\", \"center\"))\n        myMainActionGroup.add(myAdvanceFilterButton)\n\n        val rightCornerActionGroup = DefaultActionGroup()\n        rightCornerActionGroup.add(myOrderByOldestAction)\n        rightCornerActionGroup.add(myOrderByNewestAction)\n        rightCornerActionGroup.add(myOrderByRecentUpdatedAction)\n\n        val rightCornerToolbar = ActionManager.getInstance().createActionToolbar(\n            \"${MergeRequestCollectionFilter::class.java.canonicalName}${providerData.id}/toolbar-right\",\n            rightCornerActionGroup,\n            true\n        )\n\n        val textFilter = Wrapper(mySearchField)\n        textFilter.setVerticalSizeReferent(myToolbar.component)\n        textFilter.border = JBUI.Borders.emptyLeft(5)\n\n        panel.add(textFilter)\n        panel.add(myToolbar.component)\n        panel.add(rightCornerToolbar.component)\n        panel\n    }\n\n    private val myOrderByRecentUpdatedAction = OrderButton(\n        this, MergeRequestOrdering.RECENTLY_UPDATED,\n        \"Order by recent updated\", \"Order by recent updated\", AllIcons.Plugins.Updated\n    )\n    private val myOrderByNewestAction = OrderButton(\n        this, MergeRequestOrdering.NEWEST,\n        \"Order by newest\", \"Order by newest\", AllIcons.General.Modified\n    )\n    private val myOrderByOldestAction = OrderButton(\n        this, MergeRequestOrdering.OLDEST,\n        \"Order by oldest\", \"Order by oldest\", AllIcons.Vcs.History\n    )\n    private val myKeyListener = object : KeyListener {\n        private var searchingById: Boolean = false\n\n        override fun keyTyped(e: KeyEvent?) {\n        }\n\n        override fun keyPressed(e: KeyEvent?) {\n        }\n\n        override fun keyReleased(e: KeyEvent?) {\n            if (null === e || (e.keyCode != 10 && e.keyCode != 13)) {\n                handleSearchingByIdIfCurrentInputIsAnInteger()\n                return\n            }\n\n            val id = mySearchField.text.toIntOrNull()\n            val filter = if (null !== id)\n                myAdvanceFilterButton.buildFilterForSearchById(id)\n            else\n                myAdvanceFilterButton.buildFilter(mySearchField.text)\n            eventDispatcher.multicaster.filterChanged(filter)\n            saveFilterAndOrdering(filter, null)\n            mySearchField.addCurrentTextToHistory()\n        }\n\n        private fun handleSearchingByIdIfCurrentInputIsAnInteger() {\n            val id = mySearchField.text.toIntOrNull()\n            if (null !== id) {\n                // This is a special filter which filter by id, so do not save to history unless\n                val filter = myAdvanceFilterButton.buildFilterForSearchById(id)\n                eventDispatcher.multicaster.filterChanged(filter)\n                searchingById = true\n                return\n            }\n\n            if (searchingById) {\n                // If searching by id we need to reload the old result\n                val filter = myAdvanceFilterButton.buildFilter(\"\")\n                eventDispatcher.multicaster.filterChanged(filter)\n                searchingById = false\n            }\n        }\n    }\n\n    init {\n        mySearchField.addKeyboardListener(myKeyListener)\n        val pair = projectServiceProvider.filtersStorage.find(providerData.key)\n        myOrdering = pair.second\n        mySearchField.text = pair.first.search\n        myAdvanceFilterButton.setPreselectedValues(pair.first)\n    }\n\n    private fun saveFilterAndOrdering(filter: GetMergeRequestFilter?, ordering: MergeRequestOrdering?) {\n        projectServiceProvider.filtersStorage.save(\n            providerData.key,\n            if (null === filter) myAdvanceFilterButton.buildFilter(mySearchField.text) else filter,\n            if (null === ordering) myOrdering else ordering\n        )\n    }\n\n    override fun createComponent(): JComponent {\n        return myPanel\n    }\n\n    private class OrderButton(\n        private val self: MergeRequestCollectionFilter,\n        private val order: MergeRequestOrdering,\n        text: String,\n        desc: String,\n        icon: Icon\n    ) : ToggleAction(text, desc, icon) {\n        override fun isSelected(e: AnActionEvent): Boolean {\n            return self.myOrdering == order\n        }\n\n        override fun setSelected(e: AnActionEvent, state: Boolean) {\n            if (state) {\n                self.myOrdering = order\n                self.eventDispatcher.multicaster.orderChanged(order)\n                self.saveFilterAndOrdering(null, order)\n            }\n        }\n    }\n\n    private class MySearchTextField(private val self: MergeRequestCollectionFilter): SearchTextField() {\n        init {\n            setHistoryPropertyName(\"${MergeRequestCollectionFilter::class.java.canonicalName}:${self.providerData.id}\")\n        }\n\n        override fun onFieldCleared() {\n            val filter = self.myAdvanceFilterButton.buildFilter(self.mySearchField.text)\n            self.eventDispatcher.multicaster.filterChanged(filter)\n            self.saveFilterAndOrdering(filter, null)\n        }\n    }\n\n    private class AdvanceFilterButton(\n        private val projectServiceProvider: ProjectServiceProvider,\n        private val providerData: ProviderData,\n        private val preferableFocusComponent: JComponent,\n        private val onChanged: (() -> Unit)\n    ) : AnAction(null, null, AllIcons.Actions.Properties) {\n        private var myIsReady = false\n        private var myPreselectedValue: GetMergeRequestFilter? = null\n        private val myFilterPropertiesReady: (() -> Unit) = {\n            myIsReady = true\n            val preselected = myPreselectedValue\n            if (null !== preselected) {\n                myFilterPropertiesPanel.setPreselectedValues(preselected)\n                myPreselectedValue = null\n            }\n        }\n        private val myFilterPropertiesPanel = MergeRequestFilterPropertiesPanel(\n            projectServiceProvider, providerData, onChanged, myFilterPropertiesReady\n        )\n\n        override fun actionPerformed(e: AnActionEvent) {\n            val component = myFilterPropertiesPanel.createComponent()\n            val popup = JBPopupFactory.getInstance()\n                .createComponentPopupBuilder(\n                    component,\n                    preferableFocusComponent\n                )\n                .setResizable(true)\n                .setMovable(false)\n                .setRequestFocus(true)\n                .createPopup()\n\n            HelpTooltip.setMasterPopup(preferableFocusComponent, popup)\n            val point = findPopupPoint(preferableFocusComponent)\n            (popup as AbstractPopup).show(preferableFocusComponent, point.x, point.y, false)\n        }\n\n        override fun update(e: AnActionEvent) {\n            super.update(e)\n            e.presentation.isEnabled = myIsReady\n        }\n\n        private fun findPopupPoint(reference: JComponent): Point {\n            val visibleBounds = reference.visibleRect\n            val containerScreenPoint = visibleBounds.location\n            SwingUtilities.convertPointToScreen(containerScreenPoint, reference)\n            visibleBounds.location = containerScreenPoint\n            return Point(\n                visibleBounds.x,\n                visibleBounds.y + reference.height\n            )\n        }\n\n        fun buildFilter(search: String) = myFilterPropertiesPanel.buildFilter(search)\n\n        fun buildFilterForSearchById(id: Int) = myFilterPropertiesPanel.buildFilterForSearchById(id)\n\n        fun setPreselectedValues(value: GetMergeRequestFilter) {\n            if (myIsReady) {\n                myFilterPropertiesPanel.setPreselectedValues(value)\n                myPreselectedValue = null\n            } else {\n                myPreselectedValue = value\n            }\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestCollectionFilterEventListener.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest\n\nimport net.ntworld.mergeRequest.api.MergeRequestOrdering\nimport net.ntworld.mergeRequest.query.GetMergeRequestFilter\nimport java.util.*\n\ninterface MergeRequestCollectionFilterEventListener: EventListener {\n\n    fun filterChanged(filter: GetMergeRequestFilter)\n\n    fun orderChanged(order: MergeRequestOrdering)\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestCollectionFilterUI.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest\n\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\n\ninterface MergeRequestCollectionFilterUI : Component {\n    val eventDispatcher: EventDispatcher<MergeRequestCollectionFilterEventListener>\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestCollectionTree.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest\n\nimport com.intellij.ide.util.treeView.NodeRenderer\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.ui.ScrollPaneFactory\nimport com.intellij.ui.treeStructure.Tree\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport java.awt.Component\nimport javax.swing.JComponent\nimport javax.swing.JTree\nimport javax.swing.event.TreeSelectionEvent\nimport javax.swing.event.TreeSelectionListener\nimport javax.swing.tree.*\n\nclass MergeRequestCollectionTree(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData\n) : AbstractMergeRequestCollection(projectServiceProvider, providerData), TreeCellRenderer {\n\n    private val myTree = Tree()\n    private val myRoot = DefaultMutableTreeNode()\n    private val myRenderer = NodeRenderer()\n    private val myModel = DefaultTreeModel(myRoot)\n    private val myTreeSelectionListener = object : TreeSelectionListener {\n        override fun valueChanged(e: TreeSelectionEvent?) {\n            if (null === e) {\n                return\n            }\n\n            val path = e.path\n            when (val node = (path.lastPathComponent as DefaultMutableTreeNode).userObject) {\n                is MergeRequestCollectionTreeNode -> {\n                    eventDispatcher.multicaster.mergeRequestSelected(\n                        providerData = providerData,\n                        mergeRequestInfo = node.mergeRequestInfo\n                    )\n                }\n            }\n        }\n    }\n\n    init {\n        val treeSelectionModel = DefaultTreeSelectionModel()\n        treeSelectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION\n\n        myTree.model = myModel\n        myTree.cellRenderer = this\n        myTree.isRootVisible = false\n        myTree.selectionModel = treeSelectionModel\n        myTree.addTreeSelectionListener(myTreeSelectionListener)\n    }\n\n    override fun makeContent(): JComponent {\n        return ScrollPaneFactory.createScrollPane(myTree)\n    }\n\n    override fun fetchDataStarted() {\n        myRoot.removeAllChildren()\n        myTree.isVisible = false\n    }\n\n    override fun fetchDataStopped() {\n        myTree.isVisible = true\n    }\n\n    override fun dataReceived(collection: List<MergeRequestInfo>) {\n        ApplicationManager.getApplication().invokeLater {\n            myRoot.removeAllChildren()\n            collection.forEach {\n                val item = MergeRequestCollectionTreeNode(\n                    providerData,\n                    projectServiceProvider.project,\n                    it\n                )\n                myRoot.add(DefaultMutableTreeNode(item))\n                item.update()\n            }\n            myModel.nodeStructureChanged(myRoot)\n            myTree.isVisible = true\n        }\n    }\n\n    override fun getTreeCellRendererComponent(\n        tree: JTree?,\n        value: Any?,\n        selected: Boolean,\n        expanded: Boolean,\n        leaf: Boolean,\n        row: Int,\n        hasFocus: Boolean\n    ): Component {\n        return myRenderer.getTreeCellRendererComponent(\n            tree,\n            value,\n            selected,\n            expanded,\n            leaf,\n            row,\n            hasFocus\n        )\n    }\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestCollectionTreeNode.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest\n\nimport com.intellij.ide.projectView.PresentationData\nimport com.intellij.ide.util.treeView.PresentableNodeDescriptor\nimport com.intellij.openapi.project.Project as IdeaProject\nimport com.intellij.ui.SimpleTextAttributes\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\n\nclass MergeRequestCollectionTreeNode(\n    private val providerData: ProviderData,\n    ideaProject: IdeaProject,\n    var mergeRequestInfo: MergeRequestInfo\n): PresentableNodeDescriptor<MergeRequestInfo>(ideaProject, null) {\n    override fun update(presentation: PresentationData) {\n        val id = providerData.info.formatMergeRequestId(mergeRequestInfo.id)\n        presentation.addText(\"$id · \", SimpleTextAttributes.GRAYED_ATTRIBUTES)\n\n        presentation.addText(mergeRequestInfo.title, SimpleTextAttributes.REGULAR_ATTRIBUTES)\n    }\n\n    override fun getElement(): MergeRequestInfo = mergeRequestInfo\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestCollectionUI.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest\n\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.api.MergeRequestOrdering\nimport net.ntworld.mergeRequest.query.GetMergeRequestFilter\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\n\ninterface MergeRequestCollectionUI : Component {\n    val eventDispatcher: EventDispatcher<MergeRequestCollectionEventListener>\n\n    fun setFilter(filter: GetMergeRequestFilter)\n\n    fun setOrder(ordering: MergeRequestOrdering)\n\n    fun fetchData()\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestDetails.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest\n\nimport com.intellij.icons.AllIcons\nimport com.intellij.openapi.Disposable\nimport com.intellij.openapi.actionSystem.ActionGroup\nimport com.intellij.openapi.actionSystem.DefaultActionGroup\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.ui.ScrollPaneFactory\nimport com.intellij.ui.tabs.TabInfo\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequestIntegrationIde.component.Icons\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.CommentsTabFactory\nimport net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.CommentsTabPresenter\nimport net.ntworld.mergeRequestIntegrationIde.task.FindApprovalTask\nimport net.ntworld.mergeRequestIntegrationIde.task.FindMergeRequestTask\nimport net.ntworld.mergeRequestIntegrationIde.task.GetCommitsTask\nimport net.ntworld.mergeRequestIntegrationIde.task.GetPipelinesTask\nimport net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.*\nimport net.ntworld.mergeRequestIntegrationIde.ui.service.FetchService\nimport net.ntworld.mergeRequestIntegrationIde.ui.util.Tabs\nimport net.ntworld.mergeRequestIntegrationIde.ui.util.TabsUI\nimport javax.swing.JComponent\n\nclass MergeRequestDetails(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val disposable: Disposable,\n    private val providerData: ProviderData\n) : MergeRequestDetailsUI {\n    private val myToolbars = mutableListOf<MergeRequestDetailsToolbarUI>()\n\n    private val myInfoTab : MergeRequestInfoTabUI = MergeRequestInfoTab()\n    private val myInfoTabInfo: TabInfo by lazy {\n        val tabInfo = TabInfo(\n            ScrollPaneFactory.createScrollPane(myInfoTab.createComponent())\n        )\n        tabInfo.text = \"Info\"\n        tabInfo.icon = AllIcons.General.ShowInfos\n\n        tabInfo\n    }\n\n    private val myDescriptionTab: MergeRequestDescriptionTabUI = MergeRequestDescriptionTab()\n    private val myDescriptionTabInfo: TabInfo by lazy {\n        val tabInfo = TabInfo(\n            ScrollPaneFactory.createScrollPane(myDescriptionTab.createComponent())\n        )\n        tabInfo.text = \"Description\"\n        tabInfo.icon = Icons.Description\n\n        tabInfo\n    }\n\n    private val myCommitsTab: MergeRequestCommitsTabUI = MergeRequestCommitsTab(projectServiceProvider)\n    private val myCommitsTabInfo: TabInfo by lazy {\n        val tabInfo = TabInfo(myCommitsTab.createComponent())\n        tabInfo.text = \"Commit\"\n        tabInfo.icon = AllIcons.Vcs.CommitNode\n\n        tabInfo\n    }\n\n    private val myCommentsTabPresenter: CommentsTabPresenter by lazy {\n        val model = CommentsTabFactory.makeCommentsTabModel(projectServiceProvider, providerData)\n        val view = CommentsTabFactory.makeCommentsTabView(projectServiceProvider, providerData)\n        CommentsTabFactory.makeCommentsTabPresenter(projectServiceProvider, model, view)\n    }\n\n    private val myTabs: TabsUI by lazy {\n        val tabs = Tabs(projectServiceProvider.project, disposable)\n        tabs.setCommonCenterActionGroupFactory(this::toolbarCommonCenterActionGroupFactory)\n        tabs.setCommonSideComponentFactory(this::toolbarCommonSideComponentFactory)\n        tabs\n    }\n\n    private val myFindMRListener = object : FindMergeRequestTask.Listener {\n        override fun dataReceived(mergeRequest: MergeRequest) {\n            ApplicationManager.getApplication().invokeLater {\n                setMergeRequest(mergeRequest)\n            }\n        }\n    }\n    private val myGetPipelinesListener = object : GetPipelinesTask.Listener {\n        override fun onError(exception: Exception) {\n            println(exception)\n        }\n\n        override fun dataReceived(mergeRequestInfo: MergeRequestInfo, pipelines: List<Pipeline>) {\n            ApplicationManager.getApplication().invokeLater {\n                myToolbars.forEach {\n                    it.setPipelines(mergeRequestInfo, pipelines)\n                }\n            }\n        }\n    }\n    private val myGetCommitsListener = object : GetCommitsTask.Listener {\n        override fun onError(exception: Exception) {\n            println(exception)\n        }\n\n        override fun taskStarted() {\n            myCommitsTab.clear()\n        }\n\n        override fun dataReceived(mergeRequestInfo: MergeRequestInfo, commits: List<Commit>) {\n            ApplicationManager.getApplication().invokeLater {\n                myToolbars.forEach {\n                    it.setCommits(mergeRequestInfo, commits)\n                }\n                myCommitsTab.setCommits(providerData, mergeRequestInfo, commits)\n                if (commits.isEmpty()) {\n                    myCommitsTabInfo.text = \"Commits\"\n                } else {\n                    myCommitsTabInfo.text = \"Commits · ${commits.size}\"\n                }\n            }\n        }\n    }\n    private val mySelectCommitsListener = object: MergeRequestCommitsTabUI.Listener {\n        override fun commitSelected(\n            providerData: ProviderData,\n            mergeRequestInfo: MergeRequestInfo,\n            commits: List<Commit>\n        ) {\n            projectServiceProvider.reviewContextManager.updateReviewingCommits(\n                providerData.id, mergeRequestInfo.id, commits\n            )\n            myToolbars.forEach {\n                it.setCommitsForReviewing(mergeRequestInfo, commits)\n            }\n        }\n    }\n\n    private val myFindApprovalListener = object : FindApprovalTask.Listener {\n        override fun onError(exception: Exception) {\n            println(exception)\n        }\n\n        override fun dataReceived(mergeRequestInfo: MergeRequestInfo, approval: Approval) {\n            ApplicationManager.getApplication().invokeLater {\n                myToolbars.forEach {\n                    it.setApproval(mergeRequestInfo, approval)\n                }\n            }\n        }\n    }\n\n    private val myToolbarListener = object : MergeRequestDetailsToolbarUI.Listener {\n        override fun refreshRequested(mergeRequestInfo: MergeRequestInfo) {\n            setMergeRequestInfo(mergeRequestInfo)\n        }\n    }\n\n    init {\n        myTabs.addTab(myInfoTabInfo)\n        myTabs.addTab(myDescriptionTabInfo)\n        myTabs.addTab(myCommentsTabPresenter.tabInfo)\n        myTabs.addTab(myCommitsTabInfo)\n\n        myCommitsTab.dispatcher.addListener(mySelectCommitsListener)\n    }\n\n    override fun hide() {\n        myTabs.component.isVisible = false\n    }\n\n    override fun setMergeRequest(mergeRequest: MergeRequest) {\n        myInfoTab.setMergeRequest(mergeRequest)\n        myToolbars.forEach {\n            it.setMergeRequest(mergeRequest)\n        }\n    }\n\n    override fun setMergeRequestInfo(mergeRequestInfo: MergeRequestInfo) {\n        projectServiceProvider.reviewContextManager.initContext(providerData, mergeRequestInfo, true)\n        FetchService.start(\n            projectServiceProvider.applicationServiceProvider,\n            projectServiceProvider.project, providerData, mergeRequestInfo\n        )\n\n        myInfoTab.setMergeRequestInfo(providerData, mergeRequestInfo)\n        myDescriptionTab.setMergeRequestInfo(providerData, mergeRequestInfo)\n\n        myCommentsTabPresenter.model.mergeRequestInfo = mergeRequestInfo\n\n        myTabs.getTabs().select(myInfoTabInfo, false)\n        myTabs.component.isVisible = true\n        myToolbars.forEach {\n            it.setMergeRequestInfo(mergeRequestInfo)\n        }\n        FindMergeRequestTask(projectServiceProvider, providerData, mergeRequestInfo, myFindMRListener).start()\n        GetPipelinesTask(projectServiceProvider, providerData, mergeRequestInfo, myGetPipelinesListener).start()\n        GetCommitsTask(projectServiceProvider, providerData, mergeRequestInfo, myGetCommitsListener).start()\n        if (providerData.hasApprovalFeature) {\n            FindApprovalTask(projectServiceProvider, providerData, mergeRequestInfo, myFindApprovalListener).start()\n        }\n    }\n\n    override fun createComponent(): JComponent = myTabs.component\n\n    private fun toolbarCommonCenterActionGroupFactory(): ActionGroup {\n        return DefaultActionGroup()\n    }\n\n    private fun toolbarCommonSideComponentFactory(): JComponent {\n        val toolbar: MergeRequestDetailsToolbarUI = MergeRequestDetailsToolbar(\n            projectServiceProvider, providerData, this\n        )\n        toolbar.dispatcher.addListener(myToolbarListener)\n        myToolbars.add(toolbar)\n        return toolbar.createComponent()\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestDetailsToolbar.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest\n\nimport com.intellij.icons.AllIcons\nimport com.intellij.ide.BrowserUtil\nimport com.intellij.ide.HelpTooltip\nimport com.intellij.openapi.actionSystem.*\nimport com.intellij.openapi.ui.popup.JBPopupFactory\nimport com.intellij.ui.popup.AbstractPopup\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequest.command.ApproveMergeRequestCommand\nimport net.ntworld.mergeRequest.command.UnapproveMergeRequestCommand\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegrationIde.component.Icons\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.ui.panel.ApprovalPanel\nimport net.ntworld.mergeRequestIntegrationIde.ui.service.CodeReviewService\nimport net.ntworld.mergeRequestIntegrationIde.ui.util.findVisibilityIconAndTextForApproval\nimport java.awt.Dimension\nimport java.awt.Point\nimport javax.swing.JComponent\nimport javax.swing.SwingUtilities\n\nclass MergeRequestDetailsToolbar(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData,\n    private val details: MergeRequestDetailsUI\n) : MergeRequestDetailsToolbarUI {\n    override val dispatcher = EventDispatcher.create(MergeRequestDetailsToolbarUI.Listener::class.java)\n\n    private val myActionGroup = DefaultActionGroup()\n    private var myMergeRequest: MergeRequest? = null\n    private var myMergeRequestInfo: MergeRequestInfo? = null\n\n    private val myRefreshAction = MyRefreshAction(this)\n\n    private var myUpVotes = -1\n    private var myDownVotes = -1\n    private val myUpVotesAction = MyUpVotesAction(this)\n    private val myDownVotesAction = MyDownVotesAction(this)\n    private var myPipelines: List<Pipeline> = listOf()\n    private val myPipelineAction = MyPipelineAction(this)\n\n    private val myApprovalPanel = ApprovalPanel()\n    private val myApprovalPanelListener = object : ApprovalPanel.Listener {\n        override fun onApproveClicked() {\n            val approval = myApproval\n            val mr = myMergeRequest\n            if (null !== approval && null !== mr) {\n                myApprovalPanel.hide()\n                projectServiceProvider.infrastructure.commandBus() process ApproveMergeRequestCommand.make(\n                    providerId = providerData.id,\n                    mergeRequestId = mr.id,\n                    sha = mr.diffReference!!.headHash\n                )\n                details.setMergeRequestInfo(mr)\n            }\n        }\n\n        override fun onUnapproveClicked() {\n            val approval = myApproval\n            val mr = myMergeRequest\n            if (null !== approval && null !== mr) {\n                myApprovalPanel.hide()\n                projectServiceProvider.infrastructure.commandBus() process UnapproveMergeRequestCommand.make(\n                    providerId = providerData.id,\n                    mergeRequestId = mr.id\n                )\n                details.setMergeRequestInfo(mr)\n            }\n        }\n    }\n    private var myApproval: Approval? = null\n    private val myApprovalAction = MyApprovalAction(this)\n\n    private var myUrl: String = \"\"\n\n    private val myOpenUrlAction = MyOpenUrlAction(this)\n\n    private var myState: MergeRequestState = MergeRequestState.ALL\n    private val myStateAction = MyStateAction(this)\n\n    private var myCommits: List<Commit> = listOf()\n    private var myReviewCommits: List<Commit> = listOf()\n    private val myCodeReviewAction = MyCodeReviewAction(this)\n\n    private val myToolbar by lazy {\n        myActionGroup.add(myRefreshAction)\n        myActionGroup.addSeparator()\n        myActionGroup.add(myUpVotesAction)\n        myActionGroup.add(myDownVotesAction)\n        myActionGroup.add(myStateAction)\n        myActionGroup.add(myPipelineAction)\n        myActionGroup.add(myApprovalAction)\n        myActionGroup.addSeparator()\n        myActionGroup.add(myOpenUrlAction)\n        myActionGroup.addSeparator()\n        myActionGroup.add(myCodeReviewAction)\n\n        val toolbar = ActionManager.getInstance().createActionToolbar(\n            \"${MergeRequestDetailsToolbar::class.java.canonicalName}/toolbar-right\", myActionGroup, true\n        )\n        toolbar\n    }\n\n    init {\n        myApprovalPanel.addListener(myApprovalPanelListener)\n    }\n\n    override fun setPipelines(mergeRequestInfo: MergeRequestInfo, pipelines: List<Pipeline>) {\n        val currentMR = myMergeRequestInfo\n        if (currentMR != null && currentMR.id == mergeRequestInfo.id) {\n            myPipelines = pipelines\n        }\n    }\n\n    override fun setCommits(mergeRequestInfo: MergeRequestInfo, commits: List<Commit>) {\n        val currentMR = myMergeRequestInfo\n        if (currentMR != null && currentMR.id == mergeRequestInfo.id) {\n            myCommits = commits\n            myReviewCommits = commits\n        }\n    }\n\n    override fun setCommitsForReviewing(mergeRequestInfo: MergeRequestInfo, commits: List<Commit>) {\n        val currentMR = myMergeRequestInfo\n        if (currentMR != null && currentMR.id == mergeRequestInfo.id) {\n            myReviewCommits = commits\n        }\n    }\n\n    override fun setApproval(mergeRequestInfo: MergeRequestInfo, approval: Approval) {\n        val currentMR = myMergeRequestInfo\n        if (currentMR != null && currentMR.id == mergeRequestInfo.id) {\n            myApproval = approval\n        }\n    }\n\n    override fun setMergeRequest(mergeRequest: MergeRequest) {\n        myMergeRequest = mergeRequest\n        myUpVotes = mergeRequest.upVotes\n        myDownVotes = mergeRequest.downVotes\n        myState = mergeRequest.state\n        myUrl = mergeRequest.url\n        myToolbar.component.isVisible = true\n    }\n\n    override fun setMergeRequestInfo(mergeRequestInfo: MergeRequestInfo) {\n        myMergeRequestInfo = mergeRequestInfo\n        myPipelines = listOf()\n        myCommits = listOf()\n        myReviewCommits = listOf()\n        myApproval = null\n        myToolbar.component.isVisible = false\n    }\n\n    override fun createComponent(): JComponent = myToolbar.component\n\n    private class MyRefreshAction(private val self: MergeRequestDetailsToolbar) :\n        AnAction(\"Refresh\", \"Refresh merge request info\", AllIcons.Actions.Refresh) {\n        override fun actionPerformed(e: AnActionEvent) {\n            val mergeRequestInfo = self.myMergeRequestInfo\n            if (null !== mergeRequestInfo) {\n                self.dispatcher.multicaster.refreshRequested(mergeRequestInfo)\n            }\n        }\n\n        override fun update(e: AnActionEvent) {\n            e.presentation.isVisible = !self.projectServiceProvider.isDoingCodeReview()\n        }\n    }\n\n    private class MyUpVotesAction(private val self: MergeRequestDetailsToolbar) :\n        AnAction(null, \"Up votes\", Icons.ThumbsUp) {\n        override fun actionPerformed(e: AnActionEvent) {\n        }\n\n        override fun update(e: AnActionEvent) {\n            val enabled = self.projectServiceProvider.applicationSettings.displayUpVotesAndDownVotes\n            e.presentation.text = self.myUpVotes.toString()\n            e.presentation.isVisible = enabled && self.myUpVotes >= 0\n        }\n\n        override fun displayTextInToolbar(): Boolean = true\n        override fun useSmallerFontForTextInToolbar(): Boolean = true\n    }\n\n    private class MyDownVotesAction(private val self: MergeRequestDetailsToolbar) :\n        AnAction(null, \"Down votes\", Icons.ThumbsDown) {\n        override fun actionPerformed(e: AnActionEvent) {\n        }\n\n        override fun update(e: AnActionEvent) {\n            val enabled = self.projectServiceProvider.applicationSettings.displayUpVotesAndDownVotes\n            e.presentation.text = self.myDownVotes.toString()\n            e.presentation.isVisible = enabled && self.myDownVotes >= 0\n        }\n\n        override fun displayTextInToolbar(): Boolean = true\n        override fun useSmallerFontForTextInToolbar(): Boolean = true\n    }\n\n    private class MyPipelineAction(private val self: MergeRequestDetailsToolbar) : AnAction(\n        null, \"Pipeline status · Click to open pipeline in browser\", null\n    ) {\n        override fun actionPerformed(e: AnActionEvent) {\n            val pipelines = self.myPipelines\n            if (pipelines.isEmpty()) {\n                return\n            }\n            BrowserUtil.open(pipelines.first().url)\n        }\n\n        override fun update(e: AnActionEvent) {\n            if (self.myPipelines.isEmpty()) {\n                e.presentation.isVisible = false\n                return\n            }\n\n            val status = self.myPipelines.first().status\n            e.presentation.icon = when (status) {\n                PipelineStatus.FAILED -> Icons.PipelineFailed\n                PipelineStatus.RUNNING -> Icons.PipelineRunning\n                PipelineStatus.PARTIAL_FAILED -> Icons.PipelineFailed\n                PipelineStatus.SUCCESS -> Icons.PipelineSuccess\n                PipelineStatus.UNKNOWN -> Icons.PipelineRunning\n            }\n            e.presentation.text = when (status) {\n                PipelineStatus.FAILED -> \"failed\"\n                PipelineStatus.RUNNING -> \"running\"\n                PipelineStatus.PARTIAL_FAILED -> \"partial failed\"\n                PipelineStatus.SUCCESS -> \"passed\"\n                PipelineStatus.UNKNOWN -> \"\"\n            }\n\n            e.presentation.isVisible = status != PipelineStatus.UNKNOWN\n        }\n\n        override fun displayTextInToolbar(): Boolean = true\n        override fun useSmallerFontForTextInToolbar(): Boolean = true\n    }\n\n    private class MyApprovalAction(private val self: MergeRequestDetailsToolbar) : AnAction(null, \"Approval\", null) {\n        override fun actionPerformed(e: AnActionEvent) {\n            if (!self.myApprovalPanel.shouldDisplayApprovalPanel()) {\n                return\n            }\n            val reference = self.createComponent() // toolbar component\n            val popup = JBPopupFactory.getInstance()\n                .createComponentPopupBuilder(\n                    self.myApprovalPanel.createComponent(),\n                    reference\n                )\n                .setResizable(true)\n                .setMovable(false)\n                .setRequestFocus(true)\n                .createPopup()\n\n            HelpTooltip.setMasterPopup(reference, popup)\n            val point = findPopupPoint(reference, popup.content.preferredSize)\n            (popup as AbstractPopup).show(reference, point.x, point.y, false)\n        }\n\n        private fun findPopupPoint(reference: JComponent, popup: Dimension): Point {\n            val visibleBounds = reference.visibleRect\n            val containerScreenPoint = visibleBounds.location\n            SwingUtilities.convertPointToScreen(containerScreenPoint, reference)\n            visibleBounds.location = containerScreenPoint\n            return Point(\n                visibleBounds.x + reference.width - popup.width,\n                visibleBounds.y + reference.height\n            )\n        }\n\n        override fun update(e: AnActionEvent) {\n            val approval = self.myApproval\n            if (null === approval) {\n                e.presentation.isVisible = false\n                return\n            }\n\n            self.myApprovalPanel.hide()\n            self.myApprovalPanel.setApproval(approval)\n            val triple = approval.findVisibilityIconAndTextForApproval()\n            e.presentation.isVisible = triple.first\n            e.presentation.icon = triple.second\n            e.presentation.text = triple.third\n        }\n\n        override fun displayTextInToolbar(): Boolean = true\n        override fun useSmallerFontForTextInToolbar(): Boolean = true\n    }\n\n    private class MyOpenUrlAction(private val self: MergeRequestDetailsToolbar) :\n        AnAction(\"Open merge request in browser\", \"Open merge request in browser\", Icons.ExternalLink) {\n        override fun actionPerformed(e: AnActionEvent) {\n            BrowserUtil.open(self.myUrl)\n        }\n\n        override fun update(e: AnActionEvent) {\n            e.presentation.isVisible = self.myUrl.isNotEmpty()\n        }\n    }\n\n    private class MyStateAction(private val self: MergeRequestDetailsToolbar) :\n        AnAction(null, \"Merge request state\", null) {\n        override fun actionPerformed(e: AnActionEvent) {\n        }\n\n        override fun update(e: AnActionEvent) {\n            val enabled = self.projectServiceProvider.applicationSettings.displayMergeRequestState\n            if (!enabled) {\n                e.presentation.isVisible = false\n                return\n            }\n            when (self.myState) {\n                MergeRequestState.ALL -> {\n                    e.presentation.isVisible = false\n                }\n                MergeRequestState.OPENED -> {\n                    e.presentation.icon = Icons.StateOpened\n                    e.presentation.text = \"opened\"\n                    e.presentation.isVisible = true\n                }\n                MergeRequestState.CLOSED -> {\n                    e.presentation.icon = Icons.StateClosed\n                    e.presentation.text = \"closed\"\n                    e.presentation.isVisible = true\n                }\n                MergeRequestState.MERGED -> {\n                    e.presentation.icon = Icons.StateMerged\n                    e.presentation.text = \"merged\"\n                    e.presentation.isVisible = true\n                }\n            }\n        }\n\n        override fun displayTextInToolbar(): Boolean = true\n        override fun useSmallerFontForTextInToolbar(): Boolean = true\n    }\n\n    private class MyCodeReviewAction(private val self: MergeRequestDetailsToolbar) : ToggleAction(null, null, null) {\n        override fun isSelected(e: AnActionEvent): Boolean {\n            return self.projectServiceProvider.isDoingCodeReview()\n        }\n\n        override fun setSelected(e: AnActionEvent, state: Boolean) {\n            val mr = self.myMergeRequest\n            if (null === mr) {\n                return\n            }\n            if (state) {\n                CodeReviewService.start(self.projectServiceProvider, self.providerData, mr, self.myReviewCommits)\n            } else {\n                CodeReviewService.stop(self.projectServiceProvider, self.providerData, mr)\n            }\n        }\n\n        override fun update(e: AnActionEvent) {\n            val mr = self.myMergeRequest\n            if (null === mr || self.myReviewCommits.isEmpty()) {\n                e.presentation.isVisible = false\n                return\n            }\n\n            when (self.myState) {\n                MergeRequestState.ALL, MergeRequestState.CLOSED, MergeRequestState.MERGED -> {\n                    e.presentation.isVisible = false\n                }\n                MergeRequestState.OPENED -> {\n                    e.presentation.text = getCodeReviewText()\n                    e.presentation.description = \"Open diff in IDE and review\"\n                    e.presentation.isVisible = true\n                }\n                // TODO: Support view diff\n                // MergeRequestState.CLOSED, MergeRequestState.MERGED -> {\n                //     e.presentation.text = \"View Diff\"\n                //     e.presentation.isVisible = true\n                // }\n            }\n\n            val isDoingCodeReview = self.projectServiceProvider.isDoingCodeReview()\n            if (isDoingCodeReview) {\n                val draftCount = self.projectServiceProvider.reviewContextManager.getDraftCommentsCount(\n                    self.providerData.id, mr.id\n                )\n                if (draftCount > 0) {\n                    e.presentation.text = if (draftCount < 2)\n                        \"Publish $draftCount Comment and Stop Reviewing\"\n                    else\n                        \"Publish $draftCount Comments and Stop Reviewing\"\n                    e.presentation.description = \"Publish all draft comments, stop reviewing and show other MRs\"\n                } else {\n                    e.presentation.text = \"Stop Reviewing\"\n                    e.presentation.description = \"End reviewing and show other MRs\"\n                }\n                e.presentation.isEnabled = self.projectServiceProvider.reviewContextManager.isDoingCodeReview(\n                    self.providerData.id, mr.id\n                )\n            } else {\n                e.presentation.isEnabled = true\n            }\n        }\n\n        private fun getCodeReviewText(): String {\n            if (self.myCommits.size == self.myReviewCommits.size) {\n                return \"Code Review\"\n            }\n            return \"Code Review ${self.myReviewCommits.size}/${self.myCommits.size} commits\"\n        }\n\n        override fun displayTextInToolbar(): Boolean = true\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestDetailsToolbarUI.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest\n\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport java.util.*\n\ninterface MergeRequestDetailsToolbarUI : Component {\n    val dispatcher: EventDispatcher<Listener>\n\n    fun setPipelines(mergeRequestInfo: MergeRequestInfo, pipelines: List<Pipeline>)\n\n    fun setCommits(mergeRequestInfo: MergeRequestInfo, commits: List<Commit>)\n\n    fun setCommitsForReviewing(mergeRequestInfo: MergeRequestInfo, commits: List<Commit>)\n\n    fun setApproval(mergeRequestInfo: MergeRequestInfo, approval: Approval)\n\n    fun setMergeRequest(mergeRequest: MergeRequest)\n\n    fun setMergeRequestInfo(mergeRequestInfo: MergeRequestInfo)\n\n    interface Listener: EventListener {\n        fun refreshRequested(mergeRequestInfo: MergeRequestInfo)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/MergeRequestDetailsUI.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest\n\nimport net.ntworld.mergeRequest.MergeRequest\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\n\ninterface MergeRequestDetailsUI : Component {\n\n    fun hide()\n\n    fun setMergeRequest(mergeRequest: MergeRequest)\n\n    fun setMergeRequestInfo(mergeRequestInfo: MergeRequestInfo)\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/MergeRequestCommitsTab.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab\n\nimport com.intellij.openapi.Disposable\nimport com.intellij.openapi.util.Disposer\nimport com.intellij.ui.OnePixelSplitter\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.Commit\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifier\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifierAdapter\nimport net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit.CommitChanges\nimport net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit.CommitChangesUI\nimport net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit.CommitCollection\nimport net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit.CommitCollectionUI\nimport javax.swing.JComponent\n\nclass MergeRequestCommitsTab(\n    private val projectServiceProvider: ProjectServiceProvider\n) : MergeRequestCommitsTabUI, Disposable {\n    override val dispatcher = EventDispatcher.create(MergeRequestCommitsTabUI.Listener::class.java)\n\n    private val mySplitter = OnePixelSplitter(\n        MergeRequestCommitsTab::class.java.canonicalName,\n        0.5f\n    )\n    private val myCollection: CommitCollectionUI = CommitCollection()\n    private val myChanges: CommitChangesUI = CommitChanges(projectServiceProvider)\n    private val myCollectionListener = object : CommitCollectionUI.Listener {\n        override fun commitsSelected(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: List<Commit>) {\n            if (commits.isEmpty()) {\n                myChanges.clear()\n            } else {\n                myChanges.updateSelectedCommits(providerData, mergeRequestInfo, commits)\n            }\n            dispatcher.multicaster.commitSelected(providerData, mergeRequestInfo, commits)\n        }\n    }\n    private val myProjectNotifier = object : ProjectNotifierAdapter() {\n        override fun startCodeReview(reviewContext: ReviewContext) {\n            myCollection.disable()\n            myChanges.disable()\n        }\n\n        override fun stopCodeReview(reviewContext: ReviewContext) {\n            myCollection.enable()\n            myChanges.enable()\n        }\n    }\n    private val myConnection = projectServiceProvider.messageBus.connect()\n\n    init {\n        mySplitter.firstComponent = myCollection.createComponent()\n        mySplitter.secondComponent = myChanges.createComponent()\n\n        myCollection.dispatcher.addListener(myCollectionListener)\n        myConnection.subscribe(ProjectNotifier.TOPIC, myProjectNotifier)\n        Disposer.register(projectServiceProvider.project, this)\n    }\n\n    override fun clear() {\n        myCollection.clear()\n    }\n\n    override fun setCommits(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: List<Commit>) {\n        myCollection.setCommits(providerData, mergeRequestInfo, commits)\n        myChanges.setCommits(providerData, mergeRequestInfo, commits)\n    }\n\n    override fun createComponent(): JComponent = mySplitter\n\n    override fun dispose() {\n        myConnection.disconnect()\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/MergeRequestCommitsTabUI.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab\n\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.Commit\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport java.util.*\n\ninterface MergeRequestCommitsTabUI : Component {\n    val dispatcher: EventDispatcher<Listener>\n\n    fun clear()\n\n    fun setCommits(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: List<Commit>)\n\n    interface Listener : EventListener {\n        fun commitSelected(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: List<Commit>)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/MergeRequestDescriptionTab.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab\n\nimport com.intellij.ide.util.TipUIUtil\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.util.HtmlHelper\nimport javax.swing.JComponent\n\nclass MergeRequestDescriptionTab : MergeRequestDescriptionTabUI {\n    private var myMR: MergeRequestInfo? = null\n    private val myWebView = TipUIUtil.createBrowser() as TipUIUtil.Browser\n    private val myHtmlTemplate = MergeRequestDescriptionTab::class.java.getResource(\n        \"/templates/mr.description.html\"\n    ).readText()\n\n    override fun setMergeRequestInfo(providerData: ProviderData, mr: MergeRequestInfo) {\n        val currentMR = myMR\n        if (null === currentMR || currentMR.id != mr.id) {\n            myMR = mr\n            myWebView.text = buildHtml(providerData, mr)\n        }\n    }\n\n    private fun buildHtml(providerData: ProviderData, mr: MergeRequestInfo): String {\n        val output = myHtmlTemplate\n            .replace(\"{{title}}\", mr.title)\n            .replace(\"{{description}}\", HtmlHelper.convertFromMarkdown(mr.description))\n\n        return HtmlHelper.resolveRelativePath(providerData, output)\n    }\n\n    override fun createComponent(): JComponent {\n        return myWebView.component\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/MergeRequestDescriptionTabUI.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab\n\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\n\ninterface MergeRequestDescriptionTabUI: Component {\n    fun setMergeRequestInfo(providerData: ProviderData, mr: MergeRequestInfo)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/MergeRequestInfoTab.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab\n\nimport net.ntworld.mergeRequest.MergeRequest\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.ui.panel.MergeRequestInfoPanel\nimport javax.swing.JComponent\n\nclass MergeRequestInfoTab : MergeRequestInfoTabUI {\n    private val myInfoPanel = MergeRequestInfoPanel()\n\n    override fun setMergeRequestInfo(providerData: ProviderData, mr: MergeRequestInfo) {\n        myInfoPanel.setMergeRequestInfo(providerData, mr)\n    }\n\n    override fun setMergeRequest(mr: MergeRequest) = myInfoPanel.setMergeRequest(mr)\n\n    override fun createComponent(): JComponent = myInfoPanel.createComponent()\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/MergeRequestInfoTabUI.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab\n\nimport net.ntworld.mergeRequest.MergeRequest\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\n\ninterface MergeRequestInfoTabUI : Component {\n    fun setMergeRequestInfo(providerData: ProviderData, mr: MergeRequestInfo)\n\n    fun setMergeRequest(mr: MergeRequest)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/commit/CommitChanges.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit\n\nimport com.intellij.openapi.actionSystem.ActionManager\nimport com.intellij.openapi.actionSystem.DefaultActionGroup\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.openapi.vcs.changes.Change\nimport com.intellij.openapi.vcs.changes.ui.ChangesBrowserChangeNode\nimport com.intellij.openapi.vcs.changes.ui.ChangesTreeImpl\nimport com.intellij.openapi.vcs.changes.ui.TreeModelBuilder\nimport com.intellij.ui.ScrollPaneFactory\nimport net.miginfocom.swing.MigLayout\nimport net.ntworld.mergeRequest.Commit\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.ui.util.CustomSimpleToolWindowPanel\nimport net.ntworld.mergeRequestIntegrationIde.ui.util.ToolbarUtil\nimport javax.swing.JComponent\nimport javax.swing.JPanel\nimport javax.swing.event.TreeSelectionListener\nimport javax.swing.tree.DefaultTreeModel\nimport kotlin.concurrent.thread\nimport com.intellij.openapi.project.Project as IdeaProject\n\nclass CommitChanges(private val projectServiceProvider: ProjectServiceProvider) : CommitChangesUI {\n    private val myComponent = CustomSimpleToolWindowPanel(vertical = true, borderless = true)\n    private val myTree = MyTree(projectServiceProvider.project)\n    private var myProviderData: ProviderData? = null\n    private var myMergeRequestInfo: MergeRequestInfo? = null\n    private val myTreeSelectionListener = TreeSelectionListener {\n        if (null !== it && myTree.isVisible) {\n            val lastPath = it.path.lastPathComponent\n            val providerData = myProviderData\n            val mergeRequestInfo = myMergeRequestInfo\n            if (null !== providerData && null !== mergeRequestInfo && lastPath is ChangesBrowserChangeNode) {\n                val selectedContext = projectServiceProvider.reviewContextManager.findSelectedContext()\n                val reviewContext = projectServiceProvider.reviewContextManager.findContext(\n                    providerData.id, mergeRequestInfo.id\n                )\n                if (null === reviewContext) {\n                    return@TreeSelectionListener\n                }\n                if (null !== selectedContext) {\n                    if (selectedContext.providerData.id != reviewContext.providerData.id ||\n                        selectedContext.mergeRequestInfo.id != reviewContext.mergeRequestInfo.id) {\n                        return@TreeSelectionListener\n                    }\n                }\n                reviewContext.openChange(lastPath.userObject, focus = false, displayMergeRequestId = !projectServiceProvider.isDoingCodeReview())\n            }\n        }\n    }\n\n    init {\n        myComponent.setContent(ScrollPaneFactory.createScrollPane(myTree, true))\n        myComponent.toolbar = createToolbar()\n        myTree.addTreeSelectionListener(myTreeSelectionListener)\n    }\n\n    override fun clear() {\n        ApplicationManager.getApplication().invokeLater {\n            myTree.setChangesToDisplay(listOf())\n        }\n    }\n\n    override fun disable() {\n        myComponent.isVisible = false\n    }\n\n    override fun enable() {\n        myComponent.isVisible = true\n    }\n\n    override fun setCommits(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: List<Commit>) {\n        myProviderData = providerData\n        myMergeRequestInfo = mergeRequestInfo\n        thread {\n            myTree.isVisible = false\n            val changes = projectServiceProvider.repositoryFile.findChanges(providerData, commits.map { it.id })\n            projectServiceProvider.reviewContextManager.updateChanges(providerData.id, mergeRequestInfo.id, changes)\n            projectServiceProvider.reviewContextManager.updateReviewingChanges(providerData.id, mergeRequestInfo.id, changes)\n            ApplicationManager.getApplication().invokeLater {\n                myTree.setChangesToDisplay(changes)\n                myTree.isVisible = true\n            }\n        }\n    }\n\n    override fun updateSelectedCommits(\n        providerData: ProviderData,\n        mergeRequestInfo: MergeRequestInfo,\n        selectedCommits: List<Commit>\n    ) {\n        myProviderData = providerData\n        myMergeRequestInfo = mergeRequestInfo\n        thread {\n            myTree.isVisible = false\n            val partialChanges = projectServiceProvider.repositoryFile.findChanges(providerData, selectedCommits.map { it.id })\n            projectServiceProvider.reviewContextManager.updateReviewingChanges(\n                providerData.id, mergeRequestInfo.id, partialChanges\n            )\n            ApplicationManager.getApplication().invokeLater {\n                myTree.setChangesToDisplay(partialChanges)\n                myTree.isVisible = true\n            }\n        }\n    }\n\n    override fun createComponent(): JComponent = myComponent\n\n    private fun createToolbar(): JComponent {\n        val panel = JPanel(MigLayout(\"ins 0, fill\", \"[left]push[right]\", \"center\"))\n\n        val leftActionGroup = DefaultActionGroup()\n        val leftToolbar = ActionManager.getInstance().createActionToolbar(\n            \"${CommitChanges::class.java.canonicalName}/toolbar-left\",\n            leftActionGroup,\n            true\n        )\n\n        panel.add(leftToolbar.component)\n        panel.add(\n            ToolbarUtil.createExpandAndCollapseToolbar(\n                \"${CommitChanges::class.java.canonicalName}/toolbar-right\",\n                myTree\n            )\n        )\n        return panel\n    }\n\n    private class MyTree(ideaProject: IdeaProject) : ChangesTreeImpl<Change>(\n        ideaProject, false, false, Change::class.java\n    ) {\n        override fun buildTreeModel(changes: MutableList<out Change>): DefaultTreeModel {\n            return TreeModelBuilder.buildFromChanges(myProject, grouping, changes, null)\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/commit/CommitChangesUI.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit\n\nimport net.ntworld.mergeRequest.Commit\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\n\ninterface CommitChangesUI : Component {\n\n    fun clear()\n\n    fun disable()\n\n    fun enable()\n\n    fun setCommits(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: List<Commit>)\n\n    fun updateSelectedCommits(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, selectedCommits: List<Commit>)\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/commit/CommitCollection.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit\n\nimport com.intellij.openapi.actionSystem.ActionManager\nimport com.intellij.openapi.actionSystem.AnAction\nimport com.intellij.openapi.actionSystem.AnActionEvent\nimport com.intellij.openapi.actionSystem.DefaultActionGroup\nimport com.intellij.ui.JBColor\nimport com.intellij.ui.ScrollPaneFactory\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.Commit\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.ui.panel.CommitItemPanel\nimport net.ntworld.mergeRequestIntegrationIde.ui.util.CustomSimpleToolWindowPanel\nimport javax.swing.BoxLayout\nimport javax.swing.JComponent\nimport javax.swing.JPanel\nimport javax.swing.event.ChangeListener\n\nclass CommitCollection : CommitCollectionUI {\n    override val dispatcher = EventDispatcher.create(CommitCollectionUI.Listener::class.java)\n\n    private val myComponent = CustomSimpleToolWindowPanel(vertical = true, borderless = true)\n    private val myList = JPanel()\n    private val myItems = mutableListOf<CommitItemPanel>()\n    private var myProviderData: ProviderData? = null\n    private var myMergeRequestInfo: MergeRequestInfo? = null\n    private class MySelectAllButton(private val self: CommitCollection) : AnAction(\n        \"Select all\", \"Select all commits to see changes or do code review\", null\n    ) {\n        override fun actionPerformed(e: AnActionEvent) {\n            val commits = self.myItems.map {\n                it.isSelectable(selectable = true, selected = true)\n                it.commit\n            }\n            self.dispatchCommitSelectedEvent(commits)\n        }\n\n        override fun displayTextInToolbar() = true\n        override fun useSmallerFontForTextInToolbar() = true\n    }\n    private val mySelectAllButton = MySelectAllButton(this)\n\n    private class MyUnselectAllButton(private val self: CommitCollection): AnAction(\n        \"Unselect all\", \"Unselect all commits then pick a single one to see changes or do code review\", null\n    ) {\n        override fun actionPerformed(e: AnActionEvent) {\n            self.myItems.forEach {\n                it.isSelectable(selectable = true, selected = false)\n            }\n            self.dispatchCommitSelectedEvent(listOf())\n        }\n\n        override fun displayTextInToolbar() = true\n        override fun useSmallerFontForTextInToolbar() = true\n    }\n    private val myUnselectAllButton = MyUnselectAllButton(this)\n\n    private val myCommitSelectChangeListener = ChangeListener {\n        val list = myItems.map {\n            Pair(it.commit, it.isSelected())\n        }\n        myItems.forEachIndexed { index, item ->\n            item.isSelectable(CommitSelectUtil.canSelect(list, index))\n        }\n\n        val selectedCommits = mutableListOf<Commit>()\n        for (item in myItems) {\n            if (item.isSelected()) {\n                selectedCommits.add(item.commit)\n            }\n        }\n        dispatchCommitSelectedEvent(selectedCommits)\n    }\n\n    init {\n        myList.layout = BoxLayout(myList, BoxLayout.Y_AXIS)\n        myList.background = JBColor.background()\n\n        myComponent.setContent(ScrollPaneFactory.createScrollPane(myList, true))\n        myComponent.toolbar = createToolbar()\n    }\n\n    override fun clear() {\n        myList.removeAll()\n        myItems.clear()\n    }\n\n    override fun disable() {\n        myItems.forEach { it.disable() }\n    }\n\n    override fun enable() {\n        myItems.forEach { it.enable() }\n    }\n\n    override fun setCommits(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: Collection<Commit>) {\n        clear()\n        myProviderData = providerData\n        myMergeRequestInfo = mergeRequestInfo\n        commits.forEach {\n            val item = CommitItemPanel(it, myCommitSelectChangeListener)\n            myItems.add(item)\n            myList.add(item.createComponent())\n        }\n    }\n\n    override fun createComponent(): JComponent = myComponent\n\n    private fun dispatchCommitSelectedEvent(commits: List<Commit>) {\n        val providerData = myProviderData\n        val mergeRequestInfo = myMergeRequestInfo\n        if (null !== providerData && null !== mergeRequestInfo) {\n            dispatcher.multicaster.commitsSelected(providerData, mergeRequestInfo, commits)\n        }\n    }\n\n    private fun createToolbar() : JComponent {\n        val actionGroup = DefaultActionGroup()\n        actionGroup.add(myUnselectAllButton)\n        actionGroup.addSeparator()\n        actionGroup.add(mySelectAllButton)\n\n        return ActionManager.getInstance().createActionToolbar(\n            \"${CommitCollection::class.java.canonicalName}/toolbar\",\n            actionGroup,\n            true\n        ).component\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/commit/CommitCollectionUI.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit\n\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.Commit\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport java.util.*\n\ninterface CommitCollectionUI : Component {\n    val dispatcher: EventDispatcher<Listener>\n\n    fun clear()\n\n    fun disable()\n\n    fun enable()\n\n    fun setCommits(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: Collection<Commit>)\n\n    interface Listener : EventListener {\n        fun commitsSelected(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo, commits: List<Commit>)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/mergeRequest/tab/commit/CommitSelectUtil.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.commit\n\nimport net.ntworld.mergeRequest.Commit\n\nobject CommitSelectUtil {\n    fun canSelect(list: List<Pair<Commit, Boolean>>, index: Int): Boolean {\n        if (isAllSelectedOrUnselected(list)) {\n            return true\n        }\n\n        val range = findSelectedCommitRangeIndex(list)\n        return range.first - 1 <= index && index <= range.second + 1\n    }\n\n    private fun findSelectedCommitRangeIndex(list: List<Pair<Commit, Boolean>>): Pair<Int, Int> {\n        var first = -1\n        var last = -1\n        var setFirst = false\n        var index = 0\n        for (item in list) {\n            if (item.second) {\n                if (!setFirst) {\n                    first = index\n                    setFirst = true\n                }\n                last = index\n                index++\n                continue\n            }\n\n            if (setFirst) {\n                break\n            }\n            index++\n        }\n        return Pair(first, last)\n    }\n\n    private fun isAllSelectedOrUnselected(list: List<Pair<Commit, Boolean>>): Boolean {\n        if (list.isEmpty() || list.size == 1) {\n            return true\n        }\n\n        val first = list[0].second\n        for (i in 1..list.lastIndex) {\n            if (list[i].second != first) {\n                return false\n            }\n        }\n        return true\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/ApprovalPanel.form",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<form xmlns=\"http://www.intellij.com/uidesigner/form/\" version=\"1\" bind-to-class=\"net.ntworld.mergeRequestIntegrationIde.ui.panel.ApprovalPanel\">\n  <grid id=\"27dc6\" binding=\"myWholePanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n    <margin top=\"10\" left=\"10\" bottom=\"10\" right=\"10\"/>\n    <constraints>\n      <xy x=\"20\" y=\"20\" width=\"407\" height=\"199\"/>\n    </constraints>\n    <properties/>\n    <border type=\"none\"/>\n    <children>\n      <grid id=\"8632e\" layout-manager=\"GridLayoutManager\" row-count=\"3\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n        <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n        <constraints>\n          <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n        </constraints>\n        <properties/>\n        <border type=\"none\"/>\n        <children>\n          <grid id=\"9db1d\" binding=\"myApproversWrapper\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children/>\n          </grid>\n          <vspacer id=\"f6f23\">\n            <constraints>\n              <grid row=\"2\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n          </vspacer>\n          <grid id=\"da321\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"2\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"d4354\" class=\"javax.swing.JButton\" binding=\"myApproveBtn\">\n                <constraints>\n                  <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Approve\"/>\n                </properties>\n              </component>\n              <hspacer id=\"272bd\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </hspacer>\n            </children>\n          </grid>\n        </children>\n      </grid>\n    </children>\n  </grid>\n</form>\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/ApprovalPanel.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.panel\n\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.Approval\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport net.ntworld.mergeRequestIntegrationIde.component.Icons\nimport java.util.*\nimport javax.swing.BoxLayout\nimport javax.swing.JButton\nimport javax.swing.JComponent\nimport javax.swing.JPanel\n\nclass ApprovalPanel : Component {\n    var myWholePanel: JPanel? = null\n    var myApproversWrapper: JPanel? = null\n    var myApproveBtn: JButton? = null\n    private var myApproval: Approval? = null\n    private var canApprove = false\n    private var canUnapprove = false\n    private val dispatcher = EventDispatcher.create(Listener::class.java)\n\n    init {\n        myApproversWrapper!!.layout = BoxLayout(myApproversWrapper!!, BoxLayout.Y_AXIS)\n        myApproveBtn!!.addActionListener {\n            if (canApprove) {\n                dispatcher.multicaster.onApproveClicked()\n            }\n            if (canUnapprove) {\n                dispatcher.multicaster.onUnapproveClicked()\n            }\n        }\n    }\n\n    fun hide() {\n        canApprove = false\n        canUnapprove = false\n        myWholePanel!!.isVisible = false\n    }\n\n    fun setApproval(approval: Approval) {\n        myApproval = approval\n        canApprove = false\n        canUnapprove = false\n        myApproveBtn!!.isVisible = approval.canApprove || approval.hasApproved\n        if (approval.hasApproved) {\n            myApproveBtn!!.text = \"Revoke approval\"\n            canUnapprove = true\n        } else {\n            myApproveBtn!!.text = \"Approve\"\n            canApprove = true\n        }\n\n        myApproversWrapper!!.removeAll()\n        val approved = mutableListOf<String>()\n        for (approver in approval.approvedBy) {\n            myApproversWrapper!!.add(UserInfoItemPanel(approver, Icons.Approved).createComponent())\n            approved.add(approver.id)\n        }\n        for (approver in approval.approvers) {\n            if (approved.contains(approver.id)) {\n                continue\n            }\n            myApproversWrapper!!.add(UserInfoItemPanel(approver, Icons.NoApproval).createComponent())\n        }\n        myWholePanel!!.isVisible = true\n    }\n\n    fun addListener(listener: Listener) {\n        dispatcher.addListener(listener)\n    }\n\n    fun shouldDisplayApprovalPanel() : Boolean {\n        val approval = myApproval\n        if (null === approval) {\n            return false\n        }\n        return approval.canApprove || approval.hasApproved ||\n            approval.approvedBy.isNotEmpty() ||\n            approval.approvers.isNotEmpty()\n    }\n\n    override fun createComponent(): JComponent = myWholePanel!!\n\n    interface Listener : EventListener {\n        fun onApproveClicked()\n\n        fun onUnapproveClicked()\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/CommitItemPanel.form",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<form xmlns=\"http://www.intellij.com/uidesigner/form/\" version=\"1\" bind-to-class=\"net.ntworld.mergeRequestIntegrationIde.ui.panel.CommitItemPanel\">\n  <grid id=\"27dc6\" binding=\"myWholePanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n    <margin top=\"5\" left=\"5\" bottom=\"5\" right=\"5\"/>\n    <constraints>\n      <xy x=\"20\" y=\"20\" width=\"745\" height=\"82\"/>\n    </constraints>\n    <properties/>\n    <border type=\"none\"/>\n    <children>\n      <grid id=\"a4ce6\" binding=\"myWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"2\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n        <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n        <constraints>\n          <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"1\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n        </constraints>\n        <properties/>\n        <border type=\"none\"/>\n        <children>\n          <grid id=\"fb497\" binding=\"mySelectedWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"2\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"1e3fa\" class=\"javax.swing.JCheckBox\" binding=\"mySelected\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"\"/>\n                </properties>\n              </component>\n            </children>\n          </grid>\n          <grid id=\"71e3f\" binding=\"myInfoWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"2\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"7\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <grid id=\"98647\" binding=\"myTitleWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n                <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n                <border type=\"none\"/>\n                <children>\n                  <component id=\"97942\" class=\"javax.swing.JLabel\" binding=\"myTitle\">\n                    <constraints>\n                      <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"This is very important change and cool feature introduced by NP\"/>\n                    </properties>\n                  </component>\n                </children>\n              </grid>\n              <grid id=\"8200a\" binding=\"myInfoDetailsWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"3\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n                <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n                <constraints>\n                  <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n                <border type=\"none\"/>\n                <children>\n                  <component id=\"e5200\" class=\"javax.swing.JLabel\" binding=\"myAuthorAndTime\">\n                    <constraints>\n                      <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Nhat Phan\"/>\n                    </properties>\n                  </component>\n                  <hspacer id=\"b9e83\">\n                    <constraints>\n                      <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                  </hspacer>\n                  <component id=\"34f9f\" class=\"javax.swing.JTextField\" binding=\"myCommitHash\">\n                    <constraints>\n                      <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\">\n                        <preferred-size width=\"75\" height=\"-1\"/>\n                        <maximum-size width=\"75\" height=\"-1\"/>\n                      </grid>\n                    </constraints>\n                    <properties>\n                      <editable value=\"false\"/>\n                      <enabled value=\"true\"/>\n                    </properties>\n                  </component>\n                </children>\n              </grid>\n            </children>\n          </grid>\n        </children>\n      </grid>\n    </children>\n  </grid>\n</form>\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/CommitItemPanel.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.panel\n\nimport com.intellij.ui.JBColor\nimport net.ntworld.mergeRequest.Commit\nimport net.ntworld.mergeRequestIntegration.util.DateTimeUtil\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport java.awt.Dimension\nimport javax.swing.*\nimport javax.swing.event.ChangeListener\n\nclass CommitItemPanel(val commit: Commit, private val changeListener: ChangeListener) : Component {\n    var myWholePanel: JPanel? = null\n    var myWrapperPanel: JPanel? = null\n    var mySelectedWrapperPanel: JPanel? = null\n    var myInfoWrapperPanel: JPanel? = null\n    var myTitleWrapperPanel: JPanel? = null\n    var myInfoDetailsWrapperPanel: JPanel? = null\n    var mySelected: JCheckBox? = null\n    var myCommitHash: JTextField? = null\n    var myAuthorAndTime: JLabel? = null\n    var myTitle: JLabel? = null\n\n    init {\n        mySelected!!.isSelected = true\n        myTitle!!.text = commit.message\n        myAuthorAndTime!!.text = \"${commit.authorName} · ${DateTimeUtil.toPretty(DateTimeUtil.toDate(commit.createdAt))}\"\n        myCommitHash!!.text = commit.id\n\n        myWholePanel!!.border = BorderFactory.createMatteBorder(0, 0, 1, 0, JBColor.border())\n        myWholePanel!!.maximumSize = Dimension(Int.MAX_VALUE, 60)\n        mySelected!!.addChangeListener(changeListener)\n\n        myWholePanel!!.background = JBColor.background()\n        myWrapperPanel!!.background = JBColor.background()\n        mySelectedWrapperPanel!!.background = JBColor.background()\n        myInfoWrapperPanel!!.background = JBColor.background()\n        myTitleWrapperPanel!!.background = JBColor.background()\n        myInfoDetailsWrapperPanel!!.background = JBColor.background()\n    }\n\n    fun disable() {\n        if (this.isSelected()) {\n            mySelectedWrapperPanel!!.isVisible = false\n        } else {\n            myWholePanel!!.isVisible = false\n        }\n    }\n\n    fun enable() {\n        mySelectedWrapperPanel!!.isVisible = true\n        myWholePanel!!.isVisible = true\n    }\n\n    fun isSelected(): Boolean = mySelected!!.isSelected\n\n    fun isSelectable(selectable: Boolean, selected: Boolean? = null) {\n        if (selectable) {\n            mySelected!!.isEnabled = true\n            if (null !== selected) {\n                mySelected!!.isSelected = selected\n            }\n        } else {\n            mySelected!!.isEnabled = false\n            mySelected!!.toolTipText = \"Cannot select this commit\"\n            mySelected!!.isSelected = false\n        }\n    }\n\n    override fun createComponent(): JComponent = myWholePanel!!\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/MergeRequestFilterPropertiesPanel.form",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<form xmlns=\"http://www.intellij.com/uidesigner/form/\" version=\"1\" bind-to-class=\"net.ntworld.mergeRequestIntegrationIde.ui.panel.MergeRequestFilterPropertiesPanel\">\n  <grid id=\"27dc6\" binding=\"myWholePanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n    <margin top=\"10\" left=\"10\" bottom=\"10\" right=\"10\"/>\n    <constraints>\n      <xy x=\"20\" y=\"20\" width=\"424\" height=\"196\"/>\n    </constraints>\n    <properties/>\n    <border type=\"none\"/>\n    <children>\n      <grid id=\"78fa4\" layout-manager=\"GridLayoutManager\" row-count=\"3\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n        <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n        <constraints>\n          <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n        </constraints>\n        <properties/>\n        <border type=\"none\"/>\n        <children>\n          <grid id=\"2efb3\" layout-manager=\"GridLayoutManager\" row-count=\"4\" column-count=\"3\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"c2b89\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"State\"/>\n                </properties>\n              </component>\n              <grid id=\"eaa7e\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"5\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n                <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n                <constraints>\n                  <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n                <border type=\"none\"/>\n                <children>\n                  <hspacer id=\"56d07\">\n                    <constraints>\n                      <grid row=\"0\" column=\"4\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                  </hspacer>\n                  <component id=\"5fc4f\" class=\"javax.swing.JRadioButton\" binding=\"myStateAll\">\n                    <constraints>\n                      <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"All\"/>\n                    </properties>\n                  </component>\n                  <component id=\"6911d\" class=\"javax.swing.JRadioButton\" binding=\"myStateOpened\">\n                    <constraints>\n                      <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Opened\"/>\n                    </properties>\n                  </component>\n                  <component id=\"c9acf\" class=\"javax.swing.JRadioButton\" binding=\"myStateMerged\">\n                    <constraints>\n                      <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Merged\"/>\n                    </properties>\n                  </component>\n                  <component id=\"80eed\" class=\"javax.swing.JRadioButton\" binding=\"myStateClosed\">\n                    <constraints>\n                      <grid row=\"0\" column=\"3\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Closed\"/>\n                    </properties>\n                  </component>\n                </children>\n              </grid>\n              <component id=\"ded0e\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Author\"/>\n                </properties>\n              </component>\n              <component id=\"7d79\" class=\"com.intellij.openapi.ui.ComboBox\" binding=\"myAuthor\">\n                <constraints>\n                  <grid row=\"1\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"7\" hsize-policy=\"7\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n              </component>\n              <component id=\"f5680\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"2\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Assignee\"/>\n                </properties>\n              </component>\n              <component id=\"7dd76\" class=\"com.intellij.openapi.ui.ComboBox\" binding=\"myAssignee\">\n                <constraints>\n                  <grid row=\"2\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"7\" hsize-policy=\"7\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n              </component>\n              <component id=\"c8b07\" class=\"javax.swing.JLabel\">\n                <constraints>\n                  <grid row=\"3\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Approver\"/>\n                </properties>\n              </component>\n              <component id=\"a7ab\" class=\"com.intellij.openapi.ui.ComboBox\" binding=\"myApprover\">\n                <constraints>\n                  <grid row=\"3\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"7\" hsize-policy=\"7\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n              </component>\n            </children>\n          </grid>\n          <vspacer id=\"80411\">\n            <constraints>\n              <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n          </vspacer>\n        </children>\n      </grid>\n    </children>\n  </grid>\n  <buttonGroups>\n    <group name=\"stateGroup\">\n      <member id=\"5fc4f\"/>\n      <member id=\"6911d\"/>\n      <member id=\"c9acf\"/>\n      <member id=\"80eed\"/>\n    </group>\n  </buttonGroups>\n</form>\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/MergeRequestFilterPropertiesPanel.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.panel\n\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.openapi.ui.ComboBox\nimport com.intellij.util.ui.UIUtil\nimport net.ntworld.mergeRequest.MergeRequestState\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.UserInfo\nimport net.ntworld.mergeRequest.query.GetMergeRequestFilter\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.task.FetchProjectMembersTask\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport java.awt.event.ActionListener\nimport javax.swing.*\n\nclass MergeRequestFilterPropertiesPanel(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData,\n    private val onChanged: (() -> Unit),\n    private val onReady: (() -> Unit)\n) : Component {\n    var myWholePanel: JPanel? = null\n    var myStateAll: JRadioButton? = null\n    var myStateOpened: JRadioButton? = null\n    var myStateMerged: JRadioButton? = null\n    var myStateClosed: JRadioButton? = null\n    var myAuthor: ComboBox<UserInfo>? = null\n    var myAssignee: ComboBox<UserInfo>? = null\n    var myApprover: ComboBox<UserInfo>? = null\n\n    private var isFetched = false\n    private var isReady = false\n    private var shouldDispatchChanges = true\n    private val myData = mutableListOf<UserInfo>()\n    private val myListener = object : FetchProjectMembersTask.Listener {\n        override fun onError(exception: Exception) {\n            isFetched = false\n            throw exception\n        }\n\n        override fun taskStarted() {\n            myData.clear()\n        }\n\n        override fun dataReceived(collection: List<UserInfo>) {\n            ApplicationManager.getApplication().invokeLater {\n                myData.addAll(collection)\n            }\n        }\n\n        override fun taskEnded() {\n            myAuthor!!.model = MyMemberModel(myData)\n            if (providerData.hasAssigneeFeature) {\n                myAssignee!!.model = MyMemberModel(myData)\n            }\n            if (providerData.hasApprovalFeature) {\n                myApprover!!.model = MyMemberModel(myData)\n            }\n            isReady = true\n            onReady()\n        }\n    }\n    private val myListRenderer = ListCellRenderer<UserInfo> { _, value, index, isSelected, cellHasFocus ->\n        if (null === value) {\n            JPanel()\n        } else {\n            val panel = UserInfoItemPanel(value)\n            panel.setBackground(UIUtil.getListBackground(isSelected, cellHasFocus))\n            panel.createComponent()\n        }\n    }\n    private val myActionListener = ActionListener {\n        if (isReady && shouldDispatchChanges) {\n            onChanged()\n        }\n    }\n\n    init {\n        val task = FetchProjectMembersTask(projectServiceProvider, providerData, true, myListener)\n        if (!isFetched) {\n            task.start()\n        }\n        myStateOpened!!.isSelected = true\n\n        myAuthor!!.renderer = myListRenderer\n        myAssignee!!.renderer = myListRenderer\n        myApprover!!.renderer = myListRenderer\n\n        myAuthor!!.isSwingPopup = false\n        myAssignee!!.isSwingPopup = false\n        myApprover!!.isSwingPopup = false\n\n        myAuthor!!.addActionListener(myActionListener)\n        myAssignee!!.addActionListener(myActionListener)\n        myAssignee!!.addActionListener(myActionListener)\n        myStateAll!!.addActionListener(myActionListener)\n        myStateOpened!!.addActionListener(myActionListener)\n        myStateMerged!!.addActionListener(myActionListener)\n        myStateClosed!!.addActionListener(myActionListener)\n\n        myAssignee!!.isEnabled = providerData.hasAssigneeFeature\n        myApprover!!.isEnabled = providerData.hasApprovalFeature\n    }\n\n    override fun createComponent(): JComponent {\n        return myWholePanel!!\n    }\n\n    fun buildFilter(search: String): GetMergeRequestFilter {\n        return GetMergeRequestFilter.make(\n            id = null,\n            state = findState(),\n            search = search.trim(),\n            authorId = findMemberInComboBox(myAuthor!!),\n            assigneeId = findMemberInComboBox(myAssignee!!),\n            approverIds = listOf(findMemberInComboBox(myApprover!!)),\n            sourceBranch = \"\"\n        )\n    }\n\n    fun buildFilterForSearchById(id: Int): GetMergeRequestFilter {\n        return GetMergeRequestFilter.make(\n            id = id,\n            state = findState(),\n            search = \"\",\n            authorId = findMemberInComboBox(myAuthor!!),\n            assigneeId = findMemberInComboBox(myAssignee!!),\n            approverIds = listOf(findMemberInComboBox(myApprover!!)),\n            sourceBranch = \"\"\n        )\n    }\n\n    fun setPreselectedValues(value: GetMergeRequestFilter) {\n        shouldDispatchChanges = false\n        when (value.state) {\n            MergeRequestState.ALL -> myStateAll!!.isSelected = true\n            MergeRequestState.OPENED -> myStateOpened!!.isSelected = true\n            MergeRequestState.CLOSED -> myStateClosed!!.isSelected = true\n            MergeRequestState.MERGED -> myStateMerged!!.isSelected = true\n        }\n\n        if (value.assigneeId.isNotBlank()) {\n            selectMemberInComboBox(myAssignee!!, value.assigneeId)\n        }\n\n        if (value.authorId.isNotBlank()) {\n            selectMemberInComboBox(myAuthor!!, value.authorId)\n        }\n\n        if (value.approverIds.isNotEmpty() && value.approverIds[0].isNotBlank()) {\n            selectMemberInComboBox(myApprover!!, value.approverIds[0])\n        }\n        shouldDispatchChanges = true\n    }\n\n    private fun findMemberInComboBox(comboBox: ComboBox<UserInfo>) : String {\n        val selected = comboBox.selectedItem as UserInfo?\n        if (!comboBox.isEnabled || null === selected) {\n            return \"\"\n        }\n        return selected.id\n    }\n\n    private fun selectMemberInComboBox(comboBox: ComboBox<UserInfo>, userId: String) {\n        for (i in 0 until comboBox.itemCount) {\n            val item = comboBox.getItemAt(i)\n            if (item.id == userId) {\n                comboBox.selectedItem = item\n                return\n            }\n        }\n    }\n\n    private fun findState(): MergeRequestState {\n        if (myStateOpened!!.isSelected) {\n            return MergeRequestState.OPENED\n        }\n        if (myStateMerged!!.isSelected) {\n            return MergeRequestState.MERGED\n        }\n        if (myStateClosed!!.isSelected) {\n            return MergeRequestState.CLOSED\n        }\n        return MergeRequestState.ALL\n    }\n\n    private class MyMemberModel(\n        private val data: List<UserInfo>\n    ) : ComboBoxModel<UserInfo>, AbstractListModel<UserInfo>() {\n        private var mySelected: UserInfo? = null\n\n        override fun setSelectedItem(anItem: Any?) {\n            mySelected = anItem as UserInfo?\n            fireContentsChanged(this, -1, -1)\n        }\n\n        override fun getElementAt(index: Int): UserInfo {\n            if (index < 0 || index >= data.size) {\n                throw ArrayIndexOutOfBoundsException()\n            }\n            return data[index]\n        }\n\n        override fun getSelectedItem(): Any? {\n            return mySelected\n        }\n\n        override fun getSize(): Int {\n            return data.size\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/MergeRequestInfoPanel.form",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<form xmlns=\"http://www.intellij.com/uidesigner/form/\" version=\"1\" bind-to-class=\"net.ntworld.mergeRequestIntegrationIde.ui.panel.MergeRequestInfoPanel\">\n  <grid id=\"27dc6\" binding=\"myWholePanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n    <margin top=\"5\" left=\"5\" bottom=\"5\" right=\"5\"/>\n    <constraints>\n      <xy x=\"20\" y=\"20\" width=\"890\" height=\"370\"/>\n    </constraints>\n    <properties/>\n    <border type=\"none\"/>\n    <children>\n      <grid id=\"6f4de\" binding=\"myWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"11\" column-count=\"3\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n        <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n        <constraints>\n          <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n        </constraints>\n        <properties/>\n        <border type=\"none\"/>\n        <children>\n          <component id=\"ad39\" class=\"javax.swing.JLabel\">\n            <constraints>\n              <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"Title\"/>\n            </properties>\n          </component>\n          <hspacer id=\"a2ee8\">\n            <constraints>\n              <grid row=\"1\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n          </hspacer>\n          <vspacer id=\"1c9f5\">\n            <constraints>\n              <grid row=\"10\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n          </vspacer>\n          <component id=\"8cf59\" class=\"javax.swing.JLabel\">\n            <constraints>\n              <grid row=\"2\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"Created at\"/>\n            </properties>\n          </component>\n          <component id=\"7c6d6\" class=\"javax.swing.JLabel\">\n            <constraints>\n              <grid row=\"3\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"Updated at\"/>\n            </properties>\n          </component>\n          <component id=\"a17e0\" class=\"javax.swing.JLabel\">\n            <constraints>\n              <grid row=\"4\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"Author\"/>\n            </properties>\n          </component>\n          <component id=\"1f37b\" class=\"javax.swing.JLabel\">\n            <constraints>\n              <grid row=\"5\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"Assigned to\"/>\n            </properties>\n          </component>\n          <component id=\"504e9\" class=\"javax.swing.JLabel\">\n            <constraints>\n              <grid row=\"6\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"Branch\"/>\n            </properties>\n          </component>\n          <component id=\"e9675\" class=\"javax.swing.JLabel\">\n            <constraints>\n              <grid row=\"7\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"Status\"/>\n            </properties>\n          </component>\n          <grid id=\"81928\" binding=\"myBranchesWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"4\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"6\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <hspacer id=\"7036a\">\n                <constraints>\n                  <grid row=\"0\" column=\"3\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </hspacer>\n              <component id=\"f7cc0\" class=\"javax.swing.JLabel\" binding=\"myBranchIcon\">\n                <constraints>\n                  <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"\"/>\n                </properties>\n              </component>\n              <component id=\"fcfa4\" class=\"javax.swing.JTextField\" binding=\"myTargetBranch\">\n                <constraints>\n                  <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <editable value=\"false\"/>\n                  <enabled value=\"true\"/>\n                  <text value=\"master\"/>\n                </properties>\n              </component>\n              <component id=\"2ab88\" class=\"javax.swing.JTextField\" binding=\"mySourceBranch\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <editable value=\"false\"/>\n                  <enabled value=\"true\"/>\n                </properties>\n              </component>\n            </children>\n          </grid>\n          <component id=\"dc754\" class=\"javax.swing.JLabel\">\n            <constraints>\n              <grid row=\"8\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"Merged by\"/>\n            </properties>\n          </component>\n          <grid id=\"1816b\" binding=\"myMergedByWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"5\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"8\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"6238e\" class=\"javax.swing.JLabel\" binding=\"myMergedByFullName\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Nhat Phan\"/>\n                </properties>\n              </component>\n              <hspacer id=\"604f\">\n                <constraints>\n                  <grid row=\"0\" column=\"4\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </hspacer>\n              <component id=\"82b1b\" class=\"javax.swing.JLabel\" binding=\"myMergedByUsername\">\n                <constraints>\n                  <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <foreground color=\"-6710887\"/>\n                  <text value=\"nhat-phan\"/>\n                </properties>\n              </component>\n              <component id=\"51e3e\" class=\"javax.swing.JLabel\" binding=\"myMergedAtText\">\n                <constraints>\n                  <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"at\"/>\n                </properties>\n              </component>\n              <component id=\"64d1\" class=\"javax.swing.JLabel\" binding=\"myMergedAt\">\n                <constraints>\n                  <grid row=\"0\" column=\"3\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"2019/12/31 11:12 - 3 days ago\"/>\n                </properties>\n              </component>\n            </children>\n          </grid>\n          <component id=\"de3ad\" class=\"javax.swing.JLabel\">\n            <constraints>\n              <grid row=\"9\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"Closed by\"/>\n            </properties>\n          </component>\n          <grid id=\"394e2\" binding=\"myClosedByWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"5\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"9\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"5bc1f\" class=\"javax.swing.JLabel\" binding=\"myClosedByFullName\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Nhat Phan\"/>\n                </properties>\n              </component>\n              <hspacer id=\"2829c\">\n                <constraints>\n                  <grid row=\"0\" column=\"4\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </hspacer>\n              <component id=\"3f3e1\" class=\"javax.swing.JLabel\" binding=\"myClosedByUsername\">\n                <constraints>\n                  <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <foreground color=\"-6710887\"/>\n                  <text value=\"nhat-phan\"/>\n                </properties>\n              </component>\n              <component id=\"71ba6\" class=\"javax.swing.JLabel\" binding=\"myClosedAtText\">\n                <constraints>\n                  <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"at\"/>\n                </properties>\n              </component>\n              <component id=\"fb1ac\" class=\"javax.swing.JLabel\" binding=\"myClosedAt\">\n                <constraints>\n                  <grid row=\"0\" column=\"3\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"2019/12/31 11:12 - 3 days ago\"/>\n                </properties>\n              </component>\n            </children>\n          </grid>\n          <grid id=\"5b987\" binding=\"myAssignedWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"3\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"5\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"d6aeb\" class=\"javax.swing.JLabel\" binding=\"myAssigneeFullName\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Nhat Phan\"/>\n                </properties>\n              </component>\n              <hspacer id=\"41008\">\n                <constraints>\n                  <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </hspacer>\n              <component id=\"d2588\" class=\"javax.swing.JLabel\" binding=\"myAssigneeUsername\">\n                <constraints>\n                  <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <foreground color=\"-6710887\"/>\n                  <text value=\"nhat-phan\"/>\n                </properties>\n              </component>\n            </children>\n          </grid>\n          <grid id=\"35c2\" binding=\"myAuthorWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"3\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"4\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"76b50\" class=\"javax.swing.JLabel\" binding=\"myAuthorFullName\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Nhat Phan\"/>\n                </properties>\n              </component>\n              <hspacer id=\"54511\">\n                <constraints>\n                  <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </hspacer>\n              <component id=\"d8432\" class=\"javax.swing.JLabel\" binding=\"myAuthorUsername\">\n                <constraints>\n                  <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <foreground color=\"-6710887\"/>\n                  <text value=\"nhat-phan\"/>\n                </properties>\n              </component>\n            </children>\n          </grid>\n          <component id=\"6c6e3\" class=\"javax.swing.JLabel\" binding=\"myCreatedAt\">\n            <constraints>\n              <grid row=\"2\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"2019/12/31 11:12 - 3 days ago\"/>\n            </properties>\n          </component>\n          <component id=\"b8393\" class=\"javax.swing.JLabel\" binding=\"myUpdatedAt\">\n            <constraints>\n              <grid row=\"3\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"2019/12/31 11:12 - 3 days ago\"/>\n            </properties>\n          </component>\n          <component id=\"74e92\" class=\"javax.swing.JLabel\" binding=\"myTitle\">\n            <constraints>\n              <grid row=\"1\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"Really really cool feature\"/>\n            </properties>\n          </component>\n          <component id=\"e51eb\" class=\"javax.swing.JLabel\" binding=\"myStatus\">\n            <constraints>\n              <grid row=\"7\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"Opened\"/>\n            </properties>\n          </component>\n          <component id=\"6adce\" class=\"javax.swing.JLabel\">\n            <constraints>\n              <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"ID\"/>\n            </properties>\n          </component>\n          <component id=\"bd2e\" class=\"javax.swing.JLabel\" binding=\"myId\">\n            <constraints>\n              <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"\"/>\n            </properties>\n          </component>\n        </children>\n      </grid>\n    </children>\n  </grid>\n</form>\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/MergeRequestInfoPanel.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.panel\n\nimport com.intellij.icons.AllIcons\nimport com.intellij.ui.JBColor\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequestIntegration.util.DateTimeUtil\nimport javax.swing.JComponent\nimport javax.swing.JLabel\nimport javax.swing.JPanel\nimport javax.swing.JTextField\n\nclass MergeRequestInfoPanel {\n    var myWholePanel: JPanel? = null\n    var myWrapperPanel: JPanel? = null\n    var myBranchesWrapperPanel: JPanel? = null\n    var myMergedByWrapperPanel: JPanel? = null\n    var myClosedByWrapperPanel: JPanel? = null\n    var myAssignedWrapperPanel: JPanel? = null\n    var myAuthorWrapperPanel: JPanel? = null\n    var myId: JLabel? = null\n    var myTitle: JLabel? = null\n    var myCreatedAt: JLabel? = null\n    var myUpdatedAt: JLabel? = null\n    var myAuthorFullName: JLabel? = null\n    var myAuthorUsername: JLabel? = null\n    var myAssigneeFullName: JLabel? = null\n    var myAssigneeUsername: JLabel? = null\n    var mySourceBranch: JTextField? = null\n    var myBranchIcon: JLabel? = null\n    var myTargetBranch: JTextField? = null\n    var myStatus: JLabel? = null\n    var myMergedByFullName: JLabel? = null\n    var myMergedByUsername: JLabel? = null\n    var myMergedAtText: JLabel? = null\n    var myMergedAt: JLabel? = null\n    var myClosedByFullName: JLabel? = null\n    var myClosedByUsername: JLabel? = null\n    var myClosedAtText: JLabel? = null\n    var myClosedAt: JLabel? = null\n\n    init {\n        mySourceBranch!!.background = null\n        mySourceBranch!!.border = null\n        myTargetBranch!!.background = null\n        myTargetBranch!!.border = null\n\n        myBranchIcon!!.icon = AllIcons.Vcs.Arrow_right\n        myTitle!!.text = \"-\"\n        displayTime(null, myCreatedAt!!)\n        displayTime(null, myUpdatedAt!!)\n        hide()\n        setBackground()\n    }\n\n    private fun setBackground() {\n        myWholePanel!!.background = JBColor.background()\n        myWrapperPanel!!.background = JBColor.background()\n        myBranchesWrapperPanel!!.background = JBColor.background()\n        myMergedByWrapperPanel!!.background = JBColor.background()\n        myClosedByWrapperPanel!!.background = JBColor.background()\n        myAssignedWrapperPanel!!.background = JBColor.background()\n        myAuthorWrapperPanel!!.background = JBColor.background()\n    }\n\n    fun setMergeRequestInfo(providerData: ProviderData, mr: MergeRequestInfo) {\n        myId!!.text = providerData.info.formatMergeRequestId(mr.id)\n        myTitle!!.text = mr.title\n        displayTime(mr.createdAt, myCreatedAt!!)\n        displayTime(mr.updatedAt, myUpdatedAt!!)\n        hide()\n    }\n\n    fun setMergeRequest(mr: MergeRequest) {\n        myTitle!!.text = mr.title\n        displayTime(mr.createdAt, myCreatedAt!!)\n        displayTime(mr.updatedAt, myUpdatedAt!!)\n        displayStatus(mr.state, myStatus!!)\n        displayBranches(mr.sourceBranch, mr.targetBranch, mySourceBranch!!, myTargetBranch!!, myBranchIcon!!)\n        displayUserInfo(mr.author, myAuthorFullName!!, myAuthorUsername!!)\n        displayUserInfo(mr.assignee, myAssigneeFullName!!, myAssigneeUsername!!)\n        displayUserInfoWithTime(\n            mr.mergedBy,\n            mr.mergedAt,\n            myMergedByFullName!!,\n            myMergedByUsername!!,\n            myMergedAtText!!,\n            myMergedAt!!\n        )\n        displayUserInfoWithTime(\n            mr.closedBy,\n            mr.closedAt,\n            myClosedByFullName!!,\n            myClosedByUsername!!,\n            myClosedAtText!!,\n            myClosedAt!!\n        )\n    }\n\n    fun createComponent(): JComponent = myWholePanel!!\n\n    private fun hide() {\n        displayStatus(MergeRequestState.ALL, myStatus!!)\n        displayBranches(null, null, mySourceBranch!!, myTargetBranch!!, myBranchIcon!!)\n        displayUserInfo(null, myAuthorFullName!!, myAuthorUsername!!)\n        displayUserInfo(null, myAssigneeFullName!!, myAssigneeUsername!!)\n        displayUserInfoWithTime(\n            null,\n            null,\n            myMergedByFullName!!,\n            myMergedByUsername!!,\n            myMergedAtText!!,\n            myMergedAt!!\n        )\n        displayUserInfoWithTime(\n            null,\n            null,\n            myClosedByFullName!!,\n            myClosedByUsername!!,\n            myClosedAtText!!,\n            myClosedAt!!\n        )\n    }\n\n    private fun displayBranches(\n        source: String?,\n        target: String?,\n        sourceTextField: JTextField,\n        targetTextField: JTextField,\n        icon: JLabel\n    ) {\n        if (null === source || null === target) {\n            sourceTextField.text = \"-\"\n            targetTextField.isVisible = false\n            icon.isVisible = false\n            return\n        }\n\n        sourceTextField.text = source\n        targetTextField.text = target\n        targetTextField.isVisible = true\n        icon.isVisible = true\n    }\n\n    private fun displayStatus(state: MergeRequestState, label: JLabel) {\n        label.text = when (state) {\n            MergeRequestState.ALL -> \"-\"\n            MergeRequestState.OPENED -> \"opened\"\n            MergeRequestState.CLOSED -> \"closed\"\n            MergeRequestState.MERGED -> \"merged\"\n        }\n    }\n\n    private fun displayUserInfoWithTime(\n        user: UserInfo?,\n        datetime: DateTime?,\n        fullName: JLabel,\n        username: JLabel,\n        at: JLabel,\n        datetimeLabel: JLabel\n    ) {\n        displayUserInfo(user, fullName, username)\n        displayTime(datetime, datetimeLabel)\n        at.isVisible = null !== datetime\n    }\n\n    private fun displayTime(datetime: DateTime?, label: JLabel) {\n        if (null === datetime) {\n            label.text = \"\"\n        } else {\n            val date = DateTimeUtil.toDate(datetime)\n            label.text = \"${DateTimeUtil.formatDate(date)} · ${DateTimeUtil.toPretty(date)}\"\n        }\n    }\n\n    private fun displayUserInfo(user: UserInfo?, fullName: JLabel, username: JLabel) {\n        if (null === user) {\n            fullName.text = \"-\"\n            username.text = \"\"\n        } else {\n            fullName.text = user.name\n            username.text = \"@${user.username}\"\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/MergeRequestItemPanel.form",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<form xmlns=\"http://www.intellij.com/uidesigner/form/\" version=\"1\" bind-to-class=\"net.ntworld.mergeRequestIntegrationIde.ui.panel.MergeRequestItemPanel\">\n  <grid id=\"27dc6\" binding=\"myWholePanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n    <margin top=\"5\" left=\"5\" bottom=\"5\" right=\"5\"/>\n    <constraints>\n      <xy x=\"20\" y=\"20\" width=\"837\" height=\"131\"/>\n    </constraints>\n    <properties/>\n    <border type=\"none\"/>\n    <children>\n      <grid id=\"9160e\" binding=\"myWrapper\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"3\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n        <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n        <constraints>\n          <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n        </constraints>\n        <properties/>\n        <border type=\"none\"/>\n        <children>\n          <grid id=\"85588\" binding=\"myContentWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"3\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"3a36f\" class=\"javax.swing.JLabel\" binding=\"myTitle\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Adding super cool feature to contribute for an open source project\"/>\n                </properties>\n              </component>\n              <vspacer id=\"20e7f\">\n                <constraints>\n                  <grid row=\"2\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </vspacer>\n              <grid id=\"def1b\" binding=\"myTimeWrapper\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"4\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n                <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n                <constraints>\n                  <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n                <border type=\"none\"/>\n                <children>\n                  <hspacer id=\"7155f\">\n                    <constraints>\n                      <grid row=\"0\" column=\"3\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                  </hspacer>\n                  <component id=\"a6a11\" class=\"javax.swing.JLabel\" binding=\"myCreated\">\n                    <constraints>\n                      <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <font size=\"11\"/>\n                      <foreground color=\"-6710887\"/>\n                      <text value=\"Created: 3 days ago\"/>\n                    </properties>\n                  </component>\n                  <component id=\"7d695\" class=\"javax.swing.JLabel\" binding=\"myUpdated\">\n                    <constraints>\n                      <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <font size=\"11\"/>\n                      <foreground color=\"-6710887\"/>\n                      <text value=\"Updated: few seconds ago\"/>\n                    </properties>\n                  </component>\n                  <component id=\"52438\" class=\"javax.swing.JLabel\" binding=\"myId\">\n                    <constraints>\n                      <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <font size=\"11\"/>\n                      <foreground color=\"-6710887\"/>\n                      <text value=\"\"/>\n                    </properties>\n                  </component>\n                </children>\n              </grid>\n            </children>\n          </grid>\n          <hspacer id=\"c1329\">\n            <constraints>\n              <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n          </hspacer>\n          <grid id=\"b0e78\" binding=\"myApprovalStatusWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"3\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"c9ac8\" class=\"javax.swing.JLabel\" binding=\"myUserStatus\">\n                <constraints>\n                  <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"\"/>\n                </properties>\n              </component>\n              <hspacer id=\"439f8\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </hspacer>\n              <component id=\"bb435\" class=\"javax.swing.JLabel\" binding=\"myAllStatuses\">\n                <constraints>\n                  <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"\"/>\n                </properties>\n              </component>\n            </children>\n          </grid>\n        </children>\n      </grid>\n    </children>\n  </grid>\n</form>\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/MergeRequestItemPanel.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.panel\n\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.ui.JBColor\nimport com.intellij.util.ui.UIUtil\nimport net.ntworld.mergeRequest.Approval\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegration.util.DateTimeUtil\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.task.FindApprovalTask\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport net.ntworld.mergeRequestIntegrationIde.ui.provider.ProviderDetailsMRList\nimport net.ntworld.mergeRequestIntegrationIde.ui.util.findVisibilityIconAndTextForApproval\nimport javax.swing.JComponent\nimport javax.swing.JLabel\nimport javax.swing.JPanel\n\nclass MergeRequestItemPanel(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData,\n    private val mergeRequestInfo: MergeRequestInfo,\n    private val displayType: ProviderDetailsMRList.ApprovalStatusDisplayType\n): Component {\n    var myWholePanel: JPanel? = null\n    var myWrapper: JPanel? = null\n    var myContentWrapperPanel: JPanel? = null\n    var myApprovalStatusWrapperPanel: JPanel? = null\n    var myTimeWrapper: JPanel? = null\n    var myId: JLabel? = null\n    var myTitle: JLabel? = null\n    var myCreated: JLabel? = null\n    var myUpdated: JLabel? = null\n    var myUserStatus: JLabel? = null\n    var myAllStatuses: JLabel? = null\n    private var myUserStatusColor: JBColor = JBColor.RED\n    private val myFindApprovalTaskListener = object : FindApprovalTask.Listener {\n        override fun dataReceived(mergeRequestInfo: MergeRequestInfo, approval: Approval) {\n            ApplicationManager.getApplication().invokeLater {\n                val triple = approval.findVisibilityIconAndTextForApproval()\n                myAllStatuses!!.isVisible = triple.first\n                myAllStatuses!!.icon = triple.second\n                myAllStatuses!!.text = triple.third\n\n                if (displayType == ProviderDetailsMRList.ApprovalStatusDisplayType.STATUSES_AND_MINE_APPROVAL) {\n                    val approved = approval.approvedBy.firstOrNull {\n                        it.id == providerData.currentUser.id\n                    }\n                    if (null !== approved) {\n                        myUserStatus!!.text = \"Approved\"\n                        myUserStatusColor = JBColor.GREEN\n                    } else {\n                        myUserStatus!!.text = \"Waiting\"\n                        myUserStatusColor = JBColor.RED\n                    }\n                    myUserStatus!!.foreground = myUserStatusColor\n                    myUserStatus!!.isVisible = true\n                }\n                myApprovalStatusWrapperPanel!!.isVisible = true\n            }\n        }\n    }\n\n    init {\n        myId!!.text = providerData.info.formatMergeRequestId(mergeRequestInfo.id)\n        myTitle!!.text = mergeRequestInfo.title\n        val created = DateTimeUtil.toDate(mergeRequestInfo.createdAt)\n        myCreated!!.text = \"Created ${DateTimeUtil.toPretty(created)}\"\n        myCreated!!.toolTipText = DateTimeUtil.formatDate(created)\n\n        val updated = DateTimeUtil.toDate(mergeRequestInfo.updatedAt)\n        myUpdated!!.text = \"updated ${DateTimeUtil.toPretty(updated)}\"\n        myUpdated!!.toolTipText = DateTimeUtil.formatDate(updated)\n\n        if (displayType != ProviderDetailsMRList.ApprovalStatusDisplayType.NONE && providerData.hasApprovalFeature) {\n            fetchApprovalData()\n        } else {\n            myApprovalStatusWrapperPanel!!.isVisible = false\n        }\n    }\n\n    private fun fetchApprovalData() {\n        val task = FindApprovalTask(\n            projectServiceProvider = projectServiceProvider,\n            providerData = providerData,\n            mergeRequestInfo = mergeRequestInfo,\n            listener = myFindApprovalTaskListener\n        )\n        task.start()\n    }\n\n    fun changeStyle(selected: Boolean, hasFocus: Boolean) {\n        val backgroundColor = UIUtil.getListBackground(selected, hasFocus)\n\n        myWholePanel!!.background = backgroundColor\n        myWrapper!!.background = backgroundColor\n        myContentWrapperPanel!!.background = backgroundColor\n        myApprovalStatusWrapperPanel!!.background = backgroundColor\n        myTimeWrapper!!.background = backgroundColor\n\n        val foregroundColor = UIUtil.getListForeground(selected, hasFocus)\n        myTitle!!.foreground = foregroundColor\n\n        val foregroundColorOrGray = if (selected && hasFocus) foregroundColor else JBColor.gray\n        myCreated!!.foreground = foregroundColorOrGray\n        myUpdated!!.foreground = foregroundColorOrGray\n\n        if (displayType != ProviderDetailsMRList.ApprovalStatusDisplayType.NONE) {\n            val foregroundColorOrCurrent = if (selected && hasFocus) foregroundColor else myUserStatusColor\n            myUserStatus!!.foreground = foregroundColorOrCurrent\n            myAllStatuses!!.foreground = foregroundColor\n        }\n    }\n\n    override fun createComponent(): JComponent = myWholePanel!!\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/ProjectPanel.form",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<form xmlns=\"http://www.intellij.com/uidesigner/form/\" version=\"1\" bind-to-class=\"net.ntworld.mergeRequestIntegrationIde.ui.panel.ProjectPanel\">\n  <grid id=\"27dc6\" binding=\"myWrapper\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n    <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n    <constraints>\n      <xy x=\"20\" y=\"20\" width=\"570\" height=\"114\"/>\n    </constraints>\n    <properties/>\n    <border type=\"none\"/>\n    <children>\n      <grid id=\"7f5c8\" binding=\"myInner\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"3\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n        <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n        <constraints>\n          <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n        </constraints>\n        <properties/>\n        <border type=\"none\"/>\n        <children>\n          <component id=\"c4b19\" class=\"javax.swing.JLabel\" binding=\"myProjectName\">\n            <constraints>\n              <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"Label\"/>\n            </properties>\n          </component>\n          <hspacer id=\"1c316\">\n            <constraints>\n              <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n          </hspacer>\n          <component id=\"c573c\" class=\"javax.swing.JLabel\" binding=\"myUrl\">\n            <constraints>\n              <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <foreground color=\"-6710887\"/>\n              <text value=\"Label\"/>\n            </properties>\n          </component>\n        </children>\n      </grid>\n    </children>\n  </grid>\n</form>\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/ProjectPanel.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.panel\n\nimport net.ntworld.mergeRequest.Project\nimport java.awt.Color\nimport javax.swing.*\n\nclass ProjectPanel(project: Project, isSelected: Boolean) {\n    var myWrapper: JPanel? = null\n    var myInner: JPanel? = null\n    var myProjectName: JLabel? = null\n    var myUrl: JLabel? = null\n\n    init {\n        myInner!!.border = BorderFactory.createLineBorder(Color(0, 0, 0, 0), 3)\n        myProjectName!!.text = project.name\n        myUrl!!.text = project.url\n        setSelected(isSelected)\n    }\n\n    fun setSelected(value: Boolean) {\n        if (!value) {\n            myWrapper!!.border = BorderFactory.createLineBorder(Color(0, 0, 0, 0), 1)\n        } else {\n            myWrapper!!.border = BorderFactory.createLineBorder(Color(164, 55, 65), 1)\n        }\n    }\n\n    fun getComponent(): JPanel = myWrapper!!\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/ProviderInformationPanel.form",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<form xmlns=\"http://www.intellij.com/uidesigner/form/\" version=\"1\" bind-to-class=\"net.ntworld.mergeRequestIntegrationIde.ui.panel.ProviderInformationPanel\">\n  <grid id=\"27dc6\" binding=\"myWholePanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n    <margin top=\"10\" left=\"10\" bottom=\"10\" right=\"10\"/>\n    <constraints>\n      <xy x=\"20\" y=\"20\" width=\"1533\" height=\"440\"/>\n    </constraints>\n    <properties/>\n    <border type=\"none\"/>\n    <children>\n      <grid id=\"bde0f\" binding=\"myWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"7\" column-count=\"3\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n        <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n        <constraints>\n          <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n        </constraints>\n        <properties/>\n        <border type=\"none\"/>\n        <children>\n          <component id=\"e425\" class=\"javax.swing.JLabel\">\n            <constraints>\n              <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"Project name\"/>\n            </properties>\n          </component>\n          <hspacer id=\"ce41e\">\n            <constraints>\n              <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n          </hspacer>\n          <vspacer id=\"360b5\">\n            <constraints>\n              <grid row=\"6\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n          </vspacer>\n          <component id=\"e33f9\" class=\"javax.swing.JLabel\" binding=\"myProjectName\">\n            <constraints>\n              <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"Foundation\"/>\n            </properties>\n          </component>\n          <component id=\"534c7\" class=\"javax.swing.JLabel\">\n            <constraints>\n              <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"Logged User\"/>\n            </properties>\n          </component>\n          <grid id=\"5bcb7\" binding=\"myLoggedUserWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"3\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"1\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"6491d\" class=\"javax.swing.JLabel\" binding=\"myUserFullName\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Nhat Phan\"/>\n                </properties>\n              </component>\n              <hspacer id=\"f87e2\">\n                <constraints>\n                  <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </hspacer>\n              <component id=\"32719\" class=\"javax.swing.JLabel\" binding=\"myUserUsername\">\n                <constraints>\n                  <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <foreground color=\"-6710887\"/>\n                  <text value=\"nhat-phan\"/>\n                </properties>\n              </component>\n            </children>\n          </grid>\n          <component id=\"faffa\" class=\"javax.swing.JLabel\">\n            <constraints>\n              <grid row=\"2\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"Local Repository Path\"/>\n            </properties>\n          </component>\n          <component id=\"475a4\" class=\"javax.swing.JLabel\" binding=\"myLocalRepository\">\n            <constraints>\n              <grid row=\"2\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"/Users/nhat/folder/directory/path/really/long/does/not/matter/even/longer/than/ever\"/>\n            </properties>\n          </component>\n          <component id=\"ab6c1\" class=\"javax.swing.JLabel\">\n            <constraints>\n              <grid row=\"3\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"URL\"/>\n            </properties>\n          </component>\n          <grid id=\"31b2b\" binding=\"myUrlWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"3\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"3\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"b504f\" class=\"javax.swing.JLabel\" binding=\"myProjectUrl\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"https://github.com/nhat-phan/foundation\"/>\n                </properties>\n              </component>\n              <hspacer id=\"290a6\">\n                <constraints>\n                  <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </hspacer>\n              <component id=\"2560a\" class=\"javax.swing.JButton\" binding=\"myOpenURLButton\">\n                <constraints>\n                  <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Open\"/>\n                </properties>\n              </component>\n            </children>\n          </grid>\n          <component id=\"10dae\" class=\"javax.swing.JLabel\">\n            <constraints>\n              <grid row=\"4\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"4\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"Status\"/>\n            </properties>\n          </component>\n          <grid id=\"5a5f0\" binding=\"myStatusWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"4\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"4\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"6f3de\" class=\"javax.swing.JLabel\" binding=\"myProjectVisibility\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Public\"/>\n                </properties>\n              </component>\n              <hspacer id=\"7cf5c\">\n                <constraints>\n                  <grid row=\"0\" column=\"3\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </hspacer>\n              <component id=\"38f44\" class=\"javax.swing.JLabel\" binding=\"myLegalStatus\">\n                <constraints>\n                  <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <foreground color=\"-9927593\"/>\n                  <text value=\"Legal\"/>\n                </properties>\n              </component>\n              <component id=\"3c2d\" class=\"javax.swing.JButton\" binding=\"myBuyButton\">\n                <constraints>\n                  <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Buy Enterprise Edition\"/>\n                </properties>\n              </component>\n            </children>\n          </grid>\n          <grid id=\"ff680\" binding=\"myLicenseWarningWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"4\" column-count=\"2\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"5\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"2eb23\" class=\"javax.swing.JLabel\" binding=\"myLegalWarning\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <foreground color=\"-6015167\"/>\n                  <text value=\"You cannot use CE for private repositories, please buy Enterprise Edition, only 1$/month.\"/>\n                </properties>\n              </component>\n              <hspacer id=\"6ff12\">\n                <constraints>\n                  <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </hspacer>\n              <component id=\"81a70\" class=\"javax.swing.JLabel\" binding=\"myWords1\">\n                <constraints>\n                  <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"The plugin is still working properly so you don't need to hack or find a cracked version.\"/>\n                </properties>\n              </component>\n              <component id=\"8f5ed\" class=\"javax.swing.JLabel\" binding=\"myWords2\">\n                <constraints>\n                  <grid row=\"2\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"Cracking software invites virus to your computer. \"/>\n                </properties>\n              </component>\n              <component id=\"219e3\" class=\"javax.swing.JLabel\" binding=\"myWords3\">\n                <constraints>\n                  <grid row=\"3\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"It would be nice if you purchase the paid version. This would help me constantly improve the plugin.\"/>\n                </properties>\n              </component>\n            </children>\n          </grid>\n        </children>\n      </grid>\n    </children>\n  </grid>\n</form>\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/ProviderInformationPanel.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.panel\n\nimport com.intellij.ide.BrowserUtil\nimport com.intellij.ui.JBColor\nimport net.ntworld.mergeRequest.ProjectVisibility\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.ENTERPRISE_EDITION_URL\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport java.awt.Color\nimport javax.swing.JButton\nimport javax.swing.JComponent\nimport javax.swing.JLabel\nimport javax.swing.JPanel\n\nclass ProviderInformationPanel(\n    private val applicationServiceProvider: ApplicationServiceProvider,\n    private val providerData: ProviderData\n): Component {\n    var myWholePanel: JPanel? = null\n    var myWrapperPanel: JPanel? = null\n    var myLoggedUserWrapperPanel: JPanel? = null\n    var myUrlWrapperPanel: JPanel? = null\n    var myStatusWrapperPanel: JPanel? = null\n    var myLicenseWarningWrapperPanel: JPanel? = null\n    var myOpenURLButton: JButton? = null\n    var myBuyButton: JButton? = null\n    var myProjectName: JLabel? = null\n    var myProjectUrl: JLabel? = null\n    var myUserFullName: JLabel? = null\n    var myUserUsername: JLabel? = null\n    var myLocalRepository: JLabel? = null\n    var myProjectVisibility: JLabel? = null\n    var myLegalStatus: JLabel? = null\n    var myLegalWarning: JLabel? = null\n    var myWords1: JLabel? = null\n    var myWords2: JLabel? = null\n    var myWords3: JLabel? = null\n\n    init {\n        myProjectName!!.text = providerData.project.name\n        myProjectUrl!!.text = providerData.project.url\n        myUserFullName!!.text = providerData.currentUser.name\n        myUserUsername!!.text = \"@${providerData.currentUser.username}\"\n        myLocalRepository!!.text = providerData.repository\n        myProjectVisibility!!.text = when (providerData.project.visibility) {\n            ProjectVisibility.PUBLIC -> \"Public\"\n            ProjectVisibility.PRIVATE -> \"Private\"\n        }\n        myOpenURLButton!!.addActionListener {\n            BrowserUtil.open(providerData.project.url)\n        }\n\n        val isLegal = this.applicationServiceProvider.isLegal(providerData)\n        if (!isLegal) {\n            myLegalStatus!!.text = \"Illegal\"\n            myLegalStatus!!.foreground = Color(0xBA, 0x3F, 0x3C)\n        } else {\n            myLegalStatus!!.text = \"Legal\"\n            myLegalStatus!!.foreground = Color(0x68, 0x84, 0x57)\n        }\n        myBuyButton!!.isVisible = !isLegal\n        myLegalWarning!!.isVisible = !isLegal\n        myWords1!!.isVisible = !isLegal\n        myWords2!!.isVisible = !isLegal\n        myWords3!!.isVisible = !isLegal\n\n        myBuyButton!!.addActionListener {\n            BrowserUtil.open(ENTERPRISE_EDITION_URL)\n        }\n\n        setBackground()\n    }\n\n    private fun setBackground() {\n        myWholePanel!!.background = JBColor.background()\n        myOpenURLButton!!.isOpaque = true\n        myOpenURLButton!!.background = JBColor.background()\n        myBuyButton!!.isOpaque = true\n        myBuyButton!!.background = JBColor.background()\n        myWrapperPanel!!.background = JBColor.background()\n        myLoggedUserWrapperPanel!!.background = JBColor.background()\n        myUrlWrapperPanel!!.background = JBColor.background()\n        myStatusWrapperPanel!!.background = JBColor.background()\n        myLicenseWarningWrapperPanel!!.background = JBColor.background()\n    }\n\n    override fun createComponent(): JComponent = myWholePanel!!\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/ProviderItemPanel.form",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<form xmlns=\"http://www.intellij.com/uidesigner/form/\" version=\"1\" bind-to-class=\"net.ntworld.mergeRequestIntegrationIde.ui.panel.ProviderItemPanel\">\n  <grid id=\"27dc6\" binding=\"myWholePanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n    <margin top=\"5\" left=\"5\" bottom=\"5\" right=\"5\"/>\n    <constraints>\n      <xy x=\"20\" y=\"20\" width=\"516\" height=\"116\"/>\n    </constraints>\n    <properties/>\n    <border type=\"none\"/>\n    <children>\n      <grid id=\"15aa3\" binding=\"myWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"4\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n        <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n        <constraints>\n          <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n        </constraints>\n        <properties/>\n        <border type=\"none\"/>\n        <children>\n          <grid id=\"474a4\" binding=\"myAvatarWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"9ea35\" class=\"javax.swing.JLabel\" binding=\"myAvatar\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"\"/>\n                </properties>\n              </component>\n            </children>\n          </grid>\n          <hspacer id=\"f4aad\">\n            <constraints>\n              <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n          </hspacer>\n          <grid id=\"542f6\" binding=\"myInfoWrapper\" layout-manager=\"GridLayoutManager\" row-count=\"3\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"15a1b\" class=\"javax.swing.JLabel\" binding=\"myProjectName\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <font style=\"1\"/>\n                  <text value=\"Provider Custom Name\"/>\n                </properties>\n              </component>\n              <vspacer id=\"d03ce\">\n                <constraints>\n                  <grid row=\"2\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n              </vspacer>\n              <grid id=\"f9bd\" binding=\"myUserInfoWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"3\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n                <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n                <constraints>\n                  <grid row=\"1\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties/>\n                <border type=\"none\"/>\n                <children>\n                  <component id=\"bb09e\" class=\"javax.swing.JLabel\" binding=\"myName\">\n                    <constraints>\n                      <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <text value=\"Nhat Phan\"/>\n                    </properties>\n                  </component>\n                  <hspacer id=\"d8641\">\n                    <constraints>\n                      <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                  </hspacer>\n                  <component id=\"84e92\" class=\"javax.swing.JLabel\" binding=\"myUsername\">\n                    <constraints>\n                      <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                    </constraints>\n                    <properties>\n                      <foreground color=\"-6710887\"/>\n                      <text value=\"nhat-phan\"/>\n                    </properties>\n                  </component>\n                </children>\n              </grid>\n            </children>\n          </grid>\n          <grid id=\"dcd12\" binding=\"myStatusWrapperPanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n            <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n            <constraints>\n              <grid row=\"0\" column=\"3\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties/>\n            <border type=\"none\"/>\n            <children>\n              <component id=\"667d6\" class=\"javax.swing.JLabel\" binding=\"myStatus\">\n                <constraints>\n                  <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n                </constraints>\n                <properties>\n                  <text value=\"\"/>\n                </properties>\n              </component>\n            </children>\n          </grid>\n        </children>\n      </grid>\n    </children>\n  </grid>\n</form>\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/ProviderItemPanel.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.panel\n\nimport com.intellij.openapi.util.IconLoader\nimport com.intellij.ui.JBColor\nimport com.intellij.util.ui.UIUtil\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.ProviderStatus\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport net.ntworld.mergeRequestIntegrationIde.ui.util.ImageUtil\nimport javax.swing.JComponent\nimport javax.swing.JLabel\nimport javax.swing.JPanel\n\nclass ProviderItemPanel(private val providerData: ProviderData) : Component {\n    var myWholePanel: JPanel? = null\n    var myWrapperPanel: JPanel? = null\n    var myAvatarWrapperPanel: JPanel? = null\n    var myUserInfoWrapperPanel: JPanel? = null\n    var myInfoWrapper: JPanel? = null\n    var myStatusWrapperPanel: JPanel? = null\n    var myProjectName: JLabel? = null\n    var myName: JLabel? = null\n    var myUsername: JLabel? = null\n    var myAvatar: JLabel? = null\n    var myStatus: JLabel? = null\n\n    init {\n        myAvatar!!.icon = ImageUtil.loadIconFromUrl(providerData.project.avatarUrl, providerData.info.icon3xPath, 48)\n        myProjectName!!.text = providerData.name\n        myName!!.text = providerData.currentUser.name\n        myUsername!!.text = \"@${providerData.currentUser.username}\"\n        if (providerData.status == ProviderStatus.ERROR) {\n            myStatus!!.icon = IconLoader.getIcon(\"/icons/exclamation.2x.svg\", ProviderItemPanel::class.java)\n            myStatus!!.toolTipText = \"Cannot fetch data from this provider\"\n            myStatus!!.isVisible = true\n        } else {\n            myStatus!!.isVisible = false\n        }\n    }\n\n    fun changeStyle(selected: Boolean, hasFocus: Boolean) {\n        val backgroundColor = UIUtil.getListBackground(selected, hasFocus)\n\n        myWholePanel!!.background = backgroundColor\n        myWrapperPanel!!.background = backgroundColor\n        myStatusWrapperPanel!!.background = backgroundColor\n        myInfoWrapper!!.background = backgroundColor\n        myAvatarWrapperPanel!!.background = backgroundColor\n        myUserInfoWrapperPanel!!.background = backgroundColor\n\n        val foregroundColor = UIUtil.getListForeground(selected, hasFocus)\n        myProjectName!!.foreground = foregroundColor\n        myName!!.foreground = foregroundColor\n        myUsername!!.foreground = if (selected && hasFocus) foregroundColor else JBColor.gray\n    }\n\n    override fun createComponent(): JComponent = myWholePanel!!\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/UserInfoItemPanel.form",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<form xmlns=\"http://www.intellij.com/uidesigner/form/\" version=\"1\" bind-to-class=\"net.ntworld.mergeRequestIntegrationIde.ui.panel.UserInfoItemPanel\">\n  <grid id=\"27dc6\" binding=\"myWholePanel\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"1\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n    <margin top=\"5\" left=\"5\" bottom=\"5\" right=\"5\"/>\n    <constraints>\n      <xy x=\"20\" y=\"20\" width=\"269\" height=\"38\"/>\n    </constraints>\n    <properties/>\n    <border type=\"none\"/>\n    <children>\n      <grid id=\"36c10\" binding=\"myWrapper\" layout-manager=\"GridLayoutManager\" row-count=\"1\" column-count=\"4\" same-size-horizontally=\"false\" same-size-vertically=\"false\" hgap=\"-1\" vgap=\"-1\">\n        <margin top=\"0\" left=\"0\" bottom=\"0\" right=\"0\"/>\n        <constraints>\n          <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" indent=\"0\" use-parent-layout=\"false\"/>\n        </constraints>\n        <properties/>\n        <border type=\"none\"/>\n        <children>\n          <hspacer id=\"af089\">\n            <constraints>\n              <grid row=\"0\" column=\"2\" row-span=\"1\" col-span=\"1\" vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n          </hspacer>\n          <component id=\"84e37\" class=\"javax.swing.JLabel\" binding=\"myName\">\n            <constraints>\n              <grid row=\"0\" column=\"1\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"First Last\"/>\n            </properties>\n          </component>\n          <component id=\"b5f16\" class=\"javax.swing.JLabel\" binding=\"myUsername\">\n            <constraints>\n              <grid row=\"0\" column=\"3\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <foreground color=\"-6710887\"/>\n              <text value=\"username\"/>\n            </properties>\n          </component>\n          <component id=\"17cbc\" class=\"javax.swing.JLabel\" binding=\"myIcon\">\n            <constraints>\n              <grid row=\"0\" column=\"0\" row-span=\"1\" col-span=\"1\" vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" indent=\"0\" use-parent-layout=\"false\"/>\n            </constraints>\n            <properties>\n              <text value=\"\"/>\n            </properties>\n          </component>\n        </children>\n      </grid>\n    </children>\n  </grid>\n</form>\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/panel/UserInfoItemPanel.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.panel\n\nimport net.ntworld.mergeRequest.UserInfo\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport java.awt.Color\nimport javax.swing.Icon\nimport javax.swing.JComponent\nimport javax.swing.JLabel\nimport javax.swing.JPanel\n\nclass UserInfoItemPanel(private val userInfo: UserInfo, private val icon: Icon? = null): Component {\n    var myWholePanel: JPanel? = null\n    var myWrapper: JPanel? = null\n    var myName: JLabel? = null\n    var myUsername: JLabel? = null\n    var myIcon: JLabel? = null\n\n    init {\n        if (null === icon) {\n            myIcon!!.isVisible = false\n        } else {\n            myIcon!!.icon = icon\n            myIcon!!.isVisible = true\n        }\n        myName!!.text = userInfo.name\n        myUsername!!.text = \"@${userInfo.username}\"\n    }\n\n    override fun createComponent(): JComponent {\n        return myWholePanel!!\n    }\n\n    fun setBackground(color: Color) {\n        myWholePanel!!.background = color\n        myWrapper!!.background = color\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderCollection.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.provider\n\nimport com.intellij.openapi.Disposable\nimport com.intellij.openapi.ui.SimpleToolWindowPanel\nimport com.intellij.openapi.util.Disposer\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifier\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifierAdapter\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport javax.swing.JComponent\n\nclass ProviderCollection(\n    private val projectServiceProvider: ProjectServiceProvider\n): Component, Disposable {\n    private val myListUI: ProviderCollectionListUI by lazy {\n        val list = ProviderCollectionList()\n        val registered = projectServiceProvider.providerStorage.registeredProviders\n        if (registered.isNotEmpty()) {\n            registered.forEach { list.addProvider(it) }\n        }\n        list\n    }\n    private val myToolbarUI: ProviderCollectionToolbarUI by lazy {\n        ProviderCollectionToolbar()\n    }\n\n    private val myProjectNotifier = object : ProjectNotifierAdapter() {\n        override fun providerRegistered(providerData: ProviderData) {\n            myListUI.addProvider(providerData)\n        }\n\n        override fun startCodeReview(reviewContext: ReviewContext) {\n            myComponent.isVisible = false\n        }\n\n        override fun stopCodeReview(reviewContext: ReviewContext) {\n            myComponent.isVisible = true\n        }\n    }\n    private val myConnection = projectServiceProvider.messageBus.connect()\n    private val myToolbarEventListener = object: ProviderCollectionToolbarEventListener {\n        override fun refreshClicked() {\n            myListUI.clear()\n            projectServiceProvider.initialize()\n        }\n\n        override fun helpClicked() {\n        }\n    }\n\n    private val myComponent = SimpleToolWindowPanel(true, true)\n\n    init {\n        myConnection.subscribe(ProjectNotifier.TOPIC, myProjectNotifier)\n        if (!projectServiceProvider.isInitialized()) {\n            projectServiceProvider.initialize()\n        }\n\n        myComponent.setContent(myListUI.createComponent())\n        myComponent.toolbar = myToolbarUI.createComponent()\n\n        myToolbarUI.eventDispatcher.addListener(myToolbarEventListener)\n        Disposer.register(projectServiceProvider.project, this)\n    }\n\n    override fun createComponent(): JComponent = myComponent\n\n    fun getListEventDispatcher() = myListUI.eventDispatcher\n\n    fun addListEventListener(listener: ProviderCollectionListEventListener) {\n        myListUI.eventDispatcher.addListener(listener)\n    }\n\n    fun addToolbarEventListener(listener: ProviderCollectionToolbarEventListener) {\n        myToolbarUI.eventDispatcher.addListener(listener)\n    }\n\n    override fun dispose() {\n        myConnection.disconnect()\n        myListUI.clear()\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderCollectionList.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.provider\n\nimport com.intellij.ui.ScrollPaneFactory\nimport com.intellij.ui.components.JBList\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.ui.panel.ProviderItemPanel\nimport java.awt.Component\nimport java.awt.event.MouseAdapter\nimport java.awt.event.MouseEvent\nimport javax.swing.*\nimport javax.swing.event.ListSelectionEvent\nimport javax.swing.event.ListSelectionListener\n\nclass ProviderCollectionList : ProviderCollectionListUI {\n    override val eventDispatcher = EventDispatcher.create(ProviderCollectionListEventListener::class.java)\n    private val myProviderDataMap = mutableMapOf<String, ProviderData>()\n    private val myList = JBList<ProviderData?>()\n    private val myProviderItemPanels = mutableMapOf<Int, ProviderItemPanel>()\n    private val myCellRenderer = object: ListCellRenderer<ProviderData?> {\n        override fun getListCellRendererComponent(\n            list: JList<out ProviderData?>?,\n            value: ProviderData?,\n            index: Int,\n            isSelected: Boolean,\n            cellHasFocus: Boolean\n        ): Component {\n            if (null === value) {\n                return JPanel()\n            }\n            if (null === myProviderItemPanels[index]) {\n                myProviderItemPanels[index] = ProviderItemPanel(value)\n            }\n            val panel = myProviderItemPanels[index]!!\n            panel.changeStyle(isSelected, cellHasFocus)\n            return panel.createComponent()\n        }\n    }\n    private val myListSelectionListener = object: ListSelectionListener {\n        override fun valueChanged(e: ListSelectionEvent?) {\n            val value = myList.selectedValue\n            if (null === value) {\n                eventDispatcher.multicaster.providerUnselected()\n            } else {\n                eventDispatcher.multicaster.providerSelected(value)\n            }\n        }\n    }\n    private val myListMouseListener = object: MouseAdapter() {\n        override fun mouseClicked(e: MouseEvent?) {\n            if (null === e) {\n                return\n            }\n            if (e.clickCount == 1) {\n                val value = myList.selectedValue\n                if (null === value) {\n                    eventDispatcher.multicaster.providerUnselected()\n                } else {\n                    eventDispatcher.multicaster.providerSelected(value)\n                }\n            }\n            if (e.clickCount == 2) {\n                val value = myList.selectedValue\n                if (null === value) {\n                    eventDispatcher.multicaster.providerUnselected()\n                } else {\n                    eventDispatcher.multicaster.providerOpened(value)\n                }\n            }\n        }\n    }\n\n    init {\n        myProviderItemPanels.clear()\n        myProviderDataMap.clear()\n        myList.clearSelection()\n        myList.selectionMode = ListSelectionModel.SINGLE_SELECTION\n        myList.cellRenderer = myCellRenderer\n        myList.addListSelectionListener(myListSelectionListener)\n        myList.addMouseListener(myListMouseListener)\n        updateList()\n    }\n\n    private fun updateList() {\n        myProviderItemPanels.clear()\n        val data = myProviderDataMap.values.sortedBy {\n            it.name\n        }\n        myList.setListData(data.toTypedArray())\n    }\n\n    override fun clear() {\n        myProviderItemPanels.clear()\n        myProviderDataMap.clear()\n        myList.clearSelection()\n        updateList()\n    }\n\n    override fun addProvider(providerData: ProviderData) {\n        myProviderDataMap[providerData.id] = providerData\n        updateList()\n    }\n\n    override fun createComponent(): JComponent = ScrollPaneFactory.createScrollPane(myList)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderCollectionListEventListener.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.provider\n\nimport net.ntworld.mergeRequest.ProviderData\nimport java.util.*\n\ninterface ProviderCollectionListEventListener : EventListener {\n\n    fun providerUnselected()\n\n    fun providerSelected(providerData: ProviderData)\n\n    fun providerOpened(providerData: ProviderData)\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderCollectionListUI.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.provider\n\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\n\ninterface ProviderCollectionListUI: Component {\n\n    val eventDispatcher: EventDispatcher<ProviderCollectionListEventListener>\n\n    fun clear()\n\n    fun addProvider(providerData: ProviderData)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderCollectionToolbar.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.provider\n\nimport com.intellij.icons.AllIcons\nimport com.intellij.openapi.actionSystem.ActionManager\nimport com.intellij.openapi.actionSystem.AnAction\nimport com.intellij.openapi.actionSystem.AnActionEvent\nimport com.intellij.openapi.actionSystem.DefaultActionGroup\nimport com.intellij.util.EventDispatcher\nimport net.miginfocom.swing.MigLayout\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport javax.swing.JComponent\nimport javax.swing.JPanel\n\nclass ProviderCollectionToolbar : ProviderCollectionToolbarUI, Component {\n    override val eventDispatcher = EventDispatcher.create(ProviderCollectionToolbarEventListener::class.java)\n\n    private val myPanel by lazy {\n        val panel = JPanel(MigLayout(\"ins 0, fill\", \"[left, fill]push[right]\", \"center\"))\n\n        val mainActionGroup = DefaultActionGroup()\n        mainActionGroup.add(myRefreshAction)\n        // mainActionGroup.addSeparator()\n        // mainActionGroup.add(myAddAction)\n\n        val toolbar = ActionManager.getInstance().createActionToolbar(\n            \"${ProviderCollectionToolbar::class.java.canonicalName}/toolbar-left\", mainActionGroup, true\n        )\n\n        val rightCornerActionGroup = DefaultActionGroup()\n        // rightCornerActionGroup.add(myHelpAction)\n        val rightCornerToolbar = ActionManager.getInstance().createActionToolbar(\n            \"${ProviderCollectionToolbar::class.java.canonicalName}/toolbar-right\", rightCornerActionGroup, true\n        )\n\n        panel.add(toolbar.component)\n        panel.add(rightCornerToolbar.component)\n        panel\n    }\n\n    private class MyHelpAction(private val self: ProviderCollectionToolbar) :\n        AnAction(\"Help\", null, AllIcons.Actions.Help) {\n        override fun actionPerformed(e: AnActionEvent) {\n            self.eventDispatcher.multicaster.helpClicked()\n        }\n    }\n    private val myHelpAction = MyHelpAction(this)\n\n    private class MyRefreshAction(private val self: ProviderCollectionToolbar) :\n        AnAction(\"Refresh\", null, AllIcons.Actions.Refresh) {\n        override fun actionPerformed(e: AnActionEvent) {\n            self.eventDispatcher.multicaster.refreshClicked()\n        }\n    }\n    private val myRefreshAction = MyRefreshAction(this)\n\n    override fun createComponent(): JComponent = myPanel\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderCollectionToolbarEventListener.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.provider\n\nimport java.util.*\n\ninterface ProviderCollectionToolbarEventListener: EventListener {\n    fun refreshClicked()\n\n    fun helpClicked()\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderCollectionToolbarUI.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.provider\n\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\n\ninterface ProviderCollectionToolbarUI : Component {\n    val eventDispatcher: EventDispatcher<ProviderCollectionToolbarEventListener>\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderDetails.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.provider\n\nimport com.intellij.icons.AllIcons\nimport com.intellij.openapi.actionSystem.*\nimport com.intellij.openapi.wm.ToolWindow\nimport com.intellij.ui.ScrollPaneFactory\nimport com.intellij.ui.tabs.TabInfo\nimport com.intellij.ui.tabs.TabsListener\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.MergeRequestState\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.ProviderStatus\nimport net.ntworld.mergeRequest.api.MergeRequestOrdering\nimport net.ntworld.mergeRequest.query.GetMergeRequestFilter\nimport net.ntworld.mergeRequestIntegration.make\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.MergeRequestCollectionEventListener\nimport net.ntworld.mergeRequestIntegrationIde.ui.panel.ProviderInformationPanel\nimport net.ntworld.mergeRequestIntegrationIde.ui.util.Tabs\nimport net.ntworld.mergeRequestIntegrationIde.ui.util.TabsUI\nimport javax.swing.JComponent\n\nclass ProviderDetails(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val toolWindow: ToolWindow,\n    private val dispatcher: EventDispatcher<ProviderCollectionListEventListener>,\n    private val providerData: ProviderData\n) : ProviderDetailsUI {\n    override val listEventDispatcher = EventDispatcher.create(MergeRequestCollectionEventListener::class.java)\n\n    private val myListEventListener = object: MergeRequestCollectionEventListener {\n        override fun mergeRequestUnselected() {\n            listEventDispatcher.multicaster.mergeRequestUnselected()\n        }\n\n        override fun mergeRequestSelected(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) {\n            listEventDispatcher.multicaster.mergeRequestSelected(providerData, mergeRequestInfo)\n        }\n    }\n\n    private val myCommonCenterActionGroupFactory: () -> ActionGroup = {\n        DefaultActionGroup()\n    }\n    private class MyOpenAction(private val self: ProviderDetails):\n        AnAction(\"Open\", \"Open provider in new tab\", AllIcons.Actions.Menu_open) {\n        override fun actionPerformed(e: AnActionEvent) {\n            self.dispatcher.multicaster.providerOpened(self.providerData)\n        }\n\n        override fun update(e: AnActionEvent) {\n            e.presentation.isVisible = self.providerData.status == ProviderStatus.ACTIVE\n        }\n\n        override fun displayTextInToolbar(): Boolean = true\n    }\n    private val myCommonSideComponentFactory: () -> JComponent = {\n        val actionGroup = DefaultActionGroup()\n        actionGroup.add(MyOpenAction(this))\n\n        val toolbar = ActionManager.getInstance().createActionToolbar(\n            \"${ProviderDetails::class.java.canonicalName}/toolbar-right\", actionGroup, true\n        )\n        toolbar.component\n    }\n    private val myTabs: TabsUI by lazy {\n        val tabs = Tabs(projectServiceProvider.project, toolWindow.contentManager)\n        tabs.setCommonCenterActionGroupFactory(myCommonCenterActionGroupFactory)\n        tabs.setCommonSideComponentFactory(myCommonSideComponentFactory)\n        tabs\n    }\n    private val myDetailsTabInfo by lazy {\n        val tabInfo = TabInfo(\n            ScrollPaneFactory.createScrollPane(\n                ProviderInformationPanel(projectServiceProvider.applicationServiceProvider, providerData)\n                    .createComponent()\n            )\n        )\n        tabInfo.text = \"General Information\"\n\n        tabInfo\n    }\n\n    private val myOpeningMRList by lazy {\n        val list = ProviderDetailsMRList(\n            projectServiceProvider,\n            providerData,\n            GetMergeRequestFilter.make(\n                state = MergeRequestState.OPENED,\n                id = null,\n                search = \"\",\n                authorId = \"\",\n                assigneeId = \"\",\n                approverIds = listOf(),\n                sourceBranch = \"\"\n            ),\n            MergeRequestOrdering.RECENTLY_UPDATED,\n            displayType = ProviderDetailsMRList.ApprovalStatusDisplayType.NONE\n        )\n        list.eventDispatcher.addListener(myListEventListener)\n        list\n    }\n    private val myOpeningMRTabInfo by lazy {\n        val tabInfo = TabInfo(myOpeningMRList.createComponent())\n        tabInfo.text = \"Opening MRs\"\n\n        tabInfo\n    }\n\n    private val myMyMRList by lazy {\n        val list = ProviderDetailsMRList(\n            projectServiceProvider,\n            providerData,\n            GetMergeRequestFilter.make(\n                state = MergeRequestState.OPENED,\n                id = null,\n                search = \"\",\n                authorId = providerData.currentUser.id,\n                assigneeId = \"\",\n                approverIds = listOf(),\n                sourceBranch = \"\"\n            ),\n            MergeRequestOrdering.RECENTLY_UPDATED,\n            displayType = ProviderDetailsMRList.ApprovalStatusDisplayType.STATUSES\n        )\n        list.eventDispatcher.addListener(myListEventListener)\n        list\n    }\n    private val myMyMRTabInfo by lazy {\n        val tabInfo = TabInfo(myMyMRList.createComponent())\n        tabInfo.text = \"My MRs\"\n\n        tabInfo\n    }\n\n    private val myMyAssignedMRList by lazy {\n        val list = ProviderDetailsMRList(\n            projectServiceProvider,\n            providerData,\n            GetMergeRequestFilter.make(\n                state = MergeRequestState.OPENED,\n                id = null,\n                search = \"\",\n                authorId = \"\",\n                assigneeId = providerData.currentUser.id,\n                approverIds = listOf(),\n                sourceBranch = \"\"\n            ),\n            MergeRequestOrdering.RECENTLY_UPDATED,\n            displayType = ProviderDetailsMRList.ApprovalStatusDisplayType.STATUSES\n        )\n        list.eventDispatcher.addListener(myListEventListener)\n        list\n    }\n    private val myMyAssignedMRTabInfo by lazy {\n        val tabInfo = TabInfo(myMyAssignedMRList.createComponent())\n        tabInfo.text = \"MRs assigned to me\"\n\n        tabInfo\n    }\n\n    private val myWaitingForMyApprovalMRList by lazy {\n        val list = ProviderDetailsMRList(\n            projectServiceProvider,\n            providerData,\n            GetMergeRequestFilter.make(\n                state = MergeRequestState.OPENED,\n                id = null,\n                search = \"\",\n                authorId = \"\",\n                assigneeId = \"\",\n                approverIds = listOf(providerData.currentUser.id),\n                sourceBranch = \"\"\n            ),\n            MergeRequestOrdering.RECENTLY_UPDATED,\n            displayType = ProviderDetailsMRList.ApprovalStatusDisplayType.STATUSES_AND_MINE_APPROVAL\n        )\n        list.eventDispatcher.addListener(myListEventListener)\n        list\n    }\n    private val myWaitingForMyApprovalMRTabInfo by lazy {\n        val tabInfo = TabInfo(myWaitingForMyApprovalMRList.createComponent())\n        tabInfo.text = \"MRs waiting for my approval\"\n\n        tabInfo\n    }\n\n    init {\n        myTabs.addTab(myDetailsTabInfo)\n        myTabs.addTab(myOpeningMRTabInfo)\n        myTabs.addTab(myMyMRTabInfo)\n        if (providerData.hasAssigneeFeature) {\n            myTabs.addTab(myMyAssignedMRTabInfo)\n        }\n        if (providerData.hasApprovalFeature) {\n            myTabs.addTab(myWaitingForMyApprovalMRTabInfo)\n        }\n\n        myTabs.addListener(object : TabsListener {\n            override fun selectionChanged(oldSelection: TabInfo?, newSelection: TabInfo?) {\n                if (newSelection === myOpeningMRTabInfo) {\n                    myOpeningMRList.fetchIfNotLoaded()\n                    return\n                }\n                if (newSelection === myMyMRTabInfo) {\n                    myMyMRList.fetchData()\n                    return\n                }\n                if (providerData.hasAssigneeFeature && newSelection === myMyAssignedMRTabInfo) {\n                    myMyAssignedMRList.fetchData()\n                    return\n                }\n                if (providerData.hasApprovalFeature && newSelection === myWaitingForMyApprovalMRTabInfo) {\n                    myWaitingForMyApprovalMRList.fetchData()\n                    return\n                }\n            }\n        })\n    }\n\n    override fun hide() {\n        myTabs.component.isVisible = false\n    }\n\n    override fun show() {\n        myTabs.component.isVisible = true\n    }\n\n    override fun createComponent(): JComponent = myTabs.component\n}\n\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderDetailsMRList.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.provider\n\nimport com.intellij.ui.ScrollPaneFactory\nimport com.intellij.ui.components.JBList\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequest.ProviderStatus\nimport net.ntworld.mergeRequest.api.MergeRequestOrdering\nimport net.ntworld.mergeRequest.query.GetMergeRequestFilter\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.AbstractMergeRequestCollection\nimport net.ntworld.mergeRequestIntegrationIde.ui.panel.MergeRequestItemPanel\nimport java.awt.event.MouseAdapter\nimport java.awt.event.MouseEvent\nimport javax.swing.JComponent\nimport javax.swing.ListCellRenderer\nimport javax.swing.ListSelectionModel\n\nclass ProviderDetailsMRList(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val providerData: ProviderData,\n    private val filterBy: GetMergeRequestFilter,\n    private val orderBy:  MergeRequestOrdering,\n    private val displayType: ApprovalStatusDisplayType\n): AbstractMergeRequestCollection(projectServiceProvider, providerData) {\n    private var isLoaded = false\n    private val myList = JBList<MergeRequestInfo>()\n    private val myItemPanels = mutableMapOf<Int, MergeRequestItemPanel>()\n    private val myCellRenderer = ListCellRenderer<MergeRequestInfo> { _, value, index, isSelected, cellHasFocus ->\n        if (null === myItemPanels[index]) {\n            myItemPanels[index] = MergeRequestItemPanel(\n                projectServiceProvider,\n                providerData,\n                value,\n                displayType\n            )\n        }\n        val panel = myItemPanels[index]!!\n        panel.changeStyle(isSelected, cellHasFocus)\n        panel.createComponent()\n    }\n    private val myListMouseListener = object: MouseAdapter() {\n        override fun mouseClicked(e: MouseEvent?) {\n            if (null === e) {\n                return\n            }\n            if (e.clickCount == 2) {\n                val value = myList.selectedValue\n                if (null === value) {\n                    eventDispatcher.multicaster.mergeRequestUnselected()\n                } else {\n                    eventDispatcher.multicaster.mergeRequestSelected(providerData, value)\n                }\n            }\n        }\n    }\n\n    init {\n        myList.selectionMode = ListSelectionModel.SINGLE_SELECTION\n        myList.cellRenderer = myCellRenderer\n        myList.addMouseListener(myListMouseListener)\n    }\n\n    override fun makeContent(): JComponent {\n        setFilter(filterBy)\n        setOrder(orderBy)\n\n        return ScrollPaneFactory.createScrollPane(myList)\n    }\n\n    fun fetchIfNotLoaded() {\n        if (!isLoaded && providerData.status == ProviderStatus.ACTIVE) {\n            fetchData()\n            isLoaded = true\n        }\n    }\n\n    override fun fetchDataStarted() {\n        myList.isVisible = false\n    }\n\n    override fun fetchDataStopped() {\n        myList.isVisible = true\n    }\n\n    override fun dataReceived(collection: List<MergeRequestInfo>) {\n        myItemPanels.clear()\n        myList.setListData(collection.toTypedArray())\n        myList.isVisible = true\n    }\n\n    enum class ApprovalStatusDisplayType {\n        NONE,\n        STATUSES_AND_MINE_APPROVAL,\n        STATUSES\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/provider/ProviderDetailsUI.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.provider\n\nimport com.intellij.util.EventDispatcher\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.MergeRequestCollectionEventListener\n\ninterface ProviderDetailsUI : Component {\n    val listEventDispatcher: EventDispatcher<MergeRequestCollectionEventListener>\n\n    fun hide()\n\n    fun show()\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/service/CheckoutService.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.service\n\nimport com.intellij.openapi.components.ServiceManager\nimport git4idea.branch.GitBrancher\nimport com.intellij.openapi.project.Project as IdeaProject\nimport git4idea.repo.GitRepository\nimport net.ntworld.mergeRequest.MergeRequest\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil\nimport kotlin.Exception\n\nobject CheckoutService {\n    private var myCurrentBranch: String? = null\n\n    fun stop(projectServiceProvider: ProjectServiceProvider, providerData: ProviderData) {\n        val branch = myCurrentBranch\n        val repository = RepositoryUtil.findRepository(projectServiceProvider, providerData)\n        if (null !== branch && null !== repository) {\n            doCheckout(projectServiceProvider, false, repository, providerData, branch, object : Listener {\n                override fun onError(exception: Exception) {\n                    myCurrentBranch = null\n                }\n\n                override fun onSuccess() {\n                    myCurrentBranch = null\n                }\n            })\n        }\n    }\n\n    fun start(projectServiceProvider: ProjectServiceProvider, providerData: ProviderData, mergeRequest: MergeRequest, listener: Listener) {\n        val repository = RepositoryUtil.findRepository(projectServiceProvider, providerData)\n        if (null === repository) {\n            return listener.onError(Exception(\"Repository not found\"))\n        }\n        doCheckout(projectServiceProvider, true, repository, providerData, mergeRequest.sourceBranch, listener)\n    }\n\n    private fun doCheckout(\n        projectServiceProvider: ProjectServiceProvider,\n        useRemote: Boolean,\n        repository: GitRepository,\n        providerData: ProviderData,\n        branch: String,\n        listener: Listener\n    ) {\n        val currentBranch = repository.currentBranch\n        if (null !== currentBranch) {\n            if (currentBranch.name == branch) {\n                return listener.onSuccess()\n            }\n            myCurrentBranch = currentBranch.name\n        }\n\n        val branchExecutor = ServiceManager.getService(projectServiceProvider.project, GitBrancher::class.java)\n        try {\n            branchExecutor.checkout(branch, false, listOf(repository)) { listener.onSuccess() }\n        } catch (exception: Exception) {\n            if (!useRemote) {\n                return listener.onError(exception)\n            }\n\n            val remoteName = findRemoteName(repository, providerData)\n            if (remoteName.isEmpty()) {\n                return listener.onError(Exception(\"Cannot find remote of repository\"))\n            }\n\n            try {\n                branchExecutor.checkoutNewBranchStartingFrom(\n                    branch, \"$remoteName/${branch}\",\n                    false, listOf(repository)\n                ) {\n                    listener.onSuccess()\n                }\n            } catch (exception: Exception) {\n                listener.onError(exception)\n            }\n        }\n    }\n\n    private fun findRemoteName(repository: GitRepository, providerData: ProviderData): String {\n        for (remote in repository.remotes) {\n            for (url in remote.urls) {\n                if (url == providerData.project.repositoryHttpUrl || url == providerData.project.repositorySshUrl) {\n                    return remote.name\n                }\n            }\n        }\n        return \"\"\n    }\n\n    interface Listener {\n        fun onError(exception: Exception)\n\n        fun onSuccess()\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/service/CodeReviewService.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.service\n\nimport com.intellij.notification.NotificationType\nimport com.intellij.openapi.wm.ToolWindowManager\nimport net.ntworld.mergeRequest.Commit\nimport net.ntworld.mergeRequest.MergeRequest\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifier\nimport kotlin.Exception\nimport com.intellij.openapi.project.Project as IdeaProject\n\nobject CodeReviewService {\n    var checkedOut = false\n\n    fun start(\n        projectServiceProvider: ProjectServiceProvider,\n        providerData: ProviderData,\n        mergeRequest: MergeRequest,\n        commits: List<Commit>\n    ) {\n        checkedOut = false\n        projectServiceProvider.reviewContextManager.updateReviewingCommits(providerData.id, mergeRequest.id, commits)\n        projectServiceProvider.startCodeReview(providerData, mergeRequest)\n        checkout(projectServiceProvider, providerData, mergeRequest, commits)\n    }\n\n    fun stop(\n        projectServiceProvider: ProjectServiceProvider,\n        providerData: ProviderData,\n        mergeRequest: MergeRequest\n    ) {\n        projectServiceProvider.stopCodeReview()\n        if (checkedOut) {\n            CheckoutService.stop(projectServiceProvider, providerData)\n            DisplayChangesService.stop(projectServiceProvider.project, providerData, mergeRequest)\n        }\n    }\n\n    private fun checkout(\n        projectServiceProvider: ProjectServiceProvider,\n        providerData: ProviderData,\n        mergeRequest: MergeRequest,\n        commits: List<Commit>\n    ) {\n        if (!projectServiceProvider.applicationSettings.checkoutTargetBranch) {\n            return checkoutSuccess(projectServiceProvider, providerData, mergeRequest, commits)\n        }\n\n        CheckoutService.start(projectServiceProvider, providerData, mergeRequest, object : CheckoutService.Listener {\n            override fun onError(exception: Exception) {\n                projectServiceProvider.notify(\n                    \"Cannot checkout branch ${mergeRequest.sourceBranch}\\n\\nPlease do git checkout manually before click Code Review\",\n                    NotificationType.ERROR\n                )\n                this@CodeReviewService.stop(projectServiceProvider, providerData, mergeRequest)\n            }\n\n            override fun onSuccess() {\n                checkoutSuccess(projectServiceProvider, providerData, mergeRequest, commits)\n            }\n        })\n    }\n\n    private fun checkoutSuccess(\n        projectServiceProvider: ProjectServiceProvider,\n        providerData: ProviderData,\n        mergeRequest: MergeRequest,\n        commits: List<Commit>\n    ) {\n        checkedOut = true\n        EditorStateService.start(projectServiceProvider.project)\n        DisplayChangesService.start(\n            projectServiceProvider.applicationServiceProvider,\n            projectServiceProvider.project,\n            providerData, mergeRequest, commits\n        )\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/service/DisplayChangesService.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.service\n\nimport com.intellij.ide.ui.UISettings\nimport com.intellij.openapi.application.ApplicationManager\nimport com.intellij.openapi.fileEditor.ex.FileEditorManagerEx\nimport com.intellij.openapi.vcs.changes.*\nimport com.intellij.openapi.project.Project as IdeaProject\nimport com.intellij.ui.tabs.JBTabsPosition\nimport com.intellij.vcs.log.Hash\nimport com.intellij.vcs.log.impl.VcsLogManager\nimport com.intellij.vcs.log.util.VcsLogUtil\nimport git4idea.repo.GitRepository\nimport net.ntworld.mergeRequest.Commit\nimport net.ntworld.mergeRequest.MergeRequest\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.util.RepositoryUtil\nimport javax.swing.SwingConstants\nimport kotlin.concurrent.thread\n\nobject DisplayChangesService {\n    private var myTabPlacement: JBTabsPosition? = null\n    private val myChangePreviewDiffVirtualFileMap = mutableMapOf<Change, PreviewDiffVirtualFile>()\n\n    fun stop(ideaProject: IdeaProject, providerData: ProviderData, mergeRequest: MergeRequest) {\n        closeAllDiffsAndRestoreTabPlacement(ideaProject)\n        myTabPlacement = null\n        myChangePreviewDiffVirtualFileMap.clear()\n    }\n\n    fun start(\n        applicationServiceProvider: ApplicationServiceProvider,\n        ideaProject: IdeaProject,\n        providerData: ProviderData,\n        mergeRequest: MergeRequest,\n        commits: List<Commit>\n    ) {\n        if (myChangePreviewDiffVirtualFileMap.isNotEmpty()) {\n            myChangePreviewDiffVirtualFileMap.clear()\n        }\n        myTabPlacement = null\n        val diff = mergeRequest.diffReference\n        val repository = RepositoryUtil.findRepository(\n            applicationServiceProvider.findProjectServiceProvider(ideaProject),\n            providerData\n        )\n        if (null === repository || null === diff) {\n            return\n        }\n\n        applicationServiceProvider.intellijIdeApi.getVcsLogManager(ideaProject) {\n            if (commits.size <= 1) {\n                thread {\n                    val hash = if (commits.isEmpty()) diff.headHash else commits.first().id\n                    displayChangesForOneCommit(\n                        applicationServiceProvider,\n                        ideaProject,\n                        providerData,\n                        mergeRequest,\n                        repository,\n                        it,\n                        hash\n                    )\n                }\n            } else {\n                thread {\n                    displayChangesForCommits(\n                        applicationServiceProvider,\n                        ideaProject,\n                        providerData,\n                        mergeRequest,\n                        repository,\n                        it,\n                        commits\n                    )\n                }\n            }\n        }\n    }\n\n    private fun displayChangesForOneCommit(\n        applicationServiceProvider: ApplicationServiceProvider,\n        ideaProject: IdeaProject,\n        providerData: ProviderData,\n        mergeRequest: MergeRequest,\n        repository: GitRepository,\n        log: VcsLogManager,\n        hash: String\n    ) {\n        // TODO: Reduce repetition\n        val details = VcsLogUtil.getDetails(log.dataManager, repository.root, MyHash(hash))\n        displayChanges(\n            applicationServiceProvider,\n            ideaProject,\n            providerData,\n            mergeRequest,\n            details.changes.toList()\n        )\n    }\n\n    private fun displayChangesForCommits(\n        applicationServiceProvider: ApplicationServiceProvider,\n        ideaProject: IdeaProject,\n        providerData: ProviderData,\n        mergeRequest: MergeRequest,\n        repository: GitRepository,\n        log: VcsLogManager,\n        commits: List<Commit>\n    ) {\n        // TODO: Reduce repetition\n        val details = VcsLogUtil.getDetails(\n            log.dataManager.getLogProvider(repository.root),\n            repository.root,\n            commits.map { it.id }\n        )\n        if (details.isEmpty()) {\n            return\n        }\n\n        if (details.size == 1) {\n            return displayChanges(\n                applicationServiceProvider, ideaProject,\n                providerData, mergeRequest, details.first().changes.toList()\n            )\n        }\n\n        val changes = VcsLogUtil.collectChanges(details) {\n            it.changes\n        }\n        displayChanges(applicationServiceProvider, ideaProject, providerData, mergeRequest, changes)\n    }\n\n    private fun displayChanges(\n        applicationServiceProvider: ApplicationServiceProvider,\n        ideaProject: IdeaProject,\n        providerData: ProviderData,\n        mergeRequest: MergeRequest,\n        changes: List<Change>\n    ) {\n        // TODO: call updateReviewingChanges too many time, let's save some resource\n        applicationServiceProvider.findProjectServiceProvider(ideaProject)\n            .reviewContextManager.updateReviewingChanges(providerData.id, mergeRequest.id, changes)\n\n        ApplicationManager.getApplication().invokeLater {\n            val projectServiceProvider = applicationServiceProvider.findProjectServiceProvider(ideaProject)\n            val reviewContext = projectServiceProvider.reviewContextManager.findDoingCodeReviewContext()\n            val max = projectServiceProvider.applicationSettings.maxDiffChangesOpenedAutomatically\n            if (null !== reviewContext && max != 0 && changes.size < max) {\n                val limit = UISettings().editorTabLimit\n                changes.forEachIndexed { index, item ->\n                    if (index < limit) {\n                        reviewContext.openChange(item, focus = true, displayMergeRequestId = false)\n                    }\n                }\n            }\n        }\n    }\n\n    private fun closeAllDiffsAndRestoreTabPlacement(ideaProject: IdeaProject) {\n        val fileEditorManagerEx = FileEditorManagerEx.getInstanceEx(ideaProject)\n        val openFiles = fileEditorManagerEx.openFiles\n        for (openFile in openFiles) {\n            fileEditorManagerEx.closeFile(openFile)\n        }\n\n        val tabPlacement = myTabPlacement\n        if (null !== tabPlacement) {\n            val editorWindows = fileEditorManagerEx.windows\n            if (editorWindows.size == 1) {\n                val editorWindow = editorWindows.first()\n                editorWindow.tabbedPane.setTabPlacement(\n                    when (tabPlacement) {\n                        JBTabsPosition.top -> SwingConstants.TOP\n                        JBTabsPosition.left -> SwingConstants.LEFT\n                        JBTabsPosition.bottom -> SwingConstants.BOTTOM\n                        JBTabsPosition.right -> SwingConstants.RIGHT\n                    }\n                )\n            }\n        }\n    }\n\n    // TODO: Will add an option in configuration then call later\n    private fun rearrangeTabPlacement(fileEditorManagerEx: FileEditorManagerEx) {\n        val editorWindows = fileEditorManagerEx.windows\n        if (editorWindows.size == 1) {\n            val editorWindow = editorWindows.first()\n            myTabPlacement = editorWindow.tabbedPane.tabs.presentation.tabsPosition\n            editorWindow.tabbedPane.setTabPlacement(SwingConstants.LEFT)\n        }\n    }\n\n    class MyHash(private val hash: String) : Hash {\n        override fun toShortString(): String {\n            return hash.substring(0, 6)\n        }\n\n        override fun asString(): String {\n            return hash\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/service/EditorStateService.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.service\n\nimport com.intellij.openapi.fileEditor.ex.FileEditorManagerEx\nimport com.intellij.openapi.project.Project\n\nobject EditorStateService {\n    fun start(ideaProject: Project) {\n        val fileManager = FileEditorManagerEx.getInstance(ideaProject)\n        val openFiles = fileManager.openFiles\n        for (openFile in openFiles) {\n            fileManager.closeFile(openFile)\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/service/FetchService.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.service\n\nimport git4idea.GitVcs\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegration.util.DateTimeUtil\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.task.RepositoryFetchAllRemotesTask\nimport java.util.*\nimport com.intellij.openapi.project.Project as IdeaProject\n\nobject FetchService {\n    private var myLastFetchedDateTime: Date? = null\n\n    fun start(applicationServiceProvider: ApplicationServiceProvider, ideaProject: IdeaProject, providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) {\n        val lastFetch = myLastFetchedDateTime\n        val lastUpdated = DateTimeUtil.toDate(mergeRequestInfo.updatedAt)\n        if (lastFetch !== null && lastFetch > lastUpdated) {\n            return\n        }\n\n        try {\n            GitVcs.runInBackground(RepositoryFetchAllRemotesTask(\n                applicationServiceProvider.findProjectServiceProvider(ideaProject),\n                providerData\n            ))\n        } catch (exception: Exception) {\n            applicationServiceProvider.findProjectServiceProvider(ideaProject).notify(\n                \"Cannot fetch from remotes. The changes in Commits tab may not be displayed correctly. \\n Please run 'git fetch' manually if you want to see the change list.\"\n            )\n        }\n        myLastFetchedDateTime = Date(System.currentTimeMillis())\n    }\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/toolWindowTab/HomeToolWindowTab.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.toolWindowTab\n\nimport com.intellij.openapi.Disposable\nimport com.intellij.openapi.util.Disposer\nimport com.intellij.openapi.wm.ToolWindow\nimport com.intellij.ui.OnePixelSplitter\nimport com.intellij.ui.content.Content\nimport com.intellij.ui.content.ContentManagerEvent\nimport com.intellij.ui.content.ContentManagerListener\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ReviewContext\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifier\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.notifier.ProjectNotifierAdapter\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.MergeRequestCollectionEventListener\nimport net.ntworld.mergeRequestIntegrationIde.ui.provider.*\nimport javax.swing.JComponent\nimport javax.swing.JPanel\n\nclass HomeToolWindowTab(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val toolWindow: ToolWindow\n) : Component, Disposable {\n    private val mySplitter = OnePixelSplitter(HomeToolWindowTab::class.java.canonicalName, 0.35f)\n    private val myCollectionPanel = ProviderCollection(projectServiceProvider)\n    private val myDetailPanels = mutableMapOf<String, ProviderDetailsUI>()\n    private val myContents = mutableMapOf<String, Content>()\n    private val myMRToolWindowTabs = mutableMapOf<String, MergeRequestToolWindowTab>()\n    private val myProjectNotifier = object : ProjectNotifierAdapter() {\n        override fun startCodeReview(reviewContext: ReviewContext) {\n            myDetailPanels.forEach { it.value.hide() }\n            myContents.forEach {\n                if (it.key == reviewContext.providerData.id) {\n                    it.value.isCloseable = false\n                }\n            }\n        }\n\n        override fun stopCodeReview(reviewContext: ReviewContext) {\n            myDetailPanels.forEach { it.value.show() }\n            myContents.forEach {\n                if (it.key == reviewContext.providerData.id) {\n                    it.value.isCloseable = true\n                }\n            }\n        }\n    }\n    private val myConnection = projectServiceProvider.messageBus.connect()\n\n    private val myDetailsListEventListener = object : MergeRequestCollectionEventListener {\n        override fun mergeRequestUnselected() {\n        }\n\n        override fun mergeRequestSelected(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) {\n            openTab(providerData, mergeRequestInfo)\n        }\n    }\n    private val myCollectionPanelListEventListener = object : ProviderCollectionListEventListener {\n        override fun providerUnselected() {\n            mySplitter.secondComponent = null\n        }\n\n        override fun providerSelected(providerData: ProviderData) {\n            if (null === myDetailPanels[providerData.id]) {\n                val details = ProviderDetails(\n                    projectServiceProvider,\n                    toolWindow,\n                    myCollectionPanel.getListEventDispatcher(), providerData\n                )\n                myDetailPanels[providerData.id] = details\n                details.listEventDispatcher.addListener(myDetailsListEventListener)\n            }\n            mySplitter.secondComponent = myDetailPanels[providerData.id]!!.createComponent()\n        }\n\n        override fun providerOpened(providerData: ProviderData) {\n            openTab(providerData)\n        }\n    }\n    private val myContentManagerListener = object: ContentManagerListener {\n        override fun contentAdded(event: ContentManagerEvent) {\n        }\n\n        override fun contentRemoveQuery(event: ContentManagerEvent) {\n        }\n\n        override fun selectionChanged(event: ContentManagerEvent) {\n        }\n\n        override fun contentRemoved(event: ContentManagerEvent) {\n            var removedId: String? = null\n            for (entry in myContents) {\n                if (entry.value === event.content) {\n                    removedId = entry.key\n                    break\n                }\n            }\n            if (null !== removedId) {\n                myContents.remove(removedId)\n            }\n        }\n    }\n    private val myCollectionPanelToolbarEventListener = object: ProviderCollectionToolbarEventListener {\n        override fun refreshClicked() {\n            myContents.forEach {\n                toolWindow.contentManager.removeContent(it.value, true)\n            }\n            myMRToolWindowTabs.clear()\n            myContents.clear()\n        }\n\n        override fun helpClicked() {\n        }\n    }\n\n    init {\n        mySplitter.firstComponent = myCollectionPanel.createComponent()\n        mySplitter.secondComponent = JPanel()\n        toolWindow.contentManager.addContentManagerListener(myContentManagerListener)\n        myCollectionPanel.addListEventListener(myCollectionPanelListEventListener)\n        myCollectionPanel.addToolbarEventListener(myCollectionPanelToolbarEventListener)\n        myConnection.subscribe(ProjectNotifier.TOPIC, myProjectNotifier)\n        Disposer.register(projectServiceProvider.project, this)\n    }\n\n    private fun findNameForTab(providerData: ProviderData): String {\n        if (projectServiceProvider.applicationServiceProvider.isLegal(providerData)) {\n            return providerData.name\n        }\n        return if (providerData.name.length < 20) {\n            \"${providerData.name} · Not legal for private repository · please buy EE version · only 1\\$/month\"\n        } else {\n            \"Not legal for private repository · please buy EE version · only 1\\$/month · ${providerData.name}\"\n        }\n    }\n\n    private fun openTab(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo? = null) {\n        if (null === myContents[providerData.id]) {\n            val toolWindowTab = MergeRequestToolWindowTab(\n                projectServiceProvider, toolWindow.contentManager, providerData\n            )\n            myMRToolWindowTabs[providerData.id] = toolWindowTab\n\n            val content = toolWindow.contentManager.factory.createContent(\n                toolWindowTab.createComponent(),\n                findNameForTab(providerData),\n                false\n            )\n            content.isCloseable = true\n            myContents[providerData.id] = content\n            toolWindow.contentManager.addContent(content)\n        }\n\n        val tab = myContents[providerData.id]\n        if (null !== tab) {\n            toolWindow.contentManager.setSelectedContent(tab)\n        }\n\n        if (null !== mergeRequestInfo) {\n            val toolWindowTab = myMRToolWindowTabs[providerData.id]\n            if (null !== toolWindowTab) {\n                toolWindowTab.selectMergeRequest(mergeRequestInfo)\n            }\n        }\n    }\n\n    override fun createComponent(): JComponent = mySplitter\n\n    override fun dispose() {\n        myConnection.disconnect()\n    }\n\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/toolWindowTab/MergeRequestToolWindowTab.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.toolWindowTab\n\nimport com.intellij.ui.OnePixelSplitter\nimport com.intellij.ui.content.ContentManager\nimport net.ntworld.mergeRequest.MergeRequestInfo\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.*\nimport javax.swing.JComponent\n\nclass MergeRequestToolWindowTab(\n    private val projectServiceProvider: ProjectServiceProvider,\n    private val contentManager: ContentManager,\n    private val providerData: ProviderData\n) : Component {\n    private val mySplitter by lazy {\n        OnePixelSplitter(\n            \"${MergeRequestToolWindowTab::class.java.canonicalName}:${providerData.info.id}:${providerData.name}\",\n            0.35f\n        )\n    }\n    private val myCollection: MergeRequestCollectionUI = MergeRequestCollection(projectServiceProvider, providerData)\n    private val myDetails: MergeRequestDetailsUI = MergeRequestDetails(projectServiceProvider, contentManager, providerData)\n    private val myCollectionListener = object : MergeRequestCollectionEventListener {\n        override fun mergeRequestUnselected() {\n            myDetails.hide()\n        }\n\n        override fun mergeRequestSelected(providerData: ProviderData, mergeRequestInfo: MergeRequestInfo) {\n            myDetails.setMergeRequestInfo(mergeRequestInfo)\n        }\n    }\n\n    init {\n        myCollection.eventDispatcher.addListener(myCollectionListener)\n        mySplitter.firstComponent = myCollection.createComponent()\n        mySplitter.secondComponent = myDetails.createComponent()\n    }\n\n    override fun createComponent(): JComponent = mySplitter\n\n    fun selectMergeRequest(mergeRequestInfo: MergeRequestInfo) {\n        myDetails.setMergeRequestInfo(mergeRequestInfo)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/toolWindowTab/UpdateInfoTab.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.toolWindowTab\n\nimport com.intellij.ide.util.TipUIUtil\nimport com.intellij.ui.ScrollPaneFactory\nimport net.ntworld.mergeRequestIntegrationIde.ui.Component\nimport net.ntworld.mergeRequestIntegrationIde.ui.mergeRequest.tab.MergeRequestDescriptionTab\nimport javax.swing.JComponent\n\nclass UpdateInfoTab(private val updates: List<String>) : Component {\n    private val myWebView = TipUIUtil.createBrowser() as TipUIUtil.Browser\n    private val myHtmlTemplate = MergeRequestDescriptionTab::class.java.getResource(\n        \"/templates/update.html\"\n    ).readText()\n\n    init {\n        myWebView.text = buildHtml()\n    }\n\n    private fun buildHtml() : String {\n        return myHtmlTemplate\n            .replace(\"{{content}}\", updates.joinToString(\"<br /><br />\"))\n    }\n\n    override fun createComponent(): JComponent = ScrollPaneFactory.createScrollPane(myWebView.component)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/util/CustomSimpleToolWindowPanel.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.util\n\nimport com.intellij.openapi.actionSystem.ActionToolbar\nimport com.intellij.openapi.actionSystem.AnAction\nimport com.intellij.openapi.actionSystem.DataProvider\nimport com.intellij.ui.JBColor\nimport com.intellij.ui.components.JBPanelWithEmptyText\nimport com.intellij.ui.paint.LinePainter2D\nimport com.intellij.ui.switcher.QuickActionProvider\nimport com.intellij.util.ui.UIUtil\nimport org.jetbrains.annotations.NonNls\nimport java.awt.BorderLayout\nimport java.awt.Container\nimport java.awt.Graphics\nimport java.awt.Graphics2D\nimport java.awt.event.ContainerAdapter\nimport java.awt.event.ContainerEvent\nimport javax.swing.JComponent\nimport javax.swing.SwingConstants\n\nclass CustomSimpleToolWindowPanel(\n    private val vertical: Boolean,\n    private val borderless: Boolean\n) : JBPanelWithEmptyText(), QuickActionProvider, DataProvider {\n    private var myToolbar: JComponent? = null\n    private var myContent: JComponent? = null\n\n    private var myBorderless = borderless\n    private var myVertical = vertical\n    private var myProvideQuickActions = false\n\n    init {\n        layout = BorderLayout(if (vertical) 0 else 1, if (vertical) 1 else 0)\n        setProvideQuickActions(true)\n\n        addContainerListener(object : ContainerAdapter() {\n            override fun componentAdded(e: ContainerEvent?) {\n                val child = e!!.child\n\n                if (child is Container) {\n                    child.addContainerListener(this)\n                }\n                if (myBorderless) {\n                    UIUtil.removeScrollBorder(this@CustomSimpleToolWindowPanel)\n                }\n            }\n\n            override fun componentRemoved(e: ContainerEvent?) {\n                val child = e!!.child\n\n                if (child is Container) {\n                    child.removeContainerListener(this)\n                }\n            }\n        })\n    }\n\n    fun isVertical(): Boolean {\n        return myVertical\n    }\n\n    fun setVertical(vertical: Boolean) {\n        if (myVertical == vertical) return\n        removeAll()\n        myVertical = vertical\n        setContent(myContent)\n        toolbar = myToolbar\n    }\n\n    fun isToolbarVisible(): Boolean {\n        return myToolbar != null && myToolbar!!.isVisible\n    }\n\n    var toolbar: JComponent?\n        get() {\n            return myToolbar\n        }\n        set(c) {\n            if (c == null) {\n                remove(myToolbar)\n            }\n            myToolbar = c\n            if (myToolbar is ActionToolbar) {\n                (myToolbar as ActionToolbar).setOrientation(if (myVertical) SwingConstants.HORIZONTAL else SwingConstants.VERTICAL)\n            }\n            if (c != null) {\n                if (myVertical) {\n                    add(c, BorderLayout.SOUTH)\n                } else {\n                    add(c, BorderLayout.EAST)\n                }\n            }\n            revalidate()\n            repaint()\n        }\n\n    override fun getData(@NonNls dataId: String): Any? {\n        return if (QuickActionProvider.KEY.`is`(dataId) && myProvideQuickActions) this else null\n    }\n\n    fun setProvideQuickActions(provide: Boolean): CustomSimpleToolWindowPanel? {\n        myProvideQuickActions = provide\n        return this\n    }\n\n    override fun getActions(originalProvider: Boolean): List<AnAction> {\n        val toolbars = UIUtil.uiTraverser(myToolbar).traverse().filter(\n            ActionToolbar::class.java\n        )\n        return if (toolbars.size() == 0) emptyList() else toolbars.flatten { toolbar: ActionToolbar -> toolbar.actions }.toList()\n    }\n\n    override fun getComponent(): JComponent? {\n        return this\n    }\n\n    fun setContent(c: JComponent?) {\n        if (myContent != null) {\n            remove(myContent)\n        }\n        myContent = c\n        add(c, BorderLayout.CENTER)\n        if (myBorderless) {\n            UIUtil.removeScrollBorder(c)\n        }\n        revalidate()\n        repaint()\n    }\n\n    override fun paintComponent(g: Graphics) {\n        super.paintComponent(g)\n        if (myToolbar != null && myToolbar!!.parent === this && myContent != null && myContent!!.parent === this) {\n            g.color = JBColor.border()\n            if (myVertical) {\n                val y = myContent!!.bounds.maxY.toInt()\n                LinePainter2D.paint(g as Graphics2D, 0.0, y.toDouble(), width.toDouble(), y.toDouble())\n            } else {\n                val x = myContent!!.bounds.maxX.toInt()\n                LinePainter2D.paint(g as Graphics2D, x.toDouble(), 0.0, x.toDouble(), height.toDouble())\n            }\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/util/ImageUtil.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.util\n\nimport com.intellij.openapi.util.IconLoader\nimport com.intellij.util.IconUtil\nimport com.intellij.util.ui.JBUI\nimport java.net.URL\nimport javax.swing.Icon\n\nobject ImageUtil {\n\n    fun loadIconFromUrl(url: String): Icon? {\n        if (url.isEmpty()) {\n            return null\n        }\n        return IconLoader.findIcon(URL(url), true)\n    }\n\n    fun loadIconFromUrl(url: String, resourceFallback: String): Icon {\n        val icon = loadIconFromUrl(url)\n        if (null === icon) {\n            return IconLoader.getIcon(resourceFallback, ImageUtil.javaClass)\n        }\n        return icon\n    }\n\n    fun loadIconFromUrl(url: String, size: Int): Icon? {\n        val icon = loadIconFromUrl(url)\n        if (null !== icon) {\n            return resize(icon, size)\n        }\n        return null\n    }\n\n    fun loadIconFromUrl(url: String, resourceFallback: String, size: Int): Icon {\n        val icon = loadIconFromUrl(url, resourceFallback)\n        return if (icon.iconWidth >= size) {\n            resize(icon, size)\n        } else {\n            resize(IconLoader.getIcon(resourceFallback, ImageUtil.javaClass), size)\n        }\n    }\n\n    private fun resize(icon: Icon, size: Int): Icon {\n        val scale = JBUI.scale(size).toFloat() / icon.iconWidth.toFloat()\n        return IconUtil.scale(icon, null, scale)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/util/Tabs.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.util\n\nimport com.intellij.openapi.Disposable\nimport com.intellij.openapi.actionSystem.ActionGroup\nimport com.intellij.openapi.actionSystem.ActionManager\nimport com.intellij.openapi.wm.IdeFocusManager\nimport com.intellij.ui.awt.RelativePoint\nimport com.intellij.ui.tabs.*\nimport com.intellij.ui.tabs.impl.*\nimport com.intellij.ui.tabs.impl.singleRow.ScrollableSingleRowLayout\nimport com.intellij.ui.tabs.impl.singleRow.SingleRowLayout\nimport com.intellij.util.ui.JBUI\nimport java.awt.*\nimport javax.swing.JComponent\nimport com.intellij.openapi.project.Project as IdeaProject\n\nclass Tabs(\n    private val ideaProject: IdeaProject,\n    private val disposable: Disposable\n) : TabsUI {\n    private val myTabs: JBTabs = MyTabs(ideaProject, disposable)\n    private var myCommonActionGroupInCenterFactory: (() -> ActionGroup)? = null\n    private var myCommonSideComponentFactory: (() -> JComponent)? = null\n\n    override fun getTabs(): JBTabs = myTabs\n\n    override fun setCommonCenterActionGroupFactory(factory: () -> ActionGroup) {\n        myCommonActionGroupInCenterFactory = factory\n    }\n\n    override fun setCommonSideComponentFactory(factory: () -> JComponent) {\n        myCommonSideComponentFactory = factory\n    }\n\n    override fun addTab(tabInfo: TabInfo) {\n        if (null !== myCommonActionGroupInCenterFactory) {\n            tabInfo.setActions(myCommonActionGroupInCenterFactory!!.invoke(), null)\n        }\n        if (null !== myCommonSideComponentFactory) {\n            tabInfo.sideComponent = myCommonSideComponentFactory!!.invoke()\n        }\n        myTabs.addTab(tabInfo)\n    }\n\n    override fun addListener(listener: TabsListener) {\n        myTabs.addListener(listener)\n    }\n\n    private class MyTabs(\n        private val ideaProject: IdeaProject,\n        private val disposable: Disposable\n    ) : JBEditorTabs(\n        ideaProject,\n        ActionManager.getInstance(),\n        IdeFocusManager.getInstance(ideaProject),\n        disposable\n    ) {\n        override fun createTabPainterAdapter(): TabPainterAdapter? {\n            return DefaultTabPainterAdapter(JBTabPainter.DEBUGGER)\n        }\n\n        override fun createSingleRowLayout(): SingleRowLayout? {\n            return ScrollableSingleRowLayout(this)\n        }\n\n        override fun createTabBorder(): JBTabsBorder? {\n            return MyTabsBorder(this)\n        }\n\n        override fun useSmallLabels(): Boolean {\n            return true\n        }\n\n        override fun getToolbarInset(): Int {\n            return 0\n        }\n\n        fun shouldAddToGlobal(point: Point?): Boolean {\n            val label = selectedLabel\n            if (label == null || point == null) {\n                return true\n            }\n            val bounds = label.bounds\n            return point.y <= bounds.y + bounds.height\n        }\n\n        override fun layout(c: JComponent?, bounds: Rectangle): Rectangle? {\n            if (c is Toolbar) {\n                bounds.height -= separatorWidth\n                return super.layout(c, bounds)\n            }\n            return super.layout(c, bounds)\n        }\n\n        override fun processDropOver(over: TabInfo?, relativePoint: RelativePoint) {\n            val point = relativePoint.getPoint(component)\n            myShowDropLocation = shouldAddToGlobal(point)\n            super.processDropOver(over, relativePoint)\n            for ((key, label) in myInfo2Label) {\n                if (label.bounds.contains(point) && myDropInfo != key) {\n                    select(key, false)\n                    break\n                }\n            }\n        }\n\n        override fun createTabLabel(info: TabInfo): TabLabel {\n            return MyTabLabel(this, info)\n        }\n\n        private class MyTabLabel(tabs: JBTabsImpl, info: TabInfo) : TabLabel(tabs, info) {\n            override fun getPreferredSize(): Dimension {\n                val size = super.getPreferredSize()\n                return Dimension(size.width, getPreferredHeight())\n            }\n\n            private fun getPreferredHeight(): Int {\n                return JBUI.scale(28)\n            }\n        }\n\n        private class MyTabsBorder(private val jbTabs: JBTabsImpl) : JBTabsBorder(jbTabs) {\n            override val effectiveBorder: Insets\n                get() = Insets(jbTabs.borderThickness, 0, 0, 0)\n\n            override fun paintBorder(\n                c: Component,\n                g: Graphics,\n                x: Int,\n                y: Int,\n                width: Int,\n                height: Int\n            ) {\n                if (jbTabs.isEmptyVisible) return\n                jbTabs.tabPainter.paintBorderLine(\n                    g as Graphics2D, jbTabs.borderThickness, Point(x, y + jbTabs.myHeaderFitSize.height),\n                    Point(x + width, y + jbTabs.myHeaderFitSize.height)\n                )\n            }\n\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/util/TabsUI.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.util\n\nimport com.intellij.openapi.actionSystem.ActionGroup\nimport com.intellij.ui.tabs.JBTabs\nimport com.intellij.ui.tabs.TabInfo\nimport com.intellij.ui.tabs.TabsListener\nimport javax.swing.JComponent\n\ninterface TabsUI {\n    val component\n        get() = getTabs().component\n\n    fun getTabs(): JBTabs\n\n    fun setCommonCenterActionGroupFactory(factory: () -> ActionGroup)\n\n    fun setCommonSideComponentFactory(factory: () -> JComponent)\n\n    fun addTab(tabInfo: TabInfo)\n\n    fun addListener(listener: TabsListener)\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/util/ToolbarUtil.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.util\n\nimport com.intellij.openapi.actionSystem.ActionManager\nimport com.intellij.openapi.actionSystem.DefaultActionGroup\nimport com.intellij.ui.treeStructure.actions.CollapseAllAction\nimport com.intellij.ui.treeStructure.actions.ExpandAllAction\nimport java.awt.Component\nimport javax.swing.JTree\n\nobject ToolbarUtil {\n    fun createExpandAndCollapseToolbar(name: String, tree: JTree): Component {\n        val actionGroup = DefaultActionGroup()\n        actionGroup.add(ExpandAllAction(tree))\n        actionGroup.add(CollapseAllAction(tree))\n\n        val toolbar = ActionManager.getInstance().createActionToolbar(\n            name,\n            actionGroup,\n            true\n        )\n        return toolbar.component\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/ui/util/fn.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.ui.util\n\nimport net.ntworld.mergeRequest.Approval\nimport net.ntworld.mergeRequestIntegrationIde.component.Icons\nimport javax.swing.Icon\n\nfun Approval.findVisibilityIconAndTextForApproval() : Triple<Boolean, Icon?, String?> {\n    val required = this.approvalsRequired\n    val left = this.approvalsLeft\n    val visibility = required > 0\n    var icon: Icon? = null\n    var text: String? = null\n    if (required == 0) {\n        return Triple(visibility, icon, text)\n    }\n\n    if (left == 0) {\n        icon = Icons.Approved\n        text = \"approved $required/$required\"\n        return Triple(visibility, icon, text)\n    }\n    if (left == required) {\n        icon = Icons.NoApproval\n    } else {\n        icon = Icons.RequiredApproval\n    }\n    text = \"approval ${required - left}/$required\"\n    return Triple(visibility, icon, text)\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/util/CommentUtil.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.util\n\nimport net.ntworld.mergeRequest.Comment\n\nobject CommentUtil {\n\n    fun groupCommentsByThreadId(comments: List<Comment>): Map<String, List<Comment>> {\n        val groups = mutableMapOf<String, MutableList<Comment>>()\n        for (comment in comments) {\n            if (!groups.containsKey(comment.parentId)) {\n                groups[comment.parentId] = mutableListOf()\n            }\n            groups[comment.parentId]!!.add(comment)\n        }\n        return groups\n    }\n\n    fun groupCommentsByNewPath(comments: List<Comment>): Map<String, List<Comment>> {\n        val groups = mutableMapOf<String, MutableList<Comment>>()\n        for (comment in comments) {\n            val position = comment.position\n            if (null === position) {\n                continue\n            }\n            val path = position.newPath\n            if (null === path) {\n                continue\n            }\n\n            if (!groups.containsKey(path)) {\n                groups[path] = mutableListOf()\n            }\n            groups[path]!!.add(comment)\n        }\n        return groups\n    }\n\n    fun groupCommentsByPositionNewLine(comments: List<Comment>): Map<Int, List<Comment>> {\n        val groups = mutableMapOf<Int, MutableList<Comment>>()\n        for (comment in comments) {\n            val position = comment.position\n            if (null === position) {\n                continue\n            }\n            val line = position.newLine\n            if (null === line) {\n                continue\n            }\n\n            if (!groups.containsKey(line)) {\n                groups[line] = mutableListOf()\n            }\n            groups[line]!!.add(comment)\n        }\n        return groups.toSortedMap()\n    }\n\n    fun groupCommentsByPositionPath(comments: List<Comment>): Map<String, List<Comment>> {\n        val groups = mutableMapOf<String, MutableList<Comment>>()\n        for (comment in comments) {\n            val position = comment.position\n            if (null === position) {\n                continue\n            }\n            val path = if (null !== position.newPath) position.newPath else position.oldPath\n            if (null === path) {\n                continue\n            }\n\n            if (!groups.containsKey(path)) {\n                groups[path] = mutableListOf()\n            }\n            groups[path]!!.add(comment)\n        }\n        return groups\n    }\n\n    fun groupCommentsByPositionLine(comments: List<Comment>): Map<Int, List<Comment>> {\n        val groups = mutableMapOf<Int, MutableList<Comment>>()\n        for (comment in comments) {\n            val position = comment.position\n            if (null === position) {\n                continue\n            }\n            val line = if (null !== position.newLine) position.newLine else position.oldLine\n            if (null === line) {\n                continue\n            }\n\n            if (!groups.containsKey(line)) {\n                groups[line] = mutableListOf()\n            }\n            groups[line]!!.add(comment)\n        }\n        return groups.toSortedMap()\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/util/FileTypeUtil.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.util\n\nimport com.intellij.lang.Language\nimport com.intellij.openapi.fileTypes.LanguageFileType\nimport javax.swing.Icon\n\nobject FileTypeUtil {\n    private class MarkdownLanguage : Language(\"Markdown\", \"text/x-markdown\")\n    private class MarkdownFileType(language: Language): LanguageFileType(language) {\n        override fun getIcon(): Icon? = null\n\n        override fun getName(): String {\n            return \"Markdown\"\n        }\n\n        override fun getDefaultExtension(): String {\n            return \"md\"\n        }\n\n        override fun getDescription(): String {\n            return \"Markdown\"\n        }\n    }\n\n    val markdownFileType : LanguageFileType by lazy {\n        MarkdownFileType(getLanguage())\n    }\n\n    private fun getLanguage() : Language {\n        val definedLanguages = Language.findInstancesByMimeType(\"text/x-markdown\")\n        if (definedLanguages.isEmpty()) {\n            return MarkdownLanguage()\n        }\n        return definedLanguages.first()\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/util/HtmlHelper.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.util\n\nimport net.ntworld.mergeRequest.ProviderData\nimport org.commonmark.parser.Parser\nimport org.commonmark.renderer.html.HtmlRenderer\n\nobject HtmlHelper {\n    private val myCommonMarkParser = Parser.builder().build()\n    private val myHtmlRenderer = HtmlRenderer.builder().build()\n\n    fun convertFromMarkdown(md: String): String {\n        return myHtmlRenderer.render(myCommonMarkParser.parse(md))\n    }\n\n    fun resolveRelativePath(providerData: ProviderData, html: String): String {\n        return html.replace(\"<img src=\\\"/\", \"<img src=\\\"${providerData.project.url}/\")\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/util/RepositoryUtil.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.util\n\nimport com.intellij.dvcs.repo.VcsRepositoryManager\nimport git4idea.repo.GitRepository\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\nimport java.io.File\nimport com.intellij.openapi.project.Project as IdeaProject\n\nobject RepositoryUtil {\n    private val repositoryCached = mutableMapOf<String, GitRepository>()\n\n    fun findRepository(projectServiceProvider: ProjectServiceProvider, providerData: ProviderData): GitRepository? {\n        if (null === repositoryCached[providerData.id]) {\n            val vcsRepositoryManager = VcsRepositoryManager.getInstance(projectServiceProvider.project)\n            for (repository in vcsRepositoryManager.repositories) {\n                if (repository.root.path == providerData.repository) {\n                    repositoryCached[providerData.id] = repository as GitRepository\n                    return repository\n                }\n            }\n        }\n        return repositoryCached[providerData.id]\n    }\n\n    fun transformToCrossPlatformsPath(input: String): String {\n        return if (input.contains('\\\\')) input.replace('\\\\', '/') else input\n    }\n\n    fun findAbsoluteCrossPlatformsPath(repository: GitRepository?, relativePath: String): String {\n        if (null === repository) {\n            return transformToCrossPlatformsPath(relativePath)\n        }\n        return transformToCrossPlatformsPath(\"${repository.root.path}${File.separatorChar}$relativePath\")\n    }\n\n    fun findRelativePath(repository: GitRepository?, absolutePath: String): String {\n        if (null === repository) {\n            return absolutePath.replace(File.separatorChar, '/')\n        }\n        val root = repository.root.path\n        if (!absolutePath.startsWith(root)) {\n            return absolutePath.replace(File.separatorChar, '/')\n        }\n        val path = absolutePath.substring(root.length).replace(File.separatorChar, '/')\n        return if (path.startsWith('/')) {\n            path.substring(1)\n        } else {\n            path\n        }\n    }\n\n    fun getRepositoriesByProject(ideaProject: IdeaProject): List<String> {\n        val vcsRepositoryManager = VcsRepositoryManager.getInstance(ideaProject)\n        return vcsRepositoryManager.repositories.map {\n            it.root.path\n        }\n    }\n\n    fun findRepositoryByPath(ideaProject: IdeaProject, path: String): GitRepository? {\n        val vcsRepositoryManager = VcsRepositoryManager.getInstance(ideaProject)\n        val repository = vcsRepositoryManager.repositories.find {\n            it.root.path == path\n        }\n        if (null === repository || repository !is GitRepository) {\n            return null\n        }\n        return repository\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/util/TextChoiceUtil.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.util\n\nobject TextChoiceUtil {\n    fun comment(count: Int): String {\n        return if (count < 2) \"$count comment\" else \"$count comments\"\n    }\n\n    fun reply(count: Int): String {\n        return if (count < 2) \"$count reply\" else \"$count replies\"\n    }\n\n    fun draft(count: Int): String {\n        return if (count < 2) \"$count draft\" else \"$count drafts\"\n    }\n\n    fun draftComment(count: Int): String {\n        return if (count < 2) \"$count draft comment\" else \"$count draft comments\"\n    }\n\n    fun commentWithDraft(totalCount: Int, draftCount: Int): String {\n        return if (draftCount > 0) comment(totalCount) + \" - ${draft(draftCount)}\" else comment(totalCount)\n    }\n\n    fun replyWithDraft(totalCount: Int, draftCount: Int): String {\n        return if (draftCount > 0) reply(totalCount) + \" - ${draft(draftCount)}\" else reply(totalCount)\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/watcher/Watcher.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.watcher\n\ninterface Watcher {\n    val interval: Long\n\n    fun canExecute(): Boolean\n\n    fun shouldTerminate(): Boolean\n\n    fun execute()\n\n    fun terminate()\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/watcher/WatcherManager.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.watcher\n\nimport com.intellij.openapi.Disposable\n\ninterface WatcherManager : Disposable {\n    fun addWatcher(watcher: Watcher)\n\n    fun removeWatcher(watcher: Watcher)\n}\n"
  },
  {
    "path": "merge-request-integration-core/src/main/kotlin/net/ntworld/mergeRequestIntegrationIde/watcher/WatcherManagerImpl.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.watcher\n\nimport java.util.*\n\nclass WatcherManagerImpl : WatcherManager {\n    private val myWatchers = mutableMapOf<Watcher, Timer>()\n\n    override fun addWatcher(watcher: Watcher) {\n        val timer = Timer()\n        timer.schedule(MyTimerTask(this, watcher), 0L, watcher.interval)\n\n        myWatchers[watcher] = timer\n    }\n\n    override fun removeWatcher(watcher: Watcher) {\n        val timer = myWatchers[watcher]\n        if (null !== timer) {\n            timer.cancel()\n            myWatchers.remove(watcher)\n        }\n    }\n\n    override fun dispose() {\n        myWatchers.forEach {\n            it.value.cancel()\n        }\n        myWatchers.clear()\n    }\n\n    internal class MyTimerTask(\n        private val watcherManager: WatcherManager,\n        private val watcher: Watcher\n    ): TimerTask() {\n        override fun run() {\n            if (watcher.canExecute()) {\n                watcher.execute()\n            }\n\n            if (watcher.shouldTerminate()) {\n                watcher.terminate()\n                watcherManager.removeWatcher(watcher)\n            }\n        }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/main/resources/templates/mr.comment.html",
    "content": "<div style=\"padding: 0 10px 10px 10px\">\n    {{content}}\n</div>\n"
  },
  {
    "path": "merge-request-integration-core/src/main/resources/templates/mr.description.html",
    "content": "<div style=\"padding: 0 10px 10px 10px\">\n    <h1>{{title}}</h1>\n    <hr />\n    <div>{{description}}</div>\n</div>\n"
  },
  {
    "path": "merge-request-integration-core/src/main/resources/templates/update.html",
    "content": "<div style=\"padding: 0 10px 10px 10px\">\n    {{content}}\n</div>\n"
  },
  {
    "path": "merge-request-integration-core/src/test/kotlin/net/ntworld/mergeRequestIntegrationIde/configuration/vos/GitRemotePathInfoTest.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.configuration.vos\n\nimport com.intellij.util.VersionUtil\nimport com.intellij.util.text.VersionComparatorUtil\nimport org.junit.Test\nimport kotlin.test.assertEquals\n\nclass GitRemotePathInfoTest {\n    private data class Item(\n        val input: String,\n        val valid: Boolean,\n        val namespace: String = \"\",\n        val project: String = \"\",\n        val toStringValue: String = \"\"\n    )\n\n    @Test\n    fun testSshUrls() {\n        val dataset = listOf(\n            Item(\n                input = \"git@gitlab.test.de:namespace/project.git\",\n                valid = true,\n                namespace = \"namespace\",\n                project = \"project\",\n                toStringValue = \"namespace/project\"\n            ),\n            Item(\n                input = \"git@gitlab.test.de:namespace/project\",\n                valid = true,\n                namespace = \"namespace\",\n                project = \"project\",\n                toStringValue = \"namespace/project\"\n            ),\n            Item(\n                input = \"git@gitlab.test.de:namespace\",\n                valid = false\n            ),\n            Item(\n                input = \"git@gitlab.test.de:\",\n                valid = false\n            )\n        )\n\n        dataset.forEach {\n            val info = GitRemotePathInfo(it.input)\n\n            assertEquals(it.valid, info.isValid, \"failed case $it\")\n            if (it.valid) {\n                assertEquals(it.namespace, info.namespace, \"failed case $it\")\n                assertEquals(it.project, info.project, \"failed case $it\")\n                assertEquals(it.toStringValue, info.toString(), \"failed case $it\")\n            }\n        }\n    }\n\n    @Test\n    fun testHttpUrls() {\n        val dataset = listOf(\n            Item(\n                input = \"https://gitlab.test.de/namespace/project\",\n                valid = true,\n                namespace = \"namespace\",\n                project = \"project\",\n                toStringValue = \"namespace/project\"\n            ),\n            Item(\n                input = \"https://gitlab.test.de/namespace/project.git\",\n                valid = true,\n                namespace = \"namespace\",\n                project = \"project\",\n                toStringValue = \"namespace/project\"\n            ),\n            Item(\n                input = \"http://gitlab.test.de/namespace/project.git\",\n                valid = true,\n                namespace = \"namespace\",\n                project = \"project\",\n                toStringValue = \"namespace/project\"\n            ),\n            Item(\n                input = \"HTTPS://gitlab.test.de/namespace/project\",\n                valid = true,\n                namespace = \"namespace\",\n                project = \"project\",\n                toStringValue = \"namespace/project\"\n            ),\n            Item(\n                input = \"http://gitlab.test.de/namespace/PROJECT.git\",\n                valid = true,\n                namespace = \"namespace\",\n                project = \"PROJECT\",\n                toStringValue = \"namespace/PROJECT\"\n            ),\n            Item(\n                input = \"https://github.com/nhat-phan/merge-request-integration\",\n                valid = true,\n                namespace = \"nhat-phan\",\n                project = \"merge-request-integration\",\n                toStringValue = \"nhat-phan/merge-request-integration\"\n            ),\n            Item(\n                input = \"http://github.com/nhat-phan/merge-request-integration\",\n                valid = true,\n                namespace = \"nhat-phan\",\n                project = \"merge-request-integration\",\n                toStringValue = \"nhat-phan/merge-request-integration\"\n            ),\n            Item(\n                input = \"http://github.com/nhat-phan\",\n                valid = false\n            ),\n            Item(\n                input = \"http:github.com/nhat-phan/merge-request-integration\",\n                valid = false\n            ),\n            Item(\n                input = \"http://:github.com:123\\\\:123nhat-phan/merge-request-integration\",\n                valid = false\n            )\n        )\n\n        dataset.forEach {\n            val info: GitRemotePathInfo\n            try {\n                info = GitRemotePathInfo(it.input)\n                assertEquals(it.valid, info.isValid, \"failed case $it\")\n                if (it.valid) {\n                    assertEquals(it.namespace, info.namespace, \"failed case $it\")\n                    assertEquals(it.project, info.project, \"failed case $it\")\n                    assertEquals(it.toStringValue, info.toString(), \"failed case $it\")\n                }\n            } catch (exception: Exception) {\n                assertEquals(it.valid, false, \"failed case $it\")\n            }\n        }\n    }\n\n    @Test\n    fun testVersion() {\n        println(VersionComparatorUtil.compare(\"2020.2.0-eap-1-for-ide-2020.1.x\", \"2020.2.0-built-for-ide-2020.1.x\"))\n        println(VersionComparatorUtil.compare(\"2020.2.0-built-for-ide-2020.1.x\", \"2020.2.1-built-for-ide-2020.1.x\"))\n        println(VersionComparatorUtil.compare(\"2020.2.0-built-for-ide-2020.1.x\", \"2020.1.5-built-for-ide-2020.1\"))\n        println(VersionComparatorUtil.compare(\"2020.2.0-eap-1-for-ide-2020.1.x\", \"2020.1.5-built-for-ide-2020.1\"))\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/test/kotlin/net/ntworld/mergeRequestIntegrationIde/infrastructure/DummyProjectServiceProvider.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.infrastructure\n\nimport com.intellij.openapi.project.Project\n\nclass DummyProjectServiceProvider(project: Project) : AbstractProjectServiceProvider(project) {\n    override val applicationServiceProvider: ApplicationServiceProvider\n        get() = TODO(\"Not yet implemented\")\n}"
  },
  {
    "path": "merge-request-integration-core/src/test/kotlin/net/ntworld/mergeRequestIntegrationIde/internal/CodeReviewServiceImplTest.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.internal\n\nimport org.junit.Test\nimport kotlin.test.assertEquals\n\nclass CodeReviewServiceImplTest"
  },
  {
    "path": "merge-request-integration-core/src/test/kotlin/net/ntworld/mergeRequestIntegrationIde/mergeRequest/comments/tree/node/NodeSyncManagerImplTest.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.mergeRequest.comments.tree.node\n\nimport com.intellij.ide.projectView.PresentationData\nimport com.intellij.ide.util.treeView.PresentableNodeDescriptor\nimport com.intellij.openapi.command.impl.DummyProject\nimport net.ntworld.mergeRequest.*\nimport net.ntworld.mergeRequest.generated.CommentImpl\nimport net.ntworld.mergeRequest.generated.CommentPositionImpl\nimport net.ntworld.mergeRequestIntegration.internal.UserImpl\nimport javax.swing.tree.DefaultMutableTreeNode\nimport kotlin.test.Test\nimport kotlin.test.assertEquals\n\nclass NodeSyncManagerImplTest {\n    private val user = UserImpl(\n        id = \"1\", name = \"name\", username = \"username\", email = \"email\",\n        avatarUrl = \"\", url = \"\", status = UserStatus.ACTIVE, createdAt = \"\"\n    )\n    private val commentPosition = CommentPositionImpl(\n        \"\", \"\", \"\", null, null, null, null, CommentPositionSource.UNKNOWN, CommentPositionChangeType.UNKNOWN\n    )\n\n    @Test\n    fun `testSyncStructure can generate correct structure with empty tree`() {\n        val service = makeService()\n        val root = generateFullList()\n        val rootTreeNode = DefaultMutableTreeNode()\n\n        service.syncStructure(root, rootTreeNode, DEFAULT_INVOKER)\n\n        assertEquals(\"\"\"\n            --root\n            ----general-comments\n            ------thread[1]\n            --------comment[2]\n            ------thread[3]\n            ----file[/dir/file-1]\n            ------line[/dir/file-1:1]\n            --------thread[4]\n            ------line[/dir/file-1:2]\n            --------thread[5]\n            ----------comment[6]\n            ----------comment[7]\n            --------thread[8]\n            ----------comment[9]\n            ----------comment[10]\n            ----------comment[11]\n            ----file[/file-2]\n            ------line[/file-2:3]\n            --------thread[12]\n            !END\n        \"\"\".trimIndent(), generateStructureFootprint(rootTreeNode))\n    }\n\n    @Test\n    fun `testSyncStructure can add more node into current structure`() {\n        val service = makeService()\n        val root = generateFullList()\n        val rootTreeNode = DefaultMutableTreeNode()\n        service.syncStructure(root, rootTreeNode, DEFAULT_INVOKER)\n\n        root.children[1].children[1].children[0].add(CommentNode(makeComment(\"Insert\", \"new\"), null))\n        service.syncStructure(root, rootTreeNode, DEFAULT_INVOKER)\n\n        assertEquals(\"\"\"\n            --root\n            ----general-comments\n            ------thread[1]\n            --------comment[2]\n            ------thread[3]\n            ----file[/dir/file-1]\n            ------line[/dir/file-1:1]\n            --------thread[4]\n            ------line[/dir/file-1:2]\n            --------thread[5]\n            ----------comment[6]\n            ----------comment[7]\n            ----------comment[Insert]\n            --------thread[8]\n            ----------comment[9]\n            ----------comment[10]\n            ----------comment[11]\n            ----file[/file-2]\n            ------line[/file-2:3]\n            --------thread[12]\n            !END\n        \"\"\".trimIndent(), generateStructureFootprint(rootTreeNode))\n    }\n\n    @Test\n    fun `testSyncStructure can remove node out of current structure`() {\n        val service = makeService()\n        val root = generateFullList()\n        val rootTreeNode = DefaultMutableTreeNode()\n        service.syncStructure(root, rootTreeNode, DEFAULT_INVOKER)\n\n        (root.children[1].children[1].children as MutableList<Node>).removeAt(0)\n        root.children.removeAt(0)\n        (root.children[0].children[1].children[0].children as MutableList<Node>).removeAt(1)\n        service.syncStructure(root, rootTreeNode, DEFAULT_INVOKER)\n\n        assertEquals(\"\"\"\n            --root\n            ----file[/dir/file-1]\n            ------line[/dir/file-1:1]\n            --------thread[4]\n            ------line[/dir/file-1:2]\n            --------thread[8]\n            ----------comment[9]\n            ----------comment[11]\n            ----file[/file-2]\n            ------line[/file-2:3]\n            --------thread[12]\n            !END\n        \"\"\".trimIndent(), generateStructureFootprint(rootTreeNode))\n    }\n\n    @Test\n    fun `testSyncStructure can remove items and delete correct indexes`() {\n        val service = makeService()\n        val root = generateFullList()\n        val rootTreeNode = DefaultMutableTreeNode()\n        service.syncStructure(root, rootTreeNode, DEFAULT_INVOKER)\n\n        (root.children[1].children as MutableList<Node>).removeAt(0)\n        service.syncStructure(root, rootTreeNode, DEFAULT_INVOKER)\n\n        assertEquals(\"\"\"\n            --root\n            ----general-comments\n            ------thread[1]\n            --------comment[2]\n            ------thread[3]\n            ----file[/dir/file-1]\n            ------line[/dir/file-1:2]\n            --------thread[5]\n            ----------comment[6]\n            ----------comment[7]\n            --------thread[8]\n            ----------comment[9]\n            ----------comment[10]\n            ----------comment[11]\n            ----file[/file-2]\n            ------line[/file-2:3]\n            --------thread[12]\n            !END\n        \"\"\".trimIndent(), generateStructureFootprint(rootTreeNode))\n\n        (root.children as MutableList<Node>).removeAt(0)\n        service.syncStructure(root, rootTreeNode, DEFAULT_INVOKER)\n\n        assertEquals(\"\"\"\n            --root\n            ----file[/dir/file-1]\n            ------line[/dir/file-1:2]\n            --------thread[5]\n            ----------comment[6]\n            ----------comment[7]\n            --------thread[8]\n            ----------comment[9]\n            ----------comment[10]\n            ----------comment[11]\n            ----file[/file-2]\n            ------line[/file-2:3]\n            --------thread[12]\n            !END\n        \"\"\".trimIndent(), generateStructureFootprint(rootTreeNode))\n    }\n\n    private fun generateStructureFootprint(treeNode: DefaultMutableTreeNode): String {\n        val stringBuilder = StringBuilder()\n        generateStructureFootprint(stringBuilder, treeNode, 0)\n        stringBuilder.append(\"!END\")\n        return stringBuilder.toString()\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    private fun generateStructureFootprint(result: StringBuilder, treeNode: DefaultMutableTreeNode, deep: Int) {\n        var padding = \"\"\n        for (i in 0..deep) {\n            padding += \"--\"\n        }\n        val userObject = treeNode.userObject as PresentableNodeDescriptor<Node>\n        val node = userObject.element\n        result.append(\"${padding}${node.id}\")\n        result.appendln()\n        val children = treeNode.children()\n        while (children.hasMoreElements()) {\n            generateStructureFootprint(result, children.nextElement() as DefaultMutableTreeNode, deep+1)\n        }\n    }\n\n    private fun generateFullList(): RootNode {\n        val root = RootNode()\n\n        val generalComments = GeneralCommentsNode(3, 0)\n\n        val threadOne = ThreadNode(\"thread-one\", 1, 0, makeComment(\"1\", \"thread-one\"), null)\n        threadOne.add(CommentNode(makeComment(\"2\", \"thread-one\"), null))\n        generalComments.add(threadOne)\n\n        val threadTwo = ThreadNode(\"thread-two\", 0, 0, makeComment(\"3\", \"thread-two\"), null)\n        generalComments.add(threadTwo)\n\n        root.add(generalComments)\n\n        val fileOne = FileNode(\"/dir/file-1\", 0)\n        val line1 = FileLineNode(\"/dir/file-1\", 1, commentPosition, 1, 0, false)\n        val threadThree = ThreadNode(\"thread-three\", 0, 0, makeComment(\"4\", \"thread-three\"), null)\n        line1.add(threadThree)\n\n        val line2 = FileLineNode(\"/dir/file-1\", 2, commentPosition, 0, 0, false)\n        val threadFour = ThreadNode(\"thread-four\", 2, 0, makeComment(\"5\", \"thread-four\"), null)\n        threadFour.add(CommentNode(makeComment(\"6\", \"thread-four\"), null))\n        threadFour.add(CommentNode(makeComment(\"7\", \"thread-four\"), null))\n        line2.add(threadFour)\n        val threadFive = ThreadNode(\"thread-five\", 3, 0, makeComment(\"8\", \"thread-four\"), null)\n        threadFive.add(CommentNode(makeComment(\"9\", \"thread-five\"), null))\n        threadFive.add(CommentNode(makeComment(\"10\", \"thread-five\"), null))\n        threadFive.add(CommentNode(makeComment(\"11\", \"thread-five\"), null))\n        line2.add(threadFive)\n\n        fileOne.add(line1)\n        fileOne.add(line2)\n\n        val fileTwo = FileNode(\"/file-2\", 0)\n        val line3 = FileLineNode(\"/file-2\", 3, commentPosition, 1, 0, false)\n        val threadSix = ThreadNode(\"thread-six\", 0, 0, makeComment(\"12\", \"thread-six\"), null)\n        line3.add(threadSix)\n        fileTwo.add(line3)\n\n        root.add(fileOne)\n        root.add(fileTwo)\n\n        return root\n    }\n\n    private fun makeService(): NodeSyncManagerImpl {\n        return NodeSyncManagerImpl(DummyNodeDescriptorService())\n    }\n\n    private fun makeComment(id: String, threadId: String): Comment {\n        return CommentImpl(\n            id = id,\n            parentId = threadId,\n            replyId = threadId,\n            body = \"comment $id\",\n            author = user,\n            position = null,\n            createdAt = \"\",\n            updatedAt = \"\",\n            resolvable = true,\n            resolved = false,\n            resolvedBy = null,\n            isDraft = true\n        )\n    }\n\n    private class DummyPresentableNodeDescriptor(\n        private val element: Node\n    ) : PresentableNodeDescriptor<Node>(DummyProject.getInstance(), null) {\n        override fun update(presentation: PresentationData) {\n        }\n        override fun getElement(): Node = element\n    }\n\n    private class DummyNodeDescriptorService: NodeDescriptorService {\n        override fun make(node: Node): PresentableNodeDescriptor<Node> {\n            return DummyPresentableNodeDescriptor(node)\n        }\n\n        override fun findNode(input: Any?): Node? {\n            return null\n        }\n\n        override fun isHolding(input: Any?, node: Node): Boolean {\n            return false\n        }\n    }\n\n    companion object {\n        val DEFAULT_INVOKER: ((Node, DefaultMutableTreeNode) -> Unit) = { _, _ -> }\n    }\n}"
  },
  {
    "path": "merge-request-integration-core/src/test/kotlin/net/ntworld/mergeRequestIntegrationIde/watcher/WatcherManagerImplTest.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIde.watcher\n\nimport kotlin.test.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertTrue\n\nclass WatcherManagerImplTest {\n\n    @Test\n    fun testCanScheduleInfinityWatcher() {\n        var count = 0\n        class MyWatcher : Watcher {\n            override val interval: Long = 100\n\n            override fun canExecute() = true\n            override fun shouldTerminate() = false\n\n            override fun execute() {\n                count++\n            }\n\n            override fun terminate() {\n            }\n        }\n        val watcherManager = WatcherManagerImpl()\n\n        watcherManager.addWatcher(MyWatcher())\n\n        Thread.sleep(950)\n        assertTrue(count >= 10)\n    }\n\n    @Test\n    fun testCanTerminateTheWatcherByShouldTerminateCondition() {\n        var count = 0\n        class MyWatcher : Watcher {\n            override val interval: Long = 100\n\n            override fun canExecute() = true\n            override fun shouldTerminate() = count == 5\n\n            override fun execute() {\n                count++\n            }\n\n            override fun terminate() {\n                count += 10\n            }\n        }\n        val watcherManager = WatcherManagerImpl()\n\n        watcherManager.addWatcher(MyWatcher())\n\n        Thread.sleep(950)\n        assertEquals(15, count)\n    }\n\n    @Test\n    fun testNotTriggerExecuteIfThereIsCanExecuteReturnFalse() {\n        var count = 0\n        class MyWatcher : Watcher {\n            override val interval: Long = 100\n\n            override fun canExecute() = false\n            override fun shouldTerminate() = count == 5\n\n            override fun execute() {\n                count++\n            }\n\n            override fun terminate() {\n            }\n        }\n        val watcherManager = WatcherManagerImpl()\n\n        watcherManager.addWatcher(MyWatcher())\n\n        Thread.sleep(950)\n        assertEquals(0, count)\n    }\n}"
  },
  {
    "path": "merge-request-integration-ee/LICENSE",
    "content": "Merge Request Integration Enterprise Edition (EE) (c) 2019-present Nhat Phan <nhat.phan@ntworld.net> (https://github.com/nhat-phan)\n\nMerge Request Integration Enterprise Edition (EE) is licensed under a\nCreative Commons Attribution-NoDerivatives 4.0 International License.\n\nYou should have received a copy of the license along with this\nwork. If not, see <http://creativecommons.org/licenses/by-nd/4.0/>.\n\nAttribution-NoDerivatives 4.0 International\n\n=======================================================================\n\nCreative Commons Corporation (\"Creative Commons\") is not a law firm and\ndoes not provide legal services or legal advice. Distribution of\nCreative Commons public licenses does not create a lawyer-client or\nother relationship. Creative Commons makes its licenses and related\ninformation available on an \"as-is\" basis. Creative Commons gives no\nwarranties regarding its licenses, any material licensed under their\nterms and conditions, or any related information. Creative Commons\ndisclaims all liability for damages resulting from their use to the\nfullest extent possible.\n\nUsing Creative Commons Public Licenses\n\nCreative Commons public licenses provide a standard set of terms and\nconditions that creators and other rights holders may use to share\noriginal works of authorship and other material subject to copyright\nand certain other rights specified in the public license below. The\nfollowing considerations are for informational purposes only, are not\nexhaustive, and do not form part of our licenses.\n\n     Considerations for licensors: Our public licenses are\n     intended for use by those authorized to give the public\n     permission to use material in ways otherwise restricted by\n     copyright and certain other rights. Our licenses are\n     irrevocable. Licensors should read and understand the terms\n     and conditions of the license they choose before applying it.\n     Licensors should also secure all rights necessary before\n     applying our licenses so that the public can reuse the\n     material as expected. Licensors should clearly mark any\n     material not subject to the license. This includes other CC-\n     licensed material, or material used under an exception or\n     limitation to copyright. More considerations for licensors:\n\twiki.creativecommons.org/Considerations_for_licensors\n\n     Considerations for the public: By using one of our public\n     licenses, a licensor grants the public permission to use the\n     licensed material under specified terms and conditions. If\n     the licensor's permission is not necessary for any reason--for\n     example, because of any applicable exception or limitation to\n     copyright--then that use is not regulated by the license. Our\n     licenses grant only permissions under copyright and certain\n     other rights that a licensor has authority to grant. Use of\n     the licensed material may still be restricted for other\n     reasons, including because others have copyright or other\n     rights in the material. A licensor may make special requests,\n     such as asking that all changes be marked or described.\n     Although not required by our licenses, you are encouraged to\n     respect those requests where reasonable. More_considerations\n     for the public:\n\twiki.creativecommons.org/Considerations_for_licensees\n\n\n=======================================================================\n\nCreative Commons Attribution-NoDerivatives 4.0 International Public\nLicense\n\nBy exercising the Licensed Rights (defined below), You accept and agree\nto be bound by the terms and conditions of this Creative Commons\nAttribution-NoDerivatives 4.0 International Public License (\"Public\nLicense\"). To the extent this Public License may be interpreted as a\ncontract, You are granted the Licensed Rights in consideration of Your\nacceptance of these terms and conditions, and the Licensor grants You\nsuch rights in consideration of benefits the Licensor receives from\nmaking the Licensed Material available under these terms and\nconditions.\n\n\nSection 1 -- Definitions.\n\n  a. Adapted Material means material subject to Copyright and Similar\n     Rights that is derived from or based upon the Licensed Material\n     and in which the Licensed Material is translated, altered,\n     arranged, transformed, or otherwise modified in a manner requiring\n     permission under the Copyright and Similar Rights held by the\n     Licensor. For purposes of this Public License, where the Licensed\n     Material is a musical work, performance, or sound recording,\n     Adapted Material is always produced where the Licensed Material is\n     synched in timed relation with a moving image.\n\n  b. Copyright and Similar Rights means copyright and/or similar rights\n     closely related to copyright including, without limitation,\n     performance, broadcast, sound recording, and Sui Generis Database\n     Rights, without regard to how the rights are labeled or\n     categorized. For purposes of this Public License, the rights\n     specified in Section 2(b)(1)-(2) are not Copyright and Similar\n     Rights.\n\n  c. Effective Technological Measures means those measures that, in the\n     absence of proper authority, may not be circumvented under laws\n     fulfilling obligations under Article 11 of the WIPO Copyright\n     Treaty adopted on December 20, 1996, and/or similar international\n     agreements.\n\n  d. Exceptions and Limitations means fair use, fair dealing, and/or\n     any other exception or limitation to Copyright and Similar Rights\n     that applies to Your use of the Licensed Material.\n\n  e. Licensed Material means the artistic or literary work, database,\n     or other material to which the Licensor applied this Public\n     License.\n\n  f. Licensed Rights means the rights granted to You subject to the\n     terms and conditions of this Public License, which are limited to\n     all Copyright and Similar Rights that apply to Your use of the\n     Licensed Material and that the Licensor has authority to license.\n\n  g. Licensor means the individual(s) or entity(ies) granting rights\n     under this Public License.\n\n  h. Share means to provide material to the public by any means or\n     process that requires permission under the Licensed Rights, such\n     as reproduction, public display, public performance, distribution,\n     dissemination, communication, or importation, and to make material\n     available to the public including in ways that members of the\n     public may access the material from a place and at a time\n     individually chosen by them.\n\n  i. Sui Generis Database Rights means rights other than copyright\n     resulting from Directive 96/9/EC of the European Parliament and of\n     the Council of 11 March 1996 on the legal protection of databases,\n     as amended and/or succeeded, as well as other essentially\n     equivalent rights anywhere in the world.\n\n  j. You means the individual or entity exercising the Licensed Rights\n     under this Public License. Your has a corresponding meaning.\n\n\nSection 2 -- Scope.\n\n  a. License grant.\n\n       1. Subject to the terms and conditions of this Public License,\n          the Licensor hereby grants You a worldwide, royalty-free,\n          non-sublicensable, non-exclusive, irrevocable license to\n          exercise the Licensed Rights in the Licensed Material to:\n\n            a. reproduce and Share the Licensed Material, in whole or\n               in part; and\n\n            b. produce and reproduce, but not Share, Adapted Material.\n\n       2. Exceptions and Limitations. For the avoidance of doubt, where\n          Exceptions and Limitations apply to Your use, this Public\n          License does not apply, and You do not need to comply with\n          its terms and conditions.\n\n       3. Term. The term of this Public License is specified in Section\n          6(a).\n\n       4. Media and formats; technical modifications allowed. The\n          Licensor authorizes You to exercise the Licensed Rights in\n          all media and formats whether now known or hereafter created,\n          and to make technical modifications necessary to do so. The\n          Licensor waives and/or agrees not to assert any right or\n          authority to forbid You from making technical modifications\n          necessary to exercise the Licensed Rights, including\n          technical modifications necessary to circumvent Effective\n          Technological Measures. For purposes of this Public License,\n          simply making modifications authorized by this Section 2(a)\n          (4) never produces Adapted Material.\n\n       5. Downstream recipients.\n\n            a. Offer from the Licensor -- Licensed Material. Every\n               recipient of the Licensed Material automatically\n               receives an offer from the Licensor to exercise the\n               Licensed Rights under the terms and conditions of this\n               Public License.\n\n            b. No downstream restrictions. You may not offer or impose\n               any additional or different terms or conditions on, or\n               apply any Effective Technological Measures to, the\n               Licensed Material if doing so restricts exercise of the\n               Licensed Rights by any recipient of the Licensed\n               Material.\n\n       6. No endorsement. Nothing in this Public License constitutes or\n          may be construed as permission to assert or imply that You\n          are, or that Your use of the Licensed Material is, connected\n          with, or sponsored, endorsed, or granted official status by,\n          the Licensor or others designated to receive attribution as\n          provided in Section 3(a)(1)(A)(i).\n\n  b. Other rights.\n\n       1. Moral rights, such as the right of integrity, are not\n          licensed under this Public License, nor are publicity,\n          privacy, and/or other similar personality rights; however, to\n          the extent possible, the Licensor waives and/or agrees not to\n          assert any such rights held by the Licensor to the limited\n          extent necessary to allow You to exercise the Licensed\n          Rights, but not otherwise.\n\n       2. Patent and trademark rights are not licensed under this\n          Public License.\n\n       3. To the extent possible, the Licensor waives any right to\n          collect royalties from You for the exercise of the Licensed\n          Rights, whether directly or through a collecting society\n          under any voluntary or waivable statutory or compulsory\n          licensing scheme. In all other cases the Licensor expressly\n          reserves any right to collect such royalties.\n\n\nSection 3 -- License Conditions.\n\nYour exercise of the Licensed Rights is expressly made subject to the\nfollowing conditions.\n\n  a. Attribution.\n\n       1. If You Share the Licensed Material, You must:\n\n            a. retain the following if it is supplied by the Licensor\n               with the Licensed Material:\n\n                 i. identification of the creator(s) of the Licensed\n                    Material and any others designated to receive\n                    attribution, in any reasonable manner requested by\n                    the Licensor (including by pseudonym if\n                    designated);\n\n                ii. a copyright notice;\n\n               iii. a notice that refers to this Public License;\n\n                iv. a notice that refers to the disclaimer of\n                    warranties;\n\n                 v. a URI or hyperlink to the Licensed Material to the\n                    extent reasonably practicable;\n\n            b. indicate if You modified the Licensed Material and\n               retain an indication of any previous modifications; and\n\n            c. indicate the Licensed Material is licensed under this\n               Public License, and include the text of, or the URI or\n               hyperlink to, this Public License.\n\n          For the avoidance of doubt, You do not have permission under\n          this Public License to Share Adapted Material.\n\n       2. You may satisfy the conditions in Section 3(a)(1) in any\n          reasonable manner based on the medium, means, and context in\n          which You Share the Licensed Material. For example, it may be\n          reasonable to satisfy the conditions by providing a URI or\n          hyperlink to a resource that includes the required\n          information.\n\n       3. If requested by the Licensor, You must remove any of the\n          information required by Section 3(a)(1)(A) to the extent\n          reasonably practicable.\n\n\nSection 4 -- Sui Generis Database Rights.\n\nWhere the Licensed Rights include Sui Generis Database Rights that\napply to Your use of the Licensed Material:\n\n  a. for the avoidance of doubt, Section 2(a)(1) grants You the right\n     to extract, reuse, reproduce, and Share all or a substantial\n     portion of the contents of the database, provided You do not Share\n     Adapted Material;\n  b. if You include all or a substantial portion of the database\n     contents in a database in which You have Sui Generis Database\n     Rights, then the database in which You have Sui Generis Database\n     Rights (but not its individual contents) is Adapted Material; and\n  c. You must comply with the conditions in Section 3(a) if You Share\n     all or a substantial portion of the contents of the database.\n\nFor the avoidance of doubt, this Section 4 supplements and does not\nreplace Your obligations under this Public License where the Licensed\nRights include other Copyright and Similar Rights.\n\n\nSection 5 -- Disclaimer of Warranties and Limitation of Liability.\n\n  a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE\n     EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS\n     AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF\n     ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,\n     IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,\n     WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR\n     PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,\n     ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT\n     KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT\n     ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.\n\n  b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE\n     TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,\n     NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,\n     INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,\n     COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR\n     USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN\n     ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR\n     DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR\n     IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.\n\n  c. The disclaimer of warranties and limitation of liability provided\n     above shall be interpreted in a manner that, to the extent\n     possible, most closely approximates an absolute disclaimer and\n     waiver of all liability.\n\n\nSection 6 -- Term and Termination.\n\n  a. This Public License applies for the term of the Copyright and\n     Similar Rights licensed here. However, if You fail to comply with\n     this Public License, then Your rights under this Public License\n     terminate automatically.\n\n  b. Where Your right to use the Licensed Material has terminated under\n     Section 6(a), it reinstates:\n\n       1. automatically as of the date the violation is cured, provided\n          it is cured within 30 days of Your discovery of the\n          violation; or\n\n       2. upon express reinstatement by the Licensor.\n\n     For the avoidance of doubt, this Section 6(b) does not affect any\n     right the Licensor may have to seek remedies for Your violations\n     of this Public License.\n\n  c. For the avoidance of doubt, the Licensor may also offer the\n     Licensed Material under separate terms or conditions or stop\n     distributing the Licensed Material at any time; however, doing so\n     will not terminate this Public License.\n\n  d. Sections 1, 5, 6, 7, and 8 survive termination of this Public\n     License.\n\n\nSection 7 -- Other Terms and Conditions.\n\n  a. The Licensor shall not be bound by any additional or different\n     terms or conditions communicated by You unless expressly agreed.\n\n  b. Any arrangements, understandings, or agreements regarding the\n     Licensed Material not stated herein are separate from and\n     independent of the terms and conditions of this Public License.\n\n\nSection 8 -- Interpretation.\n\n  a. For the avoidance of doubt, this Public License does not, and\n     shall not be interpreted to, reduce, limit, restrict, or impose\n     conditions on any use of the Licensed Material that could lawfully\n     be made without permission under this Public License.\n\n  b. To the extent possible, if any provision of this Public License is\n     deemed unenforceable, it shall be automatically reformed to the\n     minimum extent necessary to make it enforceable. If the provision\n     cannot be reformed, it shall be severed from this Public License\n     without affecting the enforceability of the remaining terms and\n     conditions.\n\n  c. No term or condition of this Public License will be waived and no\n     failure to comply consented to unless expressly agreed to by the\n     Licensor.\n\n  d. Nothing in this Public License constitutes or may be interpreted\n     as a limitation upon, or waiver of, any privileges and immunities\n     that apply to the Licensor or You, including from the legal\n     processes of any jurisdiction or authority.\n\n=======================================================================\n\nCreative Commons is not a party to its public\nlicenses. Notwithstanding, Creative Commons may elect to apply one of\nits public licenses to material it publishes and in those instances\nwill be considered the “Licensor.” The text of the Creative Commons\npublic licenses is dedicated to the public domain under the CC0 Public\nDomain Dedication. Except for the limited purpose of indicating that\nmaterial is shared under a Creative Commons public license or as\notherwise permitted by the Creative Commons policies published at\ncreativecommons.org/policies, Creative Commons does not authorize the\nuse of the trademark \"Creative Commons\" or any other trademark or logo\nof Creative Commons without its prior written consent including,\nwithout limitation, in connection with any unauthorized modifications\nto any of its public licenses or any other arrangements,\nunderstandings, or agreements concerning use of licensed material. For\nthe avoidance of doubt, this paragraph does not form part of the\npublic licenses.\n\nCreative Commons may be contacted at creativecommons.org.\n\n"
  },
  {
    "path": "merge-request-integration-ee/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.tasks.KotlinCompile\n\nval artifactGroup: String by project\nval targetIDEVersion: String by project\nval enterpriseEditionVersion: String by project\nval intellijVersion: String by project\nval jvmTarget: String by project\nval foundationVersion: String by project\nval gitlab4jVersion: String by project\nval githubApiVersion: String by project\nval prettyTimeVersion: String by project\nval commonmarkVersion: String by project\nval intellijSinceBuild: String by project\nval intellijUntilBuild: String by project\nval eapRelease: String by project\n\ngroup = artifactGroup\nversion = if (eapRelease == \"false\") {\n    \"$enterpriseEditionVersion-built-for-ide-$targetIDEVersion\"\n} else {\n    \"$enterpriseEditionVersion-eap-$eapRelease-for-ide-$targetIDEVersion\"\n}\n\nrepositories {\n    jcenter()\n    mavenCentral()\n    maven(\"https://jitpack.io\")\n}\n\ndependencies {\n    implementation(\"org.jetbrains.kotlin:kotlin-stdlib\")\n    implementation(\"com.github.nhat-phan.foundation:foundation-jvm:$foundationVersion\")\n    implementation(\"org.gitlab4j:gitlab4j-api:$gitlab4jVersion\")\n    implementation(\"org.kohsuke:github-api:$githubApiVersion\")\n    implementation(\"org.ocpsoft.prettytime:prettytime:$prettyTimeVersion\")\n    compile(\"com.atlassian.commonmark:commonmark:$commonmarkVersion\")\n\n    implementation(project(\":contracts\"))\n    implementation(project(\":merge-request-integration-core\"))\n    implementation(project(\":merge-request-integration\"))\n}\n\n// See https://github.com/JetBrains/gradle-intellij-plugin/\nintellij {\n    version = intellijVersion\n    updateSinceUntilBuild = true\n    setPlugins(\"git4idea\")\n}\n\nval compileKotlin: KotlinCompile by tasks\nval compileTestKotlin: KotlinCompile by tasks\n\ncompileKotlin.kotlinOptions {\n    jvmTarget = jvmTarget\n}\n\ncompileTestKotlin.kotlinOptions {\n    jvmTarget = jvmTarget\n}\n\ntasks {\n    named<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>(\"compileKotlin\") {\n        kotlinOptions {\n            jvmTarget = jvmTarget\n        }\n    }\n\n    named<org.jetbrains.intellij.tasks.PatchPluginXmlTask>(\"patchPluginXml\") {\n        val version = if (!enterpriseEditionVersion.endsWith(\"eap\"))\n            enterpriseEditionVersion else enterpriseEditionVersion.substring(0, enterpriseEditionVersion.length - 3)\n        changeNotes(htmlFixer(\"./merge-request-integration-ee/doc/release-notes.$version.html\"))\n        pluginDescription(htmlFixer(\"./merge-request-integration-ee/doc/description.html\"))\n        sinceBuild(intellijSinceBuild)\n        untilBuild(intellijUntilBuild)\n    }\n}\n\nfun htmlFixer(filename: String): String {\n    if (!File(filename).exists()) {\n        throw Exception(\"File $filename not found.\")\n    }\n    return File(filename).readText().replace(\"<html lang=\\\"en\\\">\", \"\").replace(\"</html>\", \"\")\n}"
  },
  {
    "path": "merge-request-integration-ee/doc/description.html",
    "content": "<html lang=\"en\">\n\n<p>Merge Request Integration EE is an open-source plugin for JetBrains IDEs which helps you</p>\n<ul>\n    <li>Do code review right in your IDE.</li>\n    <li>Address review comments from your colleagues.</li>\n</ul>\n<br />\n<p>What you can do:</p>\n\n<ul>\n    <li>Check approval statuses of merge requests which are waiting for your approval.</li>\n    <li>Filter Merge Requests which are assigned to you, belongs to your colleagues, etc</li>\n    <li>Check pipeline status and approval status.</li>\n    <li>Do code review, navigate code with Diff View right in your IDE.</li>\n    <li>Address review comments, navigate comments in editors.</li>\n    <li>Add and reply a comment</li>\n    <li>Approve/revoke your approval</li>\n    <li>More and more features will be coming soon :)</li>\n</ul>\n\n<br />\n<p>Currently the plugin supports GitLab only (gitlab cloud and self-hosted).</p>\n\n<p><b>If you've installed Merge Request Integration CE, please uninstall before installing Enterprise Edition.</b></p>\n\n<h3>How to setup Gitlab connection</h3>\n\n<p>You need a personal api token. To get the token please follow these steps:</p>\n\n<ul>\n    <li>Log in to your Gitlab site</li>\n    <li>Go to Settings > Access Token and create a personal access token</li>\n    <li>Go to your IDE preferences, Merge Request Integration > Gitlab</li>\n    <li>Fill data, then save and click refresh button of Merge Request Integration CE window</li>\n</ul>\n\n<h3>About me</h3>\n\n<p>\n    My name is Nhat, I'm a software developer at\n    <a href=\"https://personio.com/\">Personio</a>\n    (yes, <a href=\"https://www.personio.com/about-personio/jobs/\">we are hiring</a>\n    all around the world, relocation to Munich is of course possible).\n</p>\n\n<h3>Attribution</h3>\n\n<ul>\n    <li>\n        Icons by\n        <a href=\"https://fontawesome.com/\">Font Awesome</a>\n        are licensed under\n        <a href=\"https://creativecommons.org/licenses/by/4.0/\">CC BY 4.0</a>\n    </li>\n</ul>\n</html>"
  },
  {
    "path": "merge-request-integration-ee/doc/release-notes.2019.3.1.html",
    "content": "<html lang=\"en\">\n    <ul>\n        <li>Initial release :)</li>\n    </ul>\n</html>"
  },
  {
    "path": "merge-request-integration-ee/doc/release-notes.2019.3.2.html",
    "content": "<html lang=\"en\">\n    <ul>\n        <li>Introduce commit list and changes tree</li>\n        <li>Resolve and delete comments</li>\n        <li>Display comments on editors</li>\n        <li>Bug fixes</li>\n    </ul>\n</html>"
  },
  {
    "path": "merge-request-integration-ee/doc/release-notes.2019.3.3.html",
    "content": "<html lang=\"en\">\n    <h3>Notable changes</h3>\n    <ul>\n        <li>Improved style of list</li>\n        <li>Improved approval panel which only opens if there is information to be displayed</li>\n        <li>Replaced deprecated api</li>\n        <li>Fixed bug #4: Cannot leave a comment in unified view or on Windows</li>\n        <li>Fixed bug #5: Cannot open comment editor if IDE does not support Markdown</li>\n        <li>Fixed bug: Token on Windows cannot be saved</li>\n        <li>Fixed grammatical error on GitLab configuration panel</li>\n    </ul>\n\n    <h3>Acknowledgement</h3>\n    <ul>\n        <li>Thank <a href=\"https://github.com/Savak\">@Savak</a>, <a href=\"https://github.com/SToto98\">@SToto98</a> for reporting bug #4</li>\n        <li>Thank <a href=\"https://github.com/smallcreep\">@smallcreep</a> for reporting bug #5</li>\n        <li>Thank <a href=\"https://plugins.jetbrains.com/author/7857882b-94e6-4d52-a3fd-801afb8ca0e2\">Artem Kuchyn</a> for reporting \"Token on Windows cannot be saved\" bug.</li>\n        <li>Thank Jay for correcting grammar.</li>\n    </ul>\n</html>"
  },
  {
    "path": "merge-request-integration-ee/doc/release-notes.2019.3.4.html",
    "content": "<html lang=\"en\">\n    <h3>Notable changes</h3>\n    <ul>\n        <li>Supported self-signed certificate</li>\n        <li>Supported cache option for finding merge request to improve performance</li>\n        <li>New implementation of configuration panel</li>\n        <li>Fixed #1: Support multi repositories project</li>\n        <li>Fixed #9: Connection list cannot be saved</li>\n        <li>Fixed #16: Pipeline is not displayed on Gitlab v12.2.4</li>\n        <li>Fixed bug: Empty description string on some actions</li>\n        <li>Fixed bug: UpdateManager requires weird format of metadata</li>\n    </ul>\n\n    <h3>Acknowledgement</h3>\n    <ul>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/answer-huang\">answer-huang</a>\n            for contributing to source code, the commit\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/commit/e0bdc259785fad3f11ecde1849acf32a36dc4281\">e0bdc25</a>,\n            Pull Request\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/pull/11\">#11</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/turnik\">turnik</a>\n            for contributing to source code, the commit\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/commit/9e4f7b6824963c46af1fee595de53d6363dac9f2\">9e4f7b6</a>,\n            Pull Request\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/pull/18\">#18</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/Marthym\">Marthym</a>\n            for reporting bug\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/1\">#1</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/mmjurov\">mmjurov</a>\n            for reporting bug\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/9\">#9</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/vdsirotkin\">vdsirotkin</a>\n            for reporting bug\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/16\">#16</a>\n        </li>\n    </ul>\n</html>"
  },
  {
    "path": "merge-request-integration-ee/doc/release-notes.2019.3.5.html",
    "content": "<html lang=\"en\">\n    <h3>Notable changes</h3>\n    <ul>\n        <li>Hot-fix #21: Gitlab configuration page is empty for new users who have no connections data</li>\n    </ul>\n\n    <h3>Acknowledgement</h3>\n    <ul>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/Tillerino\">Tillerino</a>,\n            <a target=\"_blank\" href=\"https://github.com/Alzhurav\">Alzhurav</a>,\n            <a target=\"_blank\" href=\"https://github.com/dylanfontaine\">dylanfontaine</a>,\n            <a target=\"_blank\" href=\"https://github.com/evangalen\">evangalen</a>,\n            <a target=\"_blank\" href=\"https://plugins.jetbrains.com/author/894191e2-5627-4e8a-a5ac-ee79b15f955a\">Ahsan Javed</a>\n            for reporting bug\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/21\">#21</a>\n        </li>\n    </ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ee/doc/release-notes.2020.1.0.html",
    "content": "<html lang=\"en\">\n    <h3>Notable changes</h3>\n    <ul>\n        <ul>\n            <li>Feature: introduce create-general-comment button</li>\n            <li>Option: allow doing code review without checking out the target branch</li>\n            <li>Option: open diff changes if the number of changes is less than configurable value</li>\n            <li>Option: keep selected values of filters and order state in Merge Request list</li>\n            <li>UX: display a comment after creating/replying comment</li>\n            <li>UX: select project automatically based on remote url in configuration</li>\n            <li>UX: remove left border of Tabs</li>\n            <li>Bug fix: cannot start an IDE if CE and EE are installed simultaneously</li>\n        </ul>\n    </ul>\n\n    <h3>Acknowledgement</h3>\n    <ul>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/riclucio\">riclucio</a>\n            for requesting features\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/33\">#31</a>,\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/33\">#33</a>,\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/35\">#35</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/turnik\">turnik</a>\n            for requesting option\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/40\">#40</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/riclucio\">riclucio</a>\n            for reporting problem\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/37\">#37</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/Tillerino\">Tillerino</a>\n            for reporting problem\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/37\">#37</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/intgr\">intgr</a>\n            for reporting problem\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/20\">#20</a>\n        </li>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/zlodes\">zlodes</a>\n            for contributing to source code, the commit\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/commit/dfdc4f9e0c4366cc555fb723e82bc922eb06cdfc\">dfdc4f9</a>,\n            Pull Request\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/pull/30\">#30</a>\n        </li>\n    </ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ee/doc/release-notes.2020.1.1.html",
    "content": "<html lang=\"en\">\n    <h3>Notable changes</h3>\n    <ul>\n        <li>Hotfix: Crashes on Intellij IDEs v2020.1 EAP</li>\n    </ul>\n\n    <h3>Acknowledgement</h3>\n    <ul>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/alexeyzimarev\">alexeyzimarev</a>,\n            <a target=\"_blank\" href=\"https://github.com/tyger\">tyger</a>\n            for reporting problem\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/41\">#41</a>\n        </li>\n    </ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ee/doc/release-notes.2020.1.2.html",
    "content": "<html lang=\"en\">\n    <h3>Notable changes</h3>\n    <ul>\n        <li>Feature: Display, resolve, delete, reply, write comments in diff view</li>\n        <li>Feature: Open diff view when selecting a group in comments tree</li>\n        <li>Bug fix: Duplicated/wrong comment position</li>\n    </ul>\n\n    <h3>Acknowledgement</h3>\n    <ul>\n        <li>\n            Thank <a target=\"_blank\" href=\"https://github.com/turnik\">turnik</a>\n            for contributing the new comment architecture\n        </li>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/theotherp\">theotherp</a>,\n            <a target=\"_blank\" href=\"https://github.com/BlacKCaT27\">BlacKCaT27</a>\n            for requesting feature\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/3\">#3</a>\n        </li>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/Pegietix\">Pegietix</a>,\n            <a target=\"_blank\" href=\"https://github.com/s-hildebrandt\">s-hildebrandt</a>\n            for reporting issue\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/23\">#23</a>\n        </li>\n    </ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ee/doc/release-notes.2020.1.3.html",
    "content": "<html lang=\"en\">\n    <h3>Notable changes</h3>\n    <ul>\n        <li>Branch project based on IDE versions, deploy plugin to EAP channel for 2020.1 EAP IDEs</li>\n        <li>Display approval status in merge request list</li>\n        <li>Improve UI response when deleting and adding a new comment</li>\n        <li>Sync comment collection when a comment gets changed in diff view</li>\n        <li>Fix #44: error when comments get destroyed in the event thread</li>\n    </ul>\n\n    <h3>Acknowledgement</h3>\n    <ul>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/PlatonovLex\">PlatonovLex</a>\n            for reporting issue\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/44\">#44</a>\n        </li>\n    </ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ee/doc/release-notes.2020.1.4.html",
    "content": "<html lang=\"en\">\n    <h3>Notable changes</h3>\n    <ul>\n        <li>Bug fixed: comments are not shown in diff view on Windows</li>\n        <li>Bug fixed: comments are filtered out when the MR has too many commits</li>\n    </ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ee/doc/release-notes.2020.1.5.html",
    "content": "<html lang=\"en\">\n    <h2>Notable changes</h2>\n    <ul>\n        <li>Support IDEA 2020.1, Android Studio 4.0.0</li>\n        <li>\n            Introduce new Comments tab which\n            <ul>\n                <li>Has all current functionalities</li>\n                <li>Can open diff view when you select a file</li>\n                <li>Can jump to code when you travel the Comments tree</li>\n            </ul>\n        </li>\n        <li>Open diff view when you click to Changes tree in Commits tab</li>\n        <li>Display merge request id in Home tab, Merge Request tree and Info tab</li>\n        <li>Fix #17: Cannot do Code Review on Android Studio</li>\n        <li>Fix #54: Merge Request tree should be invoked in application thread when get reloaded</li>\n        <li>Bug fixes</li>\n    </ul>\n\n    <h3>Acknowledgement</h3>\n    <ul>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/peper0\">peper0</a>,\n            <a target=\"_blank\" href=\"https://github.com/smec-cgint\">smec-cgint</a>\n            for requesting feature\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/14\">#14</a>\n        </li>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/LionZXY\">LionZXY</a>,\n            <a target=\"_blank\" href=\"https://github.com/mochadwi\">mochadwi</a>\n            for reporting issue\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/17\">#17</a>\n        </li>\n        <li>\n            Thank\n            <a target=\"_blank\" href=\"https://github.com/JaimeFreireB3I\">JaimeFreireB3I</a>\n            for reporting issue\n            <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/54\">#54</a>\n        </li>\n    </ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ee/doc/release-notes.2020.2.0.html",
    "content": "<html lang=\"en\">\n<h2>Notable changes</h2>\n<ul>\n    <li>Support IDEA 2020.2</li>\n    <li>\n        Introduce new Rework flow which helps\n        <ul>\n            <li>Display comments and file changes if current branch matches your MR</li>\n            <li>Jump and address comments more easily</li>\n        </ul>\n        <div>Note: because of technical difficulty Reply button only works when open a dialog</div>\n    </li>\n    <li>Introduce options to hide UpVote, DownVote or State buttons in toolbar.</li>\n    <li>Improve configuration area.</li>\n    <li>Throw all exceptions which may happen inside plugin.</li>\n    <li>Fix #65: loading data and refreshing not work properly</li>\n    <li>Fix #88: fetching merge requests error for some timezones</li>\n</ul>\n\n<h3>Acknowledgement</h3>\n<ul>\n    <li>\n        Thank\n        <a target=\"_blank\" href=\"https://github.com/lohengrin332\">lohengrin332</a>,\n        <a target=\"_blank\" href=\"https://github.com/jmatthiesen81\">jmatthiesen81</a>\n        for reporting\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/86\">#86</a>\n    </li>\n    <li>\n        Thank\n        <a target=\"_blank\" href=\"https://github.com/johnhenaot\">johnhenaot</a>,\n        <a target=\"_blank\" href=\"https://github.com/pchelk1n\">pchelk1n</a>,\n        <a target=\"_blank\" href=\"https://github.com/tlttnz2018\">tlttnz2018</a>\n        for reporting\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/87\">#87</a>\n    </li>\n    <li>\n        Thank\n        <a target=\"_blank\" href=\"https://github.com/srinathanantha\">srinathanantha</a>\n        for reporting\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/65\">#65</a>\n    </li>\n    <li>\n        Thank\n        <a target=\"_blank\" href=\"https://github.com/LuccaPrado\">LuccaPrado</a>\n        for reporting\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/88\">#88</a>\n    </li>\n</ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ee/doc/release-notes.2020.3.0.html",
    "content": "<html lang=\"en\">\n<h2>Notable changes</h2>\n<ul>\n    <li>Support IDEA 2020.3.x</li>\n    <li>Support search by ID when ID is typed in the search box</li>\n    <li>Introduce edit comment functionality</li>\n    <li>Introduce draft comment feature which can temporarily save and push when Code Review is stopped</li>\n    <li>Fix #93: Crash with empty avatar</li>\n    <li>Fix #98: Crash when project gets closed</li>\n</ul>\n\n<h3>Acknowledgement</h3>\n<ul>\n    <li>\n        Thank\n        <a target=\"_blank\" href=\"https://github.com/sleepyprof\">sleepyprof</a>,\n        <a target=\"_blank\" href=\"https://github.com/PeskovV\">PeskovV</a>,\n        <a target=\"_blank\" href=\"https://github.com/ctammes\">ctammes</a>\n        for reporting\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/93\">#93</a>\n    </li>\n\n    <li>\n        Thank\n        <a target=\"_blank\" href=\"https://github.com/dragetd\">dragetd</a>,\n        <a target=\"_blank\" href=\"https://github.com/MaDKaTZe\">MaDKaTZe</a>\n        for reporting\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/98\">#98</a>\n    </li>\n\n    <li>\n        Thank\n        <a target=\"_blank\" href=\"https://github.com/regevbr\">regevbr</a>,\n        <a target=\"_blank\" href=\"https://github.com/jsbh05\">jsbh05</a>,\n        <a target=\"_blank\" href=\"https://github.com/artitrue\">artitrue</a>,\n        <a target=\"_blank\" href=\"https://github.com/jmatthiesen81\">jmatthiesen81</a>,\n        <a target=\"_blank\" href=\"https://github.com/krishnaku\">krishnaku</a>,\n        <a target=\"_blank\" href=\"https://github.com/drewoko\">drewoko</a>,\n        <a target=\"_blank\" href=\"https://github.com/petromir\">petromir</a>,\n        <a target=\"_blank\" href=\"https://github.com/mjhamiltonus\">mjhamiltonus</a>,\n        <a target=\"_blank\" href=\"https://github.com/pchelk1n\">pchelk1n</a>\n        for reporting\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/102\">#102</a>\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/115\">#115</a>\n    </li>\n\n    <li>\n        Thank\n        <a target=\"_blank\" href=\"https://github.com/martinsefcik\">martinsefcik</a>,\n        <a target=\"_blank\" href=\"https://github.com/rrajkomar\">rrajkomar</a>,\n        <a target=\"_blank\" href=\"https://github.com/nailuoGG\">nailuoGG</a>\n        for requesting\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/69\">#69</a>,\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/71\">#71</a>,\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/118\">#118</a>\n    </li>\n\n    <li>\n        Thank\n        <a target=\"_blank\" href=\"https://github.com/bironran\">bironran</a>,\n        <a target=\"_blank\" href=\"https://github.com/Borginator\">Borginator</a>\n        for requesting\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/89\">#89</a>\n    </li>\n\n    <li>\n        Thank\n        <a target=\"_blank\" href=\"https://github.com/pionl\">pionl</a>\n        for requesting\n        <a target=\"_blank\" href=\"https://github.com/nhat-phan/merge-request-integration/issues/92\">#92</a>\n    </li>\n</ul>\n</html>\n"
  },
  {
    "path": "merge-request-integration-ee/settings.gradle.kts",
    "content": "rootProject.name = \"merge-request-integration-ee\"\n"
  },
  {
    "path": "merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/CheckLicense.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeEE\n\nimport com.intellij.openapi.application.PermanentInstallationID\nimport com.intellij.ui.LicensingFacade\nimport java.io.ByteArrayInputStream\nimport java.nio.charset.Charset\nimport java.nio.charset.StandardCharsets\nimport java.security.Signature\nimport java.security.cert.*\nimport java.util.*\nimport java.util.Base64.getMimeDecoder\n\n/**\n * @author Eugene Zhuravlev\n * Date: 26-Jul-18\n */\nobject CheckLicense {\n    /**\n     * PRODUCT_CODE must be the same specified in plugin.xml inside the <productCode> tag\n    </productCode> */\n    private const val PRODUCT_CODE = \"PMRINTEGEE\"\n    private const val KEY_PREFIX = \"key:\"\n    private const val STAMP_PREFIX = \"stamp:\"\n    private const val EVAL_PREFIX = \"eval:\"\n    /**\n     * Public root certificates needed to verify JetBrains-signed licenses\n     */\n    private val ROOT_CERTIFICATES = arrayOf(\n        \"-----BEGIN CERTIFICATE-----\\n\" +\n            \"MIIFOzCCAyOgAwIBAgIJANJssYOyg3nhMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV\\n\" +\n            \"BAMMDUpldFByb2ZpbGUgQ0EwHhcNMTUxMDAyMTEwMDU2WhcNNDUxMDI0MTEwMDU2\\n\" +\n            \"WjAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMIICIjANBgkqhkiG9w0BAQEFAAOC\\n\" +\n            \"Ag8AMIICCgKCAgEA0tQuEA8784NabB1+T2XBhpB+2P1qjewHiSajAV8dfIeWJOYG\\n\" +\n            \"y+ShXiuedj8rL8VCdU+yH7Ux/6IvTcT3nwM/E/3rjJIgLnbZNerFm15Eez+XpWBl\\n\" +\n            \"m5fDBJhEGhPc89Y31GpTzW0vCLmhJ44XwvYPntWxYISUrqeR3zoUQrCEp1C6mXNX\\n\" +\n            \"EpqIGIVbJ6JVa/YI+pwbfuP51o0ZtF2rzvgfPzKtkpYQ7m7KgA8g8ktRXyNrz8bo\\n\" +\n            \"iwg7RRPeqs4uL/RK8d2KLpgLqcAB9WDpcEQzPWegbDrFO1F3z4UVNH6hrMfOLGVA\\n\" +\n            \"xoiQhNFhZj6RumBXlPS0rmCOCkUkWrDr3l6Z3spUVgoeea+QdX682j6t7JnakaOw\\n\" +\n            \"jzwY777SrZoi9mFFpLVhfb4haq4IWyKSHR3/0BlWXgcgI6w6LXm+V+ZgLVDON52F\\n\" +\n            \"LcxnfftaBJz2yclEwBohq38rYEpb+28+JBvHJYqcZRaldHYLjjmb8XXvf2MyFeXr\\n\" +\n            \"SopYkdzCvzmiEJAewrEbPUaTllogUQmnv7Rv9sZ9jfdJ/cEn8e7GSGjHIbnjV2ZM\\n\" +\n            \"Q9vTpWjvsT/cqatbxzdBo/iEg5i9yohOC9aBfpIHPXFw+fEj7VLvktxZY6qThYXR\\n\" +\n            \"Rus1WErPgxDzVpNp+4gXovAYOxsZak5oTV74ynv1aQ93HSndGkKUE/qA/JECAwEA\\n\" +\n            \"AaOBhzCBhDAdBgNVHQ4EFgQUo562SGdCEjZBvW3gubSgUouX8bMwSAYDVR0jBEEw\\n\" +\n            \"P4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2Zp\\n\" +\n            \"bGUgQ0GCCQDSbLGDsoN54TAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkq\\n\" +\n            \"hkiG9w0BAQsFAAOCAgEAjrPAZ4xC7sNiSSqh69s3KJD3Ti4etaxcrSnD7r9rJYpK\\n\" +\n            \"BMviCKZRKFbLv+iaF5JK5QWuWdlgA37ol7mLeoF7aIA9b60Ag2OpgRICRG79QY7o\\n\" +\n            \"uLviF/yRMqm6yno7NYkGLd61e5Huu+BfT459MWG9RVkG/DY0sGfkyTHJS5xrjBV6\\n\" +\n            \"hjLG0lf3orwqOlqSNRmhvn9sMzwAP3ILLM5VJC5jNF1zAk0jrqKz64vuA8PLJZlL\\n\" +\n            \"S9TZJIYwdesCGfnN2AETvzf3qxLcGTF038zKOHUMnjZuFW1ba/12fDK5GJ4i5y+n\\n\" +\n            \"fDWVZVUDYOPUixEZ1cwzmf9Tx3hR8tRjMWQmHixcNC8XEkVfztID5XeHtDeQ+uPk\\n\" +\n            \"X+jTDXbRb+77BP6n41briXhm57AwUI3TqqJFvoiFyx5JvVWG3ZqlVaeU/U9e0gxn\\n\" +\n            \"8qyR+ZA3BGbtUSDDs8LDnE67URzK+L+q0F2BC758lSPNB2qsJeQ63bYyzf0du3wB\\n\" +\n            \"/gb2+xJijAvscU3KgNpkxfGklvJD/oDUIqZQAnNcHe7QEf8iG2WqaMJIyXZlW3me\\n\" +\n            \"0rn+cgvxHPt6N4EBh5GgNZR4l0eaFEV+fxVsydOQYo1RIyFMXtafFBqQl6DDxujl\\n\" +\n            \"FeU3FZ+Bcp12t7dlM4E0/sS1XdL47CfGVj4Bp+/VbF862HmkAbd7shs7sDQkHbU=\\n\" +\n            \"-----END CERTIFICATE-----\\n\",\n        \"-----BEGIN CERTIFICATE-----\\n\" +\n            \"MIIFTDCCAzSgAwIBAgIJAMCrW9HV+hjZMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV\\n\" +\n            \"BAMMEkxpY2Vuc2UgU2VydmVycyBDQTAgFw0xNjEwMTIxNDMwNTRaGA8yMTE2MTIy\\n\" +\n            \"NzE0MzA1NFowHTEbMBkGA1UEAwwSTGljZW5zZSBTZXJ2ZXJzIENBMIICIjANBgkq\\n\" +\n            \"hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAoT7LvHj3JKK2pgc5f02z+xEiJDcvlBi6\\n\" +\n            \"fIwrg/504UaMx3xWXAE5CEPelFty+QPRJnTNnSxqKQQmg2s/5tMJpL9lzGwXaV7a\\n\" +\n            \"rrcsEDbzV4el5mIXUnk77Bm/QVv48s63iQqUjVmvjQt9SWG2J7+h6X3ICRvF1sQB\\n\" +\n            \"yeat/cO7tkpz1aXXbvbAws7/3dXLTgAZTAmBXWNEZHVUTcwSg2IziYxL8HRFOH0+\\n\" +\n            \"GMBhHqa0ySmF1UTnTV4atIXrvjpABsoUvGxw+qOO2qnwe6ENEFWFz1a7pryVOHXg\\n\" +\n            \"P+4JyPkI1hdAhAqT2kOKbTHvlXDMUaxAPlriOVw+vaIjIVlNHpBGhqTj1aqfJpLj\\n\" +\n            \"qfDFcuqQSI4O1W5tVPRNFrjr74nDwLDZnOF+oSy4E1/WhL85FfP3IeQAIHdswNMJ\\n\" +\n            \"y+RdkPZCfXzSUhBKRtiM+yjpIn5RBY+8z+9yeGocoxPf7l0or3YF4GUpud202zgy\\n\" +\n            \"Y3sJqEsZksB750M0hx+vMMC9GD5nkzm9BykJS25hZOSsRNhX9InPWYYIi6mFm8QA\\n\" +\n            \"2Dnv8wxAwt2tDNgqa0v/N8OxHglPcK/VO9kXrUBtwCIfZigO//N3hqzfRNbTv/ZO\\n\" +\n            \"k9lArqGtcu1hSa78U4fuu7lIHi+u5rgXbB6HMVT3g5GQ1L9xxT1xad76k2EGEi3F\\n\" +\n            \"9B+tSrvru70CAwEAAaOBjDCBiTAdBgNVHQ4EFgQUpsRiEz+uvh6TsQqurtwXMd4J\\n\" +\n            \"8VEwTQYDVR0jBEYwRIAUpsRiEz+uvh6TsQqurtwXMd4J8VGhIaQfMB0xGzAZBgNV\\n\" +\n            \"BAMMEkxpY2Vuc2UgU2VydmVycyBDQYIJAMCrW9HV+hjZMAwGA1UdEwQFMAMBAf8w\\n\" +\n            \"CwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQCJ9+GQWvBS3zsgPB+1PCVc\\n\" +\n            \"oG6FY87N6nb3ZgNTHrUMNYdo7FDeol2DSB4wh/6rsP9Z4FqVlpGkckB+QHCvqU+d\\n\" +\n            \"rYPe6QWHIb1kE8ftTnwapj/ZaBtF80NWUfYBER/9c6To5moW63O7q6cmKgaGk6zv\\n\" +\n            \"St2IhwNdTX0Q5cib9ytE4XROeVwPUn6RdU/+AVqSOspSMc1WQxkPVGRF7HPCoGhd\\n\" +\n            \"vqebbYhpahiMWfClEuv1I37gJaRtsoNpx3f/jleoC/vDvXjAznfO497YTf/GgSM2\\n\" +\n            \"LCnVtpPQQ2vQbOfTjaBYO2MpibQlYpbkbjkd5ZcO5U5PGrQpPFrWcylz7eUC3c05\\n\" +\n            \"UVeygGIthsA/0hMCioYz4UjWTgi9NQLbhVkfmVQ5lCVxTotyBzoubh3FBz+wq2Qt\\n\" +\n            \"iElsBrCMR7UwmIu79UYzmLGt3/gBdHxaImrT9SQ8uqzP5eit54LlGbvGekVdAL5l\\n\" +\n            \"DFwPcSB1IKauXZvi1DwFGPeemcSAndy+Uoqw5XGRqE6jBxS7XVI7/4BSMDDRBz1u\\n\" +\n            \"a+JMGZXS8yyYT+7HdsybfsZLvkVmc9zVSDI7/MjVPdk6h0sLn+vuPC1bIi5edoNy\\n\" +\n            \"PdiG2uPH5eDO6INcisyPpLS4yFKliaO4Jjap7yzLU9pbItoWgCAYa2NpxuxHJ0tB\\n\" +\n            \"7tlDFnvaRnQukqSG+VqNWg==\\n\" +\n            \"-----END CERTIFICATE-----\"\n    )\n    private const val SECOND: Long = 1000\n    private const val MINUTE = 60 * SECOND\n    private const val HOUR = 60 * MINUTE\n    private const val TIMESTAMP_VALIDITY_PERIOD_MS =\n        1 * HOUR // configure period that suits your needs better// licensed via ticket obtained from JetBrains Floating License Server\n\n    // the license is obtained via JetBrainsAccount or entered as an activation code\n    val isLicensed: Boolean\n        get() {\n            val facade = LicensingFacade.getInstance() ?: return false\n            val cstamp = facade.getConfirmationStamp(PRODUCT_CODE) ?: return false\n            if (cstamp.startsWith(KEY_PREFIX)) { // the license is obtained via JetBrainsAccount or entered as an activation code\n                return isKeyValid(cstamp.substring(KEY_PREFIX.length))\n            }\n            if (cstamp.startsWith(STAMP_PREFIX)) { // licensed via ticket obtained from JetBrains Floating License Server\n                return isLicenseServerStampValid(cstamp.substring(STAMP_PREFIX.length))\n            }\n            return if (cstamp.startsWith(EVAL_PREFIX)) {\n                isEvaluationValid(cstamp.substring(EVAL_PREFIX.length))\n            } else false\n        }\n\n    private fun isEvaluationValid(expirationTime: String): Boolean {\n        return try {\n            val now = Date()\n            val expiration = Date(expirationTime.toLong())\n            now.before(expiration)\n        } catch (e: NumberFormatException) {\n            false\n        }\n    }\n\n    private fun isKeyValid(key: String): Boolean {\n        val licenseParts = key.split(\"-\").toTypedArray()\n        if (licenseParts.size != 4) {\n            return false // invalid format\n        }\n        val licenseId = licenseParts[0]\n        val licensePartBase64 = licenseParts[1]\n        val signatureBase64 = licenseParts[2]\n        val certBase64 = licenseParts[3]\n        try {\n            val sig = Signature.getInstance(\"SHA1withRSA\")\n            // the last parameter of 'createCertificate()' set to 'false' switches off certificate expiration checks.\n            // This might be the case if the key is at the same time a perpetual fallback license for older IDE versions.\n            // Here it is only important that the key was signed with an authentic JetBrains certificate.\n            sig.initVerify(\n                createCertificate(\n                    Base64.getMimeDecoder().decode(certBase64.toByteArray(StandardCharsets.UTF_8)),\n                    emptySet(),\n                    false\n                )\n            )\n            val licenseBytes = Base64.getMimeDecoder()\n                .decode(licensePartBase64.toByteArray(StandardCharsets.UTF_8))\n            sig.update(licenseBytes)\n            if (!sig.verify(Base64.getMimeDecoder().decode(signatureBase64.toByteArray(StandardCharsets.UTF_8)))) {\n                return false\n            }\n            // Optional additional check: the licenseId corresponds to the licenseId encoded in the signed license data\n            // The following is a 'least-effort' code. It would be more accurate to parse json and then find there the value of the attribute \"licenseId\"\n            val licenseData = String(licenseBytes, Charset.forName(\"UTF-8\"))\n            return licenseData.contains(\"\\\"licenseId\\\":\\\"$licenseId\\\"\")\n        } catch (ignored: Throwable) {\n        }\n        return false\n    }\n\n    private fun isLicenseServerStampValid(serverStamp: String): Boolean {\n        try {\n            val parts = serverStamp.split(\":\".toRegex()).toTypedArray()\n            val base64 = getMimeDecoder()\n            val expectedMachineId = parts[0]\n            val timeStamp = parts[1].toLong()\n            val machineId = parts[2]\n            val signatureType = parts[3]\n            val signatureBytes = base64.decode(parts[4].toByteArray(StandardCharsets.UTF_8))\n            val certBytes = base64.decode(parts[5].toByteArray(StandardCharsets.UTF_8))\n            val intermediate: MutableCollection<ByteArray> =\n                ArrayList()\n            for (idx in 6 until parts.size) {\n                intermediate.add(base64.decode(parts[idx].toByteArray(StandardCharsets.UTF_8)))\n            }\n            val sig = Signature.getInstance(signatureType)\n            // the last parameter of 'createCertificate()' set to 'true' causes the certificate to be checked for\n            // expiration. Expired certificates from a license server cannot be trusted\n            sig.initVerify(createCertificate(certBytes, intermediate, true))\n            sig.update(\"$timeStamp:$machineId\".toByteArray(StandardCharsets.UTF_8))\n            if (sig.verify(signatureBytes)) {\n                // machineId must match the machineId from the server reply and\n                // server reply should be relatively 'fresh'\n                return expectedMachineId == machineId && Math.abs(System.currentTimeMillis() - timeStamp) < TIMESTAMP_VALIDITY_PERIOD_MS\n            }\n        } catch (ignored: Throwable) { // consider serverStamp invalid\n        }\n        return false\n    }\n\n    @Throws(Exception::class)\n    private fun createCertificate(\n        certBytes: ByteArray,\n        intermediateCertsBytes: Collection<ByteArray>,\n        checkValidityAtCurrentDate: Boolean\n    ): X509Certificate {\n        val x509factory =\n            CertificateFactory.getInstance(\"X.509\")\n        val cert =\n            x509factory.generateCertificate(ByteArrayInputStream(certBytes)) as X509Certificate\n        val allCerts: MutableCollection<Certificate?> =\n            HashSet()\n        allCerts.add(cert)\n        for (bytes in intermediateCertsBytes) {\n            allCerts.add(x509factory.generateCertificate(ByteArrayInputStream(bytes)))\n        }\n        try { // Create the selector that specifies the starting certificate\n            val selector = X509CertSelector()\n            selector.certificate = cert\n            // Configure the PKIX certificate builder algorithm parameters\n            val trustAchors: MutableSet<TrustAnchor> = HashSet()\n            for (rc in ROOT_CERTIFICATES) {\n                trustAchors.add(\n                    TrustAnchor(\n                        x509factory.generateCertificate(ByteArrayInputStream(rc.toByteArray(StandardCharsets.UTF_8))) as X509Certificate,\n                        null\n                    )\n                )\n            }\n            val pkixParams = PKIXBuilderParameters(trustAchors, selector)\n            pkixParams.isRevocationEnabled = false\n            if (!checkValidityAtCurrentDate) { // deliberately check validity on the start date of cert validity period, so that we do not depend on\n                // the actual moment when the check is performed\n                pkixParams.date = cert.notBefore\n            }\n            pkixParams.addCertStore(\n                CertStore.getInstance(\"Collection\", CollectionCertStoreParameters(allCerts))\n            )\n            // Build and verify the certification chain\n            val path = CertPathBuilder.getInstance(\"PKIX\").build(pkixParams).certPath\n            if (path != null) {\n                CertPathValidator.getInstance(\"PKIX\").validate(path, pkixParams)\n                return cert\n            }\n        } catch (e: Exception) { // debug the reason here\n        }\n        throw Exception(\"Certificate used to sign the license is not signed by JetBrains root certificate\")\n    }\n}"
  },
  {
    "path": "merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/Configuration.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeEE\n\nimport com.intellij.openapi.components.ServiceManager\nimport net.ntworld.mergeRequestIntegrationIde.ui.configuration.ConfigurationBase\n\nclass Configuration: ConfigurationBase(ServiceManager.getService(EnterpriseApplicationServiceProvider::class.java)) {\n    override fun getId(): String {\n        return \"merge-request-integration-ee\"\n    }\n\n    override fun getDisplayName(): String {\n        return \"Merge Request Integration EE\"\n    }\n}"
  },
  {
    "path": "merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/DiffExtension.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeEE\n\nimport com.intellij.openapi.components.ServiceManager\nimport net.ntworld.mergeRequestIntegrationIde.diff.DiffExtensionBase\n\nclass DiffExtension : DiffExtensionBase(\n    ServiceManager.getService(EnterpriseApplicationServiceProvider::class.java)\n)"
  },
  {
    "path": "merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/DiffViewAddCommentAction.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeEE\n\nimport net.ntworld.mergeRequestIntegrationIde.diff.DiffViewAddCommentActionBase\n\nclass DiffViewAddCommentAction : DiffViewAddCommentActionBase()\n"
  },
  {
    "path": "merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/DiffViewToggleCommentsAction.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeEE\n\nimport net.ntworld.mergeRequestIntegrationIde.diff.DiffViewToggleCommentsActionBase\n\nclass DiffViewToggleCommentsAction : DiffViewToggleCommentsActionBase()"
  },
  {
    "path": "merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/EnterpriseApplicationServiceProvider.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeEE\n\nimport com.intellij.openapi.components.ServiceManager\nimport com.intellij.openapi.components.State\nimport com.intellij.openapi.components.Storage\nimport com.intellij.openapi.project.Project\nimport net.ntworld.mergeRequest.ProviderData\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.AbstractApplicationServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ProjectServiceProvider\n\n@State(name = \"MergeRequestIntegrationApplicationLevel\", storages = [(Storage(\"merge-request-integration.xml\"))])\nclass EnterpriseApplicationServiceProvider: AbstractApplicationServiceProvider() {\n    override fun isLegal(providerData: ProviderData): Boolean {\n        if (!super.isLegal(providerData)) {\n            return CheckLicense.isLicensed\n        }\n        return true\n    }\n\n    override fun findProjectServiceProvider(project: Project): ProjectServiceProvider {\n        val service = ServiceManager.getService(project, EnterpriseProjectServiceProvider::class.java)\n        registerProjectServiceProvider(service)\n\n        return service\n    }\n\n    override val singleMRToolWindowName: String = \"Merge Request\"\n}"
  },
  {
    "path": "merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/EnterpriseProjectServiceProvider.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeEE\n\nimport com.intellij.openapi.components.ServiceManager\nimport com.intellij.openapi.components.State\nimport com.intellij.openapi.components.Storage\nimport com.intellij.openapi.project.Project as IdeaProject\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.AbstractProjectServiceProvider\nimport net.ntworld.mergeRequestIntegrationIde.infrastructure.ApplicationServiceProvider\n\n@State(name = \"MergeRequestIntegrationProjectLevel\", storages = [(Storage(\"merge-request-integration-ee.xml\"))])\nclass EnterpriseProjectServiceProvider(ideaProject: IdeaProject) : AbstractProjectServiceProvider(ideaProject) {\n\n    override val applicationServiceProvider: ApplicationServiceProvider = ServiceManager.getService(\n        EnterpriseApplicationServiceProvider::class.java\n    )\n\n    init {\n        initWithApplicationServiceProvider(applicationServiceProvider)\n    }\n}\n\n"
  },
  {
    "path": "merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/GithubConnectionsConfigurable.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeEE\n\nimport com.intellij.openapi.components.ServiceManager\nimport com.intellij.openapi.project.Project\nimport net.ntworld.mergeRequestIntegrationIde.ui.configuration.GithubConnectionsConfigurableBase\n\nclass GithubConnectionsConfigurable(project: Project) : GithubConnectionsConfigurableBase(\n    ServiceManager.getService(project, EnterpriseProjectServiceProvider::class.java)\n) {\n    override fun getId(): String = \"MRI:github-ee\"\n\n    override fun getDisplayName(): String = \"Github\"\n}"
  },
  {
    "path": "merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/GitlabConnectionsConfigurable.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeEE\n\nimport com.intellij.openapi.components.ServiceManager\nimport com.intellij.openapi.project.Project\nimport net.ntworld.mergeRequestIntegrationIde.ui.configuration.GitlabConnectionsConfigurableBase\n\nclass GitlabConnectionsConfigurable(project: Project) : GitlabConnectionsConfigurableBase(\n    ServiceManager.getService(project, EnterpriseProjectServiceProvider::class.java)\n) {\n    override fun getId(): String = \"MRI:gitlab-ee\"\n\n    override fun getDisplayName(): String = \"Gitlab\"\n}\n\n"
  },
  {
    "path": "merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/MainToolWindowFactory.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeEE\n\nimport com.intellij.openapi.components.ServiceManager\nimport net.ntworld.mergeRequestIntegrationIde.ui.MainToolWindowFactoryBase\n\nclass MainToolWindowFactory : MainToolWindowFactoryBase(\n    ServiceManager.getService(EnterpriseApplicationServiceProvider::class.java)\n)"
  },
  {
    "path": "merge-request-integration-ee/src/main/kotlin/net/ntworld/mergeRequestIntegrationIdeEE/SingleMRToolWindowFactory.kt",
    "content": "package net.ntworld.mergeRequestIntegrationIdeEE\n\nimport com.intellij.openapi.components.ServiceManager\nimport net.ntworld.mergeRequestIntegrationIde.toolWindow.SingleMRToolWindowFactoryBase\n\nclass SingleMRToolWindowFactory : SingleMRToolWindowFactoryBase(\n    ServiceManager.getService(EnterpriseApplicationServiceProvider::class.java)\n)"
  },
  {
    "path": "merge-request-integration-ee/src/main/resources/META-INF/plugin.xml",
    "content": "<idea-plugin>\n    <id>net.ntworld.nhat-phan.merge-request-integration-ee</id>\n    <name>Merge Request Integration EE - Code Review for GitLab</name>\n    <vendor>ntworld</vendor>\n    <version>2019.3.2</version>\n    <product-descriptor code=\"PMRINTEGEE\" release-date=\"20200107\" release-version=\"20193\"/>\n\n    <depends>com.intellij.modules.platform</depends>\n    <depends>com.intellij.modules.lang</depends>\n    <depends>com.intellij.modules.vcs</depends>\n    <depends>Git4Idea</depends>\n\n    <extensions defaultExtensionNs=\"com.intellij\">\n        <applicationService serviceImplementation=\"net.ntworld.mergeRequestIntegrationIdeEE.EnterpriseApplicationServiceProvider\"/>\n        <projectService serviceImplementation=\"net.ntworld.mergeRequestIntegrationIdeEE.EnterpriseProjectServiceProvider\"/>\n        <diff.DiffExtension implementation=\"net.ntworld.mergeRequestIntegrationIdeEE.DiffExtension\"/>\n\n        <projectConfigurable id=\"merge-request-integration-ee\"\n                             displayName=\"Merge Request Integration\"\n                             instance=\"net.ntworld.mergeRequestIntegrationIdeEE.Configuration\"/>\n        <projectConfigurable groupId=\"merge-request-integration-ee\"\n                             id=\"MRI:gitlab-ee\"\n                             displayName=\"Gitlab\"\n                             instance=\"net.ntworld.mergeRequestIntegrationIdeEE.GitlabConnectionsConfigurable\"/>\n        <!--\n        <projectConfigurable groupId=\"merge-request-integration-ee\"\n                             id=\"MRI:github-ee\"\n                             displayName=\"Github\"\n                             instance=\"net.ntworld.mergeRequestIntegrationIdeEE.GithubConnectionsConfigurable\"/>\n        -->\n\n        <toolWindow id=\"Merge Request Integration\"\n                    secondary=\"false\"\n                    anchor=\"bottom\"\n                    icon=\"/icons/main-tool-window.svg\"\n                    canCloseContents=\"true\"\n                    factoryClass=\"net.ntworld.mergeRequestIntegrationIdeEE.MainToolWindowFactory\"/>\n\n        <toolWindow id=\"Merge Request\"\n                    secondary=\"true\"\n                    anchor=\"left\"\n                    icon=\"/icons/single-mr-tool-window.svg\"\n                    factoryClass=\"net.ntworld.mergeRequestIntegrationIdeEE.SingleMRToolWindowFactory\"/>\n    </extensions>\n\n    <actions>\n        <action class=\"net.ntworld.mergeRequestIntegrationIdeEE.DiffViewToggleCommentsAction\"\n                text=\"Toggle Comments\"\n                description=\"Toggle comments to this line\">\n            <keyboard-shortcut first-keystroke=\"alt V\" keymap=\"$default\"/>\n            <add-to-group group-id=\"Diff.EditorPopupMenu\" relative-to-action=\"Annotate\" anchor=\"after\" />\n        </action>\n        <action class=\"net.ntworld.mergeRequestIntegrationIdeEE.DiffViewAddCommentAction\"\n                text=\"Add Comment\"\n                description=\"Add comment to this line\">\n            <keyboard-shortcut first-keystroke=\"alt C\" keymap=\"$default\"/>\n            <add-to-group group-id=\"Diff.EditorPopupMenu\" relative-to-action=\"Annotate\" anchor=\"after\" />\n        </action>\n    </actions>\n</idea-plugin>"
  },
  {
    "path": "settings.gradle.kts",
    "content": "/*\n * This file was generated by the Gradle 'init' task.\n *\n * The settings file is used to specify which projects to include in your build.\n *\n * Detailed information about configuring a multi-project build in Gradle can be found\n * in the user manual at https://docs.gradle.org/5.3.1/userguide/multi_project_builds.html\n */\npluginManagement {\n    resolutionStrategy {\n        eachPlugin {\n            if (requested.id.id == \"kotlin-multiplatform\") {\n                useModule(\"org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}\")\n            }\n            if (requested.id.id == \"kotlinx-serialization\") {\n                useModule(\"org.jetbrains.kotlin:kotlin-serialization:${requested.version}\")\n            }\n        }\n    }\n}\n\nrootProject.name = \"merge-request-integration-workspace\"\n\ninclude(\"contracts\")\ninclude(\"merge-request-integration\")\ninclude(\"merge-request-integration-core\")\ninclude(\"merge-request-integration-ce\")\ninclude(\"merge-request-integration-ee\")\n"
  }
]