├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── build.yml │ ├── release.yml │ └── run-ui-tests.yml ├── .gitignore ├── .run ├── Run IDE for UI Tests.run.xml ├── Run IDE with Plugin.run.xml ├── Run Plugin Tests.run.xml ├── Run Plugin Verification.run.xml └── Run Qodana.run.xml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── libs └── url-0.0.10.jar ├── menu-example.png ├── qodana.yml ├── settings-example.png ├── settings.gradle.kts └── src ├── main ├── kotlin │ └── uk │ │ └── co │ │ └── ben_gibson │ │ └── git │ │ └── link │ │ ├── Context.kt │ │ ├── GitLinkBundle.kt │ │ ├── GitLinkRunner.kt │ │ ├── extension │ │ └── ListExtensions.kt │ │ ├── git │ │ ├── Commit.kt │ │ ├── File.kt │ │ ├── RemoteExtensions.kt │ │ └── RepositoryExtensions.kt │ │ ├── listener │ │ └── ApplicationStartupListener.kt │ │ ├── pipeline │ │ ├── Pass.kt │ │ ├── Pipeline.kt │ │ └── middleware │ │ │ ├── ForceHttps.kt │ │ │ ├── GenerateUrl.kt │ │ │ ├── Middleware.kt │ │ │ ├── RecordHit.kt │ │ │ ├── ResolveContext.kt │ │ │ ├── SendSupportNotification.kt │ │ │ └── Timer.kt │ │ ├── platform │ │ ├── Platform.kt │ │ ├── PlatformDetector.kt │ │ ├── PlatformLocator.kt │ │ └── PlatformRepository.kt │ │ ├── settings │ │ ├── ApplicationSettings.kt │ │ └── ProjectSettings.kt │ │ ├── ui │ │ ├── EditorExtensions.kt │ │ ├── Icons.kt │ │ ├── LineSelection.kt │ │ ├── actions │ │ │ ├── Action.kt │ │ │ ├── annotation │ │ │ │ ├── CommitBrowserAction.kt │ │ │ │ ├── CommitCopyAction.kt │ │ │ │ ├── CommitMarkdownAction.kt │ │ │ │ ├── FileBrowserAction.kt │ │ │ │ ├── FileCopyAction.kt │ │ │ │ └── FileMarkdownAction.kt │ │ │ ├── gutter │ │ │ │ ├── BrowserAction.kt │ │ │ │ ├── CopyAction.kt │ │ │ │ ├── GutterAction.kt │ │ │ │ └── MarkdownAction.kt │ │ │ ├── menu │ │ │ │ ├── BrowserAction.kt │ │ │ │ ├── CopyAction.kt │ │ │ │ ├── MarkdownAction.kt │ │ │ │ └── MenuAction.kt │ │ │ └── vcslog │ │ │ │ ├── BrowserAction.kt │ │ │ │ ├── CopyAction.kt │ │ │ │ └── MarkdownAction.kt │ │ ├── components │ │ │ ├── PlatformCellRenderer.kt │ │ │ └── SubstitutionReferenceTable.kt │ │ ├── extensions │ │ │ ├── AnnotationGutterActionProvider.kt │ │ │ └── SelectInTarget.kt │ │ ├── notification │ │ │ ├── Notification.kt │ │ │ └── Notifier.kt │ │ ├── settings │ │ │ ├── CustomPlatformSettingsConfigurable.kt │ │ │ ├── DomainRegistrySettings.kt │ │ │ └── ProjectSettingsConfigurable.kt │ │ └── validation │ │ │ └── ValidationExtensions.kt │ │ └── url │ │ ├── UrlOptions.kt │ │ ├── factory │ │ ├── AzureUrlFactory.kt │ │ ├── BitbucketServerUrlFactory.kt │ │ ├── ChromiumUrlFactory.kt │ │ ├── TemplatedUrlFactory.kt │ │ ├── TemplatedUrlFactoryProvider.kt │ │ ├── UrlFactory.kt │ │ └── UrlFactoryLocator.kt │ │ └── template │ │ └── UrlTemplates.kt └── resources │ ├── META-INF │ ├── plugin.xml │ └── pluginIcon.svg │ ├── icons │ ├── azure.svg │ ├── bitbucket.svg │ ├── chromium.svg │ ├── gerrit.svg │ ├── git.svg │ ├── gitea.svg │ ├── gitee.svg │ ├── gitlab.svg │ ├── gitlink.svg │ ├── gogs.svg │ └── sourcehut.svg │ └── messages │ ├── ActionsBundle.properties │ └── MyBundle.properties └── test └── kotlin └── uk └── co └── ben_gibson └── git └── link ├── git └── RemoteTest.kt ├── ui └── actions │ └── vcslog │ └── BrowserActionTest.kt └── url ├── AzureTest.kt ├── BitBucketCloudTest.kt ├── BitBucketServerTest.kt ├── ChromiumTest.kt ├── GerritTest.kt ├── GitHubTest.kt ├── GitLabTest.kt ├── GogsTest.kt └── SrhtTest.kt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: gitlink # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration: 2 | # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | # Maintain dependencies for Gradle dependencies 7 | - package-ecosystem: "gradle" 8 | directory: "/" 9 | target-branch: "next" 10 | schedule: 11 | interval: "daily" 12 | # Maintain dependencies for GitHub Actions 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | target-branch: "next" 16 | schedule: 17 | interval: "daily" 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for handling the release process based on the draft release prepared with the Build workflow. 2 | # Running the publishPlugin task requires all following secrets to be provided: PUBLISH_TOKEN, PRIVATE_KEY, PRIVATE_KEY_PASSWORD, CERTIFICATE_CHAIN. 3 | # See https://plugins.jetbrains.com/docs/intellij/plugin-signing.html for more information. 4 | 5 | name: Release 6 | on: 7 | release: 8 | types: [prereleased, released] 9 | 10 | jobs: 11 | 12 | # Prepare and publish the plugin to JetBrains Marketplace repository 13 | release: 14 | name: Publish Plugin 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write 18 | pull-requests: write 19 | steps: 20 | 21 | # Check out the current repository 22 | - name: Fetch Sources 23 | uses: actions/checkout@v4 24 | with: 25 | ref: ${{ github.event.release.tag_name }} 26 | 27 | # Set up Java environment for the next steps 28 | - name: Setup Java 29 | uses: actions/setup-java@v4 30 | with: 31 | distribution: zulu 32 | java-version: 17 33 | 34 | # Setup Gradle 35 | - name: Setup Gradle 36 | uses: gradle/actions/setup-gradle@v3 37 | with: 38 | gradle-home-cache-cleanup: true 39 | 40 | # Set environment variables 41 | - name: Export Properties 42 | id: properties 43 | shell: bash 44 | run: | 45 | CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' 46 | ${{ github.event.release.body }} 47 | EOM 48 | )" 49 | 50 | echo "changelog<> $GITHUB_OUTPUT 51 | echo "$CHANGELOG" >> $GITHUB_OUTPUT 52 | echo "EOF" >> $GITHUB_OUTPUT 53 | 54 | # Update the Unreleased section with the current release note 55 | - name: Patch Changelog 56 | if: ${{ steps.properties.outputs.changelog != '' }} 57 | env: 58 | CHANGELOG: ${{ steps.properties.outputs.changelog }} 59 | run: | 60 | ./gradlew patchChangelog --release-note="$CHANGELOG" 61 | 62 | # Publish the plugin to JetBrains Marketplace 63 | - name: Publish Plugin 64 | env: 65 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 66 | CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }} 67 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 68 | PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }} 69 | run: ./gradlew publishPlugin 70 | 71 | # Upload artifact as a release asset 72 | - name: Upload Release Asset 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/* 76 | 77 | # Create a pull request 78 | - name: Create Pull Request 79 | if: ${{ steps.properties.outputs.changelog != '' }} 80 | env: 81 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 82 | run: | 83 | VERSION="${{ github.event.release.tag_name }}" 84 | BRANCH="changelog-update-$VERSION" 85 | LABEL="release changelog" 86 | 87 | git config user.email "action@github.com" 88 | git config user.name "GitHub Action" 89 | 90 | git checkout -b $BRANCH 91 | git commit -am "Changelog update - $VERSION" 92 | git push --set-upstream origin $BRANCH 93 | 94 | gh label create "$LABEL" \ 95 | --description "Pull requests with release changelog update" \ 96 | --force \ 97 | || true 98 | 99 | gh pr create \ 100 | --title "Changelog update - \`$VERSION\`" \ 101 | --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ 102 | --label "$LABEL" \ 103 | --head $BRANCH -------------------------------------------------------------------------------- /.github/workflows/run-ui-tests.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow for launching UI tests on Linux, Windows, and Mac in the following steps: 2 | # - Prepare and launch IDE with your plugin and robot-server plugin, which is needed to interact with the UI. 3 | # - Wait for IDE to start. 4 | # - Run UI tests with a separate Gradle task. 5 | # 6 | # Please check https://github.com/JetBrains/intellij-ui-test-robot for information about UI tests with IntelliJ Platform. 7 | # 8 | # Workflow is triggered manually. 9 | 10 | name: Run UI Tests 11 | on: 12 | workflow_dispatch 13 | 14 | jobs: 15 | 16 | testUI: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | - os: ubuntu-latest 23 | runIde: | 24 | export DISPLAY=:99.0 25 | Xvfb -ac :99 -screen 0 1920x1080x16 & 26 | gradle runIdeForUiTests & 27 | - os: windows-latest 28 | runIde: start gradlew.bat runIdeForUiTests 29 | - os: macos-latest 30 | runIde: ./gradlew runIdeForUiTests & 31 | 32 | steps: 33 | 34 | # Check out the current repository 35 | - name: Fetch Sources 36 | uses: actions/checkout@v4 37 | 38 | # Set up Java environment for the next steps 39 | - name: Setup Java 40 | uses: actions/setup-java@v4 41 | with: 42 | distribution: zulu 43 | java-version: 17 44 | 45 | # Setup Gradle 46 | - name: Setup Gradle 47 | uses: gradle/actions/setup-gradle@v3 48 | with: 49 | gradle-home-cache-cleanup: true 50 | 51 | # Run IDEA prepared for UI testing 52 | - name: Run IDE 53 | run: ${{ matrix.runIde }} 54 | 55 | # Wait for IDEA to be started 56 | - name: Health Check 57 | uses: jtalk/url-health-check-action@v4 58 | with: 59 | url: http://127.0.0.1:8082 60 | max-attempts: 15 61 | retry-delay: 30s 62 | 63 | # Run tests 64 | - name: Tests 65 | run: ./gradlew test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | .qodana 4 | build 5 | .intellijPlatform -------------------------------------------------------------------------------- /.run/Run IDE for UI Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 15 | 17 | true 18 | true 19 | false 20 | 21 | 22 | -------------------------------------------------------------------------------- /.run/Run IDE with Plugin.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /.run/Run Plugin Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.run/Run Plugin Verification.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 25 | 26 | -------------------------------------------------------------------------------- /.run/Run Qodana.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 16 | 19 | 21 | true 22 | true 23 | false 24 | 25 | 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # GitLink Changelog 4 | 5 | ## Unreleased 6 | 7 | ## 4.5.2 - 2024-09-07 8 | 9 | - Fix Azure URL generation 10 | 11 | ## 4.5.1 - 2024-09-03 12 | 13 | - Fix warnings and deprecations 14 | - Support dynamic reloading 15 | - Fix error using BitBucket cloud host 16 | 17 | ## 4.5.0 - 2024-09-02 18 | 19 | - Fix BitBucket cloud URL generation when the remote contains scm (#333) 20 | - Remove dash character from GitLab URLs (#328) 21 | - Add actions to the Git History tool window (#336) 22 | - Support branch substitution in commit templates (#337) 23 | - Improve Azure support (#334) 24 | - Fix deprecations and warnings. 25 | 26 | ## 4.4.0 - 2023-12-02 27 | 28 | - Add sourcehut support 29 | 30 | ## 4.3.6 - 2023-08-30 31 | 32 | - Fix Azure URL generation for remotes containing company name 33 | 34 | ## 4.3.5 - 2023-07-16 35 | 36 | - Fix Azure commit URL 37 | 38 | ## 4.3.4 - 2023-07-09 39 | 40 | - Fix issue where short commit hash was used instead of the full commit hash 41 | - Fix project configurable display name error in 2023.2 EAP 42 | - Fix deprecation warnings 43 | 44 | ## 4.3.3 - 2023-06-18 45 | 46 | - Fix URL generation with custom protocols. 47 | 48 | ## 4.3.2 - 2023-03-08 49 | 50 | - Update GitLab URL format (#284) 51 | - Add URL library JAR 52 | 53 | ## 4.3.1 - 2022-12-16 54 | 55 | - Fix menu text crash on 2022.3 when no host is detected 56 | 57 | ## 4.3.0 - 2022-12-11 58 | 59 | - Add basic copy Markdown action 60 | - Fix incorrect {remote:url:host} example 61 | 62 | ## 4.2.5 63 | 64 | - Fix exceptions when using unqualified host 65 | 66 | ## 4.2.4 67 | 68 | - Fix Azure URL when repo name has .git postfix 69 | 70 | ## 4.2.3 71 | 72 | - Fix Azure URL when using SSH protocol 73 | - Fix Azure URL line selection 74 | - Remove poll notification 75 | 76 | ## 4.2.2 77 | 78 | - Replace the `Check commit on remote` option with a more general `Check remote` option that disables all checks on the remote. 79 | 80 | ## 4.2.1 81 | 82 | - Fix platform auto-detection when cloning projects 83 | - Add auto-detection support for Bitbucket server and gerrit 84 | 85 | ## 4.2.0 86 | 87 | - Switch URL library 88 | - Fix issue removing custom domains 89 | - Add `Gerrit` support 90 | 91 | ## 4.1.8 92 | 93 | - Rename 'Host' option to 'Platform' 94 | 95 | ## 4.1.7 96 | 97 | - Do not disable actions during project indexing 98 | 99 | ## 4.1.6 100 | 101 | - Trim forward slash character in file path substitution 102 | 103 | ## 4.1.5 104 | 105 | - Revert host detection fix 106 | 107 | ## 4.1.4 108 | 109 | - Strip user info component from the generated URL 110 | 111 | ## 4.1.3 112 | 113 | - Internal refactor 114 | 115 | ## 4.1.2 116 | 117 | - Fix host detection when opening a new project within the IDE. 118 | 119 | ## 4.1.1 120 | 121 | - Make 'main' the default fallback branch, replacing 'master' 122 | - Fix issue validation custom host URL templates 123 | - Don't allow duplicate domain registration 124 | 125 | ## 4.1.0 126 | 127 | - Allow custom domain registration for supported hosts. 128 | 129 | ## 4.0.9 130 | 131 | - Add Gitee host 132 | 133 | ## 4.0.8 134 | 135 | - Add host poll notification 136 | - Add 'Do not show again' button to performance tip notification 137 | 138 | ## 4.0.7 139 | 140 | - Add experimental host Chromium 141 | - Add resolve context middleware 142 | - Dismiss notifications when action pressed 143 | 144 | ## 4.0.6 145 | 146 | - Fix Azure DevOps url templates 147 | 148 | ## 4.0.5 149 | 150 | - Fix line selection around collapsed code sections 151 | 152 | ## 4.0.4 153 | 154 | - Add pipeline to handle URL logic 155 | - Fix typos 156 | 157 | ## 4.0.3 158 | 159 | - Add support for Ukraine in README 160 | - Add notification on successful copy action 161 | - Improve notification messages 162 | - Make some notifications sticky 163 | - Add request for support notification 164 | 165 | ## 4.0.2 166 | 167 | - Add images to README 168 | - Add Open Collective link to README 169 | - Fix issue where the '#' character in a file name is not correctly encoded. 170 | 171 | ## 4.0.1 172 | 173 | - Fixed settings button opening the wrong menu. 174 | - Add shortcuts to editor gutter. #166 175 | 176 | ## 4.0.0 177 | 178 | - Completely re-written in Kotlin 179 | - Improve settings UI 180 | - Improve notifications 181 | - Support the creation of multiple custom hosts 182 | - Store custom hosts across projects 183 | - Auto-detect host when opening a new project 184 | - Many more fixes and improvements. 185 | 186 | ## 3.3.1 187 | 188 | - Add preference to disable check for a commit's existence on the remote. #97 189 | 190 | ## 3.3.0 191 | 192 | - Re-added support for copying links to the clipboard. #85 193 | 194 | ## 3.2.0 195 | 196 | - Rebuilt the substitution system used for the Custom host type. This system is now also used under the hood for most 197 | pre-defined host types. #92 198 | 199 | ## 3.1.2 200 | 201 | - Fix issue resulting in an invalid URL for project/organisation names made up digits when the remote URL uses 202 | the SSH protocol in the SCP syntax. #94 203 | 204 | ## 3.1.1 205 | 206 | - Fix multi-line selection in GitLab. #86 207 | 208 | ## 3.1.0 209 | 210 | - Support for multiline selection. #77 211 | - Renamed the host Stash to Bitbucket Server 212 | 213 | ## 3.0.0 214 | 215 | - Added ability to disable the plugin per project. #79 216 | - Added support for hosts Giea and Gogs. #80 217 | - Removed copy link action. 218 | - Code base cleanup. 219 | 220 | ## 2.4.0 221 | 222 | - Add open commit action to annotation gutter. #70 223 | - Respect line number when using from the annotation gutter. #68 224 | - Removed copy link action from annotation gutter. 225 | 226 | ## 2.3.1 227 | 228 | - Fixed bug which caused an incorrect URL to be created from the VCS log. 229 | - Added GitBlit support to open a file at a specific commit. #65 230 | 231 | ## 2.3.0 232 | 233 | - Generate link to file at commit instead of branch where possible. #61 234 | - Added actions to annotation gutter. #57 235 | - Allow remote name to be configured from the preferences. #60 236 | - Minor bug fixes and improvements. 237 | 238 | ## 2.2.0 239 | 240 | - Added support for GitBlit. #41 241 | 242 | ## 2.1.2 243 | 244 | - Fixed issue preventing port numbers with more than 4 digits being removed #52. 245 | 246 | ## 2.1.1 247 | 248 | - Fixed force HTTPS option. 249 | 250 | ## 2.1.0 251 | 252 | - Code refactor 253 | - Separated shortcuts for opening in the browser and copying to the clipboard #47 254 | - Rename plugin to GitLink #46 255 | - Make default branch customisable #45 256 | - Add custom URL factory #44 257 | 258 | ## 2.0.1 259 | 260 | - Fixed encoding issue when URL contains non-ASCII characters. #40 261 | 262 | ## 2.0.0 263 | 264 | - Rebuilt the entire plugin! Note that you have to configure the plugin again! 265 | 266 | ## 1.6.6 267 | 268 | - Fixed incompatibility issue with save actions plugin. Note that you have to configure the plugin again! 269 | 270 | ## 1.6.5 271 | 272 | - Removed analytics. 273 | 274 | ## 1.6.4 275 | 276 | - Tweaked analytics 277 | 278 | ## 1.6.3 279 | 280 | - Improved analytics 281 | 282 | ## 1.6.2 283 | 284 | - Added analytics 285 | 286 | ## 1.6.1 287 | 288 | - Support more shortcuts. 289 | - Added host icons. 290 | - Minor refactors. 291 | 292 | ## 1.6.0 293 | 294 | - Support GitBlit #26 295 | 296 | ## 1.5.7 297 | 298 | - Fixed: Select target action now uses the correct line number when used from the editor. (#24 PR by markiewb) 299 | 300 | ## 1.5.6 301 | 302 | - Ability to open a specific commit from the VCS log tool window. 303 | 304 | ## 1.5.5 305 | 306 | - Run plugin process on a background thread to prevent UI freezes. 307 | 308 | ## 1.5.4 309 | 310 | - Fixed issue with branch encoding for `GitHub` and `GitLab` hosts. 311 | - Fixed issue with origins using `git` protocol. 312 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome**. 4 | 5 | ## Pull Requests 6 | 7 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 8 | 9 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 10 | 11 | - **Create feature branches** - Don't ask us to pull from your master branch. 12 | 13 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 14 | 15 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make 16 | - multiple intermediate commits while developing, please [squash them] before submitting. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Ben Gibson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitLink 2 | 3 | ![Build](https://github.com/ben-gibson/GitLink/workflows/Build/badge.svg) 4 | [![Version](https://img.shields.io/jetbrains/plugin/v/8183-gitlink.svg)](https://plugins.jetbrains.com/plugin/8183-gitlink) 5 | [![Rating](https://img.shields.io/jetbrains/plugin/r/stars/8183-gitlink.svg)](https://plugins.jetbrains.com/plugin/8183-gitlink) 6 | [![Downloads](https://img.shields.io/jetbrains/plugin/d/8183-gitlink.svg)](https://plugins.jetbrains.com/plugin/8183-gitlink) 7 | [![Donations](https://opencollective.com/gitlink/tiers/badge.svg?label=sponsor&color=brightgreen)](https://opencollective.com/gitlink) 8 | [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua) 9 | 10 | 11 | 12 | A [Jetbrains plugin](https://plugins.jetbrains.com/plugin/8183-gitlink) providing shortcuts to open or copy a file, directory or commit in `GitHub`, `Bitbucket`, 13 | `GitLab`, `Gitee` `Gitea`, `Gogs`, `Azure`, `sourcehut`, and `Gerrit`. Custom hosts can also be configured using the URL template syntax. 14 | 15 | 16 | 17 |
18 | 19 | Menu Example 20 | 21 |
22 |
23 | 24 | Settings Example 25 | 26 |
27 | 28 | ## Usage 29 | 30 | Install the plugin and configure your remote host if it hasn't been auto-detected already: 31 | 32 | Preferences → Tools → GitLink 33 | 34 | Make sure you have registered your projects root under the version control preferences: 35 | 36 | Preferences → Version Control (see unregistered roots) 37 | 38 | To open the current file in the default browser: 39 | 40 | View → Open in (your selected host) or 41 | Select in... → Browser (GitLink) 42 | 43 | Additional shortcuts are available including from the editor gutter and Git log window. 44 | 45 | A URL can be generated in one of the following ways: 46 | 47 | * File at a commit 48 | * File at a branch 49 | * Commit 50 | 51 | By default, when generating a URL to a file, the latest commit hash is used, creating a reference to a fixed version of 52 | the file's content. If the latest commit has not been pushed to the remote, the current branch is used instead. 53 | While this avoids generating a URL to a 404, it does mean the linked contents can change over time. 54 | 55 | ## Installation 56 | 57 | - Using IDE built-in plugin system: 58 | 59 | Settings/Preferences > Plugins > Marketplace > Search for "GitLink" > 60 | Install Plugin 61 | 62 | - Manually: 63 | 64 | Download the [latest release](https://github.com/ben-gibson/GitLink/releases/latest) and install it manually using 65 | Settings/Preferences > Plugins > ⚙️ > Install plugin from disk... 66 | 67 | ## Support 68 | 69 | * Star the repository 70 | * [Rate the plugin](https://plugins.jetbrains.com/plugin/8183-gitlink) 71 | * [Share the plugin](https://plugins.jetbrains.com/plugin/8183-gitlink) 72 | * [Become a Backer or Sponsor](https://opencollective.com/gitlink) 73 | 74 | 75 | ## Change log 76 | 77 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 78 | 79 | ## Contributing 80 | 81 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 82 | 83 | ## License 84 | 85 | Please see [LICENSE](LICENSE) for details. 86 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.changelog.Changelog 2 | import org.jetbrains.changelog.markdownToHTML 3 | import org.jetbrains.intellij.platform.gradle.TestFrameworkType 4 | 5 | plugins { 6 | id("java") // Java support 7 | alias(libs.plugins.kotlin) // Kotlin support 8 | alias(libs.plugins.intelliJPlatform) // IntelliJ Platform Gradle Plugin 9 | alias(libs.plugins.changelog) // Gradle Changelog Plugin 10 | alias(libs.plugins.qodana) // Gradle Qodana Plugin 11 | alias(libs.plugins.kover) // Gradle Kover Plugin 12 | } 13 | 14 | group = providers.gradleProperty("pluginGroup").get() 15 | version = providers.gradleProperty("pluginVersion").get() 16 | 17 | // Set the JVM language level used to build the project. 18 | kotlin { 19 | jvmToolchain(17) 20 | } 21 | 22 | // Configure project's dependencies 23 | repositories { 24 | mavenCentral() 25 | 26 | // IntelliJ Platform Gradle Plugin Repositories Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-repositories-extension.html 27 | intellijPlatform { 28 | defaultRepositories() 29 | } 30 | } 31 | 32 | // Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog 33 | dependencies { 34 | testImplementation(libs.junit) 35 | testImplementation(libs.junitJupiter) 36 | testImplementation(libs.junitJupiterParams) 37 | testImplementation(libs.mockk) 38 | 39 | implementation(files("libs/url-0.0.10.jar")) 40 | 41 | // IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html 42 | intellijPlatform { 43 | create(providers.gradleProperty("platformType"), providers.gradleProperty("platformVersion")) 44 | 45 | // Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins. 46 | bundledPlugins(providers.gradleProperty("platformBundledPlugins").map { it.split(',') }) 47 | 48 | // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file for plugin from JetBrains Marketplace. 49 | plugins(providers.gradleProperty("platformPlugins").map { it.split(',') }) 50 | 51 | instrumentationTools() 52 | pluginVerifier() 53 | zipSigner() 54 | testFramework(TestFrameworkType.Platform) 55 | } 56 | } 57 | 58 | // Configure IntelliJ Platform Gradle Plugin - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-extension.html 59 | intellijPlatform { 60 | pluginConfiguration { 61 | version = providers.gradleProperty("pluginVersion") 62 | 63 | // Extract the section from README.md and provide for the plugin's manifest 64 | description = providers.fileContents(layout.projectDirectory.file("README.md")).asText.map { 65 | val start = "" 66 | val end = "" 67 | 68 | with(it.lines()) { 69 | if (!containsAll(listOf(start, end))) { 70 | throw GradleException("Plugin description section not found in README.md:\n$start ... $end") 71 | } 72 | subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML) 73 | } 74 | } 75 | 76 | val changelog = project.changelog // local variable for configuration cache compatibility 77 | // Get the latest available change notes from the changelog file 78 | changeNotes = providers.gradleProperty("pluginVersion").map { pluginVersion -> 79 | with(changelog) { 80 | renderItem( 81 | (getOrNull(pluginVersion) ?: getUnreleased()) 82 | .withHeader(false) 83 | .withEmptySections(false), 84 | Changelog.OutputType.HTML, 85 | ) 86 | } 87 | } 88 | 89 | ideaVersion { 90 | sinceBuild = providers.gradleProperty("pluginSinceBuild") 91 | untilBuild = providers.gradleProperty("pluginUntilBuild") 92 | } 93 | } 94 | 95 | signing { 96 | certificateChain = providers.environmentVariable("CERTIFICATE_CHAIN") 97 | privateKey = providers.environmentVariable("PRIVATE_KEY") 98 | password = providers.environmentVariable("PRIVATE_KEY_PASSWORD") 99 | } 100 | 101 | publishing { 102 | token = providers.environmentVariable("PUBLISH_TOKEN") 103 | // The pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 104 | // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: 105 | // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel 106 | channels = providers.gradleProperty("pluginVersion").map { listOf(it.substringAfter('-', "").substringBefore('.').ifEmpty { "default" }) } 107 | } 108 | 109 | pluginVerification { 110 | ides { 111 | recommended() 112 | } 113 | } 114 | } 115 | 116 | // Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin 117 | changelog { 118 | groups.empty() 119 | repositoryUrl = providers.gradleProperty("pluginRepositoryUrl") 120 | } 121 | 122 | // Configure Gradle Kover Plugin - read more: https://github.com/Kotlin/kotlinx-kover#configuration 123 | kover { 124 | reports { 125 | total { 126 | xml { 127 | onCheck = true 128 | } 129 | } 130 | } 131 | } 132 | 133 | tasks { 134 | wrapper { 135 | gradleVersion = providers.gradleProperty("gradleVersion").get() 136 | } 137 | 138 | publishPlugin { 139 | dependsOn(patchChangelog) 140 | } 141 | } 142 | 143 | intellijPlatformTesting { 144 | runIde { 145 | register("runIdeForUiTests") { 146 | task { 147 | jvmArgumentProviders += CommandLineArgumentProvider { 148 | listOf( 149 | "-Drobot-server.port=8082", 150 | "-Dide.mac.message.dialogs.as.sheets=false", 151 | "-Djb.privacy.policy.text=", 152 | "-Djb.consents.confirmation.enabled=false", 153 | ) 154 | } 155 | } 156 | 157 | plugins { 158 | robotServerPlugin() 159 | } 160 | } 161 | } 162 | } 163 | 164 | tasks.test { 165 | useJUnitPlatform() 166 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories 2 | # -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 3 | 4 | pluginGroup = uk.co.ben_gibson.git.link 5 | pluginName = GitLink 6 | # SemVer format -> https://semver.org 7 | pluginVersion = 4.5.2 8 | 9 | # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 10 | # for insight into build numbers and IntelliJ Platform versions. 11 | pluginSinceBuild = 233 12 | #pluginUntilBuild = 222.* 13 | 14 | # IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties 15 | platformType = IC 16 | platformVersion = 2023.3.7 17 | 18 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 19 | # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 20 | platformPlugins = 21 | 22 | # Example: platformBundledPlugins = com.intellij.java 23 | platformBundledPlugins = Git4Idea 24 | 25 | # Gradle Releases -> https://github.com/gradle/gradle/releases 26 | gradleVersion = 8.9 27 | 28 | # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib 29 | kotlin.stdlib.default.dependency = false 30 | 31 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 32 | org.gradle.configuration-cache = true 33 | 34 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 35 | org.gradle.caching = true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | # libraries 3 | junit = "4.13.2" 4 | junitJupiter = "5.8.2" # Version for junit-jupiter 5 | junitJupiterParams = "5.9.0" # Version for junit-jupiter-params 6 | mockk = "1.13.5" # Version for MockK 7 | 8 | # plugins 9 | changelog = "2.2.1" 10 | intelliJPlatform = "2.0.1" 11 | kotlin = "1.9.25" 12 | kover = "0.8.3" 13 | qodana = "2024.1.9" 14 | 15 | [libraries] 16 | junit = { group = "junit", name = "junit", version.ref = "junit" } 17 | junitJupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junitJupiter" } 18 | junitJupiterParams = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junitJupiterParams" } 19 | mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } 20 | 21 | [plugins] 22 | changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } 23 | intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" } 24 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 25 | kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } 26 | qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-gibson/GitLink/9e4584b5845c50487e844fc054c3f28dd81d5886/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Aug 27 21:05:28 BST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /libs/url-0.0.10.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-gibson/GitLink/9e4584b5845c50487e844fc054c3f28dd81d5886/libs/url-0.0.10.jar -------------------------------------------------------------------------------- /menu-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-gibson/GitLink/9e4584b5845c50487e844fc054c3f28dd81d5886/menu-example.png -------------------------------------------------------------------------------- /qodana.yml: -------------------------------------------------------------------------------- 1 | # Qodana configuration: 2 | # https://www.jetbrains.com/help/qodana/qodana-yaml.html 3 | 4 | version: 1.0 5 | linter: jetbrains/qodana-jvm-community:latest 6 | projectJDK: "17" 7 | profile: 8 | name: qodana.recommended 9 | exclude: 10 | - name: All 11 | paths: 12 | - .qodana -------------------------------------------------------------------------------- /settings-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ben-gibson/GitLink/9e4584b5845c50487e844fc054c3f28dd81d5886/settings-example.png -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "GitLink" 2 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/Context.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link 2 | 3 | import com.intellij.openapi.vfs.VirtualFile 4 | import uk.co.ben_gibson.git.link.git.Commit 5 | import uk.co.ben_gibson.git.link.ui.LineSelection 6 | 7 | sealed class Context(val file: VirtualFile) 8 | 9 | class ContextCommit(file: VirtualFile, val commit: Commit) : Context(file) 10 | class ContextFileAtCommit(file: VirtualFile, val commit: Commit, val lineSelection: LineSelection? = null) : Context(file) 11 | class ContextCurrentFile(file: VirtualFile, val lineSelection: LineSelection? = null) : Context(file) 12 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/GitLinkBundle.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link 2 | 3 | import com.intellij.DynamicBundle 4 | import com.intellij.ide.browsers.BrowserLauncher 5 | import com.intellij.ide.plugins.PluginManagerCore 6 | import com.intellij.openapi.extensions.PluginId 7 | import com.intellij.openapi.options.ShowSettingsUtil 8 | import com.intellij.openapi.project.Project 9 | import org.jetbrains.annotations.NonNls 10 | import org.jetbrains.annotations.PropertyKey 11 | 12 | @NonNls 13 | private const val BUNDLE = "messages.MyBundle" 14 | 15 | object GitLinkBundle : DynamicBundle(BUNDLE) { 16 | const val URL_BUG_REPORT = "https://github.com/ben-gibson/GitLink/issues" 17 | 18 | @Suppress("SpreadOperator") 19 | @JvmStatic 20 | fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = 21 | getMessage(key, *params) 22 | 23 | @Suppress("SpreadOperator", "unused") 24 | @JvmStatic 25 | fun messagePointer(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = 26 | getLazyMessage(key, *params) 27 | 28 | fun openPluginSettings(project: Project) { 29 | ShowSettingsUtil.getInstance().showSettingsDialog(project, message("settings.general.group.title")) 30 | } 31 | 32 | fun openRepository() { 33 | BrowserLauncher.instance.open("https://github.com/ben-gibson/GitLink") 34 | } 35 | 36 | fun plugin() = PluginManagerCore.getPlugin(PluginId.getId("uk.co.ben-gibson.remote.repository.mapper")) 37 | } 38 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/GitLinkRunner.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link 2 | 3 | import com.intellij.ide.BrowserUtil 4 | import com.intellij.openapi.components.service 5 | import com.intellij.openapi.progress.runBackgroundableTask 6 | import com.intellij.openapi.project.Project 7 | import uk.co.ben_gibson.git.link.GitLinkBundle.message 8 | import uk.co.ben_gibson.git.link.pipeline.Pipeline 9 | import uk.co.ben_gibson.git.link.ui.notification.Notification 10 | import uk.co.ben_gibson.git.link.ui.notification.sendNotification 11 | import uk.co.ben_gibson.url.URL 12 | import java.awt.Toolkit 13 | import java.awt.datatransfer.StringSelection 14 | 15 | fun openInBrowser(project: Project, context: Context) { 16 | processGitLink(project, context) { BrowserUtil.browse(it.toString()) } 17 | } 18 | 19 | fun copyToClipBoard(project: Project, context: Context, asMarkdown: Boolean = false) { 20 | processGitLink(project, context) { 21 | val url = if (asMarkdown) { 22 | val label = when(context) { 23 | is ContextCommit -> context.commit.shortHash 24 | is ContextCurrentFile -> context.file.name 25 | is ContextFileAtCommit -> context.file.name 26 | } 27 | 28 | "[${label}](${it})" 29 | } else { 30 | it.toString() 31 | } 32 | 33 | Toolkit.getDefaultToolkit().systemClipboard.setContents( 34 | StringSelection(url), 35 | null 36 | ) 37 | 38 | sendNotification(Notification.linkCopied(it), project) 39 | } 40 | } 41 | 42 | private fun processGitLink(project: Project, context: Context, handle: (URL) -> Unit) { 43 | runBackgroundableTask(message("name"), project, false) { 44 | val pipeline = project.service() 45 | 46 | pipeline.accept(context)?.let(handle) 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/extension/ListExtensions.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.extension 2 | 3 | fun List.replaceAt(index: Int, value: T): List { 4 | val mutable = this.toMutableList() 5 | 6 | mutable[index] = value 7 | 8 | return mutable.toList() 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/git/Commit.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.git 2 | 3 | data class Commit(private val hash: String) { 4 | 5 | val shortHash get() = hash.substring(0, 6) 6 | 7 | override fun toString() = hash 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/git/File.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.git 2 | 3 | import com.intellij.openapi.vfs.VirtualFile 4 | import git4idea.repo.GitRepository 5 | 6 | /** 7 | * Represents a repository file, where the path is relative to the repository it lives in. 8 | */ 9 | data class File(val name: String, val isDirectory: Boolean, val path: String, val isRoot: Boolean) { 10 | companion object { 11 | fun forRepository(file: VirtualFile, repository: GitRepository): File { 12 | return File( 13 | file.name, 14 | file.isDirectory, 15 | file.path.substring(repository.root.path.length).replace(file.name, "").trim('/'), 16 | file.path == repository.root.path 17 | ) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/git/RemoteExtensions.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.git 2 | 3 | import git4idea.GitLocalBranch 4 | import git4idea.commands.Git 5 | import git4idea.commands.GitCommand 6 | import git4idea.commands.GitCommandResult 7 | import git4idea.commands.GitLineHandler 8 | import git4idea.repo.GitRemote 9 | import git4idea.repo.GitRepository 10 | import uk.co.ben_gibson.url.Host 11 | import uk.co.ben_gibson.url.URL 12 | 13 | val GitRemote.domain : Host? get() = httpUrl?.host 14 | 15 | val GitRemote.httpUrl : URL? get() { 16 | var url = firstUrl ?: return null 17 | 18 | url = url.trim() 19 | 20 | // Azure supports a .git suffix on the repo name, so don't remove it. 21 | if (!url.contains("dev.azure")) { 22 | url = url.removeSuffix(".git") 23 | } 24 | 25 | // Do not try to remove the port if the URL uses the SSH protocol in the SCP syntax e.g. 26 | // 'git@github.com:foo.git' as it does not support port definitions. Attempting to remove the port 27 | // will result in an invalid URL when the repository name is made up of digits. 28 | // See https://github.com/ben-gibson/GitLink/issues/94 29 | if (!url.startsWith("git@")) { 30 | url = url.replace(":\\d{1,5}".toRegex(), "") // remove the port 31 | } 32 | 33 | if (!url.startsWith("http")) { 34 | url = url 35 | .replace("git@", "") 36 | .replace(Regex("^[^:]+://"), "") 37 | .replace(":", "/") 38 | 39 | url = "http://".plus(url) 40 | } 41 | 42 | return URL.fromString(url) 43 | } 44 | 45 | fun GitRemote.contains(repository: GitRepository, branch: GitLocalBranch): Boolean { 46 | val result = Git.getInstance().lsRemote( 47 | repository.project, 48 | repository.root, 49 | this, 50 | this.firstUrl, 51 | branch.fullName, 52 | "--heads" 53 | ) 54 | 55 | if (result.success()) { 56 | return result.output.size == 1 57 | } 58 | 59 | return branch.findTrackedBranch(repository) != null 60 | } 61 | 62 | fun GitRemote.contains(repository: GitRepository, commit: Commit): Boolean { 63 | val command = GitLineHandler(repository.project, repository.root, GitCommand.BRANCH) 64 | 65 | command.addParameters("-r", "--contains", commit.toString()) 66 | 67 | val result: GitCommandResult = Git.getInstance().runCommand(command) 68 | 69 | if (!result.success()) { 70 | return false 71 | } 72 | 73 | return result.output.find{ it.trim().startsWith(name) } != null 74 | } 75 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/git/RepositoryExtensions.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.git 2 | 3 | import git4idea.GitUtil 4 | import git4idea.repo.GitRepository 5 | 6 | fun GitRepository.locateRemote(name: String) = GitUtil.findRemoteByName(this, name) 7 | fun GitRepository.currentCommit() = currentRevision?.let { Commit(it) } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/listener/ApplicationStartupListener.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.listener 2 | 3 | import com.intellij.openapi.components.service 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.startup.ProjectActivity 6 | import uk.co.ben_gibson.git.link.GitLinkBundle 7 | import uk.co.ben_gibson.git.link.platform.PlatformDetector 8 | import uk.co.ben_gibson.git.link.settings.ApplicationSettings 9 | import uk.co.ben_gibson.git.link.settings.ProjectSettings 10 | import uk.co.ben_gibson.git.link.ui.notification.Notification 11 | import uk.co.ben_gibson.git.link.ui.notification.sendNotification 12 | 13 | class ApplicationStartupListener : ProjectActivity { 14 | override suspend fun execute(project: Project) { 15 | showVersionNotification(project) 16 | detectPlatform(project) 17 | } 18 | 19 | private fun showVersionNotification(project: Project) { 20 | val settings = service() 21 | val version = GitLinkBundle.plugin()?.version 22 | 23 | if (version == settings.lastVersion) { 24 | return 25 | } 26 | 27 | settings.lastVersion = version 28 | sendNotification(Notification.welcome(version ?: "Unknown"), project) 29 | } 30 | 31 | private fun detectPlatform(project: Project) { 32 | val projectSettings = project.service() 33 | 34 | if (projectSettings.host != null) { 35 | return 36 | } 37 | 38 | project.service().detect { platform -> 39 | if (platform == null) { 40 | sendNotification(Notification.couldNotDetectPlatform(project), project) 41 | return@detect 42 | } 43 | 44 | sendNotification(Notification.platformAutoDetected(platform, project), project) 45 | 46 | projectSettings.host = platform.id.toString() 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/pipeline/Pass.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.pipeline 2 | 3 | import com.intellij.openapi.project.Project 4 | import git4idea.repo.GitRemote 5 | import git4idea.repo.GitRepository 6 | import uk.co.ben_gibson.git.link.Context 7 | import uk.co.ben_gibson.git.link.platform.Platform 8 | 9 | class Pass(val project: Project, val context: Context) { 10 | var platform: Platform? = null 11 | var repository: GitRepository? = null 12 | var remote: GitRemote? = null 13 | 14 | fun platformOrThrow() = platform ?: throw IllegalStateException("Platform not set") 15 | fun repositoryOrThrow() = repository ?: throw IllegalStateException("Repository not set") 16 | fun remoteOrThrow() = remote ?: throw IllegalStateException("Remote not set") 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/pipeline/Pipeline.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.pipeline 2 | 3 | import com.intellij.openapi.components.Service 4 | import com.intellij.openapi.components.service 5 | import com.intellij.openapi.project.Project 6 | import uk.co.ben_gibson.git.link.Context 7 | import uk.co.ben_gibson.git.link.pipeline.middleware.* 8 | import uk.co.ben_gibson.git.link.pipeline.middleware.Timer 9 | import uk.co.ben_gibson.url.URL 10 | import java.util.* 11 | import kotlin.collections.Set 12 | 13 | @Service(Service.Level.PROJECT) 14 | class Pipeline(private val project: Project) { 15 | private val middlewares: Set = setOf( 16 | service(), 17 | service(), 18 | service(), 19 | service(), 20 | service(), 21 | service(), 22 | ) 23 | 24 | fun accept(context: Context) : URL? { 25 | if (middlewares.isEmpty()) { 26 | throw IllegalStateException("No middleware registered") 27 | } 28 | 29 | val queue = PriorityQueue(middlewares) 30 | 31 | return next(queue, Pass(project, context)) 32 | } 33 | 34 | private fun next(queue: PriorityQueue, pass: Pass) : URL? { 35 | val middleware = queue.remove() 36 | 37 | return middleware(pass) { 38 | return@middleware next(queue, pass) 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/pipeline/middleware/ForceHttps.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.pipeline.middleware 2 | 3 | import com.intellij.openapi.components.Service 4 | import com.intellij.openapi.components.service 5 | import uk.co.ben_gibson.git.link.pipeline.Pass 6 | import uk.co.ben_gibson.git.link.settings.ProjectSettings 7 | import uk.co.ben_gibson.url.URL 8 | 9 | @Service 10 | class ForceHttps : Middleware { 11 | override val priority = 30 12 | 13 | override fun invoke(pass: Pass, next: () -> URL?) : URL? { 14 | val url = next() ?: return null 15 | 16 | val settings = pass.project.service() 17 | 18 | if (settings.forceHttps) { 19 | return url.toHttps() 20 | } 21 | 22 | return url 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/pipeline/middleware/GenerateUrl.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.pipeline.middleware 2 | 3 | import com.intellij.openapi.components.Service 4 | import com.intellij.openapi.components.service 5 | import git4idea.repo.GitRemote 6 | import git4idea.repo.GitRepository 7 | import uk.co.ben_gibson.git.link.* 8 | import uk.co.ben_gibson.git.link.git.* 9 | import uk.co.ben_gibson.git.link.pipeline.Pass 10 | import uk.co.ben_gibson.git.link.settings.ProjectSettings 11 | import uk.co.ben_gibson.git.link.url.UrlOptions 12 | import uk.co.ben_gibson.git.link.url.factory.UrlFactoryLocator 13 | import uk.co.ben_gibson.url.URL 14 | 15 | // Must be the last middleware in the pipeline! 16 | @Service 17 | class GenerateUrl : Middleware { 18 | override val priority = 50 19 | 20 | override fun invoke(pass: Pass, next: () -> URL?) : URL? { 21 | // We can't reach this point unless the platform, repository, and remote have been resolved 22 | val baseUrl = pass.remoteOrThrow().httpUrl ?: return null 23 | 24 | val platform = pass.platformOrThrow() 25 | 26 | val options = createUrlOptions(pass, platform.pullRequestWorkflowSupported) 27 | 28 | return service().locate(platform).createUrl(baseUrl, options) 29 | } 30 | 31 | private fun createUrlOptions(pass: Pass, pullRequestWorkflowSupported: Boolean): UrlOptions { 32 | val remote = pass.remoteOrThrow() 33 | val repository = pass.repositoryOrThrow() 34 | val context = pass.context 35 | val settings = pass.project.service() 36 | 37 | val repositoryFile = File.forRepository(context.file, repository) 38 | 39 | return when (context) { 40 | is ContextFileAtCommit -> UrlOptions.UrlOptionsFileAtCommit( 41 | repositoryFile, 42 | repository.currentBranch?.name ?: settings.fallbackBranch, 43 | context.commit, 44 | context.lineSelection 45 | ) 46 | is ContextCommit -> UrlOptions.UrlOptionsCommit( 47 | context.commit, 48 | repository.currentBranch?.name ?: settings.fallbackBranch 49 | ) 50 | is ContextCurrentFile -> { 51 | val commit = resolveCommit(repository, remote, settings, pullRequestWorkflowSupported) 52 | 53 | if (commit != null) { 54 | UrlOptions.UrlOptionsFileAtCommit( 55 | repositoryFile, 56 | repository.currentBranch?.name ?: settings.fallbackBranch, 57 | commit, 58 | context.lineSelection 59 | ) 60 | } else { 61 | UrlOptions.UrlOptionsFileAtBranch( 62 | repositoryFile, 63 | resolveBranch(repository, remote, settings), 64 | context.lineSelection 65 | ) 66 | } 67 | } 68 | } 69 | } 70 | 71 | private fun resolveBranch(repository: GitRepository, remote: GitRemote, settings: ProjectSettings): String { 72 | val branch = repository.currentBranch ?: return settings.fallbackBranch 73 | 74 | if (!settings.shouldCheckRemote) { 75 | return branch.name 76 | } 77 | 78 | return if (remote.contains(repository, branch)) branch.name else settings.fallbackBranch 79 | } 80 | 81 | private fun resolveCommit(repository: GitRepository, remote: GitRemote, settings: ProjectSettings, pullRequestWorkflowSupported: Boolean): Commit? { 82 | val commit = repository.currentCommit() ?: return null 83 | 84 | if (!pullRequestWorkflowSupported || !settings.shouldCheckRemote) { 85 | return commit 86 | } 87 | 88 | return if (remote.contains(repository, commit)) commit else null 89 | } 90 | } 91 | 92 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/pipeline/middleware/Middleware.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.pipeline.middleware 2 | 3 | import uk.co.ben_gibson.git.link.pipeline.Pass 4 | import uk.co.ben_gibson.url.URL 5 | 6 | interface Middleware : Comparable { 7 | val priority: Int 8 | 9 | operator fun invoke(pass: Pass, next: () -> URL?) : URL? 10 | 11 | override fun compareTo(other: Middleware): Int { 12 | return priority - other.priority 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/pipeline/middleware/RecordHit.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.pipeline.middleware 2 | 3 | import com.intellij.openapi.components.Service 4 | import com.intellij.openapi.components.service 5 | import uk.co.ben_gibson.git.link.pipeline.Pass 6 | import uk.co.ben_gibson.git.link.settings.ApplicationSettings 7 | import uk.co.ben_gibson.url.URL 8 | 9 | @Service 10 | class RecordHit : Middleware { 11 | override val priority = 20 12 | 13 | override fun invoke(pass: Pass, next: () -> URL?) : URL? { 14 | val url = next() ?: return null 15 | 16 | service().recordHit() 17 | 18 | return url 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/pipeline/middleware/ResolveContext.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.pipeline.middleware 2 | 3 | import com.intellij.openapi.components.Service 4 | import com.intellij.openapi.components.service 5 | import git4idea.repo.GitRemote 6 | import git4idea.repo.GitRepository 7 | import git4idea.repo.GitRepositoryManager 8 | import uk.co.ben_gibson.git.link.git.* 9 | import uk.co.ben_gibson.git.link.pipeline.Pass 10 | import uk.co.ben_gibson.git.link.platform.Platform 11 | import uk.co.ben_gibson.git.link.platform.PlatformLocator 12 | import uk.co.ben_gibson.git.link.settings.ProjectSettings 13 | import uk.co.ben_gibson.git.link.ui.notification.Notification 14 | import uk.co.ben_gibson.git.link.ui.notification.sendNotification 15 | import uk.co.ben_gibson.url.URL 16 | 17 | @Service 18 | class ResolveContext : Middleware { 19 | override val priority = 5 20 | 21 | override fun invoke(pass: Pass, next: () -> URL?): URL? { 22 | val repository = locateRepository(pass) ?: return null 23 | val remote = locateRemote(pass, repository) ?: return null 24 | val platform = localePlatform(pass) ?: return null 25 | 26 | pass.platform = platform 27 | pass.repository = repository 28 | pass.remote = remote 29 | 30 | return next() 31 | } 32 | 33 | private fun localePlatform(pass: Pass): Platform? { 34 | val platform = pass.project.service().locate() 35 | 36 | if (platform == null) { 37 | sendNotification(Notification.hostNotSet(pass.project), pass.project) 38 | } 39 | 40 | return platform 41 | } 42 | 43 | private fun locateRepository(pass: Pass): GitRepository? { 44 | val repository = GitRepositoryManager.getInstance(pass.project).getRepositoryForFile(pass.context.file) 45 | 46 | repository ?: sendNotification(Notification.repositoryNotFound(), pass.project) 47 | 48 | return repository 49 | } 50 | 51 | private fun locateRemote(pass: Pass, repository: GitRepository): GitRemote? { 52 | val remote = repository.locateRemote(pass.project.service().remote) 53 | 54 | remote ?: sendNotification(Notification.remoteNotFound(), pass.project) 55 | 56 | return remote 57 | } 58 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/pipeline/middleware/SendSupportNotification.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.pipeline.middleware 2 | 3 | import com.intellij.openapi.components.Service 4 | import com.intellij.openapi.components.service 5 | import uk.co.ben_gibson.git.link.pipeline.Pass 6 | import uk.co.ben_gibson.git.link.settings.ApplicationSettings 7 | import uk.co.ben_gibson.git.link.ui.notification.Notification 8 | import uk.co.ben_gibson.git.link.ui.notification.sendNotification 9 | import uk.co.ben_gibson.url.URL 10 | 11 | @Service 12 | class SendSupportNotification : Middleware { 13 | override val priority = 10 14 | 15 | override fun invoke(pass: Pass, next: () -> URL?) : URL? { 16 | val url = next() 17 | 18 | val settings = service() 19 | 20 | if (settings.requestSupport && (settings.hits == 5 || settings.hits % 50 == 0)) { 21 | sendNotification(Notification.star()) 22 | } 23 | 24 | return url 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/pipeline/middleware/Timer.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.pipeline.middleware 2 | 3 | import com.intellij.openapi.components.Service 4 | import com.intellij.openapi.components.service 5 | import uk.co.ben_gibson.git.link.pipeline.Pass 6 | import uk.co.ben_gibson.git.link.settings.ProjectSettings 7 | import uk.co.ben_gibson.git.link.ui.notification.Notification 8 | import uk.co.ben_gibson.git.link.ui.notification.sendNotification 9 | import uk.co.ben_gibson.url.URL 10 | 11 | @Service 12 | class Timer : Middleware { 13 | override val priority = 40 14 | 15 | override fun invoke(pass: Pass, next: () -> URL?) : URL? { 16 | val settings = pass.project.service() 17 | 18 | if (!settings.shouldCheckRemote || !settings.showPerformanceTip) { 19 | return next() 20 | } 21 | 22 | val startTime = System.currentTimeMillis() 23 | 24 | val url = next() 25 | 26 | val total = System.currentTimeMillis() - startTime 27 | 28 | if (total > 1000) { 29 | sendNotification(Notification.performanceTips(pass.project), pass.project) 30 | } 31 | 32 | return url 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/platform/Platform.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.platform 2 | 3 | import com.intellij.icons.AllIcons 4 | import uk.co.ben_gibson.git.link.GitLinkBundle.message 5 | import uk.co.ben_gibson.git.link.ui.Icons 6 | import java.util.UUID 7 | import javax.swing.Icon 8 | import uk.co.ben_gibson.url.Host 9 | import java.util.regex.Pattern 10 | 11 | sealed class Platform(val id: UUID, val name: String, val icon: Icon, val domains: Set = setOf(), val domainPattern: Pattern? = null, val pullRequestWorkflowSupported: Boolean = true) { 12 | override fun equals(other: Any?): Boolean { 13 | if (this === other) return true 14 | if (javaClass != other?.javaClass) return false 15 | 16 | other as Platform 17 | 18 | return id == other.id 19 | } 20 | 21 | override fun hashCode(): Int { 22 | return id.hashCode() 23 | } 24 | } 25 | 26 | class GitHub : Platform( 27 | UUID.fromString("72037fcc-cb9c-4c22-960a-ffe73fd5e229"), 28 | message("platform.github.name"), 29 | AllIcons.Vcs.Vendors.Github, 30 | setOf(Host("github.com")) 31 | ) 32 | 33 | class GitLab : Platform( 34 | UUID.fromString("16abfb4c-4717-4d04-a8f1-7a40fcac9b07"), 35 | message("platform.gitlab.name"), 36 | Icons.GITLAB, 37 | setOf(Host("gitlab.com")) 38 | ) 39 | 40 | class BitbucketCloud : Platform( 41 | UUID.fromString("00c4b661-b32a-4d36-90d7-88db786edadd"), 42 | message("platform.bitbucket.cloud.name"), 43 | Icons.BITBUCKET, 44 | setOf(Host("bitbucket.org")) 45 | ) 46 | 47 | class BitbucketServer : Platform( 48 | UUID.fromString("dba5941d-821c-49b3-83b0-75deb9462acb"), 49 | message("platform.bitbucket.server.name"), 50 | Icons.BITBUCKET, 51 | setOf(), 52 | Pattern.compile(".*bitbucket.*", Pattern.CASE_INSENSITIVE) 53 | ) 54 | 55 | class Gogs : Platform( 56 | UUID.fromString("fd2d9cfc-1eef-4b1b-80bd-b02def58576c"), 57 | message("platform.gogs.name"), 58 | Icons.GOGS, 59 | setOf(Host("gogs.io")) 60 | ) 61 | 62 | class Srht : Platform( 63 | UUID.fromString("aa358239-5c11-4b53-8b97-723181c48f4f"), 64 | message("platform.srht.name"), 65 | Icons.SOURCEHUT, 66 | setOf(Host("git.sr.ht")) 67 | ) 68 | 69 | class Gitea : Platform( 70 | UUID.fromString("e0f86390-1091-4871-8aeb-f534fbc99cf0"), 71 | message("platform.gitea.name"), 72 | Icons.GITEA, 73 | setOf(Host("gitea.io")), 74 | ) 75 | 76 | class Gitee : Platform( 77 | UUID.fromString("5c2d3009-7e3e-4c9f-9c0f-d76bc7e926bf"), 78 | message("platform.gitee.name"), 79 | Icons.GITEE, 80 | setOf(Host("gitee.com")) 81 | ) 82 | 83 | class Azure : Platform( 84 | UUID.fromString("83008277-73fa-4faa-b9b2-0a60fecb030e"), 85 | message("platform.azure.name"), 86 | Icons.AZURE, 87 | setOf(Host("dev.azure.com")), 88 | Pattern.compile(".*azure.*", Pattern.CASE_INSENSITIVE) 89 | ) 90 | 91 | class Chromium : Platform( 92 | UUID.fromString("97bf87bc-99ef-4e1f-8d37-7948a2082df4"), 93 | message("platform.chromium.name"), 94 | Icons.CHROMIUM, 95 | setOf(Host("googlesource.com")) 96 | ) 97 | 98 | class Gerrit : Platform( 99 | UUID.fromString("a28d7024-f390-40d1-8554-db65a9120a38"), 100 | message("platform.gerrit.name"), 101 | Icons.GERRIT, 102 | setOf(), 103 | Pattern.compile(".*gerrit.*", Pattern.CASE_INSENSITIVE), 104 | false 105 | ) 106 | 107 | class Custom(id: UUID, name: String, icon: Icon, domains: Set = setOf()) : Platform(id, name, icon, domains) 108 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/platform/PlatformDetector.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.platform 2 | 3 | import com.intellij.dvcs.repo.VcsRepositoryManager 4 | import com.intellij.dvcs.repo.VcsRepositoryMappingListener 5 | import com.intellij.openapi.components.Service 6 | import com.intellij.openapi.components.service 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.openapi.project.guessProjectDir 9 | import com.intellij.openapi.vfs.VirtualFile 10 | import git4idea.repo.GitRepository 11 | import git4idea.repo.GitRepositoryManager 12 | import uk.co.ben_gibson.git.link.git.domain 13 | import uk.co.ben_gibson.git.link.git.locateRemote 14 | import uk.co.ben_gibson.git.link.settings.ApplicationSettings 15 | import uk.co.ben_gibson.git.link.settings.ProjectSettings 16 | 17 | @Service(Service.Level.PROJECT) 18 | class PlatformDetector(val project: Project) { 19 | fun detect(consumer: (Platform?) -> Unit) { 20 | val projectDirectory = project.guessProjectDir() 21 | if (projectDirectory == null) { 22 | consumer.invoke(null) 23 | return 24 | } 25 | 26 | getRepositoryForFile(projectDirectory) { repository -> getPlatformForRepository(repository).let(consumer) } 27 | } 28 | 29 | private fun getPlatformForRepository(repository: GitRepository): Platform? { 30 | val settings = project.service() 31 | 32 | val remote = repository.locateRemote(settings.remote) ?: return null 33 | 34 | val applicationSettings = service() 35 | val platforms = service() 36 | 37 | return remote.domain?.let { 38 | platforms.getByDomain(it) ?: applicationSettings.findPlatformIdByCustomDomain(it)?.let { id -> platforms.getById(id) } 39 | } 40 | } 41 | 42 | private fun getRepositoryForFile(projectDirectory: VirtualFile, consumer: (GitRepository) -> Unit) { 43 | val gitRepositoryManager = GitRepositoryManager.getInstance(project) 44 | val repository = gitRepositoryManager.getRepositoryForFile(projectDirectory) 45 | if (repository != null) { 46 | consumer.invoke(repository) 47 | return 48 | } 49 | 50 | val busConnection = project.messageBus.connect() 51 | busConnection 52 | .subscribe( 53 | VcsRepositoryManager.VCS_REPOSITORY_MAPPING_UPDATED, 54 | VcsRepositoryMappingListener { 55 | busConnection.disconnect() 56 | gitRepositoryManager.getRepositoryForFile(projectDirectory)?.let(consumer) 57 | } 58 | ) 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/platform/PlatformLocator.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.platform 2 | 3 | import com.intellij.openapi.components.Service 4 | import com.intellij.openapi.components.service 5 | import com.intellij.openapi.project.Project 6 | import uk.co.ben_gibson.git.link.settings.ProjectSettings 7 | 8 | @Service(Service.Level.PROJECT) 9 | class PlatformLocator(val project: Project) { 10 | fun locate() : Platform? { 11 | val settings = project.service() 12 | 13 | val platformId = settings.host?: return null 14 | 15 | val platforms = service() 16 | 17 | return platforms.getById(platformId) 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/platform/PlatformRepository.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.platform 2 | 3 | import com.intellij.openapi.components.Service 4 | import com.intellij.openapi.components.service 5 | import uk.co.ben_gibson.git.link.settings.ApplicationSettings 6 | import uk.co.ben_gibson.git.link.ui.Icons 7 | import uk.co.ben_gibson.url.Host 8 | import java.util.UUID 9 | 10 | private val EXISTING_PLATFORMS = setOf( 11 | GitHub(), 12 | GitLab(), 13 | BitbucketCloud(), 14 | BitbucketServer(), 15 | Gitee(), 16 | Gitea(), 17 | Gogs(), 18 | Srht(), 19 | Azure(), 20 | Chromium(), 21 | Gerrit() 22 | ) 23 | 24 | @Service 25 | class PlatformRepository { 26 | fun getById(id: String) = getById(UUID.fromString(id)) 27 | fun getById(id: UUID) = load().firstOrNull { it.id == id } 28 | fun getByDomain(domain: Host): Platform? { 29 | val platforms = load() 30 | return platforms.firstOrNull { it.domains.contains(domain) } ?: platforms.firstOrNull { it.domainPattern?.matcher(domain.toString())?.matches() == true } 31 | } 32 | fun getAll() = load() 33 | 34 | private fun load(): Set { 35 | val settings = service() 36 | 37 | val customPlatforms: List = settings.customHosts.map { 38 | Custom( 39 | UUID.fromString(it.id), 40 | it.displayName, 41 | Icons.GIT, 42 | setOf(Host(it.baseUrl)) 43 | ) 44 | } 45 | 46 | return EXISTING_PLATFORMS.plus(customPlatforms) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/settings/ApplicationSettings.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.settings 2 | 3 | import com.intellij.openapi.components.PersistentStateComponent 4 | import com.intellij.openapi.components.State 5 | import com.intellij.openapi.components.Storage 6 | import com.intellij.util.xmlb.XmlSerializerUtil 7 | import com.intellij.util.xmlb.annotations.Tag 8 | import uk.co.ben_gibson.url.Host 9 | import java.util.UUID 10 | 11 | /** 12 | * Supports storing the application settings in a persistent way. 13 | * The [State] and [Storage] annotations define the name of the data and the file name where 14 | * these persistent application settings are stored. 15 | */ 16 | @State(name = "uk.co.ben_gibson.git.link.SettingsState", storages = [Storage("GitLink.xml")]) 17 | class ApplicationSettings : PersistentStateComponent { 18 | private var listeners: List = listOf() 19 | 20 | var customHosts: List = listOf() 21 | set(value) { 22 | field = value 23 | notifyListeners() 24 | } 25 | 26 | var customHostDomains: Map> = mapOf() 27 | 28 | var lastVersion: String? = null 29 | var hits = 0 30 | var requestSupport = true 31 | 32 | override fun getState() = this 33 | 34 | override fun loadState(state: ApplicationSettings) { 35 | XmlSerializerUtil.copyBean(state, this) 36 | } 37 | 38 | @Tag("custom_hosts") 39 | data class CustomHostSettings( 40 | var id: String = UUID.randomUUID().toString(), 41 | var displayName: String = "", 42 | var baseUrl: String = "", 43 | var fileAtBranchTemplate: String = "", 44 | var fileAtCommitTemplate: String = "", 45 | var commitTemplate: String = "" 46 | ) 47 | 48 | fun findPlatformIdByCustomDomain(domain: Host) = customHostDomains 49 | .entries 50 | .firstOrNull { entry -> entry.value.contains(domain.toString()) } 51 | ?.key 52 | 53 | fun registerListener(listener: ChangeListener) { 54 | listeners = listeners.plus(listener) 55 | } 56 | 57 | fun recordHit() { 58 | hits++ 59 | } 60 | 61 | private fun notifyListeners() { 62 | listeners.forEach(ChangeListener::onChange) 63 | } 64 | 65 | interface ChangeListener { 66 | fun onChange() 67 | } 68 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/settings/ProjectSettings.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.settings 2 | 3 | import com.intellij.openapi.components.PersistentStateComponent 4 | import com.intellij.openapi.components.Service 5 | import com.intellij.openapi.components.State 6 | import com.intellij.openapi.components.Storage 7 | import com.intellij.util.xmlb.XmlSerializerUtil 8 | 9 | /** 10 | * Supports storing the application settings in a persistent way. 11 | * The [State] and [Storage] annotations define the name of the data and the file name where 12 | * these persistent application settings are stored. 13 | */ 14 | @Service(Service.Level.PROJECT) 15 | @State(name = "uk.co.ben_gibson.git.link.SettingsState", storages = [Storage("GitLink.xml")]) 16 | class ProjectSettings : PersistentStateComponent { 17 | var host: String? = null 18 | var fallbackBranch = "main" 19 | var remote = "origin" 20 | private var checkCommitOnRemote = true 21 | var shouldCheckRemote 22 | get() = checkCommitOnRemote 23 | set(value) { 24 | checkCommitOnRemote = value 25 | } 26 | 27 | var forceHttps = true 28 | var showPerformanceTip = true 29 | 30 | override fun getState() = this 31 | 32 | override fun loadState(state: ProjectSettings) { 33 | XmlSerializerUtil.copyBean(state, this) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/EditorExtensions.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui 2 | 3 | import com.intellij.openapi.editor.Editor 4 | 5 | val Editor.lineSelection: LineSelection 6 | get() { 7 | val caretStates = caretModel.caretsAndSelections 8 | 9 | if (caretStates.size < 1) { 10 | return LineSelection(caretModel.logicalPosition.line + 1) 11 | } 12 | 13 | val caretState = caretStates[0] 14 | 15 | val start = caretState.selectionStart 16 | val end = caretState.selectionEnd 17 | 18 | if (start == null || end == null) { 19 | return LineSelection(caretModel.logicalPosition.line + 1) 20 | } 21 | 22 | return LineSelection(start.line + 1, end.line + 1) 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/Icons.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui 2 | 3 | import com.intellij.openapi.util.IconLoader 4 | 5 | object Icons { 6 | val GITLAB = IconLoader.getIcon("/icons/gitlab.svg", javaClass) 7 | val BITBUCKET = IconLoader.getIcon("/icons/bitbucket.svg", javaClass) 8 | val GOGS = IconLoader.getIcon("/icons/gogs.svg", javaClass) 9 | val SOURCEHUT = IconLoader.getIcon("/icons/sourcehut.svg", javaClass) 10 | val AZURE = IconLoader.getIcon("/icons/azure.svg", javaClass) 11 | val GITEA = IconLoader.getIcon("/icons/gitea.svg", javaClass) 12 | val GIT = IconLoader.getIcon("/icons/git.svg", javaClass) 13 | val GIT_LINK = IconLoader.getIcon("/icons/gitlink.svg", javaClass) 14 | val CHROMIUM = IconLoader.getIcon("/icons/chromium.svg", javaClass) 15 | val GITEE = IconLoader.getIcon("/icons/gitee.svg", javaClass) 16 | val GERRIT = IconLoader.getIcon("/icons/gerrit.svg", javaClass) 17 | } 18 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/LineSelection.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui 2 | 3 | data class LineSelection(val start: Int, val end: Int) { 4 | constructor(start: Int) : this(start, start) 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/Action.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions 2 | 3 | import com.intellij.openapi.actionSystem.ActionUpdateThread 4 | import com.intellij.openapi.actionSystem.AnActionEvent 5 | import com.intellij.openapi.components.service 6 | import com.intellij.openapi.project.DumbAwareAction 7 | import com.intellij.openapi.project.Project 8 | import uk.co.ben_gibson.git.link.Context 9 | import uk.co.ben_gibson.git.link.GitLinkBundle 10 | import uk.co.ben_gibson.git.link.copyToClipBoard 11 | import uk.co.ben_gibson.git.link.openInBrowser 12 | import uk.co.ben_gibson.git.link.platform.PlatformLocator 13 | 14 | abstract class Action(private val type: Type): DumbAwareAction() { 15 | 16 | enum class Type(val key: String) { 17 | BROWSER("browser"), 18 | COPY("copy"), 19 | COPY_MARKDOWN("copy-markdown") 20 | } 21 | 22 | abstract fun buildContext(project: Project, event: AnActionEvent) : Context? 23 | 24 | override fun actionPerformed(event: AnActionEvent) { 25 | val project = event.project ?: return 26 | 27 | val context = buildContext(project, event) ?: return 28 | 29 | when(type) { 30 | Type.BROWSER -> openInBrowser(project, context) 31 | Type.COPY -> copyToClipBoard(project, context) 32 | Type.COPY_MARKDOWN -> copyToClipBoard(project, context, true) 33 | } 34 | } 35 | 36 | open fun shouldBeEnabled(event: AnActionEvent) = true 37 | 38 | override fun update(event: AnActionEvent) { 39 | super.update(event) 40 | 41 | event.presentation.isEnabled = event.project != null 42 | 43 | val project = event.project ?: return 44 | 45 | val host = project.service().locate() 46 | 47 | if (host == null) { 48 | event.presentation.isEnabledAndVisible = false 49 | return 50 | } 51 | 52 | event.presentation.isEnabled = shouldBeEnabled(event) 53 | 54 | event.presentation.icon = host.icon 55 | event.presentation.text = GitLinkBundle.message("actions.${type.key}.title", host.name) 56 | } 57 | 58 | override fun getActionUpdateThread(): ActionUpdateThread { 59 | return ActionUpdateThread.BGT 60 | } 61 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/annotation/CommitBrowserAction.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.annotation 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.vcs.actions.ShowAnnotateOperationsPopup 6 | import git4idea.annotate.GitFileAnnotation 7 | import uk.co.ben_gibson.git.link.ui.actions.Action 8 | import uk.co.ben_gibson.git.link.Context 9 | import uk.co.ben_gibson.git.link.ContextCommit 10 | import uk.co.ben_gibson.git.link.git.Commit 11 | 12 | class CommitBrowserAction(private val annotation: GitFileAnnotation): Action(Type.BROWSER) { 13 | 14 | override fun buildContext(project: Project, event: AnActionEvent): Context? { 15 | val lineNumber = ShowAnnotateOperationsPopup.getAnnotationLineNumber(event.dataContext) 16 | 17 | val revision = annotation.getLineRevisionNumber(lineNumber) ?: return null 18 | 19 | return ContextCommit(annotation.file, Commit(revision.toString())) 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/annotation/CommitCopyAction.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.annotation 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.vcs.actions.ShowAnnotateOperationsPopup 6 | import git4idea.annotate.GitFileAnnotation 7 | import uk.co.ben_gibson.git.link.Context 8 | import uk.co.ben_gibson.git.link.ContextCommit 9 | import uk.co.ben_gibson.git.link.git.Commit 10 | import uk.co.ben_gibson.git.link.ui.actions.Action 11 | 12 | class CommitCopyAction(private val annotation: GitFileAnnotation): Action(Type.COPY) { 13 | 14 | override fun buildContext(project: Project, event: AnActionEvent): Context? { 15 | val lineNumber = ShowAnnotateOperationsPopup.getAnnotationLineNumber(event.dataContext) 16 | 17 | val revision = annotation.getLineRevisionNumber(lineNumber) ?: return null 18 | 19 | return ContextCommit(annotation.file, Commit(revision.toString())) 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/annotation/CommitMarkdownAction.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.annotation 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.vcs.actions.ShowAnnotateOperationsPopup 6 | import git4idea.annotate.GitFileAnnotation 7 | import uk.co.ben_gibson.git.link.Context 8 | import uk.co.ben_gibson.git.link.ContextCommit 9 | import uk.co.ben_gibson.git.link.git.Commit 10 | import uk.co.ben_gibson.git.link.ui.actions.Action 11 | 12 | class CommitMarkdownAction(private val annotation: GitFileAnnotation): Action(Type.COPY_MARKDOWN) { 13 | override fun buildContext(project: Project, event: AnActionEvent): Context? { 14 | val lineNumber = ShowAnnotateOperationsPopup.getAnnotationLineNumber(event.dataContext) 15 | 16 | val revision = annotation.getLineRevisionNumber(lineNumber) ?: return null 17 | 18 | return ContextCommit(annotation.file, Commit(revision.toString())) 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/annotation/FileBrowserAction.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.annotation 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.vcs.actions.ShowAnnotateOperationsPopup 6 | import git4idea.annotate.GitFileAnnotation 7 | import uk.co.ben_gibson.git.link.Context 8 | import uk.co.ben_gibson.git.link.ContextFileAtCommit 9 | import uk.co.ben_gibson.git.link.git.Commit 10 | import uk.co.ben_gibson.git.link.ui.actions.Action 11 | 12 | class FileBrowserAction(private val annotation: GitFileAnnotation): Action(Type.BROWSER) { 13 | 14 | override fun buildContext(project: Project, event: AnActionEvent): Context? { 15 | val lineNumber = ShowAnnotateOperationsPopup.getAnnotationLineNumber(event.dataContext) 16 | 17 | val revision = annotation.getLineRevisionNumber(lineNumber) ?: return null 18 | 19 | return ContextFileAtCommit(annotation.file, Commit(revision.toString())) 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/annotation/FileCopyAction.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.annotation 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.vcs.actions.ShowAnnotateOperationsPopup 6 | import git4idea.annotate.GitFileAnnotation 7 | import uk.co.ben_gibson.git.link.Context 8 | import uk.co.ben_gibson.git.link.ContextFileAtCommit 9 | import uk.co.ben_gibson.git.link.git.Commit 10 | import uk.co.ben_gibson.git.link.ui.actions.Action 11 | 12 | class FileCopyAction(private val annotation: GitFileAnnotation): Action(Type.COPY) { 13 | 14 | override fun buildContext(project: Project, event: AnActionEvent): Context? { 15 | val lineNumber = ShowAnnotateOperationsPopup.getAnnotationLineNumber(event.dataContext) 16 | 17 | val revision = annotation.getLineRevisionNumber(lineNumber) ?: return null 18 | 19 | return ContextFileAtCommit(annotation.file, Commit(revision.toString())) 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/annotation/FileMarkdownAction.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.annotation 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.vcs.actions.ShowAnnotateOperationsPopup 6 | import git4idea.annotate.GitFileAnnotation 7 | import uk.co.ben_gibson.git.link.Context 8 | import uk.co.ben_gibson.git.link.ContextFileAtCommit 9 | import uk.co.ben_gibson.git.link.git.Commit 10 | import uk.co.ben_gibson.git.link.ui.actions.Action 11 | 12 | class FileMarkdownAction(private val annotation: GitFileAnnotation): Action(Type.COPY_MARKDOWN) { 13 | 14 | override fun buildContext(project: Project, event: AnActionEvent): Context? { 15 | val lineNumber = ShowAnnotateOperationsPopup.getAnnotationLineNumber(event.dataContext) 16 | 17 | val revision = annotation.getLineRevisionNumber(lineNumber) ?: return null 18 | 19 | return ContextFileAtCommit(annotation.file, Commit(revision.toString())) 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/gutter/BrowserAction.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.gutter 2 | 3 | class BrowserAction : GutterAction(Type.BROWSER) -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/gutter/CopyAction.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.gutter 2 | 3 | class CopyAction : GutterAction(Type.COPY) -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/gutter/GutterAction.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.gutter 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.actionSystem.CommonDataKeys 5 | import com.intellij.openapi.editor.ex.EditorGutterComponentEx 6 | import com.intellij.openapi.project.Project 7 | import uk.co.ben_gibson.git.link.Context 8 | import uk.co.ben_gibson.git.link.ContextCurrentFile 9 | import uk.co.ben_gibson.git.link.ui.LineSelection 10 | import uk.co.ben_gibson.git.link.ui.actions.Action 11 | 12 | abstract class GutterAction(type: Type) : Action(type) { 13 | override fun buildContext(project: Project, event: AnActionEvent): Context? { 14 | val file = event.getData(CommonDataKeys.VIRTUAL_FILE) ?: return null 15 | val line = event.getData(EditorGutterComponentEx.LOGICAL_LINE_AT_CURSOR) 16 | 17 | return ContextCurrentFile(file, line?.plus(1)?.let { LineSelection(it, it) }) 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/gutter/MarkdownAction.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.gutter 2 | 3 | class MarkdownAction : GutterAction(Type.COPY_MARKDOWN) -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/menu/BrowserAction.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.menu 2 | 3 | class BrowserAction : MenuAction(Type.BROWSER) -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/menu/CopyAction.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.menu 2 | 3 | class CopyAction : MenuAction(Type.COPY) -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/menu/MarkdownAction.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.menu 2 | 3 | class MarkdownAction : MenuAction(Type.COPY_MARKDOWN) -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/menu/MenuAction.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.menu 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.actionSystem.CommonDataKeys 5 | import com.intellij.openapi.editor.Editor 6 | import com.intellij.openapi.fileEditor.FileEditorManager 7 | import com.intellij.openapi.project.Project 8 | import uk.co.ben_gibson.git.link.* 9 | import uk.co.ben_gibson.git.link.ui.actions.Action 10 | import uk.co.ben_gibson.git.link.ui.lineSelection 11 | 12 | abstract class MenuAction(type: Type) : Action(type) { 13 | override fun buildContext(project: Project, event: AnActionEvent): Context? { 14 | val file = event.getData(CommonDataKeys.VIRTUAL_FILE) ?: return null 15 | 16 | val editor: Editor? = FileEditorManager.getInstance(project).selectedTextEditor 17 | val lineSelection = editor?.lineSelection 18 | 19 | return ContextCurrentFile(file, lineSelection) 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/vcslog/BrowserAction.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.vcslog 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.vcs.log.VcsLogDataKeys.VCS_LOG_COMMIT_SELECTION 6 | import uk.co.ben_gibson.git.link.Context 7 | import uk.co.ben_gibson.git.link.ContextCommit 8 | import uk.co.ben_gibson.git.link.git.Commit 9 | import uk.co.ben_gibson.git.link.ui.actions.Action 10 | 11 | class BrowserAction: Action(Type.BROWSER) { 12 | override fun buildContext(project: Project, event: AnActionEvent) : Context? { 13 | val vcsCommit = event.getData(VCS_LOG_COMMIT_SELECTION)?.cachedFullDetails?.get(0) ?: return null 14 | 15 | return ContextCommit(vcsCommit.root, Commit(vcsCommit.id.asString())) 16 | } 17 | 18 | override fun shouldBeEnabled(event: AnActionEvent): Boolean { 19 | return event.getData(VCS_LOG_COMMIT_SELECTION)?.cachedFullDetails?.size == 1 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/vcslog/CopyAction.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.vcslog 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.vcs.log.VcsLogDataKeys.VCS_LOG_COMMIT_SELECTION 6 | import uk.co.ben_gibson.git.link.Context 7 | import uk.co.ben_gibson.git.link.ContextCommit 8 | import uk.co.ben_gibson.git.link.git.Commit 9 | import uk.co.ben_gibson.git.link.ui.actions.Action 10 | 11 | class CopyAction: Action(Type.COPY) { 12 | override fun buildContext(project: Project, event: AnActionEvent): Context? { 13 | val vcsCommit = event.getData(VCS_LOG_COMMIT_SELECTION)?.cachedFullDetails?.get(0) ?: return null 14 | 15 | return ContextCommit(vcsCommit.root, Commit(vcsCommit.id.toString())) 16 | } 17 | 18 | override fun shouldBeEnabled(event: AnActionEvent): Boolean { 19 | return event.getData(VCS_LOG_COMMIT_SELECTION)?.cachedFullDetails?.size == 1 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/actions/vcslog/MarkdownAction.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.vcslog 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.vcs.log.VcsLogDataKeys.VCS_LOG_COMMIT_SELECTION 6 | import uk.co.ben_gibson.git.link.Context 7 | import uk.co.ben_gibson.git.link.ContextCommit 8 | import uk.co.ben_gibson.git.link.git.Commit 9 | import uk.co.ben_gibson.git.link.ui.actions.Action 10 | 11 | class MarkdownAction: Action(Type.COPY_MARKDOWN) { 12 | override fun buildContext(project: Project, event: AnActionEvent): Context? { 13 | val vcsCommit = event.getData(VCS_LOG_COMMIT_SELECTION)?.cachedFullDetails?.get(0) ?: return null 14 | 15 | return ContextCommit(vcsCommit.root, Commit(vcsCommit.id.toString())) 16 | } 17 | 18 | override fun shouldBeEnabled(event: AnActionEvent): Boolean { 19 | return event.getData(VCS_LOG_COMMIT_SELECTION)?.cachedFullDetails?.size == 1 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/components/PlatformCellRenderer.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.components 2 | 3 | import com.intellij.ui.SimpleListCellRenderer 4 | import uk.co.ben_gibson.git.link.platform.Platform 5 | import javax.swing.JList 6 | 7 | class PlatformCellRenderer : SimpleListCellRenderer() { 8 | override fun customize(list: JList, value: Platform?, index: Int, selected: Boolean, hasFocus: Boolean) { 9 | text = value?.name ?: "" 10 | icon = value?.icon 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/components/SubstitutionReferenceTable.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.components 2 | 3 | import com.intellij.ui.table.TableView 4 | import com.intellij.util.ui.ColumnInfo 5 | import com.intellij.util.ui.ListTableModel 6 | 7 | private val references = listOf( 8 | SubstitutionReference( 9 | "{commit}", 10 | "The complete hash of the commit.", 11 | "05fc48765f69d52aa229fc5edc3842ab3d9ff517" 12 | ), 13 | SubstitutionReference( 14 | "{commit:short}", 15 | "The first 6 characters of the commit hash.", 16 | "05fc48" 17 | ), 18 | SubstitutionReference( 19 | "{branch}", 20 | "The branch.", 21 | "master" 22 | ), 23 | SubstitutionReference( 24 | "{file:name}", 25 | "The selected file name.", 26 | "NotificationDispatcher.java" 27 | ), 28 | SubstitutionReference( 29 | "{file:path}", 30 | "The selected file path.", 31 | "src/uk/co/ben_gibson/git/link" 32 | ), 33 | SubstitutionReference( 34 | "{line:start}", 35 | "The line selection start.", 36 | "10" 37 | ), 38 | SubstitutionReference( 39 | "{line:end}", 40 | "The line selection end.", 41 | "30" 42 | ), 43 | SubstitutionReference( 44 | "{remote:url}", 45 | "The full remote url.", 46 | "https://example.com/ben-gibson/super-project" 47 | ), 48 | SubstitutionReference( 49 | "{remote:url:host}", 50 | "The remote url host.", 51 | "example.com" 52 | ), 53 | SubstitutionReference( 54 | "{remote:url:path}", 55 | "The remote url path.", 56 | "ben-gibson/super-project" 57 | ), 58 | SubstitutionReference( 59 | "{remote:url:path:n}", 60 | "A specific part of the remote url path starting at 0.", 61 | "super-project" 62 | ), 63 | ) 64 | 65 | class SubstitutionReferenceTable: TableView( 66 | ListTableModel( 67 | arrayOf( 68 | SubstitutionColumnInfo("Substitution") { it.substitution }, 69 | SubstitutionColumnInfo("Description") { it.description }, 70 | SubstitutionColumnInfo("Example") { it.example } 71 | ), 72 | references 73 | ) 74 | ) 75 | 76 | private class SubstitutionColumnInfo(name: String, val formatter: (SubstitutionReference) -> String) : 77 | ColumnInfo(name) { 78 | override fun valueOf(item: SubstitutionReference): String { 79 | return formatter(item) 80 | } 81 | } 82 | 83 | data class SubstitutionReference(val substitution: String, val description: String, val example: String) -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/extensions/AnnotationGutterActionProvider.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.extensions 2 | 3 | import com.intellij.openapi.actionSystem.ActionGroup 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.intellij.openapi.actionSystem.DefaultActionGroup 7 | import com.intellij.openapi.vcs.annotate.FileAnnotation 8 | import git4idea.annotate.GitFileAnnotation 9 | import uk.co.ben_gibson.git.link.ui.actions.annotation.* 10 | import com.intellij.openapi.vcs.annotate.AnnotationGutterActionProvider as IntellijAnnotationGutterActionProvider 11 | 12 | class AnnotationGutterActionProvider : IntellijAnnotationGutterActionProvider { 13 | override fun createAction(annotation: FileAnnotation): AnAction { 14 | return FileAndCommitGroup(annotation) 15 | } 16 | 17 | private class FileAndCommitGroup(annotation: FileAnnotation): ActionGroup("GitLink", true) { 18 | private val children: Array = when (annotation) { 19 | is GitFileAnnotation -> arrayOf( 20 | DefaultActionGroup( 21 | "File", 22 | listOf(FileBrowserAction(annotation), FileCopyAction(annotation), FileMarkdownAction(annotation)) 23 | ).apply { 24 | isPopup = true 25 | }, 26 | DefaultActionGroup( 27 | "Commit", 28 | listOf(CommitBrowserAction(annotation), CommitCopyAction(annotation), CommitMarkdownAction(annotation)) 29 | ).apply { 30 | isPopup = true 31 | } 32 | ) 33 | else -> arrayOf() 34 | } 35 | 36 | override fun getChildren(e: AnActionEvent?): Array = children 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/extensions/SelectInTarget.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.extensions 2 | 3 | import com.intellij.ide.SelectInContext 4 | import com.intellij.openapi.components.service 5 | import com.intellij.openapi.project.Project 6 | import uk.co.ben_gibson.git.link.ContextCurrentFile 7 | import uk.co.ben_gibson.git.link.platform.PlatformLocator 8 | import uk.co.ben_gibson.git.link.openInBrowser 9 | import com.intellij.ide.SelectInTarget as IntellijSelectInTarget 10 | 11 | class SelectInTarget(private val project: Project) : IntellijSelectInTarget { 12 | override fun canSelect(context: SelectInContext) = project.service().locate() != null 13 | 14 | override fun selectIn(context: SelectInContext, requestFocus: Boolean) { 15 | openInBrowser(context.project, ContextCurrentFile(context.virtualFile)) 16 | } 17 | 18 | override fun toString(): String { 19 | val platform = project.service().locate() 20 | 21 | return platform?.name ?: "Gitlink" 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/notification/Notification.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.notification 2 | 3 | import com.intellij.ide.browsers.BrowserLauncher 4 | import com.intellij.openapi.components.service 5 | import com.intellij.openapi.project.Project 6 | import uk.co.ben_gibson.git.link.GitLinkBundle 7 | import uk.co.ben_gibson.git.link.GitLinkBundle.openPluginSettings 8 | import uk.co.ben_gibson.git.link.GitLinkBundle.message 9 | import uk.co.ben_gibson.git.link.platform.Platform 10 | import uk.co.ben_gibson.git.link.settings.ApplicationSettings 11 | import uk.co.ben_gibson.git.link.settings.ProjectSettings 12 | import uk.co.ben_gibson.url.URL 13 | 14 | data class Notification( 15 | val title: String? = null, 16 | val message: String, 17 | val actions: Set = setOf(), 18 | val type: Type = Type.PERSISTENT 19 | ) { 20 | enum class Type { 21 | PERSISTENT, 22 | TRANSIENT 23 | } 24 | 25 | companion object { 26 | private val DEFAULT_TITLE = message("name") 27 | 28 | fun hostNotSet(project: Project) = Notification( 29 | DEFAULT_TITLE, 30 | message("notifications.platform-not-set"), 31 | actions = setOf(NotificationAction.settings(project)) 32 | ) 33 | 34 | fun repositoryNotFound() = Notification(DEFAULT_TITLE, message("notifications.repository-not-found")) 35 | 36 | fun remoteNotFound() = Notification(DEFAULT_TITLE, message("notifications.remote-not-found")) 37 | 38 | fun welcome(version: String) = Notification(message = message("notifications.welcome", version)) 39 | 40 | fun star() = Notification( 41 | message = """ 42 | Finding GitLink useful? Show your support 💖 and ⭐ the repository 🙏. 43 | """.trimIndent(), 44 | actions = setOf( 45 | NotificationAction.openRepository { 46 | service().requestSupport = false 47 | }, 48 | NotificationAction.doNotAskAgain { 49 | service().requestSupport = false 50 | } 51 | ) 52 | ) 53 | 54 | fun performanceTips(project: Project) = Notification( 55 | message = message("notifications.performance"), 56 | actions = setOf( 57 | NotificationAction.disableRemoteCheck(project), 58 | NotificationAction.doNotAskAgain { project.service().showPerformanceTip = false }, 59 | ) 60 | ) 61 | 62 | fun couldNotDetectPlatform(project: Project) = Notification( 63 | message = message("notifications.could-not-detect-platform"), 64 | actions = setOf(NotificationAction.settings(project, message("actions.configure-manually"))) 65 | ) 66 | 67 | fun platformAutoDetected(remotePlatform: Platform, project: Project) = Notification( 68 | message = message("notifications.platform-detected.message", remotePlatform.name), 69 | actions = setOf(NotificationAction.settings(project, message("notifications.platform-detected.action"))) 70 | ) 71 | 72 | fun linkCopied(link: URL) = Notification( 73 | DEFAULT_TITLE, 74 | message("notifications.copied-to-clipboard"), 75 | setOf(NotificationAction.openUrl(link)), 76 | Type.TRANSIENT, 77 | ) 78 | } 79 | } 80 | 81 | data class NotificationAction(val title: String, val run: (dismiss: () -> Unit) -> Unit) { 82 | companion object { 83 | fun settings(project: Project, title: String = message("title.settings")) = NotificationAction(title) { dismiss -> 84 | dismiss() 85 | openPluginSettings(project) 86 | } 87 | 88 | fun openRepository(onComplete: () -> Unit) = NotificationAction(message("actions.sure-take-me-there")) { dismiss -> 89 | GitLinkBundle.openRepository() 90 | dismiss() 91 | onComplete() 92 | } 93 | 94 | fun doNotAskAgain(onComplete: () -> Unit) = NotificationAction(message("actions.do-not-ask-again")) { dismiss -> 95 | dismiss() 96 | onComplete() 97 | } 98 | 99 | fun openUrl(url: URL, title: String = message("actions.take-me-there")) = NotificationAction(title) { dismiss -> 100 | dismiss() 101 | BrowserLauncher.instance.open(url.toString()) 102 | } 103 | 104 | fun disableRemoteCheck(project: Project) = NotificationAction(message("actions.disable")) { dismiss -> 105 | dismiss() 106 | project.service().shouldCheckRemote = false 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/notification/Notifier.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.notification 2 | 3 | import com.intellij.notification.NotificationGroupManager 4 | import com.intellij.notification.NotificationType 5 | import com.intellij.openapi.project.DumbAwareAction 6 | import com.intellij.openapi.project.Project 7 | import uk.co.ben_gibson.git.link.ui.Icons.GIT_LINK 8 | 9 | private const val IMPORTANT_GROUP_ID = "git.link.notification.important" 10 | private const val GENERAL_GROUP_ID = "git.link.notification.general" 11 | 12 | fun sendNotification(notification : Notification, project : Project? = null) { 13 | val groupId = when(notification.type) { 14 | Notification.Type.PERSISTENT -> IMPORTANT_GROUP_ID 15 | Notification.Type.TRANSIENT -> GENERAL_GROUP_ID 16 | } 17 | 18 | val notificationManager = NotificationGroupManager 19 | .getInstance() 20 | .getNotificationGroup(groupId) 21 | 22 | val intellijNotification = notificationManager.createNotification( 23 | notification.title ?: "", 24 | notification.message, 25 | NotificationType.INFORMATION 26 | ) 27 | 28 | intellijNotification.icon = GIT_LINK 29 | 30 | notification.actions.forEach { action -> 31 | intellijNotification.addAction(DumbAwareAction.create(action.title) { 32 | action.run { 33 | intellijNotification.expire() 34 | } 35 | }) 36 | } 37 | 38 | intellijNotification.notify(project) 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/settings/CustomPlatformSettingsConfigurable.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.settings 2 | 3 | import com.intellij.openapi.components.service 4 | import com.intellij.openapi.options.BoundConfigurable 5 | import com.intellij.openapi.ui.DialogWrapper 6 | import com.intellij.ui.ToolbarDecorator 7 | import com.intellij.ui.dsl.builder.Align 8 | import com.intellij.ui.dsl.builder.bindText 9 | import com.intellij.ui.dsl.builder.panel 10 | import com.intellij.ui.table.TableView 11 | import com.intellij.util.ui.ColumnInfo 12 | import com.intellij.util.ui.ListTableModel 13 | import uk.co.ben_gibson.git.link.GitLinkBundle 14 | import uk.co.ben_gibson.git.link.settings.ApplicationSettings 15 | import uk.co.ben_gibson.git.link.settings.ApplicationSettings.CustomHostSettings 16 | import javax.swing.ListSelectionModel.SINGLE_SELECTION 17 | import uk.co.ben_gibson.git.link.GitLinkBundle.message 18 | import uk.co.ben_gibson.git.link.extension.replaceAt 19 | import uk.co.ben_gibson.git.link.ui.components.SubstitutionReferenceTable 20 | import uk.co.ben_gibson.git.link.ui.validation.* 21 | 22 | class CustomPlatformSettingsConfigurable : BoundConfigurable(message("settings.custom-platform.group.title")) { 23 | private var settings = service() 24 | private var customPlatforms = settings.customHosts 25 | private val tableModel = createTableModel() 26 | 27 | private val table = TableView(tableModel).apply { 28 | setShowColumns(true) 29 | setSelectionMode(SINGLE_SELECTION) 30 | emptyText.text = message("settings.custom-platform.table.empty") 31 | } 32 | 33 | private val tableContainer = ToolbarDecorator.createDecorator(table) 34 | .setAddAction { addCustomPlatform() } 35 | .setEditAction { editCustomPlatform() } 36 | .setRemoveAction { removeCustomPlatform() } 37 | .createPanel() 38 | 39 | override fun createPanel() = panel { 40 | row { 41 | cell(tableContainer) 42 | .align(Align.FILL) 43 | } 44 | row { 45 | browserLink(message("actions.report-bug.title"), GitLinkBundle.URL_BUG_REPORT) 46 | } 47 | } 48 | 49 | private fun createTableModel(): ListTableModel = ListTableModel( 50 | arrayOf( 51 | createColumn(message("settings.custom-platform.table.column.name")) { customPlatform -> customPlatform?.displayName }, 52 | createColumn(message("settings.custom-platform.table.column.domain")) { customPlatform -> customPlatform?.baseUrl }, 53 | ), 54 | customPlatforms 55 | ) 56 | 57 | private fun createColumn(name: String, formatter: (CustomHostSettings?) -> String?) : ColumnInfo { 58 | return object : ColumnInfo(name) { 59 | override fun valueOf(item: CustomHostSettings?): String? { 60 | return formatter(item) 61 | } 62 | } 63 | } 64 | 65 | private fun addCustomPlatform() { 66 | val dialog = CustomPlatformDialog() 67 | 68 | if (dialog.showAndGet()) { 69 | customPlatforms = customPlatforms.plus(dialog.platform) 70 | refreshTableModel() 71 | } 72 | } 73 | 74 | private fun removeCustomPlatform() { 75 | val row = table.selectedObject ?: return 76 | 77 | customPlatforms = customPlatforms.minus(row) 78 | refreshTableModel() 79 | } 80 | 81 | private fun editCustomPlatform() { 82 | val row = table.selectedObject ?: return 83 | 84 | val dialog = CustomPlatformDialog(row.copy()) 85 | 86 | if (dialog.showAndGet()) { 87 | customPlatforms = customPlatforms.replaceAt(table.selectedRow, dialog.platform) 88 | refreshTableModel() 89 | } 90 | } 91 | 92 | private fun refreshTableModel() { 93 | tableModel.items = customPlatforms 94 | } 95 | 96 | override fun reset() { 97 | super.reset() 98 | 99 | customPlatforms = settings.customHosts 100 | refreshTableModel() 101 | } 102 | 103 | override fun isModified() : Boolean { 104 | return super.isModified() || customPlatforms != settings.customHosts 105 | } 106 | 107 | override fun apply() { 108 | super.apply() 109 | 110 | settings.customHosts = customPlatforms 111 | } 112 | } 113 | 114 | private class CustomPlatformDialog(customPlatform: CustomHostSettings? = null) : DialogWrapper(false) { 115 | val platform = customPlatform ?: CustomHostSettings() 116 | private val substitutionReferenceTable = SubstitutionReferenceTable().apply { setShowColumns(true) } 117 | 118 | init { 119 | title = message("settings.custom-platform.add-dialog.title") 120 | setOKButtonText(customPlatform?.let { message("actions.update") } ?: message("actions.add")) 121 | setSize(700, 700) 122 | init() 123 | } 124 | 125 | override fun createCenterPanel() = panel { 126 | row(message("settings.custom-platform.add-dialog.field.name.label")) { 127 | textField() 128 | .bindText(platform::displayName) 129 | .focused() 130 | .validationOnApply { notBlank(it.text) ?: alphaNumeric(it.text) ?: length(it.text, 3, 15) } 131 | .comment(message("settings.custom-platform.add-dialog.field.name.comment")) 132 | } 133 | row(message("settings.custom-platform.add-dialog.field.domain.label")) { 134 | textField() 135 | .bindText(platform::baseUrl) 136 | .validationOnApply { notBlank(it.text) ?: domain(it.text) } 137 | .comment(message("settings.custom-platform.add-dialog.field.domain.comment")) 138 | } 139 | row(message("settings.custom-platform.add-dialog.field.file-at-branch-template.label")) { 140 | textField() 141 | .bindText(platform::fileAtBranchTemplate) 142 | .validationOnApply { notBlank(it.text) ?: fileAtBranchTemplate(it.text) } 143 | .comment(message("settings.custom-platform.add-dialog.field.file-at-branch-template.comment")) 144 | } 145 | row(message("settings.custom-platform.add-dialog.field.file-at-commit-template.label")) { 146 | textField() 147 | .bindText(platform::fileAtCommitTemplate) 148 | .validationOnApply { notBlank(it.text) ?: fileAtCommitTemplate(it.text) } 149 | .comment(message("settings.custom-platform.add-dialog.field.file-at-commit-template.comment")) 150 | } 151 | row(message("settings.custom-platform.add-dialog.field.commit-template.label")) { 152 | textField() 153 | .bindText(platform::commitTemplate) 154 | .validationOnApply { notBlank(it.text) ?: commitTemplate(it.text) } 155 | .comment(message("settings.custom-platform.add-dialog.field.commit-template.comment")) 156 | } 157 | row { 158 | scrollCell(substitutionReferenceTable) 159 | .align(Align.FILL) 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/settings/ProjectSettingsConfigurable.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.settings 2 | 3 | import com.intellij.openapi.components.service 4 | import com.intellij.openapi.options.BoundConfigurable 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.ui.CollectionComboBoxModel 7 | import com.intellij.ui.dsl.builder.* 8 | import uk.co.ben_gibson.git.link.GitLinkBundle 9 | import uk.co.ben_gibson.git.link.GitLinkBundle.message 10 | import uk.co.ben_gibson.git.link.platform.Platform 11 | import uk.co.ben_gibson.git.link.platform.PlatformRepository 12 | import uk.co.ben_gibson.git.link.settings.ProjectSettings 13 | import uk.co.ben_gibson.git.link.settings.ApplicationSettings 14 | import uk.co.ben_gibson.git.link.ui.components.PlatformCellRenderer 15 | import uk.co.ben_gibson.git.link.ui.validation.notBlank 16 | 17 | class ProjectSettingsConfigurable(project : Project) : BoundConfigurable(message("settings.general.group.title")), ApplicationSettings.ChangeListener { 18 | private val platforms = service() 19 | private val settings = project.service() 20 | private val platformComboBoxModel = CollectionComboBoxModel(platforms.getAll().toList()) 21 | private val initialPlatform = settings.host?.let { platforms.getById(it) } 22 | 23 | init { 24 | service().registerListener(this) 25 | } 26 | 27 | override fun createPanel() = panel { 28 | row(message("settings.general.field.platform.label")) { 29 | comboBox(platformComboBoxModel, PlatformCellRenderer()) 30 | .bindItem({ initialPlatform }, { settings.host = it?.id?.toString() }) 31 | .comment(message("settings.general.field.platform.help")) 32 | } 33 | row(message("settings.general.field.fallback-branch.label")) { 34 | textField() 35 | .bindText(settings::fallbackBranch) 36 | .comment(message("settings.general.field.fallback-branch.help")) 37 | .validationOnApply { notBlank(it.text) } 38 | } 39 | row(message("settings.general.field.remote.label")) { 40 | textField() 41 | .bindText(settings::remote) 42 | .validationOnApply { notBlank(it.text) } 43 | } 44 | group(message("settings.general.section.advanced.label")) { 45 | row { 46 | checkBox(message("settings.general.field.force-https.label")) 47 | .bindSelected(settings::forceHttps) 48 | } 49 | row { 50 | checkBox(message("settings.general.field.should-check-remote.label")) 51 | .comment(message("settings.general.field.check-commit-on-remote.help")) 52 | .bindSelected(settings::shouldCheckRemote) 53 | } 54 | } 55 | row { 56 | browserLink(message("actions.report-bug.title"), GitLinkBundle.URL_BUG_REPORT) 57 | } 58 | } 59 | 60 | override fun onChange() { 61 | val current = platformComboBoxModel.selectedItem as? Platform 62 | 63 | platformComboBoxModel.removeAll() 64 | platformComboBoxModel.add(platforms.getAll().toList()) 65 | platformComboBoxModel.selectedItem = current?.let { platforms.getById(it.id) } 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/ui/validation/ValidationExtensions.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.validation 2 | 3 | import com.intellij.openapi.ui.ValidationInfo 4 | import com.intellij.ui.layout.ValidationInfoBuilder 5 | import uk.co.ben_gibson.git.link.GitLinkBundle.message 6 | import uk.co.ben_gibson.git.link.git.Commit 7 | import uk.co.ben_gibson.git.link.git.File 8 | import uk.co.ben_gibson.git.link.ui.LineSelection 9 | import uk.co.ben_gibson.git.link.url.* 10 | import uk.co.ben_gibson.git.link.url.factory.TemplatedUrlFactory 11 | import uk.co.ben_gibson.git.link.url.template.UrlTemplates 12 | import uk.co.ben_gibson.url.Host 13 | import uk.co.ben_gibson.url.URL 14 | import java.lang.IllegalArgumentException 15 | 16 | fun ValidationInfoBuilder.notBlank(value: String): ValidationInfo? = if (value.isEmpty()) error(message("validation.required")) else null 17 | 18 | fun ValidationInfoBuilder.domain(value: String): ValidationInfo? { 19 | if (value.isEmpty()) { 20 | return null 21 | } 22 | 23 | return try { 24 | Host(value) 25 | null 26 | } catch (e: IllegalArgumentException) { 27 | error(message("validation.invalid-domain")) 28 | } 29 | } 30 | 31 | fun ValidationInfoBuilder.alphaNumeric(value: String): ValidationInfo? { 32 | if (value.isEmpty()) { 33 | return null 34 | } 35 | 36 | return if (!value.matches("[\\w\\s]+".toRegex())) error(message("validation.alpha-numeric")) else null 37 | } 38 | 39 | fun ValidationInfoBuilder.exists(value: String, existing: Collection): ValidationInfo? { 40 | if (value.isEmpty()) { 41 | return null 42 | } 43 | 44 | return if (existing.contains(value)) return error(message("validation.exists")) else null 45 | } 46 | 47 | fun ValidationInfoBuilder.length(value: String, min: Int, max: Int): ValidationInfo? { 48 | if (value.isEmpty()) { 49 | return null 50 | } 51 | 52 | return when { 53 | value.length < min -> error(message("validation.min-length", min)) 54 | value.length > max -> error(message("validation.max-length", max)) 55 | else -> null 56 | } 57 | } 58 | 59 | fun ValidationInfoBuilder.fileAtCommitTemplate(value: String): ValidationInfo? { 60 | if (value.isEmpty()) { 61 | return null 62 | } 63 | 64 | val options = UrlOptions.UrlOptionsFileAtCommit( 65 | File("foo.kt", false, "src/main", false), 66 | "main", 67 | Commit("734232a3c18f0625843bd161c3f5da272b9d53c1"), 68 | LineSelection(10, 20) 69 | ) 70 | 71 | return urlTemplate(options, fileAtCommit = value) 72 | } 73 | 74 | fun ValidationInfoBuilder.fileAtBranchTemplate(value: String): ValidationInfo? { 75 | if (value.isEmpty()) { 76 | return null 77 | } 78 | 79 | val options = UrlOptions.UrlOptionsFileAtBranch( 80 | File("foo.kt", false, "src/main", false), 81 | "master", 82 | LineSelection(10, 20) 83 | ) 84 | 85 | return urlTemplate(options, fileAtBranch = value) 86 | } 87 | 88 | fun ValidationInfoBuilder.commitTemplate(value: String): ValidationInfo? { 89 | if (value.isEmpty()) { 90 | return null 91 | } 92 | 93 | val options = UrlOptions.UrlOptionsCommit(Commit("734232a3c18f0625843bd161c3f5da272b9d53c1"), "main") 94 | 95 | return urlTemplate(options, commit = value) 96 | } 97 | 98 | private fun ValidationInfoBuilder.urlTemplate( 99 | options: UrlOptions, 100 | fileAtBranch: String = "", 101 | fileAtCommit: String = "", 102 | commit: String = "" 103 | ) : ValidationInfo? { 104 | val factory = TemplatedUrlFactory(UrlTemplates(fileAtBranch, fileAtCommit, commit)) 105 | 106 | return try { 107 | factory.createUrl(URL.fromString("https://example.com"), options) 108 | null 109 | } catch (e: Exception) { 110 | when(e) { 111 | is IllegalArgumentException -> error(message("validation.invalid-url-template")) 112 | else -> throw e 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/url/UrlOptions.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.url 2 | 3 | import uk.co.ben_gibson.git.link.git.Commit 4 | import uk.co.ben_gibson.git.link.git.File 5 | import uk.co.ben_gibson.git.link.ui.LineSelection 6 | 7 | sealed interface UrlOptions { 8 | class UrlOptionsCommit(val commit: Commit, val currentBranch: String) : UrlOptions 9 | class UrlOptionsFileAtCommit(val file: File, val currentBranch: String, val commit: Commit, val lineSelection: LineSelection? = null) : UrlOptions 10 | class UrlOptionsFileAtBranch(val file: File, val branch: String, val lineSelection: LineSelection? = null) : UrlOptions 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/url/factory/AzureUrlFactory.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.url.factory 2 | 3 | import com.intellij.openapi.components.Service 4 | import uk.co.ben_gibson.git.link.git.File 5 | import uk.co.ben_gibson.git.link.ui.LineSelection 6 | import uk.co.ben_gibson.git.link.url.* 7 | import uk.co.ben_gibson.url.* 8 | 9 | @Service 10 | class AzureUrlFactory: UrlFactory { 11 | override fun createUrl(baseUrl: URL, options: UrlOptions): URL { 12 | val normalisedBaseUrl = normaliseBaseUrl(baseUrl) 13 | 14 | return when (options) { 15 | is UrlOptions.UrlOptionsFileAtBranch -> createUrlToFileAtBranch(normalisedBaseUrl, options) 16 | is UrlOptions.UrlOptionsFileAtCommit -> createUrlToFileAtCommit(normalisedBaseUrl, options) 17 | is UrlOptions.UrlOptionsCommit -> createUrlToCommit(normalisedBaseUrl, options) 18 | } 19 | } 20 | 21 | private fun createUrlToFileAtCommit(baseUrl: URL, options: UrlOptions.UrlOptionsFileAtCommit) : URL { 22 | var queryString = QueryString.fromMap( 23 | mapOf( 24 | "version" to listOf("GC".plus(options.commit)), 25 | "path" to listOf(createFileParameter(options.file)) 26 | ) 27 | ) 28 | 29 | if (options.lineSelection != null) { 30 | queryString = addLineSelectionParameters(queryString, options.lineSelection) 31 | } 32 | 33 | return baseUrl.withQueryString(queryString) 34 | } 35 | 36 | private fun createUrlToFileAtBranch(baseUrl: URL, options: UrlOptions.UrlOptionsFileAtBranch) : URL { 37 | var queryString = QueryString.fromMap( 38 | mapOf( 39 | "version" to listOf("GB".plus(options.branch)), 40 | "path" to listOf(createFileParameter(options.file)) 41 | ) 42 | ) 43 | 44 | if (options.lineSelection != null) { 45 | queryString = addLineSelectionParameters(queryString, options.lineSelection) 46 | } 47 | 48 | return baseUrl.withQueryString(queryString) 49 | } 50 | 51 | private fun createFileParameter(file: File) : String { 52 | val fileName = if (file.isRoot) "" else file.name 53 | 54 | return file.path.plus("/").plus(fileName) 55 | } 56 | 57 | private fun createUrlToCommit(baseUrl: URL, options: UrlOptions.UrlOptionsCommit): URL { 58 | val path = requireNotNull(baseUrl.path) { "Unexpected error: repository path must be present in remote URL" } 59 | return baseUrl.withPath(path.with(Path.fromSegments(listOf("commit", options.commit.toString())))) 60 | } 61 | 62 | private fun addLineSelectionParameters(queryString: QueryString, lineSelection: LineSelection) : QueryString { 63 | return queryString 64 | .withParameter("line", lineSelection.start.toString()) 65 | .withParameter("lineEnd", (lineSelection.end + 1).toString()) 66 | 67 | // TODO: Pass column selection in. 68 | .withParameter("lineStartColumn", "1") 69 | .withParameter("lineEndColumn", "1") 70 | } 71 | 72 | private fun normaliseBaseUrl(baseUrl: URL): URL { 73 | // Convert ssh.dev.azure.com:v3/ben-gibson/test/test to dev.azure.com:ben-gibson/test/_git/test.git 74 | val basePathParts = baseUrl.path 75 | .toString() 76 | .removePrefix("v3/") 77 | .split("/") 78 | .toMutableList() 79 | 80 | // Azure expects this to be in the path between the project and repo name. It's already included when cloning the project using HTTPS, but not when cloning the project using SSH. 81 | if (!basePathParts.contains("_git")) { 82 | // urls might have an option company component, if that's the case we need to insert _git in between company/project and repository parts 83 | val indexToAddGit = if (basePathParts.size >= 3) 2 else 1 84 | basePathParts.add(indexToAddGit, "_git") 85 | } 86 | 87 | var normalisedBaseUrl = baseUrl.copy(path = Path(basePathParts.joinToString("/"))) 88 | 89 | if (baseUrl.host.toString().startsWith("ssh.")) { 90 | normalisedBaseUrl = normalisedBaseUrl.copy(host = Host(baseUrl.host.toString().removePrefix("ssh."))) 91 | } 92 | return normalisedBaseUrl 93 | } 94 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/url/factory/BitbucketServerUrlFactory.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.url.factory 2 | 3 | import com.intellij.openapi.components.Service 4 | import uk.co.ben_gibson.git.link.url.UrlOptions 5 | import uk.co.ben_gibson.git.link.url.template.UrlTemplates 6 | import uk.co.ben_gibson.url.Path 7 | import uk.co.ben_gibson.url.URL 8 | 9 | @Service 10 | class BitbucketServerUrlFactory : TemplatedUrlFactory(UrlTemplates.bitbucketServer()) { 11 | override fun createUrl(baseUrl: URL, options: UrlOptions): URL { 12 | return super.createUrl(normaliseBaseUrl(baseUrl), options) 13 | } 14 | 15 | private fun normaliseBaseUrl(baseUrl: URL): URL { 16 | return baseUrl.copy(path = Path(baseUrl.path.toString().removePrefix("scm/"))) 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/url/factory/ChromiumUrlFactory.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.url.factory 2 | 3 | import com.intellij.openapi.components.Service 4 | import uk.co.ben_gibson.git.link.git.File 5 | import uk.co.ben_gibson.git.link.ui.LineSelection 6 | import uk.co.ben_gibson.git.link.url.* 7 | import uk.co.ben_gibson.url.Host 8 | import uk.co.ben_gibson.url.Path 9 | import uk.co.ben_gibson.url.URL 10 | 11 | private val HOST = Host("source.chromium.org") 12 | 13 | private const val IDENTIFIER_CHROMIUMOS = "chromiumos" 14 | 15 | @Service 16 | class ChromiumUrlFactory: UrlFactory { 17 | override fun createUrl(baseUrl: URL, options: UrlOptions): URL { 18 | val path = if (baseUrl.path.toString().contains(IDENTIFIER_CHROMIUMOS)) 19 | createPathForChromiumos(baseUrl, options) 20 | else 21 | createPathForChromium(baseUrl, options) 22 | 23 | return URL(scheme = baseUrl.scheme, host = HOST, path = path) 24 | } 25 | 26 | private fun createPathForChromium(baseUrl: URL, options: UrlOptions) : Path { 27 | val path = Path("chromium") 28 | .with(Path(baseUrl.path.toString())) 29 | .with(Path("+")) 30 | 31 | return when (options) { 32 | is UrlOptions.UrlOptionsFileAtBranch -> path.with(createChromiumFileSubPath(options.file, options.branch, options.lineSelection)) 33 | is UrlOptions.UrlOptionsFileAtCommit -> path.with(createChromiumFileSubPath(options.file, options.commit.toString(), options.lineSelection)) 34 | is UrlOptions.UrlOptionsCommit -> path.with(Path(options.commit.toString())) 35 | } 36 | } 37 | 38 | private fun createPathForChromiumos(baseUrl: URL, options: UrlOptions) : Path { 39 | return when (options) { 40 | is UrlOptions.UrlOptionsFileAtBranch -> createChromiumosFileSubPath(baseUrl, options.file, options.branch, options.lineSelection) 41 | is UrlOptions.UrlOptionsFileAtCommit -> createChromiumosFileSubPath(baseUrl, options.file, options.commit.toString(), options.lineSelection) 42 | is UrlOptions.UrlOptionsCommit -> Path("chromiumos/_/chromium/chromiumos") 43 | .withSegments(baseUrl.path.toString().split('/').filter{ it.isNotBlank() }.drop(1)) 44 | .with(Path("+")) 45 | .with(Path(options.commit.toString())) 46 | } 47 | } 48 | 49 | private fun createChromiumFileSubPath(file: File, ref: String, lineSelection: LineSelection?): Path { 50 | 51 | var path = Path.fromSegments("${ref}:".plus(file.path.trim('/')).split("/")) 52 | 53 | if (!file.isRoot) { 54 | path = path.withSegment(file.name) 55 | } 56 | 57 | if (file.isDirectory) { 58 | return path 59 | } 60 | 61 | lineSelection ?: return path 62 | 63 | return Path(path.toString() + createLineSelection(lineSelection)) 64 | } 65 | 66 | private fun createChromiumosFileSubPath(baseUrl: URL, file: File, ref: String, lineSelection: LineSelection?): Path { 67 | var path = Path("chromiumos/chromiumos/codesearch") 68 | .with("+") 69 | .with(ref.plus(":src")) 70 | .withSegments(baseUrl.path.toString().split('/').filter{ it.isNotBlank() }.drop(1)) 71 | .withSegments(file.path.split("/").filter { it.isNotBlank() }) 72 | 73 | if (!file.isRoot) { 74 | path = path.withSegment(file.name) 75 | } 76 | 77 | if (file.isDirectory) { 78 | return path 79 | } 80 | 81 | lineSelection ?: return path 82 | 83 | return Path(path.toString() + createLineSelection(lineSelection)) 84 | } 85 | 86 | private fun createLineSelection(selection: LineSelection) = ";l=${selection.start}-${selection.end}" 87 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/url/factory/TemplatedUrlFactory.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.url.factory 2 | 3 | import uk.co.ben_gibson.git.link.git.Commit 4 | import uk.co.ben_gibson.git.link.git.File 5 | import uk.co.ben_gibson.git.link.ui.LineSelection 6 | import uk.co.ben_gibson.git.link.url.* 7 | import uk.co.ben_gibson.git.link.url.template.UrlTemplates 8 | import uk.co.ben_gibson.url.URL 9 | import java.util.regex.Pattern 10 | import com.google.common.net.UrlEscapers 11 | 12 | open class TemplatedUrlFactory(private val templates: UrlTemplates) : UrlFactory { 13 | private val escape = UrlEscapers.urlPathSegmentEscaper().asFunction() 14 | 15 | private val remotePathPattern = Pattern.compile("\\{remote:url:path:(\\d)}") 16 | 17 | override fun createUrl(baseUrl: URL, options: UrlOptions): URL { 18 | var processTemplate = when (options) { 19 | is UrlOptions.UrlOptionsFileAtCommit -> processTemplate(options) 20 | is UrlOptions.UrlOptionsFileAtBranch -> processTemplate(options) 21 | is UrlOptions.UrlOptionsCommit -> processTemplate(options) 22 | } 23 | 24 | processTemplate = processBaseUrl(processTemplate, baseUrl) 25 | processTemplate = removeUnmatchedSubstitutions(processTemplate) 26 | processTemplate = processTemplate.replace("(?().customHosts.first { UUID.fromString(it.id).equals(host.id) } 33 | 34 | return TemplatedUrlFactory(UrlTemplates(config.fileAtBranchTemplate, config.fileAtCommitTemplate, config.commitTemplate)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/url/factory/UrlFactory.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.url.factory 2 | 3 | import uk.co.ben_gibson.git.link.url.UrlOptions 4 | import uk.co.ben_gibson.url.URL 5 | 6 | interface UrlFactory { 7 | fun createUrl(baseUrl: URL, options: UrlOptions) : URL 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/url/factory/UrlFactoryLocator.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.url.factory 2 | 3 | import com.intellij.openapi.components.Service 4 | import com.intellij.openapi.components.service 5 | import uk.co.ben_gibson.git.link.platform.* 6 | 7 | @Service 8 | class UrlFactoryLocator { 9 | fun locate(platform: Platform) : UrlFactory { 10 | return when(platform) { 11 | is Chromium -> service() 12 | is Azure -> service() 13 | is BitbucketServer -> service() 14 | else -> service().forPlatform(platform) 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/uk/co/ben_gibson/git/link/url/template/UrlTemplates.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.url.template 2 | 3 | data class UrlTemplates(val fileAtBranch: String, val fileAtCommit : String, val commit : String) { 4 | companion object { 5 | fun gitHub(): UrlTemplates { 6 | return UrlTemplates( 7 | "{remote:url}/{object}/{branch}/{file:path}/{file:name}{line-block:start}#L{line:start}-L{line:end}{line-block:end}", 8 | "{remote:url}/{object}/{commit}/{file:path}/{file:name}{line-block:start}#L{line:start}-L{line:end}{line-block:end}", 9 | "{remote:url}/commit/{commit}" 10 | ) 11 | } 12 | 13 | fun gitLab(): UrlTemplates { 14 | return UrlTemplates( 15 | "{remote:url}/{object}/{branch}/{file:path}/{file:name}{line-block:start}#L{line:start}-{line:end}{line-block:end}", 16 | "{remote:url}/{object}/{commit}/{file:path}/{file:name}{line-block:start}#L{line:start}-{line:end}{line-block:end}", 17 | "{remote:url}/commit/{commit}" 18 | ) 19 | } 20 | 21 | fun bitbucketCloud(): UrlTemplates { 22 | return UrlTemplates( 23 | "{remote:url}/src/{branch}/{file:path}/{file:name}{line-block:start}#lines-{line:start}:{line:end}{line-block:end}", 24 | "{remote:url}/src/{commit}/{file:path}/{file:name}{line-block:start}#lines-{line:start}:{line:end}{line-block:end}", 25 | "{remote:url}/commits/{commit}" 26 | ) 27 | } 28 | 29 | fun bitbucketServer(): UrlTemplates { 30 | return UrlTemplates( 31 | "{remote:url:protocol}://{remote:url:host}/projects/{remote:url:path:0}/repos/{remote:url:path:1}/browse/{file:path}/{file:name}?at=refs/heads/{branch}{line-block:start}#{line:start}-{line:end}{line-block:end}", 32 | "{remote:url:protocol}://{remote:url:host}/projects/{remote:url:path:0}/repos/{remote:url:path:1}/browse/{file:path}/{file:name}?at={commit}{line-block:start}#{line:start}-{line:end}{line-block:end}", 33 | "{remote:url:protocol}://{remote:url:host}/projects/{remote:url:path:0}/repos/{remote:url:path:1}/commits/{commit}" 34 | ) 35 | } 36 | 37 | fun gitea(): UrlTemplates { 38 | return UrlTemplates( 39 | "{remote:url}/src/{branch}/{file:path}/{file:name}{line-block:start}#L{line:start}-L{line:end}{line-block:end}", 40 | "{remote:url}/src/{commit}/{file:path}/{file:name}{line-block:start}#L{line:start}-L{line:end}{line-block:end}", 41 | "{remote:url}/commit/{commit}" 42 | ) 43 | } 44 | 45 | fun gogs(): UrlTemplates { 46 | return UrlTemplates( 47 | "{remote:url}/src/{branch}/{file:path}/{file:name}{line-block:start}#L{line:start}-L{line:end}{line-block:end}", 48 | "{remote:url}/src/{commit}/{file:path}/{file:name}{line-block:start}#L{line:start}-L{line:end}{line-block:end}", 49 | "{remote:url}/commit/{commit}" 50 | ) 51 | } 52 | 53 | fun srht(): UrlTemplates { 54 | return UrlTemplates( 55 | "{remote:url}/tree/{branch}/item/{file:path}/{file:name}{line-block:start}#L{line:start}{line-block:end}", 56 | "{remote:url}/tree/{commit}/item/{file:path}/{file:name}{line-block:start}#L{line:start}{line-block:end}", 57 | "{remote:url}/tree/{commit}" 58 | 59 | ) 60 | } 61 | 62 | fun gitee() = gitHub() 63 | 64 | fun gerrit(): UrlTemplates { 65 | return UrlTemplates( 66 | "{remote:url:protocol}://{remote:url:host}/plugins/gitiles/{remote:url:path}/+/refs/heads/{branch}/{file:path}/{file:name}{line-block:start}#{line:start}{line-block:end}", 67 | "{remote:url:protocol}://{remote:url:host}/plugins/gitiles/{remote:url:path}/+/{commit}/{file:path}/{file:name}{line-block:start}#{line:start}{line-block:end}", 68 | "{remote:url:protocol}://{remote:url:host}/plugins/gitiles/{remote:url:path}/+/{commit}" 69 | ) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | uk.co.ben-gibson.remote.repository.mapper 4 | GitLink 5 | Ben Gibson 6 | com.intellij.modules.platform 7 | Git4Idea 8 | 9 | A Jetbrains plugin open or copy git URLs to a remote host. Supports `GitHub`, `Bitbucket`, `GitLab`, `Gitee`, `Gitea`, `Gogs`, 10 | `Azure` and `Gerrit` out of the box, while additional hosts can be configured using custom URL templates. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 27 | 33 | 39 | 40 | 41 | 42 | messages.ActionsBundle 43 | 44 | 45 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 71 | 72 | 73 | 74 | 75 | 76 | 80 | 81 | 82 | 83 | 84 | 88 | 89 | 90 | 91 | 92 | 96 | 97 | 98 | 99 | 100 | 104 | 105 | 106 | 107 | 111 | 112 | 113 | 114 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/main/resources/icons/azure.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | -------------------------------------------------------------------------------- /src/main/resources/icons/bitbucket.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/resources/icons/gerrit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/icons/git.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/icons/gitea.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 9 | 11 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/resources/icons/gitee.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/resources/icons/gitlab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 35 | 36 | 37 | 39 | 41 | 43 | 45 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 1x 57 | 58 | 59 | 1x 60 | 61 | 62 | 1x 63 | 64 | 65 | 66 | 1x 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/main/resources/icons/gitlink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/main/resources/icons/sourcehut.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 14 | 32 | 36 | 37 | -------------------------------------------------------------------------------- /src/main/resources/messages/ActionsBundle.properties: -------------------------------------------------------------------------------- 1 | notification.group.important.name=GitLink important 2 | notification.group.general.name=GitLink general -------------------------------------------------------------------------------- /src/main/resources/messages/MyBundle.properties: -------------------------------------------------------------------------------- 1 | name=GitLink 2 | 3 | actions.browser.title = Open in {0} 4 | actions.copy.title = Copy {0} link 5 | actions.copy-markdown.title = Copy {0} Markdown link 6 | actions.report-bug.title = Report a bug 7 | actions.add = Add 8 | actions.register = Register 9 | actions.update = Update 10 | actions.disable = Disable 11 | actions.sure-take-me-there = Sure, take me there 12 | actions.do-not-ask-again = Do not ask me again 13 | actions.take-me-there = Take me there 14 | actions.configure-manually = Configure manually... 15 | 16 | notifications.platform-not-set=You have not set a platform 17 | notifications.repository-not-found=Could not find repository for selected file 18 | notifications.remote-not-found=Could not find remote for selected file 19 | notifications.welcome=Thanks for installing GitLink {0} 20 | notifications.performance=GitLink taking too long? Try disabling the 'Check remote for commit/branch existence' setting 21 | notifications.could-not-detect-platform=GitLink could not detect which platform you use 22 | notifications.platform-detected.message=GitLink has automatically detected {0} as your platform. 23 | notifications.platform-detected.action=Not right? Configure manually... 24 | notifications.copied-to-clipboard=URL copied to clipboard 25 | 26 | title.settings = Settings 27 | 28 | settings.general.group.title=GitLink 29 | settings.general.field.platform.label=Platform 30 | settings.general.field.platform.help=Determines the URL format. Note, both cloud hosted and self-hosted deployments are supported. 31 | settings.general.field.fallback-branch.label=Fallback branch 32 | settings.general.field.fallback-branch.help=When generating a file URL, if the current branch does not exist on the remote, this will be used instead 33 | settings.general.field.remote.label=Remote 34 | settings.general.field.should-check-remote.label=Check remote 35 | settings.general.field.check-commit-on-remote.help=By default, when generating a URL, GitLink will check the remote to determine if the commit or branch exists. If it does not, \ 36 | a fallback will be used to avoid a 404. On large repositories, this check can be slow, in which case in may be preferable to disable it. 37 | settings.general.field.force-https.label=Force HTTPS 38 | settings.general.section.advanced.label=Advanced 39 | 40 | settings.domain-registry.group.title=Domain Registry 41 | settings.domain-registry.table.empty=No domains registered 42 | settings.auto-detect.table.column.domain=Domain 43 | 44 | settings.auto-detect.register-domain-dialog.title=Register Domain 45 | 46 | settings.custom-platform.group.title=Custom Platform 47 | settings.custom-platform.table.empty=No custom platforms 48 | settings.custom-platform.table.column.name=Name 49 | settings.custom-platform.table.column.domain=Domain 50 | 51 | settings.custom-platform.add-dialog.title=Add Custom Platform 52 | settings.custom-platform.add-dialog.field.name.label=Name 53 | settings.custom-platform.add-dialog.field.name.comment=What's the Platform called? e.g. GitHub 54 | settings.custom-platform.add-dialog.field.domain.label=Domain 55 | settings.custom-platform.add-dialog.field.domain.comment=The domain, for example, foo.com that this platform should be used on 56 | settings.custom-platform.add-dialog.field.file-at-branch-template.label=File at branch template 57 | settings.custom-platform.add-dialog.field.file-at-branch-template.comment=This template will be used when opening a file on a \ 58 | specific branch. For example, https://my-custom-host.com/repo/foo/blob/{branch}/{file:path}/{file:name}{line-block:start}#L{line:start}-L{line:end}{line-block:end} 59 | settings.custom-platform.add-dialog.field.file-at-commit-template.label=File at commit template 60 | settings.custom-platform.add-dialog.field.file-at-commit-template.comment=This template will be used when opening a file at \ 61 | specific commit. For example, {remote:url}/blob/{commit}/{file:path}/{file:name}{line-block:start}#L{line:start}-L{line:end}{line-block:end} 62 | settings.custom-platform.add-dialog.field.commit-template.label=Commit template 63 | settings.custom-platform.add-dialog.field.commit-template.comment=This template will be used when opening a commit. For example, {remote:url}/commit/{commit} 64 | 65 | validation.required=Required 66 | validation.alpha-numeric=Invalid characters, only alphanumeric characters are supported 67 | validation.min-length=Must be at least {0} characters 68 | validation.max-length=Cannot be more than {0} characters 69 | validation.invalid-domain=Invalid domain 70 | validation.invalid-url-template=Invalid URL template 71 | validation.exists=Value already exists 72 | 73 | platform.github.name=GitHub 74 | platform.gitlab.name=GitLab 75 | platform.bitbucket.cloud.name=Bitbucket Cloud 76 | platform.bitbucket.server.name=Bitbucket Server 77 | platform.gitea.name=Gitea 78 | platform.gogs.name=Gogs 79 | platform.srht.name=SourceHut 80 | platform.azure.name=Azure 81 | platform.gitee.name=Gitee 82 | platform.chromium.name=Chromium (Experimental) 83 | platform.gerrit.name=Gerrit 84 | -------------------------------------------------------------------------------- /src/test/kotlin/uk/co/ben_gibson/git/link/git/RemoteTest.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.git 2 | 3 | import git4idea.repo.GitRemote 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.params.ParameterizedTest 6 | import org.junit.jupiter.params.provider.Arguments 7 | import org.junit.jupiter.params.provider.MethodSource 8 | import java.util.stream.Stream 9 | 10 | class RemoteTest { 11 | 12 | companion object { 13 | 14 | @JvmStatic 15 | fun httpUrlExpectationsProvider(): Stream = Stream.of( 16 | Arguments.of( 17 | "git@github.com:ben-gibson/GitLink.git", 18 | "http://github.com/ben-gibson/GitLink" 19 | ), 20 | Arguments.of( 21 | "https://username:password@github.com/ben-gibson/GitLink.git", 22 | "https://github.com/ben-gibson/GitLink" 23 | ), 24 | Arguments.of( 25 | "git@github.com:500/test.git", 26 | "http://github.com/500/test" 27 | ), 28 | Arguments.of( 29 | "https://foo@bitbucket.org/foo/bar", 30 | "https://bitbucket.org/foo/bar" 31 | ), 32 | Arguments.of( 33 | "ssh://git@stash.example.com:7999/foo/bar.git", 34 | "http://stash.example.com/foo/bar" 35 | ), 36 | Arguments.of( 37 | "git://github.com/foo/bar", 38 | "http://github.com/foo/bar" 39 | ), 40 | Arguments.of( 41 | "ssh://git@custom.gitlab.url:10022/group/project.git", 42 | "http://custom.gitlab.url/group/project" 43 | ), 44 | Arguments.of( 45 | "xy://custom.gitlab.url/group/project.git", 46 | "http://custom.gitlab.url/group/project" 47 | ), 48 | Arguments.of( 49 | "http://ben-gibson@dev.azure.com/ben-gibson/test/_git/test.git", 50 | "http://dev.azure.com/ben-gibson/test/_git/test.git" 51 | ), 52 | ) 53 | } 54 | 55 | @ParameterizedTest 56 | @MethodSource("httpUrlExpectationsProvider") 57 | fun canGetHttpUrl(gitUrl: String, expectedUrl: String) { 58 | val remote = GitRemote("origin", listOf(gitUrl), listOf(), listOf(), listOf()) 59 | 60 | assertEquals(expectedUrl, remote.httpUrl.toString()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/kotlin/uk/co/ben_gibson/git/link/ui/actions/vcslog/BrowserActionTest.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.ui.actions.vcslog 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.vfs.VirtualFile 6 | import com.intellij.vcs.log.* 7 | import com.intellij.vcs.log.impl.HashImpl 8 | import com.intellij.vcs.log.impl.VcsFileStatusInfo 9 | import git4idea.GitCommit 10 | import git4idea.history.GitCommitRequirements 11 | import io.mockk.every 12 | import io.mockk.mockk 13 | import org.junit.jupiter.api.Assertions.* 14 | import org.junit.jupiter.params.ParameterizedTest 15 | import org.junit.jupiter.params.provider.ValueSource 16 | import uk.co.ben_gibson.git.link.ContextCommit 17 | import uk.co.ben_gibson.git.link.ui.actions.Action 18 | 19 | class BrowserActionTest { 20 | @ParameterizedTest(name = "{0} retrieves full commit hash from VCS log") 21 | @ValueSource(classes = [BrowserAction::class, CopyAction::class, MarkdownAction::class]) 22 | fun `retrieves full commit hash from VCS log`(actionClassToTest: Class) { 23 | val projectDummy = mockk() 24 | val anActionEventMock = prepareActionEventWithSingleSelectedCommitInVcsLog( 25 | commitHash = "b032a0707beac9a2f24b1b7d97ee4f7156de182c" 26 | ) 27 | val sut = actionClassToTest.constructors[0].newInstance() as Action 28 | 29 | val context = sut.buildContext(projectDummy, anActionEventMock) 30 | 31 | assertNotNull(context) 32 | assertInstanceOf(ContextCommit::class.java, context) 33 | assertEquals("b032a0707beac9a2f24b1b7d97ee4f7156de182c", (context as ContextCommit).commit.toString()) 34 | } 35 | 36 | private fun prepareActionEventWithSingleSelectedCommitInVcsLog(commitHash: String): AnActionEvent { 37 | val authorDummy = mockk() 38 | val commit = GitCommit( 39 | mockk(), 40 | HashImpl.build(commitHash), 41 | emptyList(), 42 | 0, 43 | mockk(), 44 | "subject", 45 | authorDummy, 46 | "commit message", 47 | authorDummy, 48 | 0, 49 | emptyList>(), 50 | mockk() 51 | ) 52 | 53 | val vcsLog = mockk() 54 | every { vcsLog.cachedFullDetails } returns listOf(commit) 55 | 56 | val anActionEvent = mockk() 57 | every { anActionEvent.getData(VcsLogDataKeys.VCS_LOG_COMMIT_SELECTION) } returns vcsLog 58 | 59 | return anActionEvent 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/kotlin/uk/co/ben_gibson/git/link/url/AzureTest.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.url 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.params.ParameterizedTest 5 | import org.junit.jupiter.params.provider.Arguments 6 | import org.junit.jupiter.params.provider.MethodSource 7 | import uk.co.ben_gibson.git.link.git.* 8 | import uk.co.ben_gibson.git.link.ui.LineSelection 9 | import uk.co.ben_gibson.git.link.url.factory.AzureUrlFactory 10 | import java.util.stream.Stream 11 | import uk.co.ben_gibson.url.URL 12 | 13 | class AzureTest { 14 | 15 | companion object { 16 | 17 | private val REMOTE_BASE_URL_WITH_GIT = URL.fromString("https://dev.azure.com/ben-gibson/_git/test") 18 | private val REMOTE_BASE_URL_WITHOUT_GIT = URL.fromString("https://dev.azure.com/ben-gibson/test") 19 | private val REMOTE_BASE_URL_WITH_COMPANY_AND_GIT = URL.fromString("https://dev.azure.com/company/project/_git/test") 20 | private val REMOTE_BASE_URL_WITH_COMPANY_WITHOUT_GIT = URL.fromString("https://dev.azure.com/company/project/test") 21 | private const val BRANCH = "master" 22 | private val COMMIT = Commit("b032a0707beac9a2f24b1b7d97ee4f7156de182c") 23 | private val FILE = File("Foo.java", false, "src", false) 24 | private val LINE_SELECTION = LineSelection(10, 20) 25 | 26 | @JvmStatic 27 | fun urlExpectationsProvider(): Stream = Stream.of( 28 | Arguments.of( 29 | REMOTE_BASE_URL_WITH_GIT, 30 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH, LINE_SELECTION), 31 | "https://dev.azure.com/ben-gibson/_git/test?version=GBmaster&path=src%2FFoo.java&line=10&lineEnd=21&lineStartColumn=1&lineEndColumn=1" 32 | ), 33 | Arguments.of( 34 | REMOTE_BASE_URL_WITH_GIT, 35 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH), 36 | "https://dev.azure.com/ben-gibson/_git/test?version=GBmaster&path=src%2FFoo.java" 37 | ), 38 | Arguments.of( 39 | REMOTE_BASE_URL_WITH_GIT, 40 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT, LINE_SELECTION), 41 | "https://dev.azure.com/ben-gibson/_git/test?version=GCb032a0707beac9a2f24b1b7d97ee4f7156de182c&path=src%2FFoo.java&line=10&lineEnd=21&lineStartColumn=1&lineEndColumn=1" 42 | ), 43 | Arguments.of( 44 | REMOTE_BASE_URL_WITH_GIT, 45 | UrlOptions.UrlOptionsFileAtCommit( 46 | File("resources", true, "src/foo", false), 47 | "main", 48 | COMMIT 49 | ), 50 | "https://dev.azure.com/ben-gibson/_git/test?version=GCb032a0707beac9a2f24b1b7d97ee4f7156de182c&path=src%2Ffoo%2Fresources" 51 | ), 52 | Arguments.of( 53 | REMOTE_BASE_URL_WITH_GIT, 54 | UrlOptions.UrlOptionsFileAtCommit( 55 | File("my-project", true, "", true), 56 | "main", 57 | COMMIT 58 | ), 59 | "https://dev.azure.com/ben-gibson/_git/test?version=GCb032a0707beac9a2f24b1b7d97ee4f7156de182c&path=%2F"), 60 | Arguments.of( 61 | REMOTE_BASE_URL_WITH_GIT, 62 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT), 63 | "https://dev.azure.com/ben-gibson/_git/test?version=GCb032a0707beac9a2f24b1b7d97ee4f7156de182c&path=src%2FFoo.java" 64 | ), 65 | Arguments.of( 66 | REMOTE_BASE_URL_WITH_GIT, 67 | UrlOptions.UrlOptionsCommit(COMMIT, "main"), 68 | "https://dev.azure.com/ben-gibson/_git/test/commit/b032a0707beac9a2f24b1b7d97ee4f7156de182c" 69 | ), 70 | Arguments.of( 71 | REMOTE_BASE_URL_WITHOUT_GIT, 72 | UrlOptions.UrlOptionsCommit(COMMIT, "main"), 73 | "https://dev.azure.com/ben-gibson/_git/test/commit/b032a0707beac9a2f24b1b7d97ee4f7156de182c" 74 | ), 75 | Arguments.of( 76 | REMOTE_BASE_URL_WITH_COMPANY_AND_GIT, 77 | UrlOptions.UrlOptionsCommit(COMMIT, "main"), 78 | "https://dev.azure.com/company/project/_git/test/commit/b032a0707beac9a2f24b1b7d97ee4f7156de182c" 79 | ), 80 | Arguments.of( 81 | REMOTE_BASE_URL_WITH_COMPANY_WITHOUT_GIT, 82 | UrlOptions.UrlOptionsCommit(COMMIT, "main"), 83 | "https://dev.azure.com/company/project/_git/test/commit/b032a0707beac9a2f24b1b7d97ee4f7156de182c" 84 | ), 85 | Arguments.of( 86 | URL.fromString("https://ssh.dev.azure.com/v3/ben-gibson/test/test"), 87 | UrlOptions.UrlOptionsCommit(COMMIT, "main"), 88 | "https://dev.azure.com/ben-gibson/test/_git/test/commit/b032a0707beac9a2f24b1b7d97ee4f7156de182c" 89 | ) 90 | ) 91 | } 92 | 93 | @ParameterizedTest 94 | @MethodSource("urlExpectationsProvider") 95 | fun canGenerateUrl(baseUrl: URL, options: UrlOptions, expectedUrl: String) { 96 | val factory = AzureUrlFactory() 97 | val url = factory.createUrl(baseUrl, options) 98 | 99 | assertEquals(expectedUrl, url.toString()) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/test/kotlin/uk/co/ben_gibson/git/link/url/BitBucketCloudTest.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.url 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.params.ParameterizedTest 5 | import org.junit.jupiter.params.provider.Arguments 6 | import org.junit.jupiter.params.provider.MethodSource 7 | import uk.co.ben_gibson.git.link.git.File 8 | import uk.co.ben_gibson.git.link.ui.LineSelection 9 | import java.util.stream.Stream 10 | import uk.co.ben_gibson.git.link.git.Commit 11 | import uk.co.ben_gibson.git.link.url.factory.TemplatedUrlFactory 12 | import uk.co.ben_gibson.git.link.url.template.UrlTemplates 13 | import uk.co.ben_gibson.url.URL 14 | 15 | class BitBucketCloudTest { 16 | 17 | companion object { 18 | 19 | private val REMOTE_BASE_URL = URL.fromString("https://bitbucket.org/foo/bar") 20 | private const val BRANCH = "master" 21 | private val COMMIT = Commit("b032a0707beac9a2f24b1b7d97ee4f7156de182c") 22 | private val FILE = File("Foo.java", false, "src", false) 23 | private val LINE_SELECTION = LineSelection(10, 20) 24 | 25 | @JvmStatic 26 | fun urlExpectationsProvider(): Stream = Stream.of( 27 | Arguments.of( 28 | REMOTE_BASE_URL, 29 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH, LINE_SELECTION), 30 | "https://bitbucket.org/foo/bar/src/master/src/Foo.java#lines-10:20" 31 | ), 32 | Arguments.of( 33 | REMOTE_BASE_URL, 34 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH), 35 | "https://bitbucket.org/foo/bar/src/master/src/Foo.java" 36 | ), 37 | Arguments.of( 38 | REMOTE_BASE_URL, 39 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT, LineSelection(10, 20)), 40 | "https://bitbucket.org/foo/bar/src/b032a0707beac9a2f24b1b7d97ee4f7156de182c/src/Foo.java#lines-10:20" 41 | ), 42 | Arguments.of( 43 | REMOTE_BASE_URL, 44 | UrlOptions.UrlOptionsFileAtCommit( 45 | File("resources", true, "src/foo", false), 46 | "main", 47 | COMMIT 48 | ), 49 | "https://bitbucket.org/foo/bar/src/b032a0707beac9a2f24b1b7d97ee4f7156de182c/src/foo/resources" 50 | ), 51 | Arguments.of( 52 | REMOTE_BASE_URL, 53 | UrlOptions.UrlOptionsFileAtCommit( 54 | File("my-project", true, "", true), 55 | "main", 56 | COMMIT 57 | ), 58 | "https://bitbucket.org/foo/bar/src/b032a0707beac9a2f24b1b7d97ee4f7156de182c" 59 | ), 60 | Arguments.of( 61 | REMOTE_BASE_URL, 62 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT), 63 | "https://bitbucket.org/foo/bar/src/b032a0707beac9a2f24b1b7d97ee4f7156de182c/src/Foo.java" 64 | ), 65 | Arguments.of( 66 | REMOTE_BASE_URL, 67 | UrlOptions.UrlOptionsCommit(COMMIT, "main"), 68 | "https://bitbucket.org/foo/bar/commits/b032a0707beac9a2f24b1b7d97ee4f7156de182c" 69 | ) 70 | ) 71 | } 72 | 73 | @ParameterizedTest 74 | @MethodSource("urlExpectationsProvider") 75 | fun canGenerateUrl(baseUrl: URL, options: UrlOptions, expectedUrl: String) { 76 | val factory = TemplatedUrlFactory(UrlTemplates.bitbucketCloud()) 77 | 78 | val url = factory.createUrl(baseUrl, options) 79 | 80 | assertEquals(expectedUrl, url.toString()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/test/kotlin/uk/co/ben_gibson/git/link/url/BitBucketServerTest.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.url 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.params.ParameterizedTest 5 | import org.junit.jupiter.params.provider.Arguments 6 | import org.junit.jupiter.params.provider.MethodSource 7 | import uk.co.ben_gibson.git.link.git.* 8 | import uk.co.ben_gibson.git.link.ui.LineSelection 9 | import uk.co.ben_gibson.git.link.url.factory.BitbucketServerUrlFactory 10 | import java.util.stream.Stream 11 | import uk.co.ben_gibson.url.URL 12 | 13 | class BitBucketServerTest { 14 | 15 | companion object { 16 | 17 | private val REMOTE_BASE_URL = URL.fromString("https://stash.example.com/foo/bar") 18 | private const val BRANCH = "master" 19 | private val COMMIT = Commit("b032a0707beac9a2f24b1b7d97ee4f7156de182c") 20 | private val FILE = File("Foo.java", false, "src", false) 21 | private val LINE_SELECTION = LineSelection(10, 20) 22 | 23 | @JvmStatic 24 | fun urlExpectationsProvider(): Stream = Stream.of( 25 | Arguments.of( 26 | URL.fromString("https://stash.example.com/scm/foo/bar"), 27 | UrlOptions.UrlOptionsFileAtBranch( 28 | FILE, 29 | BRANCH, 30 | LINE_SELECTION 31 | ), 32 | "https://stash.example.com/projects/foo/repos/bar/browse/src/Foo.java?at=refs/heads/master#10-20" 33 | ), 34 | Arguments.of( 35 | REMOTE_BASE_URL, 36 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH, LINE_SELECTION), 37 | "https://stash.example.com/projects/foo/repos/bar/browse/src/Foo.java?at=refs/heads/master#10-20" 38 | ), 39 | Arguments.of( 40 | REMOTE_BASE_URL, 41 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH), 42 | "https://stash.example.com/projects/foo/repos/bar/browse/src/Foo.java?at=refs/heads/master" 43 | ), 44 | Arguments.of( 45 | REMOTE_BASE_URL, 46 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT, LineSelection(10, 20)), 47 | "https://stash.example.com/projects/foo/repos/bar/browse/src/Foo.java?at=b032a0707beac9a2f24b1b7d97ee4f7156de182c#10-20" 48 | ), 49 | Arguments.of( 50 | REMOTE_BASE_URL, 51 | UrlOptions.UrlOptionsFileAtCommit( 52 | File("resources", true, "src/foo", false), 53 | "main", 54 | COMMIT 55 | ), 56 | "https://stash.example.com/projects/foo/repos/bar/browse/src/foo/resources?at=b032a0707beac9a2f24b1b7d97ee4f7156de182c" 57 | ), 58 | Arguments.of( 59 | REMOTE_BASE_URL, 60 | UrlOptions.UrlOptionsFileAtCommit( 61 | File("my-project", true, "", true), 62 | "main", 63 | COMMIT 64 | ), 65 | "https://stash.example.com/projects/foo/repos/bar/browse?at=b032a0707beac9a2f24b1b7d97ee4f7156de182c" 66 | ), 67 | Arguments.of( 68 | REMOTE_BASE_URL, 69 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT), 70 | "https://stash.example.com/projects/foo/repos/bar/browse/src/Foo.java?at=b032a0707beac9a2f24b1b7d97ee4f7156de182c" 71 | ), 72 | Arguments.of( 73 | REMOTE_BASE_URL, 74 | UrlOptions.UrlOptionsCommit(COMMIT, "main"), 75 | "https://stash.example.com/projects/foo/repos/bar/commits/b032a0707beac9a2f24b1b7d97ee4f7156de182c" 76 | ) 77 | ) 78 | } 79 | 80 | @ParameterizedTest 81 | @MethodSource("urlExpectationsProvider") 82 | fun canGenerateUrl(baseUrl: URL, options: UrlOptions, expectedUrl: String) { 83 | val factory = BitbucketServerUrlFactory() 84 | val url = factory.createUrl(baseUrl, options) 85 | 86 | assertEquals(expectedUrl, url.toString()) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/kotlin/uk/co/ben_gibson/git/link/url/ChromiumTest.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.url 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.params.ParameterizedTest 5 | import org.junit.jupiter.params.provider.Arguments 6 | import org.junit.jupiter.params.provider.MethodSource 7 | import uk.co.ben_gibson.git.link.git.File 8 | import uk.co.ben_gibson.git.link.ui.LineSelection 9 | import java.util.stream.Stream 10 | import uk.co.ben_gibson.git.link.git.Commit 11 | import uk.co.ben_gibson.git.link.url.factory.ChromiumUrlFactory 12 | import uk.co.ben_gibson.url.URL 13 | 14 | class ChromiumTest { 15 | 16 | companion object { 17 | 18 | private val REMOTE_BASE_URL_CHROMIUMOS = URL.fromString("https://chromium.googlesource.com/chromiumos/platform/ec") 19 | private val REMOTE_BASE_URL_CHROMIUM = URL.fromString("https://chromium.googlesource.com/chromium/tools/build") 20 | private const val BRANCH = "master" 21 | private val COMMIT = Commit("b032a0707beac9a2f24b1b7d97ee4f7156de182c") 22 | private val FILE = File("foo.c", false, "board", false) 23 | private val LINE_SELECTION = LineSelection(10, 20) 24 | 25 | @JvmStatic 26 | fun urlExpectationsProvider(): Stream = Stream.of( 27 | // Chromiumos file at branch 28 | Arguments.of( 29 | REMOTE_BASE_URL_CHROMIUMOS, 30 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH, LINE_SELECTION), 31 | "https://source.chromium.org/chromiumos/chromiumos/codesearch/+/master:src/platform/ec/board/foo.c;l=10-20" 32 | ), 33 | Arguments.of( 34 | REMOTE_BASE_URL_CHROMIUMOS, 35 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH), 36 | "https://source.chromium.org/chromiumos/chromiumos/codesearch/+/master:src/platform/ec/board/foo.c" 37 | ), 38 | 39 | // Chromium file at branch 40 | Arguments.of( 41 | REMOTE_BASE_URL_CHROMIUM, 42 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH), 43 | "https://source.chromium.org/chromium/chromium/tools/build/+/master:board/foo.c" 44 | ), 45 | Arguments.of( 46 | REMOTE_BASE_URL_CHROMIUM, 47 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH, LINE_SELECTION), 48 | "https://source.chromium.org/chromium/chromium/tools/build/+/master:board/foo.c;l=10-20" 49 | ), 50 | 51 | // Chromiumos file at commit 52 | Arguments.of( 53 | REMOTE_BASE_URL_CHROMIUMOS, 54 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT, LINE_SELECTION), 55 | "https://source.chromium.org/chromiumos/chromiumos/codesearch/+/b032a0707beac9a2f24b1b7d97ee4f7156de182c:src/platform/ec/board/foo.c;l=10-20" 56 | ), 57 | Arguments.of( 58 | REMOTE_BASE_URL_CHROMIUMOS, 59 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT), 60 | "https://source.chromium.org/chromiumos/chromiumos/codesearch/+/b032a0707beac9a2f24b1b7d97ee4f7156de182c:src/platform/ec/board/foo.c" 61 | ), 62 | 63 | // Chromium file at commit 64 | Arguments.of( 65 | REMOTE_BASE_URL_CHROMIUM, 66 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT, LINE_SELECTION), 67 | "https://source.chromium.org/chromium/chromium/tools/build/+/b032a0707beac9a2f24b1b7d97ee4f7156de182c:board/foo.c;l=10-20" 68 | ), 69 | Arguments.of( 70 | REMOTE_BASE_URL_CHROMIUM, 71 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT), 72 | "https://source.chromium.org/chromium/chromium/tools/build/+/b032a0707beac9a2f24b1b7d97ee4f7156de182c:board/foo.c" 73 | ), 74 | 75 | // Chromiumos commit 76 | Arguments.of( 77 | REMOTE_BASE_URL_CHROMIUMOS, 78 | UrlOptions.UrlOptionsCommit(COMMIT, "main"), 79 | "https://source.chromium.org/chromiumos/_/chromium/chromiumos/platform/ec/+/b032a0707beac9a2f24b1b7d97ee4f7156de182c" 80 | ), 81 | 82 | // Chromium commit 83 | Arguments.of( 84 | REMOTE_BASE_URL_CHROMIUM, 85 | UrlOptions.UrlOptionsCommit(COMMIT, "main"), 86 | "https://source.chromium.org/chromium/chromium/tools/build/+/b032a0707beac9a2f24b1b7d97ee4f7156de182c" 87 | ), 88 | ) 89 | } 90 | 91 | @ParameterizedTest 92 | @MethodSource("urlExpectationsProvider") 93 | fun canGenerateUrl(baseUrl: URL, options: UrlOptions, expectedUrl: String) { 94 | val factory = ChromiumUrlFactory() 95 | val url = factory.createUrl(baseUrl, options) 96 | 97 | assertEquals(expectedUrl, url.toString()) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/kotlin/uk/co/ben_gibson/git/link/url/GerritTest.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.url 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.params.ParameterizedTest 5 | import org.junit.jupiter.params.provider.Arguments 6 | import org.junit.jupiter.params.provider.MethodSource 7 | import uk.co.ben_gibson.git.link.git.* 8 | import uk.co.ben_gibson.git.link.ui.LineSelection 9 | import java.util.stream.Stream 10 | import uk.co.ben_gibson.git.link.url.factory.TemplatedUrlFactory 11 | import uk.co.ben_gibson.git.link.url.template.UrlTemplates 12 | import uk.co.ben_gibson.url.URL 13 | 14 | class GerritTest { 15 | 16 | companion object { 17 | 18 | private val REMOTE_BASE_URL = URL.fromString("https://gerrit.example.com/foo/bar") 19 | private const val BRANCH = "master" 20 | private val COMMIT = Commit("b032a0707beac9a2f24b1b7d97ee4f7156de182c") 21 | private val FILE = File("Foo.java", false, "src", false) 22 | private val LINE_SELECTION = LineSelection(10, 20) 23 | 24 | @JvmStatic 25 | fun urlExpectationsProvider(): Stream = Stream.of( 26 | Arguments.of( 27 | REMOTE_BASE_URL, 28 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH, LINE_SELECTION), 29 | "https://gerrit.example.com/plugins/gitiles/foo/bar/+/refs/heads/master/src/Foo.java#10" 30 | ), 31 | Arguments.of( 32 | REMOTE_BASE_URL, 33 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH), 34 | "https://gerrit.example.com/plugins/gitiles/foo/bar/+/refs/heads/master/src/Foo.java" 35 | ), 36 | Arguments.of( 37 | REMOTE_BASE_URL, 38 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT, LineSelection(10, 20)), 39 | "https://gerrit.example.com/plugins/gitiles/foo/bar/+/b032a0707beac9a2f24b1b7d97ee4f7156de182c/src/Foo.java#10" 40 | ), 41 | Arguments.of( 42 | REMOTE_BASE_URL, 43 | UrlOptions.UrlOptionsFileAtCommit( 44 | File("resources", true, "src/foo", false), 45 | "main", 46 | COMMIT 47 | ), 48 | "https://gerrit.example.com/plugins/gitiles/foo/bar/+/b032a0707beac9a2f24b1b7d97ee4f7156de182c/src/foo/resources" 49 | ), 50 | Arguments.of( 51 | REMOTE_BASE_URL, 52 | UrlOptions.UrlOptionsFileAtCommit( 53 | File("my-project", true, "", true), 54 | "main", 55 | COMMIT 56 | ), 57 | "https://gerrit.example.com/plugins/gitiles/foo/bar/+/b032a0707beac9a2f24b1b7d97ee4f7156de182c" 58 | ), 59 | Arguments.of( 60 | REMOTE_BASE_URL, 61 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT), 62 | "https://gerrit.example.com/plugins/gitiles/foo/bar/+/b032a0707beac9a2f24b1b7d97ee4f7156de182c/src/Foo.java" 63 | ), 64 | Arguments.of( 65 | REMOTE_BASE_URL, 66 | UrlOptions.UrlOptionsCommit(COMMIT, "main"), 67 | "https://gerrit.example.com/plugins/gitiles/foo/bar/+/b032a0707beac9a2f24b1b7d97ee4f7156de182c" 68 | ) 69 | ) 70 | } 71 | 72 | @ParameterizedTest 73 | @MethodSource("urlExpectationsProvider") 74 | fun canGenerateUrl(baseUrl: URL, options: UrlOptions, expectedUrl: String) { 75 | val factory = TemplatedUrlFactory(UrlTemplates.gerrit()) 76 | val url = factory.createUrl(baseUrl, options) 77 | 78 | assertEquals(expectedUrl, url.toString()) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/kotlin/uk/co/ben_gibson/git/link/url/GitHubTest.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.url 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.params.ParameterizedTest 5 | import org.junit.jupiter.params.provider.Arguments 6 | import org.junit.jupiter.params.provider.MethodSource 7 | import uk.co.ben_gibson.git.link.git.* 8 | import uk.co.ben_gibson.git.link.ui.LineSelection 9 | import java.util.stream.Stream 10 | import uk.co.ben_gibson.git.link.url.factory.TemplatedUrlFactory 11 | import uk.co.ben_gibson.git.link.url.template.UrlTemplates 12 | import uk.co.ben_gibson.url.URL 13 | 14 | class GitHubTest { 15 | 16 | companion object { 17 | 18 | private val REMOTE_BASE_URL = URL.fromString("https://github.com/my/repo") 19 | private const val BRANCH = "master" 20 | private val COMMIT = Commit("b032a0707beac9a2f24b1b7d97ee4f7156de182c") 21 | private val FILE = File("Foo.java", false, "src", false) 22 | private val LINE_SELECTION = LineSelection(10, 20) 23 | 24 | @JvmStatic 25 | fun urlExpectationsProvider(): Stream = Stream.of( 26 | Arguments.of( 27 | REMOTE_BASE_URL, 28 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH, LINE_SELECTION), 29 | "https://github.com/my/repo/blob/master/src/Foo.java#L10-L20" 30 | ), 31 | Arguments.of( 32 | REMOTE_BASE_URL, 33 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH, LINE_SELECTION), 34 | "https://github.com/my/repo/blob/master/src/Foo.java#L10-L20" 35 | ), 36 | Arguments.of( 37 | REMOTE_BASE_URL, 38 | UrlOptions.UrlOptionsFileAtBranch( 39 | File("my-image.png", false, "src/foo bar baz/images", false), 40 | BRANCH 41 | ), 42 | "https://github.com/my/repo/blob/master/src/foo%20bar%20baz/images/my-image.png" 43 | ), 44 | Arguments.of( 45 | REMOTE_BASE_URL, 46 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT, LineSelection(10, 20)), 47 | "https://github.com/my/repo/blob/b032a0707beac9a2f24b1b7d97ee4f7156de182c/src/Foo.java#L10-L20" 48 | ), 49 | Arguments.of( 50 | REMOTE_BASE_URL, 51 | UrlOptions.UrlOptionsFileAtCommit( 52 | File("resources", true, "src/foo", false), 53 | "main", 54 | COMMIT 55 | ), 56 | "https://github.com/my/repo/tree/b032a0707beac9a2f24b1b7d97ee4f7156de182c/src/foo/resources" 57 | ), 58 | Arguments.of( 59 | REMOTE_BASE_URL, 60 | UrlOptions.UrlOptionsFileAtCommit( 61 | File("my-project", true, "", true), 62 | "main", 63 | COMMIT 64 | ), 65 | "https://github.com/my/repo/tree/b032a0707beac9a2f24b1b7d97ee4f7156de182c" 66 | ), 67 | Arguments.of( 68 | REMOTE_BASE_URL, 69 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT), 70 | "https://github.com/my/repo/blob/b032a0707beac9a2f24b1b7d97ee4f7156de182c/src/Foo.java" 71 | ), 72 | Arguments.of( 73 | REMOTE_BASE_URL, 74 | UrlOptions.UrlOptionsCommit(COMMIT, "main"), 75 | "https://github.com/my/repo/commit/b032a0707beac9a2f24b1b7d97ee4f7156de182c" 76 | ) 77 | ) 78 | } 79 | 80 | @ParameterizedTest 81 | @MethodSource("urlExpectationsProvider") 82 | fun canGenerateUrl(baseUrl: URL, options: UrlOptions, expectedUrl: String) { 83 | val factory = TemplatedUrlFactory(UrlTemplates.gitHub()) 84 | 85 | val url = factory.createUrl(baseUrl, options) 86 | 87 | assertEquals(expectedUrl, url.toString()) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/test/kotlin/uk/co/ben_gibson/git/link/url/GitLabTest.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.url 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.params.ParameterizedTest 5 | import org.junit.jupiter.params.provider.Arguments 6 | import org.junit.jupiter.params.provider.MethodSource 7 | import uk.co.ben_gibson.git.link.git.File 8 | import uk.co.ben_gibson.git.link.ui.LineSelection 9 | import java.util.stream.Stream 10 | import uk.co.ben_gibson.git.link.git.Commit 11 | import uk.co.ben_gibson.git.link.url.factory.TemplatedUrlFactory 12 | import uk.co.ben_gibson.git.link.url.template.UrlTemplates 13 | import uk.co.ben_gibson.url.URL 14 | 15 | class GitLabTest { 16 | 17 | companion object { 18 | 19 | private val REMOTE_BASE_URL = URL.fromString("https://gitlab.com/my/repo/") 20 | private const val BRANCH = "master" 21 | private val COMMIT = Commit("b032a0707beac9a2f24b1b7d97ee4f7156de182c") 22 | private val FILE = File("Foo.java", false, "src", false) 23 | private val LINE_SELECTION = LineSelection(10, 20) 24 | 25 | @JvmStatic 26 | fun urlExpectationsProvider(): Stream = Stream.of( 27 | Arguments.of( 28 | REMOTE_BASE_URL, 29 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH, LINE_SELECTION), 30 | "https://gitlab.com/my/repo/blob/master/src/Foo.java#L10-20" 31 | ), 32 | Arguments.of( 33 | REMOTE_BASE_URL, 34 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH), 35 | "https://gitlab.com/my/repo/blob/master/src/Foo.java" 36 | ), 37 | Arguments.of( 38 | REMOTE_BASE_URL, 39 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT, LineSelection(10, 20)), 40 | "https://gitlab.com/my/repo/blob/b032a0707beac9a2f24b1b7d97ee4f7156de182c/src/Foo.java#L10-20" 41 | ), 42 | Arguments.of( 43 | REMOTE_BASE_URL, 44 | UrlOptions.UrlOptionsFileAtBranch( 45 | File("Code.cs", false, "Assets/#/Sources", false), 46 | BRANCH, 47 | LINE_SELECTION 48 | ), 49 | "https://gitlab.com/my/repo/blob/master/Assets/%23/Sources/Code.cs#L10-20" 50 | ), 51 | Arguments.of( 52 | REMOTE_BASE_URL, 53 | UrlOptions.UrlOptionsFileAtCommit( 54 | File("resources", true, "src/foo", false), 55 | "main", 56 | COMMIT 57 | ), 58 | "https://gitlab.com/my/repo/tree/b032a0707beac9a2f24b1b7d97ee4f7156de182c/src/foo/resources" 59 | ), 60 | Arguments.of( 61 | REMOTE_BASE_URL, 62 | UrlOptions.UrlOptionsFileAtCommit( 63 | File("my-project", true, "", true), 64 | "main", 65 | COMMIT 66 | ), 67 | "https://gitlab.com/my/repo/tree/b032a0707beac9a2f24b1b7d97ee4f7156de182c" 68 | ), 69 | Arguments.of( 70 | REMOTE_BASE_URL, 71 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT), 72 | "https://gitlab.com/my/repo/blob/b032a0707beac9a2f24b1b7d97ee4f7156de182c/src/Foo.java" 73 | ), 74 | Arguments.of( 75 | REMOTE_BASE_URL, 76 | UrlOptions.UrlOptionsCommit(COMMIT, "main"), 77 | "https://gitlab.com/my/repo/commit/b032a0707beac9a2f24b1b7d97ee4f7156de182c" 78 | ) 79 | ) 80 | } 81 | 82 | @ParameterizedTest 83 | @MethodSource("urlExpectationsProvider") 84 | fun canGenerateUrl(baseUrl: URL, options: UrlOptions, expectedUrl: String) { 85 | val factory = TemplatedUrlFactory(UrlTemplates.gitLab()) 86 | 87 | val url = factory.createUrl(baseUrl, options) 88 | 89 | assertEquals(expectedUrl, url.toString()) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/kotlin/uk/co/ben_gibson/git/link/url/GogsTest.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.url 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.params.ParameterizedTest 5 | import org.junit.jupiter.params.provider.Arguments 6 | import org.junit.jupiter.params.provider.MethodSource 7 | import uk.co.ben_gibson.git.link.git.* 8 | import uk.co.ben_gibson.git.link.ui.LineSelection 9 | import java.util.stream.Stream 10 | import uk.co.ben_gibson.git.link.url.factory.TemplatedUrlFactory 11 | import uk.co.ben_gibson.git.link.url.template.UrlTemplates 12 | import uk.co.ben_gibson.url.URL 13 | 14 | class GogsTest { 15 | 16 | companion object { 17 | 18 | private val REMOTE_BASE_URL = URL.fromString("https://try.gogs.io/foo/bar") 19 | private const val BRANCH = "master" 20 | private val COMMIT = Commit("b032a0707beac9a2f24b1b7d97ee4f7156de182c") 21 | private val FILE = File("Foo.java", false, "src", false) 22 | private val LINE_SELECTION = LineSelection(10, 20) 23 | 24 | @JvmStatic 25 | fun urlExpectationsProvider(): Stream = Stream.of( 26 | Arguments.of( 27 | REMOTE_BASE_URL, 28 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH, LINE_SELECTION), 29 | "https://try.gogs.io/foo/bar/src/master/src/Foo.java#L10-L20" 30 | ), 31 | Arguments.of( 32 | REMOTE_BASE_URL, 33 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH), 34 | "https://try.gogs.io/foo/bar/src/master/src/Foo.java" 35 | ), 36 | Arguments.of( 37 | REMOTE_BASE_URL, 38 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT, LINE_SELECTION), 39 | "https://try.gogs.io/foo/bar/src/b032a0707beac9a2f24b1b7d97ee4f7156de182c/src/Foo.java#L10-L20" 40 | ), 41 | Arguments.of( 42 | REMOTE_BASE_URL, 43 | UrlOptions.UrlOptionsFileAtCommit( 44 | File("resources", true, "src/foo", false), 45 | "main", 46 | COMMIT 47 | ), 48 | "https://try.gogs.io/foo/bar/src/b032a0707beac9a2f24b1b7d97ee4f7156de182c/src/foo/resources" 49 | ), 50 | Arguments.of( 51 | REMOTE_BASE_URL, 52 | UrlOptions.UrlOptionsFileAtCommit( 53 | File("my-project", true, "", true), 54 | "main", 55 | COMMIT 56 | ), 57 | "https://try.gogs.io/foo/bar/src/b032a0707beac9a2f24b1b7d97ee4f7156de182c"), 58 | Arguments.of( 59 | REMOTE_BASE_URL, 60 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT), 61 | "https://try.gogs.io/foo/bar/src/b032a0707beac9a2f24b1b7d97ee4f7156de182c/src/Foo.java" 62 | ), 63 | Arguments.of( 64 | REMOTE_BASE_URL, 65 | UrlOptions.UrlOptionsCommit(COMMIT, "main"), 66 | "https://try.gogs.io/foo/bar/commit/b032a0707beac9a2f24b1b7d97ee4f7156de182c" 67 | ) 68 | ) 69 | } 70 | 71 | @ParameterizedTest 72 | @MethodSource("urlExpectationsProvider") 73 | fun canGenerateUrl(baseUrl: URL, options: UrlOptions, expectedUrl: String) { 74 | val factory = TemplatedUrlFactory(UrlTemplates.gogs()) 75 | val url = factory.createUrl(baseUrl, options) 76 | 77 | assertEquals(expectedUrl, url.toString()) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/kotlin/uk/co/ben_gibson/git/link/url/SrhtTest.kt: -------------------------------------------------------------------------------- 1 | package uk.co.ben_gibson.git.link.url 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.params.ParameterizedTest 5 | import org.junit.jupiter.params.provider.Arguments 6 | import org.junit.jupiter.params.provider.MethodSource 7 | import uk.co.ben_gibson.git.link.git.* 8 | import uk.co.ben_gibson.git.link.ui.LineSelection 9 | import java.util.stream.Stream 10 | import uk.co.ben_gibson.git.link.url.factory.TemplatedUrlFactory 11 | import uk.co.ben_gibson.git.link.url.template.UrlTemplates 12 | import uk.co.ben_gibson.url.URL 13 | 14 | class SourceHutTest { 15 | 16 | companion object { 17 | 18 | private val REMOTE_BASE_URL = URL.fromString("https://git.sr.ht/~myuser/myproject") 19 | private const val BRANCH = "main" 20 | private val COMMIT = Commit("23471005d2d874bb7ab400d45a2360f988c0be33") 21 | private val FILE = File("main.rs", false, "src", false) 22 | private val LINE_SELECTION = LineSelection(1, 2) 23 | 24 | @JvmStatic 25 | fun urlExpectationsProvider(): Stream = Stream.of( 26 | Arguments.of( 27 | REMOTE_BASE_URL, 28 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH, LINE_SELECTION), 29 | "https://git.sr.ht/~myuser/myproject/tree/main/item/src/main.rs#L1" 30 | ), 31 | Arguments.of( 32 | REMOTE_BASE_URL, 33 | UrlOptions.UrlOptionsFileAtBranch(FILE, BRANCH), 34 | "https://git.sr.ht/~myuser/myproject/tree/main/item/src/main.rs" 35 | ), 36 | Arguments.of( 37 | REMOTE_BASE_URL, 38 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT, LINE_SELECTION), 39 | "https://git.sr.ht/~myuser/myproject/tree/23471005d2d874bb7ab400d45a2360f988c0be33/item/src/main.rs#L1" 40 | ), 41 | Arguments.of( 42 | REMOTE_BASE_URL, 43 | UrlOptions.UrlOptionsFileAtCommit(FILE, "main", COMMIT), 44 | "https://git.sr.ht/~myuser/myproject/tree/23471005d2d874bb7ab400d45a2360f988c0be33/item/src/main.rs" 45 | ), 46 | Arguments.of( 47 | REMOTE_BASE_URL, 48 | UrlOptions.UrlOptionsCommit(COMMIT, "main"), 49 | "https://git.sr.ht/~myuser/myproject/tree/23471005d2d874bb7ab400d45a2360f988c0be33" 50 | ) 51 | ) 52 | } 53 | 54 | @ParameterizedTest 55 | @MethodSource("urlExpectationsProvider") 56 | fun canGenerateUrl(baseUrl: URL, options: UrlOptions, expectedUrl: String) { 57 | val factory = TemplatedUrlFactory(UrlTemplates.srht()) 58 | val url = factory.createUrl(baseUrl, options) 59 | 60 | assertEquals(expectedUrl, url.toString()) 61 | } 62 | } --------------------------------------------------------------------------------