├── .github ├── CODEOWNERS ├── FUNDING.yml ├── dependabot.yml ├── pr-labeler.yml ├── release-drafter.yml └── workflows │ ├── actionlint.yml │ ├── build.yml │ ├── pr-labeler.yml │ ├── release-drafter.yml │ ├── release.yml │ └── run-ui-tests.yml ├── .gitignore ├── .idea ├── compiler.xml ├── jarRepositories.xml ├── kotlinc.xml ├── misc.xml ├── modules.xml ├── modules │ ├── intellij-mob.iml │ ├── intellij-mob.main.iml │ └── intellij-mob.test.iml └── vcs.xml ├── .run ├── Run IDE for UI Tests.run.xml ├── Run IDE with Plugin.run.xml ├── Run Plugin Tests.run.xml └── Run Verifications.run.xml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── build.gradle.kts ├── config └── detekt │ └── detekt.yml ├── documents ├── logo.svg ├── menu.png ├── next.png ├── preferences.png ├── preferences_notification.png ├── start.png └── timer_expired.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main ├── java │ └── com │ │ └── nowsprinting │ │ └── intellij_mob │ │ ├── MobBundle.java │ │ ├── action │ │ ├── done │ │ │ └── ui │ │ │ │ ├── DoneDialog.form │ │ │ │ └── DoneDialog.java │ │ ├── next │ │ │ └── ui │ │ │ │ ├── NextDialog.form │ │ │ │ └── NextDialog.java │ │ ├── reset │ │ │ └── ui │ │ │ │ ├── ResetDialog.form │ │ │ │ └── ResetDialog.java │ │ └── start │ │ │ └── ui │ │ │ ├── StartDialog.form │ │ │ └── StartDialog.java │ │ └── config │ │ ├── MobProjectSettings.java │ │ ├── MobSettingsConfigurable.java │ │ └── ui │ │ ├── MobSettingsForm.form │ │ └── MobSettingsForm.java ├── kotlin │ └── com │ │ └── nowsprinting │ │ └── intellij_mob │ │ ├── action │ │ ├── done │ │ │ ├── DoneAction.kt │ │ │ ├── DoneNotificationAction.kt │ │ │ ├── DonePrecondition.kt │ │ │ ├── DoneTask.kt │ │ │ └── GitCommitAndPushExecutorHelper.kt │ │ ├── next │ │ │ ├── NextAction.kt │ │ │ ├── NextNotificationAction.kt │ │ │ ├── NextPrecondition.kt │ │ │ └── NextTask.kt │ │ ├── reset │ │ │ ├── ResetAction.kt │ │ │ ├── ResetPrecondition.kt │ │ │ └── ResetTask.kt │ │ ├── share │ │ │ └── ShareAction.kt │ │ └── start │ │ │ ├── StartAction.kt │ │ │ ├── StartPrecondition.kt │ │ │ └── StartTask.kt │ │ ├── config │ │ └── MobProjectSettingsExtension.kt │ │ ├── git │ │ ├── Add.kt │ │ ├── Branch.kt │ │ ├── Checkout.kt │ │ ├── Commit.kt │ │ ├── Config.kt │ │ ├── Diff.kt │ │ ├── Fetch.kt │ │ ├── GitCommandUtil.kt │ │ ├── GitLocalBranchExtension.kt │ │ ├── GitRepositoryExtension.kt │ │ ├── GitRepositoryUtil.kt │ │ ├── Log.kt │ │ ├── Merge.kt │ │ ├── Pull.kt │ │ ├── Push.kt │ │ └── Status.kt │ │ ├── timer │ │ ├── TimerListener.kt │ │ ├── TimerService.kt │ │ └── statusbar │ │ │ ├── TimerWidget.kt │ │ │ └── TimerWidgetFactory.kt │ │ └── util │ │ ├── Notification.kt │ │ └── Status.kt └── resources │ ├── META-INF │ ├── plugin.xml │ └── pluginIcon.svg │ └── com │ └── nowsprinting │ └── intellij_mob │ └── MobBundle.properties └── test └── kotlin └── com └── nowsprinting └── intellij_mob ├── action ├── next │ └── NextPreconditionKtTest.kt └── start │ └── StartPreconditionKtTest.kt ├── config ├── MobProjectSettingsExtensionKtTest.kt ├── MobProjectSettingsTest.kt └── ui │ └── MobSettingsFormTest.kt ├── git ├── GitLocalBranchExtensionKtTest.kt ├── GitRepositoryExtensionKtTest.kt ├── GitRepositoryUtilKtTest.kt └── LogKtTest.kt ├── testdouble ├── DummyChangeListManager.kt ├── DummyGitRepository.kt ├── DummyHash.kt ├── DummyProject.kt ├── DummyVirtualFile.kt ├── FakeChange.kt └── FakeLogger.kt └── timer └── TimerServiceTest.kt /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @nowsprinting 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [nowsprinting] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # 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 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.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 | schedule: 10 | interval: "daily" 11 | # Maintain dependencies for GitHub Actions 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | 17 | -------------------------------------------------------------------------------- /.github/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | enhancement: [ 'feature/*', 'feat/*' ] 2 | bug: [ 'fix/*', 'hotfix/*' ] 3 | chore: 'chore/*' 4 | skip-changelog: 'release/*' 5 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | 4 | template: | 5 | ## What’s Changed 6 | $CHANGES 7 | 8 | categories: 9 | - title: '🦑 Features' 10 | labels: 11 | - 'enhancement' 12 | - 'feature' 13 | - title: '💔 Breaking Changes' 14 | labels: 15 | - 'breaking-change' 16 | - title: '🐛 Bug Fixes' 17 | labels: 18 | - 'bug' 19 | - 'bugfix' 20 | - 'fix' 21 | - title: '🧰 Maintenance' 22 | labels: 23 | - 'chore' 24 | - 'documentation' 25 | - 'dependencies' 26 | 27 | exclude-labels: 28 | - 'skip-changelog' 29 | 30 | category-template: '### $TITLE' 31 | change-template: '- $TITLE by @$AUTHOR in #$NUMBER' 32 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 33 | 34 | version-resolver: 35 | major: 36 | labels: 37 | - 'major' 38 | - 'breaking-change' 39 | minor: 40 | labels: 41 | - 'minor' 42 | - 'enhancement' 43 | - 'feature' 44 | patch: 45 | labels: 46 | - 'patch' 47 | default: patch 48 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint workflow files 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - .github/workflows/** 9 | pull_request: 10 | types: [ opened, synchronize, reopened ] # Same as default 11 | paths: 12 | - .github/workflows/** 13 | 14 | permissions: 15 | contents: read 16 | jobs: 17 | lint: 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 5 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Lint 25 | shell: bash 26 | run: | 27 | bash <(curl -LsS --retry 2 https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 28 | ./actionlint -color 29 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for testing and preparing the plugin release in following steps: 2 | # - validate Gradle Wrapper, 3 | # - run 'test' and 'verifyPlugin' tasks, 4 | # - run ~Qodana~ SonarCloud inspections, 5 | # - run 'buildPlugin' task and prepare artifact for the further tests, 6 | # - run 'runPluginVerifier' task, 7 | # - create a draft release. 8 | # 9 | # Workflow is triggered on push and pull_request events. 10 | # 11 | # GitHub Actions reference: https://help.github.com/en/actions 12 | # 13 | ## JBIJPPTPL 14 | 15 | name: Build 16 | on: 17 | # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g. for dependabot pull requests) 18 | push: 19 | branches: 20 | - master 21 | # Trigger the workflow on any pull request 22 | pull_request: 23 | 24 | concurrency: 25 | group: ${{ github.workflow }}-${{ github.ref }} 26 | cancel-in-progress: true 27 | 28 | permissions: write-all 29 | jobs: 30 | 31 | # Run Gradle Wrapper Validation Action to verify the wrapper's checksum 32 | # Run verifyPlugin, IntelliJ Plugin Verifier, and test Gradle tasks 33 | # Build plugin and provide the artifact for the next workflow jobs 34 | build: 35 | name: Build 36 | runs-on: ubuntu-latest 37 | timeout-minutes: 15 38 | outputs: 39 | version: ${{ steps.properties.outputs.version }} 40 | changelog: ${{ steps.properties.outputs.changelog }} 41 | steps: 42 | 43 | # Free GitHub Actions Environment Disk Space 44 | - name: Maximize Build Space 45 | run: | 46 | sudo rm -rf /usr/share/dotnet 47 | sudo rm -rf /usr/local/lib/android 48 | sudo rm -rf /opt/ghc 49 | 50 | # Check out current repository 51 | - name: Fetch Sources 52 | uses: actions/checkout@v4 53 | 54 | # Validate wrapper 55 | - name: Gradle Wrapper Validation 56 | uses: gradle/wrapper-validation-action@v3 57 | 58 | # Setup Java 11 environment for the next steps 59 | - name: Setup Java 60 | uses: actions/setup-java@v4 61 | with: 62 | distribution: zulu 63 | java-version: 11 64 | cache: gradle 65 | 66 | # Set environment variables 67 | - name: Export Properties 68 | id: properties 69 | shell: bash 70 | run: | 71 | PROPERTIES="$(./gradlew properties --console=plain -q)" 72 | VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" 73 | NAME="$(echo "$PROPERTIES" | grep "^pluginName:" | cut -f2- -d ' ')" 74 | CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" 75 | CHANGELOG="${CHANGELOG//'%'/'%25'}" 76 | CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" 77 | CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" 78 | 79 | { 80 | echo "version=$VERSION" 81 | echo "name=$NAME" 82 | echo "changelog=$CHANGELOG" 83 | echo "pluginVerifierHomeDir=~/.pluginVerifier" 84 | } >> "$GITHUB_OUTPUT" 85 | 86 | ./gradlew listProductsReleases # prepare list of IDEs for Plugin Verifier 87 | 88 | # Run tests 89 | - name: Run Tests 90 | run: ./gradlew check 91 | 92 | # Collect Tests Result of failed tests 93 | - name: Collect Tests Result 94 | if: ${{ failure() }} 95 | uses: actions/upload-artifact@v4 96 | with: 97 | name: tests-result 98 | path: ${{ github.workspace }}/build/reports/tests 99 | 100 | # Collect code coverage 101 | - name: Collect code coverage 102 | uses: actions/upload-artifact@v4 103 | with: 104 | name: code-coverage 105 | path: ${{ github.workspace }}/build/reports/jacoco/test/html 106 | 107 | # Sonar 108 | - name: SonarCloud Scan 109 | uses: sonarsource/sonarcloud-github-action@v3.1 110 | with: 111 | args: > 112 | -Dsonar.projectKey=remotemobprogramming_intellij-mob 113 | -Dsonar.organization=remotemobprogramming 114 | -Dsonar.sources=src/main/kotlin 115 | -Dsonar.exclusions=*.form,**/resources 116 | -Dsonar.tests=src/test/kotlin 117 | -Dsonar.junit.reportPaths=build/test-results/test/*.xml 118 | -Dsonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml 119 | env: 120 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 121 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 122 | JAVA_HOME: '' # Avoid 'java: not found' error 123 | if: github.repository_owner == 'remotemobprogramming' # Skip because can not read secrets from the public fork 124 | 125 | # Cache Plugin Verifier IDEs 126 | - name: Setup Plugin Verifier IDEs Cache 127 | uses: actions/cache@v4 128 | with: 129 | path: ${{ steps.properties.outputs.pluginVerifierHomeDir }}/ides 130 | key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }} 131 | 132 | # Run Verify Plugin task and IntelliJ Plugin Verifier tool 133 | - name: Run Plugin Verification tasks 134 | run: ./gradlew runPluginVerifier -Pplugin.verifier.home.dir=${{ steps.properties.outputs.pluginVerifierHomeDir }} 135 | 136 | # Collect Plugin Verifier Result 137 | - name: Collect Plugin Verifier Result 138 | if: ${{ always() }} 139 | uses: actions/upload-artifact@v4 140 | with: 141 | name: pluginVerifier-result 142 | path: ${{ github.workspace }}/build/reports/pluginVerifier 143 | 144 | # Prepare plugin archive content for creating artifact 145 | - name: Prepare Plugin Artifact 146 | id: artifact 147 | shell: bash 148 | run: | 149 | cd ${{ github.workspace }}/build/distributions 150 | FILENAME=$(ls ./*.zip) 151 | unzip "$FILENAME" -d content 152 | 153 | echo "filename=${FILENAME:2:-4}" >> "$GITHUB_OUTPUT" 154 | 155 | # Store already-built plugin as an artifact for downloading 156 | - name: Upload artifact 157 | uses: actions/upload-artifact@v4 158 | with: 159 | name: ${{ steps.artifact.outputs.filename }} 160 | path: ./build/distributions/content/*/* 161 | 162 | # Notification 163 | - uses: 8398a7/action-slack@v3 164 | with: 165 | status: ${{ job.status }} 166 | fields: repo,message,job,pullRequest 167 | mention: here 168 | if_mention: failure 169 | env: 170 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required 171 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # optional 172 | if: always() # Pick up events even if the job fails or is canceled. 173 | -------------------------------------------------------------------------------- /.github/workflows/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | name: PR Labeler 2 | 3 | on: 4 | pull_request: 5 | types: [ opened ] 6 | 7 | permissions: write-all 8 | jobs: 9 | pr-labeler: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 5 12 | 13 | steps: 14 | - uses: TimonVS/pr-labeler-action@v5 15 | with: 16 | configuration-path: .github/pr-labeler.yml 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | permissions: write-all 14 | 15 | jobs: 16 | update_release_draft: 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 5 19 | 20 | steps: 21 | - uses: release-drafter/release-drafter@v6 22 | with: 23 | config-name: release-drafter.yml 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for handling the release process based on the draft release prepared 2 | # with the Build workflow. Running the publishPlugin task requires the PUBLISH_TOKEN secret provided. 3 | 4 | name: Release 5 | on: 6 | release: 7 | types: [prereleased, released] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | 15 | # Prepare and publish the plugin to the Marketplace repository 16 | release: 17 | name: Publish Plugin 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 5 20 | permissions: 21 | contents: write 22 | pull-requests: write 23 | steps: 24 | 25 | # Check out current repository 26 | - name: Fetch Sources 27 | uses: actions/checkout@v4 28 | with: 29 | ref: ${{ github.event.release.tag_name }} 30 | 31 | # Setup Java 11 environment for the next steps 32 | - name: Setup Java 33 | uses: actions/setup-java@v4 34 | with: 35 | distribution: zulu 36 | java-version: 17 37 | cache: gradle 38 | 39 | # Set environment variables 40 | - name: Export Properties 41 | id: properties 42 | shell: bash 43 | run: | 44 | CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' 45 | ${{ github.event.release.body }} 46 | EOM 47 | )" 48 | 49 | CHANGELOG="${CHANGELOG//'%'/'%25'}" 50 | CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" 51 | CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" 52 | 53 | echo "changelog=$CHANGELOG" >> "$GITHUB_OUTPUT" 54 | 55 | # Update Unreleased section with the current release note 56 | - name: Patch Changelog 57 | if: ${{ steps.properties.outputs.changelog != '' }} 58 | env: 59 | CHANGELOG: ${{ steps.properties.outputs.changelog }} 60 | run: | 61 | ./gradlew patchChangelog --release-note="$CHANGELOG" 62 | 63 | # Publish the plugin to the Marketplace 64 | - name: Publish Plugin 65 | env: 66 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 67 | CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }} 68 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 69 | PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }} 70 | run: ./gradlew publishPlugin 71 | 72 | # Upload artifact as a release asset 73 | - name: Upload Release Asset 74 | env: 75 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 76 | run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/* 77 | 78 | # Create pull request 79 | - name: Create Pull Request 80 | if: ${{ steps.properties.outputs.changelog != '' }} 81 | env: 82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 83 | run: | 84 | VERSION="${{ github.event.release.tag_name }}" 85 | BRANCH="changelog-update-$VERSION" 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 pr create \ 95 | --title "Changelog update - \`$VERSION\`" \ 96 | --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ 97 | --base main \ 98 | --head $BRANCH 99 | 100 | # Notification 101 | - uses: 8398a7/action-slack@v3 102 | with: 103 | status: ${{ job.status }} 104 | fields: repo,message,job,pullRequest 105 | mention: here 106 | if_mention: failure 107 | env: 108 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required 109 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # optional 110 | if: always() # Pick up events even if the job fails or is canceled. 111 | -------------------------------------------------------------------------------- /.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 UI 3 | # - wait for IDE to start 4 | # - run UI tests with 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 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | permissions: write-all 19 | jobs: 20 | 21 | testUI: 22 | runs-on: ${{ matrix.os }} 23 | timeout-minutes: 5 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | include: 28 | - os: ubuntu-latest 29 | runIde: | 30 | export DISPLAY=:99.0 31 | Xvfb -ac :99 -screen 0 1920x1080x16 & 32 | gradle runIdeForUiTests & 33 | - os: windows-latest 34 | runIde: start gradlew.bat runIdeForUiTests 35 | - os: macos-latest 36 | runIde: ./gradlew runIdeForUiTests & 37 | 38 | steps: 39 | 40 | # Check out current repository 41 | - name: Fetch Sources 42 | uses: actions/checkout@v4 43 | 44 | # Setup Java 11 environment for the next steps 45 | - name: Setup Java 46 | uses: actions/setup-java@v4 47 | with: 48 | distribution: zulu 49 | java-version: 17 50 | cache: gradle 51 | 52 | # Run IDEA prepared for UI testing 53 | - name: Run IDE 54 | run: ${{ matrix.runIde }} 55 | 56 | # Wait for IDEA to be started 57 | - name: Health Check 58 | uses: jtalk/url-health-check-action@v4 59 | with: 60 | url: http://127.0.0.1:8082 61 | max-attempts: 15 62 | retry-delay: 30s 63 | 64 | # Run tests 65 | - name: Tests 66 | run: ./gradlew test 67 | 68 | # Notification 69 | - uses: 8398a7/action-slack@v3 70 | with: 71 | status: ${{ job.status }} 72 | fields: repo,message,job,pullRequest 73 | mention: here 74 | if_mention: failure 75 | env: 76 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required 77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # optional 78 | if: always() # Pick up events even if the job fails or is canceled. 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Generated by gibo (https://github.com/simonwhitaker/gibo) 2 | ### https://raw.github.com/github/gitignore/e5323759e387ba347a9d50f8b0ddd16502eb71d4/Gradle.gitignore 3 | 4 | .gradle 5 | **/build/ 6 | !src/**/build/ 7 | 8 | # Ignore Gradle GUI config 9 | gradle-app.setting 10 | 11 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 12 | !gradle-wrapper.jar 13 | 14 | # Avoid ignore Gradle wrappper properties 15 | !gradle-wrapper.properties 16 | 17 | # Cache of project 18 | .gradletasknamecache 19 | 20 | # Eclipse Gradle plugin generated files 21 | # Eclipse Core 22 | .project 23 | # JDT-specific (Eclipse Java Development Tools) 24 | .classpath 25 | 26 | 27 | ### https://raw.github.com/github/gitignore/e5323759e387ba347a9d50f8b0ddd16502eb71d4/Java.gitignore 28 | 29 | # Compiled class file 30 | *.class 31 | 32 | # Log file 33 | *.log 34 | 35 | # BlueJ files 36 | *.ctxt 37 | 38 | # Mobile Tools for Java (J2ME) 39 | .mtj.tmp/ 40 | 41 | # Package Files # 42 | *.jar 43 | *.war 44 | *.nar 45 | *.ear 46 | *.zip 47 | *.tar.gz 48 | *.rar 49 | 50 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 51 | hs_err_pid* 52 | replay_pid* 53 | 54 | 55 | ### https://raw.github.com/github/gitignore/e5323759e387ba347a9d50f8b0ddd16502eb71d4/Kotlin.gitignore 56 | 57 | # Compiled class file 58 | *.class 59 | 60 | # Log file 61 | *.log 62 | 63 | # BlueJ files 64 | *.ctxt 65 | 66 | # Mobile Tools for Java (J2ME) 67 | .mtj.tmp/ 68 | 69 | # Package Files # 70 | *.jar 71 | *.war 72 | *.nar 73 | *.ear 74 | *.zip 75 | *.tar.gz 76 | *.rar 77 | 78 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 79 | hs_err_pid* 80 | replay_pid* 81 | 82 | 83 | ### https://raw.github.com/github/gitignore/e5323759e387ba347a9d50f8b0ddd16502eb71d4/Global/JetBrains.gitignore 84 | 85 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 86 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 87 | 88 | # User-specific stuff 89 | .idea/**/workspace.xml 90 | .idea/**/tasks.xml 91 | .idea/**/usage.statistics.xml 92 | .idea/**/dictionaries 93 | .idea/**/shelf 94 | 95 | # AWS User-specific 96 | .idea/**/aws.xml 97 | 98 | # Generated files 99 | .idea/**/contentModel.xml 100 | 101 | # Sensitive or high-churn files 102 | .idea/**/dataSources/ 103 | .idea/**/dataSources.ids 104 | .idea/**/dataSources.local.xml 105 | .idea/**/sqlDataSources.xml 106 | .idea/**/dynamic.xml 107 | .idea/**/uiDesigner.xml 108 | .idea/**/dbnavigator.xml 109 | 110 | # Gradle 111 | .idea/**/gradle.xml 112 | .idea/**/libraries 113 | 114 | # Gradle and Maven with auto-import 115 | # When using Gradle or Maven with auto-import, you should exclude module files, 116 | # since they will be recreated, and may cause churn. Uncomment if using 117 | # auto-import. 118 | # .idea/artifacts 119 | # .idea/compiler.xml 120 | # .idea/jarRepositories.xml 121 | # .idea/modules.xml 122 | # .idea/*.iml 123 | # .idea/modules 124 | # *.iml 125 | # *.ipr 126 | 127 | # CMake 128 | cmake-build-*/ 129 | 130 | # Mongo Explorer plugin 131 | .idea/**/mongoSettings.xml 132 | 133 | # File-based project format 134 | *.iws 135 | 136 | # IntelliJ 137 | out/ 138 | 139 | # mpeltonen/sbt-idea plugin 140 | .idea_modules/ 141 | 142 | # JIRA plugin 143 | atlassian-ide-plugin.xml 144 | 145 | # Cursive Clojure plugin 146 | .idea/replstate.xml 147 | 148 | # SonarLint plugin 149 | .idea/sonarlint/ 150 | 151 | # Crashlytics plugin (for Android Studio and IntelliJ) 152 | com_crashlytics_export_strings.xml 153 | crashlytics.properties 154 | crashlytics-build.properties 155 | fabric.properties 156 | 157 | # Editor-based Rest Client 158 | .idea/httpRequests 159 | 160 | # Android studio 3.1+ serialized cache file 161 | .idea/caches/build_file_checksums.ser 162 | 163 | 164 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/modules/intellij-mob.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.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 Verifications.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # intellij-mob Changelog 4 | 5 | ## [Unreleased] 6 | - Support for 2022.2 7 | 8 | ## 1.0.3 9 | - Support for 2020.3 10 | 11 | ## 1.0.2 12 | - Fix meta wording 13 | - Fix versioning 14 | 15 | ## 1.0.1-dirty 16 | - Support for 2020.2 17 | - Include icon 18 | 19 | ## 1.0.0 20 | - Launch mob start, next, done, reset in IDE menu | VCS | Mob 21 | - Display timer on statusbar 22 | - Timer expired notification 23 | - Open commit & push dialog after "done", and set `Co-authored-by:` trailer 24 | - "start" also activates screenshare in Zoom (optional) 25 | 26 | ## 1.0.0-rc1 (not released) 27 | - Open commit & push dialog after "done" 28 | - Set `Co-authored-by:` trailer to commit message automatically 29 | - Support start Zoom screenshare on Windows 30 | 31 | ## 1.0.0-beta2 32 | - Add shortcut for show done dialog (ALT+M, D) 33 | - Fix bug: refresh project files after start/next/done/reset 34 | - Fix bug: notify error at done without changes 35 | 36 | ## 1.0.0-beta1 37 | - Display timer on statusbar 38 | 39 | ## 1.0.0-alpha3 40 | - Implements mob start, next, done, reset 41 | - Timer expired notification 42 | - Start screenshare in Zoom 43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REPO?=$(PWD)/sandbox 2 | REMOTE=$(REPO)-remote.git 3 | 4 | .PHONY: sandbox 5 | sandbox: 6 | git init --bare $(REMOTE) 7 | git clone $(REMOTE) $(REPO) 8 | 9 | .PHONY: clean 10 | clean: 11 | rm -rf $(REMOTE) 12 | rm -rf $(REPO) 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift git handover with mob IntelliJ plugin 2 | 3 | ![build](https://github.com/remotemobprogramming/intellij-mob/workflows/build/badge.svg) 4 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=remotemobprogramming_intellij-mob&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=remotemobprogramming_intellij-mob) 5 | [![Version](https://img.shields.io/jetbrains/plugin/v/14266-mob.svg)](https://plugins.jetbrains.com/plugin/14266-mob) 6 | [![Downloads](https://img.shields.io/jetbrains/plugin/d/14266-mob.svg)](https://plugins.jetbrains.com/plugin/14266-mob) 7 | [![rating](https://img.shields.io/jetbrains/plugin/r/rating/14266-mob.svg)](https://plugins.jetbrains.com/plugin/14266-mob) 8 | 9 | ![mob Logo](documents/logo.svg) 10 | 11 | 12 | Swift [git handover](https://www.remotemobprogramming.org/#git-handover) and timer with mob IntelliJ plugin, 13 | it useful for [Remote Mob Programming](https://www.remotemobprogramming.org). 14 | 15 | - mob IntelliJ plugin is a port of [mob command line tool](https://github.com/remotemobprogramming/mob) 16 | - mob is the fast way to [handover code via git](https://www.remotemobprogramming.org/#git-handover) 17 | - mob keeps your `master` branch clean 18 | - mob creates WIP commits on the `mob-session` branch 19 | - mob notifies you when it's time to handover 20 | - mob squash commits at done session and set `Co-authored-by:` trailer in commit message 21 | 22 | 23 | 24 | ## How to install 25 | 26 | There are three ways to install. 27 | 28 | ### Stable channel of JetBrains Plugins Repository 29 | 30 | 1. Open Settings(Windows, Linux) / Preferences(macOS)... | Plugins 31 | 1. Search "Mob" and install 32 | 33 | ### EAP channel of JetBrains Plugins Repository 34 | 35 | Alpha, Beta, and RC versions will only be released on EAP channel. 36 | 37 | 1. Open Settings(Windows, Linux) / Preferences(macOS)... | Plugins | :gear: | Manage Plugin Repositories... 38 | 1. Add `https://plugins.jetbrains.com/plugins/eap/list` 39 | 1. Search "Mob" and install 40 | 41 | ### Download from plugin page 42 | 43 | 1. Open [Mob - plugin for IntelliJ IDEs](https://plugins.jetbrains.com/plugin/14266-mob) page and download latest zip file 44 | 1. Open Settings(Windows, Linux) / Preferences(macOS)... | Plugins | :gear: | Install Plugin from Disk... 45 | 1. Select downloaded zip file to install plugin 46 | 47 | 48 | ## How to use 49 | 50 | Git | Mob | Start Mob Programming as Typist... (shortcut: ALT+M, S) 51 | 52 | ![menu](documents/menu.png) 53 | 54 | ![start dialog](documents/start.png) 55 | 56 | Click OK, so switched to a separate branch. Start mob programming! 57 | 58 | When handover to the next person, 59 | 60 | Git | Mob | Next : Handover to Next Typist... (shortcut: ALT+M, N) 61 | 62 | ![next dialog](documents/next.png) 63 | 64 | Continue with "Start" and handover to the next person with "Next". 65 | Continue with "Start" and handover to the next person with "Next". 66 | Continue with "Start" and handover to the next person with "Next". 67 | ... 68 | 69 | When you're done, 70 | 71 | Git | Mob | Done : Finish Mob Session... (shortcut: ALT+M, D) 72 | 73 | After confirm in a dialog, 74 | get your changes into the staging area of the `master` branch. 75 | Please commit & push into base branch yourself. 76 | 77 | 78 | ## How does it work 79 | 80 | - Start Mob Programming as Typist : creates branch `mob-session` and pulls from `origin/mob-session` 81 | - Next : pushes all changes to `origin/mob-session`in a `mob next [ci-skip]` commit 82 | - Done : squashes all changes in `mob-session` into staging of `master` and removes `mob-session` and `origin/mob-session` 83 | - Set "Timer" in start dialog : start a specific minute timer, notify by a balloon if expired 84 | - Select "Also activates screenshare in Zoom" in start dialog : start screen sharing in Zoom (requires Zoom configuration) 85 | - Select "Stay in WIP branch after executing 'Next' and checkout base branch" in next dialog : after handover code, stay on mob session branch 86 | - Resets Any Unfinished Mob Session : deletes `mob-session` and `origin/mob-session` 87 | 88 | ### Zoom Screen Share Integration 89 | 90 | The "Also activates screenshare in Zoom" feature uses the Zoom keyboard shortcut "Start/Stop Screen Sharing". This only works if you 91 | 92 | - make the shortcut globally available (Zoom > Preferences > Keyboard Shortcuts), and 93 | - keep the default shortcut at CMD+SHIFT+S (macOS)/ ALT+S (Windows, Linux). 94 | - setting under System Preferences required on macOS Catalina (or later?); Security & Privacy -> Privacy tab -> Accessibility, and add your IntelliJ Platform Based IDEs .app 95 | 96 | [More tips on setting up Zoom for effective screen sharing.](https://effectivehomeoffice.com/setup-zoom-for-effective-screen-sharing/) 97 | 98 | 99 | ## How to configure 100 | 101 | Open Settings(Windows, Linux) / Preferences(macOS) | Tools | Mob 102 | 103 | ![preferences](documents/preferences.png) 104 | 105 | Settings are saves to `.idea/mob.xml`. 106 | 107 | If you want a voice notification when the timer expires, 108 | open Notifications settings and turn on "Read aloud" on "Mob Timer" row (macOS only). 109 | 110 | ![notification settings](documents/preferences_notification.png) 111 | 112 | There are two ways to open Notification settings. 113 | 114 | - Preferences... | Appearance & Behavior | Notifications 115 | - Event Log window | :wrench: 116 | 117 | 118 | ## Troubleshoot 119 | 120 | To see `idea.log`, in JetBrains Toolbox, open Settings | Configuration, and click "Show logs directory" button. 121 | 122 | If necessary, get the trace level log. 123 | Open Help | Diagnostic Tools | Debug Log Settings…, and input `#com.nowsprinting.intellij-mob:trace`. 124 | 125 | 126 | ## Milestones 127 | 128 | ### 1.1 129 | 130 | - Good looking timer widget on statusbar 131 | - Pause/resume timer action launch from statusbar 132 | 133 | ### 1.x 134 | 135 | - Integration tests 136 | - Refactor: dialogs uses `DialogWrapper` 137 | - Refactor: tests about `GitRepository` 138 | - Input validation on dialogs 139 | - Support multiple repository 140 | - Can rollback on "done" by splitting the done process into two phases 141 | 142 | 143 | ## How to contribute 144 | 145 | Open an issue or create a pull request. 146 | 147 | 148 | ## Credits 149 | 150 | Original [mob](https://github.com/remotemobprogramming/mob) developed and maintained by [Dr. Simon Harrer](https://twitter.com/simonharrer). 151 | 152 | 153 | 154 | Original and plugin logo designed by [Sonja Scheungrab](https://twitter.com/multebaerr). 155 | 156 | Port to IntelliJ plugin by [Koji Hasegawa](https://twitter.com/nowsprinting) -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.changelog.Changelog 2 | import org.jetbrains.changelog.markdownToHTML 3 | 4 | fun properties(key: String) = project.findProperty(key).toString() 5 | 6 | plugins { 7 | // Java support 8 | id("java") 9 | // Kotlin support 10 | id("org.jetbrains.kotlin.jvm") version "1.8.22" 11 | // Gradle IntelliJ Plugin 12 | id("org.jetbrains.intellij") version "1.13.1" 13 | // Gradle Changelog Plugin 14 | id("org.jetbrains.changelog") version "2.2.1" 15 | // JaCoCo Plugin 16 | id("jacoco") 17 | } 18 | 19 | group = properties("pluginGroup") 20 | version = properties("pluginVersion") 21 | 22 | // Configure project's dependencies 23 | repositories { 24 | mavenCentral() 25 | } 26 | 27 | dependencies { 28 | testImplementation(platform("org.junit:junit-bom:5.11.0")) 29 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") { 30 | because("Only needed to run tests in a version of IntelliJ IDEA that bundles older versions") 31 | } 32 | testImplementation("org.junit.jupiter", "junit-jupiter", "5.8.2") 33 | testImplementation("org.junit.jupiter", "junit-jupiter-params", "5.8.2") 34 | testImplementation("io.mockk:mockk:1.13.12") 35 | } 36 | 37 | // Set the JVM language level used to build project. Use Java 11 for 2020.3+, and Java 17 for 2022.2+. See: https://jb.gg/intellij-platform-versions 38 | kotlin { 39 | jvmToolchain(11) 40 | } 41 | 42 | // Configure Gradle IntelliJ Plugin - read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html 43 | intellij { 44 | pluginName.set(properties("pluginName")) 45 | version.set(properties("platformVersion")) 46 | type.set(properties("platformType")) 47 | 48 | // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file. 49 | plugins.set(properties("platformPlugins").split(',').map(String::trim).filter(String::isNotEmpty)) 50 | } 51 | 52 | // Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin 53 | changelog { 54 | groups.set(emptyList()) 55 | repositoryUrl.set(properties("pluginRepositoryUrl")) 56 | } 57 | 58 | tasks { 59 | wrapper { 60 | gradleVersion = properties("gradleVersion") 61 | } 62 | 63 | patchPluginXml { 64 | version.set(properties("pluginVersion")) 65 | sinceBuild.set(properties("pluginSinceBuild")) 66 | untilBuild.set(properties("pluginUntilBuild")) 67 | 68 | // Extract the section from README.md and provide for the plugin's manifest 69 | pluginDescription.set( 70 | file("README.md").readText().lines().run { 71 | val start = "" 72 | val end = "" 73 | 74 | if (!containsAll(listOf(start, end))) { 75 | throw GradleException("Plugin description section not found in README.md:\n$start ... $end") 76 | } 77 | subList(indexOf(start) + 1, indexOf(end)) 78 | }.joinToString("\n").let { markdownToHTML(it) } 79 | ) 80 | 81 | // Get the latest available change notes from the changelog file 82 | changeNotes.set(provider { 83 | with(changelog) { 84 | renderItem( 85 | getOrNull(properties("pluginVersion")) ?: getLatest(), 86 | Changelog.OutputType.HTML, 87 | ) 88 | } 89 | }) 90 | } 91 | 92 | test { 93 | useJUnitPlatform { 94 | includeEngines("junit-jupiter") 95 | } 96 | finalizedBy(jacocoTestReport) 97 | } 98 | jacocoTestReport { 99 | dependsOn(test) 100 | reports.xml.required.set(true) 101 | } 102 | 103 | // Configure UI tests plugin 104 | // Read more: https://github.com/JetBrains/intellij-ui-test-robot 105 | runIdeForUiTests { 106 | systemProperty("robot-server.port", "8082") 107 | systemProperty("ide.mac.message.dialogs.as.sheets", "false") 108 | systemProperty("jb.privacy.policy.text", "") 109 | systemProperty("jb.consents.confirmation.enabled", "false") 110 | } 111 | 112 | signPlugin { 113 | certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) 114 | privateKey.set(System.getenv("PRIVATE_KEY")) 115 | password.set(System.getenv("PRIVATE_KEY_PASSWORD")) 116 | } 117 | 118 | publishPlugin { 119 | dependsOn("patchChangelog") 120 | token.set(System.getenv("PUBLISH_TOKEN")) 121 | // pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 122 | // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: 123 | // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel 124 | channels.set(listOf(properties("pluginVersion").split('-').getOrElse(1) { "default" }.split('.').first())) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /documents/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | mobo3 9 | 14 | 20 | 25 | 28 | 29 | -------------------------------------------------------------------------------- /documents/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotemobprogramming/intellij-mob/be9851e40fa90b4a7b0512680e62640d535f2d6b/documents/menu.png -------------------------------------------------------------------------------- /documents/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotemobprogramming/intellij-mob/be9851e40fa90b4a7b0512680e62640d535f2d6b/documents/next.png -------------------------------------------------------------------------------- /documents/preferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotemobprogramming/intellij-mob/be9851e40fa90b4a7b0512680e62640d535f2d6b/documents/preferences.png -------------------------------------------------------------------------------- /documents/preferences_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotemobprogramming/intellij-mob/be9851e40fa90b4a7b0512680e62640d535f2d6b/documents/preferences_notification.png -------------------------------------------------------------------------------- /documents/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotemobprogramming/intellij-mob/be9851e40fa90b4a7b0512680e62640d535f2d6b/documents/start.png -------------------------------------------------------------------------------- /documents/timer_expired.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotemobprogramming/intellij-mob/be9851e40fa90b4a7b0512680e62640d535f2d6b/documents/timer_expired.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 2 | 3 | pluginGroup = com.nowsprinting.intellij-mob 4 | pluginName = Mob 5 | pluginRepositoryUrl = https://github.com/remotemobprogramming/intellij-mob 6 | # SemVer format -> https://semver.org 7 | pluginVersion = 1.0.3 8 | 9 | # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 10 | pluginSinceBuild = 222 11 | pluginUntilBuild = 12 | 13 | # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension 14 | platformType = IC 15 | platformVersion = 2022.2 16 | 17 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 18 | # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 19 | platformPlugins = Git4Idea 20 | 21 | # Gradle Releases -> https://github.com/gradle/gradle/releases 22 | gradleVersion = 7.5.1 23 | 24 | # Opt-out flag for bundling Kotlin standard library -> https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library 25 | # suppress inspection "UnusedProperty" 26 | kotlin.stdlib.default.dependency = false 27 | 28 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 29 | org.gradle.unsafe.configuration-cache = true 30 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotemobprogramming/intellij-mob/be9851e40fa90b4a7b0512680e62640d535f2d6b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /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 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | rootProject.name = "intellij-mob" 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/com/nowsprinting/intellij_mob/MobBundle.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob; 6 | 7 | import com.intellij.AbstractBundle; 8 | import com.intellij.reference.SoftReference; 9 | import org.jetbrains.annotations.NonNls; 10 | import org.jetbrains.annotations.PropertyKey; 11 | 12 | import java.lang.ref.Reference; 13 | import java.util.ResourceBundle; 14 | 15 | public class MobBundle { 16 | private static Reference ourBundle; 17 | 18 | @NonNls 19 | private static final String BUNDLE = "com.nowsprinting.intellij_mob.MobBundle"; 20 | 21 | public static String message(@PropertyKey(resourceBundle = BUNDLE) String key, Object... params) { 22 | return AbstractBundle.message(getBundle(), key, params); 23 | } 24 | 25 | private static ResourceBundle getBundle() { 26 | ResourceBundle bundle = null; 27 | 28 | if (ourBundle != null) bundle = ourBundle.get(); 29 | 30 | if (bundle == null) { 31 | bundle = ResourceBundle.getBundle(BUNDLE); 32 | ourBundle = new SoftReference(bundle); 33 | } 34 | return bundle; 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/java/com/nowsprinting/intellij_mob/action/done/ui/DoneDialog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.done.ui; 6 | 7 | import com.nowsprinting.intellij_mob.MobBundle; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import javax.swing.*; 11 | import java.awt.event.*; 12 | 13 | public class DoneDialog extends JDialog { 14 | private JPanel contentPane; 15 | private JButton buttonOK; 16 | private JButton buttonCancel; 17 | private JButton buttonOpenSettings; 18 | private JLabel message; 19 | private boolean openSettings = false; 20 | private boolean ok = false; 21 | 22 | public DoneDialog() { 23 | setContentPane(contentPane); 24 | setModal(true); 25 | getRootPane().setDefaultButton(buttonOK); 26 | 27 | buttonOK.addActionListener(new ActionListener() { 28 | public void actionPerformed(ActionEvent e) { 29 | onOK(); 30 | } 31 | }); 32 | 33 | buttonCancel.addActionListener(new ActionListener() { 34 | public void actionPerformed(ActionEvent e) { 35 | onCancel(); 36 | } 37 | }); 38 | 39 | // call onCancel() when cross is clicked 40 | setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); 41 | addWindowListener(new WindowAdapter() { 42 | public void windowClosing(WindowEvent e) { 43 | onCancel(); 44 | } 45 | }); 46 | 47 | // call onCancel() on ESCAPE 48 | contentPane.registerKeyboardAction(new ActionListener() { 49 | public void actionPerformed(ActionEvent e) { 50 | onCancel(); 51 | } 52 | }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 53 | 54 | buttonOpenSettings.addActionListener(new ActionListener() { 55 | @Override 56 | public void actionPerformed(ActionEvent e) { 57 | onOpenSettings(); 58 | } 59 | }); 60 | } 61 | 62 | /** 63 | * Set pre-condition check results 64 | * 65 | * @param canExecute enable ok button 66 | * @param reason display message 67 | */ 68 | public void setPreconditionResult(boolean canExecute, @Nullable String reason) { 69 | buttonOK.setEnabled(canExecute); 70 | message.setVisible(!canExecute); 71 | message.setText(String.format(MobBundle.message("mob.done.error.precondition"), reason)); 72 | } 73 | 74 | private void onOpenSettings() { 75 | openSettings = true; 76 | dispose(); 77 | } 78 | 79 | private void onOK() { 80 | // add your code here 81 | ok = true; 82 | dispose(); 83 | } 84 | 85 | private void onCancel() { 86 | // add your code here if necessary 87 | dispose(); 88 | } 89 | 90 | public boolean isOpenSettings() { 91 | return openSettings; 92 | } 93 | 94 | public boolean isOk() { 95 | return ok; 96 | } 97 | } -------------------------------------------------------------------------------- /src/main/java/com/nowsprinting/intellij_mob/action/next/ui/NextDialog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.next.ui; 6 | 7 | import com.nowsprinting.intellij_mob.MobBundle; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import javax.swing.*; 11 | import java.awt.event.*; 12 | 13 | public class NextDialog extends JDialog { 14 | private JPanel contentPane; 15 | private JButton buttonOK; 16 | private JButton buttonCancel; 17 | private JTextField wipCommitMessage; 18 | private JCheckBox nextStay; 19 | private JButton buttonOpenSettings; 20 | private JLabel message; 21 | private boolean openSettings = false; 22 | private boolean ok = false; 23 | 24 | public NextDialog() { 25 | setContentPane(contentPane); 26 | setModal(true); 27 | getRootPane().setDefaultButton(buttonOK); 28 | 29 | buttonOK.addActionListener(new ActionListener() { 30 | public void actionPerformed(ActionEvent e) { 31 | onOK(); 32 | } 33 | }); 34 | 35 | buttonCancel.addActionListener(new ActionListener() { 36 | public void actionPerformed(ActionEvent e) { 37 | onCancel(); 38 | } 39 | }); 40 | 41 | // call onCancel() when cross is clicked 42 | setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); 43 | addWindowListener(new WindowAdapter() { 44 | public void windowClosing(WindowEvent e) { 45 | onCancel(); 46 | } 47 | }); 48 | 49 | // call onCancel() on ESCAPE 50 | contentPane.registerKeyboardAction(new ActionListener() { 51 | public void actionPerformed(ActionEvent e) { 52 | onCancel(); 53 | } 54 | }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 55 | 56 | buttonOpenSettings.addActionListener(new ActionListener() { 57 | @Override 58 | public void actionPerformed(ActionEvent e) { 59 | onOpenSettings(); 60 | } 61 | }); 62 | } 63 | 64 | /** 65 | * Set pre-condition check results 66 | * 67 | * @param canExecute enable ok button 68 | * @param reason display message 69 | */ 70 | public void setPreconditionResult(boolean canExecute, @Nullable String reason) { 71 | buttonOK.setEnabled(canExecute); 72 | message.setVisible(!canExecute); 73 | message.setText(String.format(MobBundle.message("mob.next.error.precondition"), reason)); 74 | } 75 | 76 | private void onOpenSettings() { 77 | openSettings = true; 78 | dispose(); 79 | } 80 | 81 | private void onOK() { 82 | // add your code here 83 | ok = true; 84 | dispose(); 85 | } 86 | 87 | private void onCancel() { 88 | // add your code here if necessary 89 | dispose(); 90 | } 91 | 92 | public String getWipCommitMessage() { 93 | return wipCommitMessage.getText(); 94 | } 95 | 96 | public void setWipCommitMessage(String wipCommitMessage) { 97 | this.wipCommitMessage.setText(wipCommitMessage); 98 | } 99 | 100 | public boolean isNextStay() { 101 | return nextStay.isSelected(); 102 | } 103 | 104 | public void setNextStay(boolean nextStay) { 105 | this.nextStay.setSelected(nextStay); 106 | } 107 | 108 | public boolean isOpenSettings() { 109 | return openSettings; 110 | } 111 | 112 | public boolean isOk() { 113 | return ok; 114 | } 115 | } -------------------------------------------------------------------------------- /src/main/java/com/nowsprinting/intellij_mob/action/reset/ui/ResetDialog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.reset.ui; 6 | 7 | import com.nowsprinting.intellij_mob.MobBundle; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import javax.swing.*; 11 | import javax.swing.event.ChangeEvent; 12 | import javax.swing.event.ChangeListener; 13 | import java.awt.event.*; 14 | 15 | public class ResetDialog extends JDialog { 16 | private JPanel contentPane; 17 | private JButton buttonOK; 18 | private JButton buttonCancel; 19 | private JButton buttonOpenSettings; 20 | private JLabel message; 21 | private JCheckBox confirmCheckBox; 22 | private boolean openSettings = false; 23 | private boolean ok = false; 24 | 25 | public ResetDialog() { 26 | setContentPane(contentPane); 27 | setModal(true); 28 | getRootPane().setDefaultButton(buttonOK); 29 | 30 | buttonOK.addActionListener(new ActionListener() { 31 | public void actionPerformed(ActionEvent e) { 32 | onOK(); 33 | } 34 | }); 35 | 36 | buttonCancel.addActionListener(new ActionListener() { 37 | public void actionPerformed(ActionEvent e) { 38 | onCancel(); 39 | } 40 | }); 41 | 42 | // call onCancel() when cross is clicked 43 | setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); 44 | addWindowListener(new WindowAdapter() { 45 | public void windowClosing(WindowEvent e) { 46 | onCancel(); 47 | } 48 | }); 49 | 50 | // call onCancel() on ESCAPE 51 | contentPane.registerKeyboardAction(new ActionListener() { 52 | public void actionPerformed(ActionEvent e) { 53 | onCancel(); 54 | } 55 | }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 56 | 57 | buttonOpenSettings.addActionListener(new ActionListener() { 58 | @Override 59 | public void actionPerformed(ActionEvent e) { 60 | onOpenSettings(); 61 | } 62 | }); 63 | 64 | confirmCheckBox.addChangeListener(new ChangeListener() { 65 | @Override 66 | public void stateChanged(ChangeEvent e) { 67 | buttonOK.setEnabled(confirmCheckBox.isSelected()); 68 | } 69 | }); 70 | 71 | buttonOK.setEnabled(false); 72 | } 73 | 74 | /** 75 | * Set pre-condition check results 76 | * 77 | * @param canExecute enable ok button 78 | * @param reason display message 79 | */ 80 | public void setPreconditionResult(boolean canExecute, @Nullable String reason) { 81 | confirmCheckBox.setEnabled(canExecute); 82 | message.setVisible(!canExecute); 83 | message.setText(String.format(MobBundle.message("mob.done.error.precondition"), reason)); 84 | } 85 | 86 | private void onOpenSettings() { 87 | openSettings = true; 88 | dispose(); 89 | } 90 | 91 | private void onOK() { 92 | // add your code here 93 | ok = true; 94 | dispose(); 95 | } 96 | 97 | private void onCancel() { 98 | // add your code here if necessary 99 | dispose(); 100 | } 101 | 102 | public boolean isOpenSettings() { 103 | return openSettings; 104 | } 105 | 106 | public boolean isOk() { 107 | return ok; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/nowsprinting/intellij_mob/action/start/ui/StartDialog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.start.ui; 6 | 7 | import com.nowsprinting.intellij_mob.MobBundle; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import javax.swing.*; 11 | import java.awt.event.*; 12 | 13 | public class StartDialog extends JDialog { 14 | private JPanel contentPane; 15 | private JButton buttonOK; 16 | private JButton buttonCancel; 17 | private JTextField timerMinutes; 18 | private JCheckBox startWithShare; 19 | private JButton buttonOpenSettings; 20 | private JLabel message; 21 | private boolean openSettings = false; 22 | private boolean ok = false; 23 | 24 | public StartDialog() { 25 | setContentPane(contentPane); 26 | setModal(true); 27 | getRootPane().setDefaultButton(buttonOK); 28 | 29 | buttonOK.addActionListener(new ActionListener() { 30 | public void actionPerformed(ActionEvent e) { 31 | onOK(); 32 | } 33 | }); 34 | 35 | buttonCancel.addActionListener(new ActionListener() { 36 | public void actionPerformed(ActionEvent e) { 37 | onCancel(); 38 | } 39 | }); 40 | 41 | // call onCancel() when cross is clicked 42 | setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); 43 | addWindowListener(new WindowAdapter() { 44 | public void windowClosing(WindowEvent e) { 45 | onCancel(); 46 | } 47 | }); 48 | 49 | // call onCancel() on ESCAPE 50 | contentPane.registerKeyboardAction(new ActionListener() { 51 | public void actionPerformed(ActionEvent e) { 52 | onCancel(); 53 | } 54 | }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 55 | 56 | buttonOpenSettings.addActionListener(new ActionListener() { 57 | @Override 58 | public void actionPerformed(ActionEvent e) { 59 | onOpenSettings(); 60 | } 61 | }); 62 | 63 | // TODO: Add control that can input only numerical value in `timer` 64 | } 65 | 66 | /** 67 | * Set pre-condition check results 68 | * 69 | * @param canExecute enable ok button 70 | * @param reason display message 71 | */ 72 | public void setPreconditionResult(boolean canExecute, @Nullable String reason) { 73 | buttonOK.setEnabled(canExecute); 74 | message.setVisible(!canExecute); 75 | message.setText(String.format(MobBundle.message("mob.start.error.precondition"), reason)); 76 | } 77 | 78 | private void onOpenSettings() { 79 | openSettings = true; 80 | dispose(); 81 | } 82 | 83 | private void onOK() { 84 | // add your code here 85 | ok = true; 86 | dispose(); 87 | } 88 | 89 | private void onCancel() { 90 | // add your code here if necessary 91 | dispose(); 92 | } 93 | 94 | public int getTimerMinutes() { 95 | return Integer.parseInt(this.timerMinutes.getText()); // Only numerical because restrict in JTextField 96 | } 97 | 98 | public void setTimerMinutes(int timerMinutes) { 99 | this.timerMinutes.setText(Integer.toString(timerMinutes)); 100 | } 101 | 102 | public boolean isStartWithShare() { 103 | return this.startWithShare.isSelected(); 104 | } 105 | 106 | public void setStartWithShare(boolean startWithShare) { 107 | this.startWithShare.setSelected(startWithShare); 108 | } 109 | 110 | public boolean isOk() { 111 | return ok; 112 | } 113 | 114 | public boolean isOpenSettings() { 115 | return openSettings; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/nowsprinting/intellij_mob/config/MobProjectSettings.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.config; 6 | 7 | import com.intellij.openapi.components.PersistentStateComponent; 8 | import com.intellij.openapi.components.ServiceManager; 9 | import com.intellij.openapi.components.State; 10 | import com.intellij.openapi.components.Storage; 11 | import com.intellij.openapi.project.Project; 12 | import com.intellij.util.xmlb.XmlSerializerUtil; 13 | import com.nowsprinting.intellij_mob.MobBundle; 14 | import org.jetbrains.annotations.NotNull; 15 | import org.jetbrains.annotations.Nullable; 16 | 17 | @State( 18 | name = "MobProjectSettings", 19 | storages = { 20 | @Storage("mob.xml") 21 | } 22 | ) 23 | public class MobProjectSettings implements PersistentStateComponent { 24 | public String remoteName; 25 | public String baseBranch; 26 | public String wipBranch; 27 | public int timerMinutes; 28 | public boolean startWithShare; 29 | public String wipCommitMessage; 30 | public boolean nextStay; 31 | 32 | public static MobProjectSettings getInstance(Project project) { 33 | return project.getService(MobProjectSettings.class); 34 | } 35 | 36 | @Override 37 | public void loadState(@NotNull MobProjectSettings state) { 38 | XmlSerializerUtil.copyBean(state, this); 39 | } 40 | 41 | @Nullable 42 | @Override 43 | public MobProjectSettings getState() { 44 | return this; 45 | } 46 | 47 | public void noStateLoaded() { 48 | remoteName = readStringDefaultFromResourceBundle("mob.settings.default.remote_name"); 49 | baseBranch = readStringDefaultFromResourceBundle("mob.settings.default.base_branch"); 50 | wipBranch = readStringDefaultFromResourceBundle("mob.settings.default.wip_branch"); 51 | timerMinutes = readIntDefaultFromResourceBundle("mob.settings.default.timer_minutes", 0); 52 | startWithShare = readBooleanDefaultFromResourceBundle("mob.settings.default.start_with_share"); 53 | wipCommitMessage = readStringDefaultFromResourceBundle("mob.settings.default.wip_commit_message"); 54 | nextStay = readBooleanDefaultFromResourceBundle("mob.settings.default.next_stay"); 55 | } 56 | 57 | private String readStringDefaultFromResourceBundle(String key) { 58 | return MobBundle.message(key); 59 | } 60 | 61 | private int readIntDefaultFromResourceBundle(String key, int defaultValue) { 62 | try { 63 | return Integer.parseInt(readStringDefaultFromResourceBundle(key)); 64 | } catch (NumberFormatException e) { 65 | return defaultValue; 66 | } 67 | } 68 | 69 | private boolean readBooleanDefaultFromResourceBundle(String key) { 70 | return readStringDefaultFromResourceBundle(key).toLowerCase().equals("true"); 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/java/com/nowsprinting/intellij_mob/config/MobSettingsConfigurable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.config; 6 | 7 | import com.intellij.openapi.options.ConfigurationException; 8 | import com.intellij.openapi.options.SearchableConfigurable; 9 | import com.intellij.openapi.project.Project; 10 | import com.nowsprinting.intellij_mob.MobBundle; 11 | import com.nowsprinting.intellij_mob.config.ui.MobSettingsForm; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | import javax.swing.*; 15 | 16 | public class MobSettingsConfigurable implements SearchableConfigurable { 17 | private MobSettingsForm mySettingsPane; 18 | private final Project myProject; 19 | 20 | public MobSettingsConfigurable(Project project) { 21 | myProject = project; 22 | } 23 | 24 | public String getDisplayName() { 25 | return MobBundle.message("mob.settings.name"); 26 | } 27 | 28 | @NotNull 29 | public String getId() { 30 | return "mob.settings"; 31 | } 32 | 33 | public String getHelpTopic() { 34 | return null; 35 | } 36 | 37 | public JComponent createComponent() { 38 | if (mySettingsPane == null) { 39 | mySettingsPane = new MobSettingsForm(); 40 | } 41 | reset(); 42 | return mySettingsPane.getPanel(); 43 | } 44 | 45 | public boolean isModified() { 46 | return mySettingsPane != null && mySettingsPane.isModified(getSettings()); 47 | } 48 | 49 | public void apply() throws ConfigurationException { 50 | if (mySettingsPane != null) { 51 | mySettingsPane.applyEditorTo(getSettings()); 52 | } 53 | } 54 | 55 | public void reset() { 56 | if (mySettingsPane != null) { 57 | mySettingsPane.resetEditorFrom(getSettings()); 58 | } 59 | } 60 | 61 | private MobProjectSettings getSettings() { 62 | return MobProjectSettings.getInstance(myProject); 63 | } 64 | 65 | public void disposeUIResources() { 66 | mySettingsPane = null; 67 | } 68 | 69 | public Runnable enableSearch(String option) { 70 | return null; 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/java/com/nowsprinting/intellij_mob/config/ui/MobSettingsForm.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.config.ui; 6 | 7 | import com.nowsprinting.intellij_mob.config.MobProjectSettings; 8 | 9 | import javax.swing.*; 10 | 11 | public class MobSettingsForm { 12 | JPanel panel1; 13 | JTextField wipBranch; 14 | JTextField baseBranch; 15 | JTextField remoteName; 16 | JTextField timerMinutes; 17 | JCheckBox startWithShare; 18 | JTextField wipCommitMessage; 19 | JCheckBox nextStay; 20 | 21 | public JComponent getPanel() { 22 | return panel1; 23 | } 24 | 25 | public boolean isModified(MobProjectSettings settings) { 26 | if (!wipBranch.getText().equals(settings.wipBranch)) return true; 27 | if (!baseBranch.getText().equals(settings.baseBranch)) return true; 28 | if (!remoteName.getText().equals(settings.remoteName)) return true; 29 | if (!timerMinutes.getText().equals(timerMinutesIfZeroReturnEmpty(settings))) return true; 30 | if (!startWithShare.isSelected() == settings.startWithShare) return true; 31 | if (!wipCommitMessage.getText().equals(settings.wipCommitMessage)) return true; 32 | if (!nextStay.isSelected() == settings.nextStay) return true; 33 | return false; 34 | } 35 | 36 | public void applyEditorTo(MobProjectSettings settings) { 37 | settings.wipBranch = wipBranch.getText().trim(); 38 | settings.baseBranch = baseBranch.getText().trim(); 39 | settings.remoteName = remoteName.getText().trim(); 40 | try { 41 | settings.timerMinutes = Integer.parseInt(timerMinutes.getText()); 42 | } catch (NumberFormatException e) { 43 | settings.timerMinutes = 0; 44 | } 45 | settings.startWithShare = startWithShare.isSelected(); 46 | settings.wipCommitMessage = wipCommitMessage.getText(); 47 | settings.nextStay = nextStay.isSelected(); 48 | } 49 | 50 | public void resetEditorFrom(MobProjectSettings settings) { 51 | wipBranch.setText(settings.wipBranch); 52 | baseBranch.setText(settings.baseBranch); 53 | remoteName.setText(settings.remoteName); 54 | timerMinutes.setText(timerMinutesIfZeroReturnEmpty(settings)); 55 | startWithShare.setSelected(settings.startWithShare); 56 | wipCommitMessage.setText(settings.wipCommitMessage); 57 | nextStay.setSelected(settings.nextStay); 58 | } 59 | 60 | private String timerMinutesIfZeroReturnEmpty(MobProjectSettings settings) { 61 | if (settings.timerMinutes > 0) { 62 | return Integer.toString(settings.timerMinutes); 63 | } else { 64 | return ""; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/action/done/DoneAction.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.done 6 | 7 | import com.intellij.openapi.actionSystem.AnAction 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | import com.intellij.openapi.diagnostic.Logger 10 | import com.intellij.openapi.fileEditor.FileDocumentManager 11 | import com.intellij.openapi.options.ShowSettingsUtil 12 | import com.nowsprinting.intellij_mob.MobBundle 13 | import com.nowsprinting.intellij_mob.action.done.ui.DoneDialog 14 | import com.nowsprinting.intellij_mob.config.MobProjectSettings 15 | import com.nowsprinting.intellij_mob.config.MobSettingsConfigurable 16 | import com.nowsprinting.intellij_mob.git.GitRepositoryResult 17 | import com.nowsprinting.intellij_mob.git.getGitRepository 18 | import com.nowsprinting.intellij_mob.git.stayBranch 19 | import com.nowsprinting.intellij_mob.util.notifyError 20 | 21 | class DoneAction : AnAction() { 22 | private val logger = Logger.getInstance(javaClass) 23 | 24 | override fun update(e: AnActionEvent) { 25 | super.update(e) 26 | 27 | val project = e.project ?: throw NullPointerException("AnActionEvent#getProject() was return null") 28 | val settings = MobProjectSettings.getInstance(project) 29 | val repository = when (val result = getGitRepository(project)) { 30 | is GitRepositoryResult.Success -> { 31 | result.repository 32 | } 33 | is GitRepositoryResult.Failure -> { 34 | notifyError(result.reason) 35 | return 36 | } 37 | } 38 | val enabled = repository.stayBranch(settings.wipBranch) 39 | e.presentation.isEnabled = enabled 40 | } 41 | 42 | override fun actionPerformed(e: AnActionEvent) { 43 | val project = e.project ?: throw NullPointerException("AnActionEvent#getProject() was return null") 44 | val settings = MobProjectSettings.getInstance(project) 45 | 46 | FileDocumentManager.getInstance().saveAllDocuments() 47 | logger.debug(MobBundle.message("mob.logging.save_all_documents")) 48 | val (canExecute, reason) = checkDonePrecondition(settings, project) 49 | 50 | val dialog = DoneDialog() 51 | dialog.title = e.presentation.text.removeSuffix("...") 52 | dialog.setPreconditionResult(canExecute, reason) 53 | dialog.pack() 54 | dialog.setLocationRelativeTo(null) // set on screen center 55 | dialog.isVisible = true 56 | 57 | if (dialog.isOpenSettings) { 58 | ShowSettingsUtil.getInstance().showSettingsDialog(project, MobSettingsConfigurable::class.java) 59 | } 60 | 61 | if (dialog.isOk) { 62 | FileDocumentManager.getInstance().saveAllDocuments() 63 | logger.debug(MobBundle.message("mob.logging.save_all_documents")) 64 | DoneTask(settings, e, project, dialog.title).queue() 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/action/done/DoneNotificationAction.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.done 6 | 7 | import com.intellij.notification.Notification 8 | import com.intellij.notification.NotificationAction 9 | import com.intellij.openapi.actionSystem.AnActionEvent 10 | import com.nowsprinting.intellij_mob.MobBundle 11 | 12 | class DoneNotificationAction(text: String? = MobBundle.message("mob.timer.expired.done")) : 13 | NotificationAction(text) { 14 | override fun actionPerformed(e: AnActionEvent, notification: Notification) { 15 | notification.expire() 16 | DoneAction().actionPerformed(e) 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/action/done/DonePrecondition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.done 6 | 7 | import com.intellij.openapi.project.Project 8 | import com.nowsprinting.intellij_mob.config.MobProjectSettings 9 | import com.nowsprinting.intellij_mob.config.validateForDonePrecondition 10 | import com.nowsprinting.intellij_mob.git.GitRepositoryResult 11 | import com.nowsprinting.intellij_mob.git.getGitRepository 12 | import com.nowsprinting.intellij_mob.git.validateForDone 13 | import git4idea.repo.GitRepository 14 | 15 | /** 16 | * Check precondition for mob done command 17 | * 18 | * @return success/failure, with error message 19 | */ 20 | internal fun checkDonePrecondition(settings: MobProjectSettings, project: Project): Pair { 21 | return when (val result = getGitRepository(project)) { 22 | is GitRepositoryResult.Success -> { 23 | result.repository 24 | checkDonePrecondition(settings, result.repository) 25 | } 26 | is GitRepositoryResult.Failure -> { 27 | Pair(false, result.reason) 28 | } 29 | } 30 | } 31 | 32 | internal fun checkDonePrecondition(settings: MobProjectSettings, repository: GitRepository): Pair { 33 | val (validSettings, reasonInvalidSettings) = settings.validateForDonePrecondition() 34 | if (!validSettings) { 35 | return Pair(validSettings, reasonInvalidSettings) 36 | } 37 | val (validRepository, reasonInvalidRepository) = repository.validateForDone(settings) 38 | if (!validRepository) { 39 | return Pair(validRepository, reasonInvalidRepository) 40 | } 41 | return Pair(true, null) 42 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/action/done/GitCommitAndPushExecutorHelper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.done 6 | 7 | import com.intellij.notification.NotificationType 8 | import com.intellij.openapi.application.ApplicationManager 9 | import com.intellij.openapi.application.ModalityState 10 | import com.intellij.openapi.diagnostic.Logger 11 | import com.intellij.openapi.project.Project 12 | import com.intellij.openapi.vcs.changes.ChangeListManager 13 | import com.intellij.openapi.vcs.changes.LocalChangeList 14 | import com.intellij.openapi.vcs.changes.ui.CommitChangeListDialog 15 | import com.nowsprinting.intellij_mob.MobBundle 16 | import com.nowsprinting.intellij_mob.util.notify 17 | 18 | private val logger = Logger.getInstance("#com.nowsprinting.intellij_mob.action.done.GitCommitAndPushExecutorHelperKt") 19 | 20 | /** 21 | * Open git commit & push dialog. 22 | * 23 | * TODO: select modal dialog or non-modal tool window 24 | * Settings/Preferences | Version Control | Commit, select "Use non-modal commit interface" 25 | */ 26 | fun openGitCommitAndPushDialog(project: Project, coAuthors: Set) { 27 | ApplicationManager.getApplication().invokeLater({ 28 | try { 29 | val changes = ChangeListManager.getInstance(project).allChanges 30 | val initialSelection = LocalChangeList.createEmptyChangeList(project, LocalChangeList.getDefaultName()) 31 | val initialCommitMessage = StringBuilder(MobBundle.message("mob.done.commit_dialog.initial_commit_message")) 32 | for (author in coAuthors) { 33 | initialCommitMessage.append("%nCo-authored-by: $author") 34 | } 35 | 36 | logger.debug(MobBundle.message("mob.done.commit_dialog.open_start")) 37 | CommitChangeListDialog.commitChanges( 38 | project, 39 | changes, 40 | initialSelection, 41 | null, 42 | String.format(initialCommitMessage.toString()) 43 | ) 44 | logger.debug(MobBundle.message("mob.done.commit_dialog.closed")) 45 | 46 | } catch (t: Throwable) { 47 | val message = String.format(MobBundle.message("mob.done.commit_dialog.open_failure"), t.toString()) 48 | logger.error(message, t) 49 | notify( 50 | project = project, 51 | title = MobBundle.message("mob.done.please_commit_and_push"), 52 | content = String.format("%n%s", message), 53 | type = NotificationType.WARNING 54 | ) 55 | } 56 | }, ModalityState.defaultModalityState()) 57 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/action/next/NextAction.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.next 6 | 7 | import com.intellij.openapi.actionSystem.AnAction 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | import com.intellij.openapi.diagnostic.Logger 10 | import com.intellij.openapi.fileEditor.FileDocumentManager 11 | import com.intellij.openapi.options.ShowSettingsUtil 12 | import com.nowsprinting.intellij_mob.MobBundle 13 | import com.nowsprinting.intellij_mob.action.next.ui.NextDialog 14 | import com.nowsprinting.intellij_mob.config.MobProjectSettings 15 | import com.nowsprinting.intellij_mob.config.MobSettingsConfigurable 16 | import com.nowsprinting.intellij_mob.git.GitRepositoryResult 17 | import com.nowsprinting.intellij_mob.git.getGitRepository 18 | import com.nowsprinting.intellij_mob.git.stayBranch 19 | import com.nowsprinting.intellij_mob.util.notifyError 20 | 21 | class NextAction : AnAction() { 22 | private val logger = Logger.getInstance(javaClass) 23 | 24 | override fun update(e: AnActionEvent) { 25 | super.update(e) 26 | 27 | val project = e.project ?: throw NullPointerException("AnActionEvent#getProject() was return null") 28 | val settings = MobProjectSettings.getInstance(project) 29 | val repository = when (val result = getGitRepository(project)) { 30 | is GitRepositoryResult.Success -> { 31 | result.repository 32 | } 33 | is GitRepositoryResult.Failure -> { 34 | notifyError(result.reason) 35 | return 36 | } 37 | } 38 | val enabled = repository.stayBranch(settings.wipBranch) 39 | e.presentation.isEnabled = enabled 40 | } 41 | 42 | override fun actionPerformed(e: AnActionEvent) { 43 | val project = e.project ?: throw NullPointerException("AnActionEvent#getProject() was return null") 44 | val settings = MobProjectSettings.getInstance(project) 45 | 46 | FileDocumentManager.getInstance().saveAllDocuments() 47 | logger.debug(MobBundle.message("mob.logging.save_all_documents")) 48 | val (canExecute, reason) = checkNextPrecondition(settings, project) 49 | 50 | val dialog = NextDialog() 51 | dialog.title = e.presentation.text.removeSuffix("...") 52 | dialog.wipCommitMessage = settings.wipCommitMessage 53 | dialog.isNextStay = settings.nextStay 54 | dialog.setPreconditionResult(canExecute, reason) 55 | dialog.pack() 56 | dialog.setLocationRelativeTo(null) // set on screen center 57 | dialog.isVisible = true 58 | 59 | if (dialog.isOpenSettings) { 60 | ShowSettingsUtil.getInstance().showSettingsDialog(project, MobSettingsConfigurable::class.java) 61 | } 62 | 63 | if (dialog.isOk) { 64 | settings.wipCommitMessage = dialog.wipCommitMessage 65 | settings.nextStay = dialog.isNextStay 66 | FileDocumentManager.getInstance().saveAllDocuments() 67 | logger.debug(MobBundle.message("mob.logging.save_all_documents")) 68 | NextTask(settings, project, dialog.title).queue() 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/action/next/NextNotificationAction.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.next 6 | 7 | import com.intellij.notification.Notification 8 | import com.intellij.notification.NotificationAction 9 | import com.intellij.openapi.actionSystem.AnActionEvent 10 | import com.nowsprinting.intellij_mob.MobBundle 11 | 12 | class NextNotificationAction(text: String? = MobBundle.message("mob.timer.expired.next")) : 13 | NotificationAction(text) { 14 | override fun actionPerformed(e: AnActionEvent, notification: Notification) { 15 | notification.expire() 16 | NextAction().actionPerformed(e) 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/action/next/NextPrecondition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.next 6 | 7 | import com.intellij.openapi.project.Project 8 | import com.nowsprinting.intellij_mob.config.MobProjectSettings 9 | import com.nowsprinting.intellij_mob.config.validateForNextPrecondition 10 | import com.nowsprinting.intellij_mob.git.GitRepositoryResult 11 | import com.nowsprinting.intellij_mob.git.getGitRepository 12 | import com.nowsprinting.intellij_mob.git.validateForNext 13 | import git4idea.repo.GitRepository 14 | 15 | /** 16 | * Check precondition for mob next command 17 | * 18 | * @return success/failure, with error message 19 | */ 20 | internal fun checkNextPrecondition(settings: MobProjectSettings, project: Project): Pair { 21 | return when (val result = getGitRepository(project)) { 22 | is GitRepositoryResult.Success -> { 23 | result.repository 24 | checkNextPrecondition(settings, result.repository) 25 | } 26 | is GitRepositoryResult.Failure -> { 27 | Pair(false, result.reason) 28 | } 29 | } 30 | } 31 | 32 | internal fun checkNextPrecondition(settings: MobProjectSettings, repository: GitRepository): Pair { 33 | val (validSettings, reasonInvalidSettings) = settings.validateForNextPrecondition() 34 | if (!validSettings) { 35 | return Pair(validSettings, reasonInvalidSettings) 36 | } 37 | val (validRepository, reasonInvalidRepository) = repository.validateForNext(settings) 38 | if (!validRepository) { 39 | return Pair(validRepository, reasonInvalidRepository) 40 | } 41 | return Pair(true, null) 42 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/action/next/NextTask.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.next 6 | 7 | import com.intellij.notification.NotificationType 8 | import com.intellij.openapi.diagnostic.Logger 9 | import com.intellij.openapi.progress.ProgressIndicator 10 | import com.intellij.openapi.progress.Task.Backgroundable 11 | import com.intellij.openapi.project.Project 12 | import com.intellij.openapi.vfs.VirtualFileManager 13 | import com.nowsprinting.intellij_mob.MobBundle 14 | import com.nowsprinting.intellij_mob.config.MobProjectSettings 15 | import com.nowsprinting.intellij_mob.config.validateForNextTask 16 | import com.nowsprinting.intellij_mob.git.* 17 | import com.nowsprinting.intellij_mob.timer.TimerService 18 | import com.nowsprinting.intellij_mob.util.notify 19 | import git4idea.repo.GitRepository 20 | 21 | class NextTask(val settings: MobProjectSettings, project: Project, title: String) : Backgroundable(project, title) { 22 | private val logger = Logger.getInstance(javaClass) 23 | private val notifyContents = mutableListOf() 24 | private var completed = false 25 | private var doNotRun = false 26 | private lateinit var repository: GitRepository 27 | 28 | override fun run(indicator: ProgressIndicator) { 29 | val fractionPerCommandSection = 1.0 / 6 30 | indicator.isIndeterminate = false 31 | indicator.fraction = 0.0 32 | logger.debug(String.format(MobBundle.message("mob.notify_content.begin"), title)) 33 | 34 | repository = when (val result = getGitRepository(project)) { 35 | is GitRepositoryResult.Success -> { 36 | result.repository 37 | } 38 | is GitRepositoryResult.Failure -> { 39 | logger.warn(result.reason) 40 | notifyContents.add(String.format(MobBundle.message("mob.notify_content.failure"), result.reason)) 41 | return 42 | } 43 | } 44 | 45 | val (validSettings, reasonInvalidSettings) = settings.validateForNextTask() 46 | if (!validSettings) { 47 | val format = MobBundle.message("mob.next.error.precondition") 48 | val message = String.format(format, reasonInvalidSettings) 49 | logger.warn(message) 50 | notifyContents.add(String.format(MobBundle.message("mob.notify_content.failure"), message)) 51 | return 52 | } 53 | 54 | val (validRepository, reasonInvalidRepository) = repository.validateForNext(settings) 55 | if (!validRepository) { 56 | val format = MobBundle.message("mob.next.error.precondition") 57 | val message = String.format(format, reasonInvalidRepository) 58 | logger.warn(message) 59 | notifyContents.add(String.format(MobBundle.message("mob.notify_content.failure"), message)) 60 | return 61 | } 62 | indicator.fraction += fractionPerCommandSection 63 | 64 | stopTimer() 65 | 66 | val hasUncommittedChanges = hasUncommittedChanges(repository) 67 | val hasUnpushedCommit = hasUnpushedCommit(settings, repository) 68 | if (!hasUncommittedChanges && !hasUnpushedCommit) { 69 | val format = MobBundle.message("mob.next.error.precondition") 70 | val message = String.format(format, MobBundle.message("mob.next.error.reason.has_not_changes")) 71 | logger.warn(message) 72 | notifyContents.add(String.format(MobBundle.message("mob.notify_content.warning"), message)) 73 | doNotRun = true 74 | return 75 | } 76 | indicator.fraction += fractionPerCommandSection 77 | 78 | if (hasUncommittedChanges) { 79 | if (!add(repository, notifyContents)) { 80 | return 81 | } 82 | if (!commit(settings.wipCommitMessage, repository, notifyContents)) { 83 | return 84 | } 85 | } 86 | indicator.fraction += fractionPerCommandSection 87 | 88 | val pushCommits = StringBuilder() 89 | for (v in diffFrom(settings.remoteName, settings.wipBranch, repository)) { 90 | pushCommits.append("%n| ").append(v) 91 | } 92 | indicator.fraction += fractionPerCommandSection 93 | 94 | if (!push(settings.remoteName, settings.wipBranch, repository, notifyContents)) { 95 | return 96 | } 97 | indicator.fraction += fractionPerCommandSection 98 | 99 | notifyContents.add(pushCommits.substring(2)) 100 | 101 | showNextTypist(settings, repository, notifyContents) 102 | indicator.fraction += fractionPerCommandSection 103 | 104 | if (!settings.nextStay) { 105 | checkout(settings.baseBranch, repository, notifyContents) 106 | // If it fails, it does not cause an error 107 | } 108 | 109 | indicator.fraction = 1.0 110 | completed = true 111 | } 112 | 113 | override fun onFinished() { 114 | if (completed) { 115 | logger.debug(String.format(MobBundle.message("mob.notify_content.success"), title)) 116 | notify( 117 | project = project, 118 | title = MobBundle.message("mob.next.task_successful"), 119 | contents = notifyContents, 120 | type = NotificationType.INFORMATION 121 | ) 122 | } else if (doNotRun) { 123 | logger.debug(String.format(MobBundle.message("mob.notify_content.warning"), title)) 124 | notify( 125 | project = project, 126 | title = MobBundle.message("mob.next.task_not_run"), 127 | contents = notifyContents, 128 | type = NotificationType.WARNING 129 | ) 130 | } else { 131 | logger.debug(String.format(MobBundle.message("mob.notify_content.failure"), title)) 132 | notify( 133 | project = project, 134 | title = MobBundle.message("mob.next.task_failure"), 135 | contents = notifyContents, 136 | type = NotificationType.ERROR 137 | ) 138 | } 139 | VirtualFileManager.getInstance().asyncRefresh { 140 | logger.debug(MobBundle.message("mob.logging.refresh")) 141 | } 142 | } 143 | 144 | private fun stopTimer() { 145 | val timer = TimerService.getInstance(project) 146 | timer?.stop() 147 | } 148 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/action/reset/ResetAction.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.reset 6 | 7 | import com.intellij.openapi.actionSystem.AnAction 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | import com.intellij.openapi.diagnostic.Logger 10 | import com.intellij.openapi.fileEditor.FileDocumentManager 11 | import com.intellij.openapi.options.ShowSettingsUtil 12 | import com.nowsprinting.intellij_mob.MobBundle 13 | import com.nowsprinting.intellij_mob.action.reset.ui.ResetDialog 14 | import com.nowsprinting.intellij_mob.config.MobProjectSettings 15 | import com.nowsprinting.intellij_mob.config.MobSettingsConfigurable 16 | import com.nowsprinting.intellij_mob.git.GitRepositoryResult 17 | import com.nowsprinting.intellij_mob.git.getGitRepository 18 | import com.nowsprinting.intellij_mob.git.stayBranch 19 | import com.nowsprinting.intellij_mob.util.notifyError 20 | 21 | class ResetAction : AnAction() { 22 | private val logger = Logger.getInstance(javaClass) 23 | 24 | override fun update(e: AnActionEvent) { 25 | super.update(e) 26 | 27 | val project = e.project ?: throw NullPointerException("AnActionEvent#getProject() was return null") 28 | val settings = MobProjectSettings.getInstance(project) 29 | val repository = when (val result = getGitRepository(project)) { 30 | is GitRepositoryResult.Success -> { 31 | result.repository 32 | } 33 | is GitRepositoryResult.Failure -> { 34 | notifyError(result.reason) 35 | return 36 | } 37 | } 38 | val enabled = repository.stayBranch(settings.wipBranch) 39 | e.presentation.isEnabled = enabled 40 | } 41 | 42 | override fun actionPerformed(e: AnActionEvent) { 43 | val project = e.project ?: throw NullPointerException("AnActionEvent#getProject() was return null") 44 | val settings = MobProjectSettings.getInstance(project) 45 | 46 | FileDocumentManager.getInstance().saveAllDocuments() 47 | logger.debug(MobBundle.message("mob.logging.save_all_documents")) 48 | val (canExecute, reason) = checkResetPrecondition(settings, project) 49 | 50 | val dialog = ResetDialog() 51 | dialog.title = e.presentation.text.removeSuffix("...") 52 | dialog.setPreconditionResult(canExecute, reason) 53 | dialog.pack() 54 | dialog.setLocationRelativeTo(null) // set on screen center 55 | dialog.isVisible = true 56 | 57 | if (dialog.isOpenSettings) { 58 | ShowSettingsUtil.getInstance().showSettingsDialog(project, MobSettingsConfigurable::class.java) 59 | } 60 | 61 | if (dialog.isOk) { 62 | ResetTask(settings, project, dialog.title).queue() 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/action/reset/ResetPrecondition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.reset 6 | 7 | import com.intellij.openapi.project.Project 8 | import com.nowsprinting.intellij_mob.config.MobProjectSettings 9 | import com.nowsprinting.intellij_mob.config.validateForResetPrecondition 10 | import com.nowsprinting.intellij_mob.git.GitRepositoryResult 11 | import com.nowsprinting.intellij_mob.git.getGitRepository 12 | import com.nowsprinting.intellij_mob.git.validateForReset 13 | import git4idea.repo.GitRepository 14 | 15 | /** 16 | * Check precondition for mob reset command 17 | * 18 | * @return success/failure, with error message 19 | */ 20 | internal fun checkResetPrecondition(settings: MobProjectSettings, project: Project): Pair { 21 | return when (val result = getGitRepository(project)) { 22 | is GitRepositoryResult.Success -> { 23 | result.repository 24 | checkResetPrecondition(settings, result.repository) 25 | } 26 | is GitRepositoryResult.Failure -> { 27 | Pair(false, result.reason) 28 | } 29 | } 30 | } 31 | 32 | internal fun checkResetPrecondition(settings: MobProjectSettings, repository: GitRepository): Pair { 33 | val (validSettings, reasonInvalidSettings) = settings.validateForResetPrecondition() 34 | if (!validSettings) { 35 | return Pair(validSettings, reasonInvalidSettings) 36 | } 37 | val (validRepository, reasonInvalidRepository) = repository.validateForReset(settings) 38 | if (!validRepository) { 39 | return Pair(validRepository, reasonInvalidRepository) 40 | } 41 | return Pair(true, null) 42 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/action/reset/ResetTask.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.reset 6 | 7 | import com.intellij.notification.NotificationType 8 | import com.intellij.openapi.diagnostic.Logger 9 | import com.intellij.openapi.progress.ProgressIndicator 10 | import com.intellij.openapi.progress.Task.Backgroundable 11 | import com.intellij.openapi.project.Project 12 | import com.intellij.openapi.vfs.VirtualFileManager 13 | import com.nowsprinting.intellij_mob.MobBundle 14 | import com.nowsprinting.intellij_mob.config.MobProjectSettings 15 | import com.nowsprinting.intellij_mob.config.validateForResetTask 16 | import com.nowsprinting.intellij_mob.git.* 17 | import com.nowsprinting.intellij_mob.timer.TimerService 18 | import com.nowsprinting.intellij_mob.util.notify 19 | import git4idea.repo.GitRepository 20 | 21 | class ResetTask(val settings: MobProjectSettings, project: Project, title: String) : Backgroundable(project, title) { 22 | private val logger = Logger.getInstance(javaClass) 23 | private val notifyContents = mutableListOf() 24 | private var completed = false 25 | private lateinit var repository: GitRepository 26 | 27 | override fun run(indicator: ProgressIndicator) { 28 | val fractionPerCommandSection = 1.0 / 5 29 | indicator.isIndeterminate = false 30 | indicator.fraction = 0.0 31 | logger.debug(String.format(MobBundle.message("mob.notify_content.begin"), title)) 32 | 33 | repository = when (val result = getGitRepository(project)) { 34 | is GitRepositoryResult.Success -> { 35 | result.repository 36 | } 37 | is GitRepositoryResult.Failure -> { 38 | notifyContents.add(String.format(MobBundle.message("mob.notify_content.failure"), result.reason)) 39 | return 40 | } 41 | } 42 | 43 | val (validSettings, reasonInvalidSettings) = settings.validateForResetTask() 44 | if (!validSettings) { 45 | val format = MobBundle.message("mob.reset.error.precondition") 46 | val message = String.format(format, reasonInvalidSettings) 47 | logger.warn(message) 48 | notifyContents.add(String.format(MobBundle.message("mob.notify_content.failure"), message)) 49 | return 50 | } 51 | 52 | val (validRepository, reasonInvalidRepository) = repository.validateForReset(settings) 53 | if (!validRepository) { 54 | val format = MobBundle.message("mob.reset.error.precondition") 55 | val message = String.format(format, reasonInvalidRepository) 56 | logger.warn(message) 57 | notifyContents.add(String.format(MobBundle.message("mob.notify_content.failure"), message)) 58 | return 59 | } 60 | indicator.fraction += fractionPerCommandSection 61 | 62 | stopTimer() 63 | 64 | if (!fetch(repository, notifyContents)) { 65 | return 66 | } 67 | indicator.fraction += fractionPerCommandSection 68 | 69 | if (!checkout(settings.baseBranch, repository, notifyContents)) { 70 | return 71 | } 72 | indicator.fraction += fractionPerCommandSection 73 | 74 | if (repository.hasMobProgrammingBranch(settings)) { 75 | if (!deleteBranch(settings.wipBranch, repository, notifyContents)) { 76 | return 77 | } 78 | } 79 | indicator.fraction += fractionPerCommandSection 80 | 81 | if (repository.hasMobProgrammingBranchOrigin(settings)) { 82 | if (!deleteRemoteBranch(settings.remoteName, settings.wipBranch, repository, notifyContents)) { 83 | return 84 | } 85 | } 86 | indicator.fraction += fractionPerCommandSection 87 | 88 | indicator.fraction = 1.0 89 | completed = true 90 | } 91 | 92 | override fun onFinished() { 93 | if (completed) { 94 | logger.debug(String.format(MobBundle.message("mob.notify_content.success"), title)) 95 | notify( 96 | project = project, 97 | title = MobBundle.message("mob.reset.task_successful"), 98 | contents = notifyContents, 99 | type = NotificationType.INFORMATION 100 | ) 101 | } else { 102 | logger.debug(String.format(MobBundle.message("mob.notify_content.failure"), title)) 103 | notify( 104 | project = project, 105 | title = MobBundle.message("mob.reset.task_failure"), 106 | contents = notifyContents, 107 | type = NotificationType.ERROR 108 | ) 109 | } 110 | VirtualFileManager.getInstance().asyncRefresh { 111 | logger.debug(MobBundle.message("mob.logging.refresh")) 112 | } 113 | } 114 | 115 | private fun stopTimer() { 116 | val timer = TimerService.getInstance(project) 117 | timer?.stop() 118 | } 119 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/action/share/ShareAction.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.share 6 | 7 | import com.intellij.openapi.actionSystem.AnAction 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | import com.intellij.openapi.diagnostic.Logger 10 | import com.nowsprinting.intellij_mob.MobBundle 11 | import com.nowsprinting.intellij_mob.util.notifyError 12 | import com.nowsprinting.intellij_mob.util.notifyWarning 13 | import java.awt.AWTException 14 | import java.awt.Robot 15 | import java.awt.event.KeyEvent 16 | import java.util.* 17 | 18 | /** 19 | * Start screenshare with Zoom (requires configuration in zoom to work) 20 | * It only works if you activate make the screenshare hotkey in zoom globally available, and keep the default shortcut at CMD+SHIFT+S (macOS)/ ALT+S (Windows, Linux). 21 | * And if run on macOS Catalina (or later?), Got to Security & Privacy > Privacy tab > Accessibility > Add `IntelliJ IDEA.app` 22 | */ 23 | class ShareAction : AnAction() { 24 | private val logger = Logger.getInstance(javaClass) 25 | 26 | override fun actionPerformed(e: AnActionEvent) { 27 | val keys = keysByOS() 28 | if (keys.isEmpty()) { 29 | val message = MobBundle.message("mob.screenshare.share_not_supported_os") 30 | logger.warn(message) 31 | notifyWarning(message) 32 | return 33 | } 34 | try { 35 | val robot = Robot() 36 | robot.autoDelay = 200 37 | for (key in keys) { 38 | robot.keyPress(key) 39 | } 40 | for (i in keys.indices.reversed()) { 41 | robot.keyRelease(keys[i]) 42 | } 43 | } catch (e: AWTException) { 44 | val message = MobBundle.message("mob.screenshare.share_failure") 45 | logger.error(message, e) 46 | notifyError(message) 47 | return 48 | } 49 | val message = MobBundle.message("mob.screenshare.share_successful") 50 | logger.info(message) 51 | } 52 | 53 | private fun keysByOS(): IntArray { 54 | val os = System.getProperty("os.name").lowercase(Locale.getDefault()) 55 | if (os.startsWith("mac")) { 56 | return intArrayOf( 57 | KeyEvent.VK_SHIFT, 58 | KeyEvent.VK_META, // command key 59 | KeyEvent.VK_S 60 | ) 61 | } else if (os.startsWith("windows")) { 62 | return intArrayOf( 63 | KeyEvent.VK_ALT, 64 | KeyEvent.VK_S 65 | ) 66 | } else if (os.startsWith("linux")) { // TODO: not test yet on Linux 67 | return intArrayOf( 68 | KeyEvent.VK_ALT, 69 | KeyEvent.VK_S 70 | ) 71 | } 72 | return intArrayOf() 73 | } 74 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/action/start/StartAction.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.start 6 | 7 | import com.intellij.openapi.actionSystem.AnAction 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | import com.intellij.openapi.diagnostic.Logger 10 | import com.intellij.openapi.fileEditor.FileDocumentManager 11 | import com.intellij.openapi.options.ShowSettingsUtil 12 | import com.nowsprinting.intellij_mob.MobBundle 13 | import com.nowsprinting.intellij_mob.action.start.ui.StartDialog 14 | import com.nowsprinting.intellij_mob.config.MobProjectSettings 15 | import com.nowsprinting.intellij_mob.config.MobSettingsConfigurable 16 | import com.nowsprinting.intellij_mob.timer.TimerService 17 | 18 | class StartAction : AnAction() { 19 | private val logger = Logger.getInstance(javaClass) 20 | 21 | override fun update(e: AnActionEvent) { 22 | super.update(e) 23 | 24 | val timer = e.project?.let { TimerService.getInstance(it) } 25 | val enabled = timer?.let { !it.isRunning() } ?: false 26 | e.presentation.isEnabled = enabled 27 | } 28 | 29 | override fun actionPerformed(e: AnActionEvent) { 30 | val project = e.project ?: throw NullPointerException("AnActionEvent#getProject() was return null") 31 | val settings = MobProjectSettings.getInstance(project) 32 | 33 | FileDocumentManager.getInstance().saveAllDocuments() 34 | logger.debug(MobBundle.message("mob.logging.save_all_documents")) 35 | val (canExecute, reason) = checkStartPrecondition(settings, project) 36 | 37 | val dialog = StartDialog() 38 | dialog.title = e.presentation.text.removeSuffix("...") 39 | dialog.timerMinutes = settings.timerMinutes 40 | dialog.isStartWithShare = settings.startWithShare 41 | dialog.setPreconditionResult(canExecute, reason) 42 | dialog.pack() 43 | dialog.setLocationRelativeTo(null) // set on screen center 44 | dialog.isVisible = true 45 | 46 | if (dialog.isOpenSettings) { 47 | ShowSettingsUtil.getInstance().showSettingsDialog(project, MobSettingsConfigurable::class.java) 48 | } 49 | 50 | if (dialog.isOk) { 51 | settings.timerMinutes = dialog.timerMinutes 52 | settings.startWithShare = dialog.isStartWithShare 53 | // Do not call saveAllDocuments() before run start task, Because there may be changes in mob.xml 54 | StartTask(settings, e, project, dialog.title).queue() 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/action/start/StartPrecondition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.start 6 | 7 | import com.intellij.openapi.diagnostic.Logger 8 | import com.intellij.openapi.project.Project 9 | import com.nowsprinting.intellij_mob.config.MobProjectSettings 10 | import com.nowsprinting.intellij_mob.config.validateForStartPrecondition 11 | import com.nowsprinting.intellij_mob.git.GitRepositoryResult 12 | import com.nowsprinting.intellij_mob.git.getGitRepository 13 | import com.nowsprinting.intellij_mob.git.validateForStart 14 | import git4idea.repo.GitRepository 15 | 16 | private val logger = Logger.getInstance("#com.nowsprinting.intellij_mob.action.start.StartPreconditionKt") 17 | 18 | /** 19 | * Check precondition for mob start command 20 | * 21 | * @return success/failure, with error message 22 | */ 23 | internal fun checkStartPrecondition(settings: MobProjectSettings, project: Project): Pair { 24 | return when (val result = getGitRepository(project)) { 25 | is GitRepositoryResult.Success -> { 26 | result.repository 27 | checkStartPrecondition(settings, result.repository) 28 | } 29 | is GitRepositoryResult.Failure -> { 30 | Pair(false, result.reason) 31 | } 32 | } 33 | } 34 | 35 | internal fun checkStartPrecondition(settings: MobProjectSettings, repository: GitRepository): Pair { 36 | val (validSettings, reasonInvalidSettings) = settings.validateForStartPrecondition() 37 | if (!validSettings) { 38 | return Pair(validSettings, reasonInvalidSettings) 39 | } 40 | val (validRepository, reasonInvalidRepository) = repository.validateForStart(settings) 41 | if (!validRepository) { 42 | return Pair(validRepository, reasonInvalidRepository) 43 | } 44 | return Pair(true, null) 45 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/config/MobProjectSettingsExtension.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.config 6 | 7 | import com.nowsprinting.intellij_mob.MobBundle 8 | 9 | private fun MobProjectSettings.validateCommonPrecondition(): Pair { 10 | if (remoteName.isNullOrEmpty()) { 11 | return Pair(false, MobBundle.message("mob.validate_reason.unset_remote_name")) 12 | } 13 | if (baseBranch.isNullOrEmpty()) { 14 | return Pair(false, MobBundle.message("mob.validate_reason.unset_base_branch")) 15 | } 16 | if (wipBranch.isNullOrEmpty()) { 17 | return Pair(false, MobBundle.message("mob.validate_reason.unset_wip_branch")) 18 | } 19 | return Pair(true, null) 20 | } 21 | 22 | fun MobProjectSettings.validateForStartPrecondition(): Pair { 23 | return validateCommonPrecondition() 24 | } 25 | 26 | fun MobProjectSettings.validateForStartTask(): Pair { 27 | return validateForStartPrecondition() 28 | } 29 | 30 | fun MobProjectSettings.validateForNextPrecondition(): Pair { 31 | return validateCommonPrecondition() 32 | } 33 | 34 | fun MobProjectSettings.validateForNextTask(): Pair { 35 | val (valid, reason) = validateForNextPrecondition() 36 | if (!valid) { 37 | return Pair(valid, reason) 38 | } 39 | if (wipCommitMessage.isNullOrEmpty()) { 40 | return Pair(false, MobBundle.message("mob.validate_reason.unset_wip_commit_message")) 41 | } 42 | return Pair(true, null) 43 | } 44 | 45 | fun MobProjectSettings.validateForDonePrecondition(): Pair { 46 | return validateCommonPrecondition() 47 | } 48 | 49 | fun MobProjectSettings.validateForDoneTask(): Pair { 50 | return validateForDonePrecondition() 51 | } 52 | 53 | fun MobProjectSettings.validateForResetPrecondition(): Pair { 54 | return validateCommonPrecondition() 55 | } 56 | 57 | fun MobProjectSettings.validateForResetTask(): Pair { 58 | return validateForResetPrecondition() 59 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/git/Add.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import git4idea.commands.GitCommand 8 | import git4idea.repo.GitRepository 9 | 10 | /** 11 | * git add --all 12 | * 13 | * Must be called from `Task.Backgroundable#run()`. 14 | * If an error occurs, show a notification within this function. 15 | * 16 | * @param repository Git repository 17 | * @param notifyContents Notification content when executing as a series of commands. Add results with this function. 18 | * @param verbose Add `--verbose` option (default: false) 19 | * @return true: Git command successful 20 | */ 21 | fun add(repository: GitRepository, notifyContents: MutableList? = null, verbose: Boolean = false): Boolean { 22 | val command = GitCommand.ADD 23 | val options = listOf("--all") 24 | 25 | return git(command, options, repository, notifyContents, verbose) 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/git/Branch.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import git4idea.commands.GitCommand 8 | import git4idea.repo.GitRepository 9 | 10 | /** 11 | * git branch $branch 12 | * 13 | * Must be called from `Task.Backgroundable#run()`. 14 | * If an error occurs, show a notification within this function. 15 | * 16 | * @param branch Target branch name 17 | * @param repository Git repository 18 | * @param notifyContents Notification content when executing as a series of commands. Add results with this function. 19 | * @param verbose Add `--verbose` option (default: false) 20 | * @return true: Git command successful 21 | */ 22 | fun createBranch( 23 | branch: String, repository: GitRepository, notifyContents: MutableList? = null, verbose: Boolean = false 24 | ): Boolean { 25 | val command = GitCommand.BRANCH 26 | val options = listOf(branch) 27 | 28 | return git(command, options, repository, notifyContents, verbose) 29 | } 30 | 31 | /** 32 | * git branch -D $branch 33 | * 34 | * Must be called from `Task.Backgroundable#run()`. 35 | * If an error occurs, show a notification within this function. 36 | * 37 | * @param branch Target branch name 38 | * @param repository Git repository 39 | * @param notifyContents Notification content when executing as a series of commands. Add results with this function. 40 | * @param verbose Add `--verbose` option (default: false) 41 | * @return true: Git command successful 42 | */ 43 | fun deleteBranch( 44 | branch: String, repository: GitRepository, notifyContents: MutableList? = null, verbose: Boolean = false 45 | ): Boolean { 46 | val command = GitCommand.BRANCH 47 | val options = listOf("-D", branch) 48 | 49 | return git(command, options, repository, notifyContents, verbose) 50 | } 51 | 52 | /** 53 | * git branch --set-upstream-to=$remote/$branch $branch 54 | * 55 | * Must be called from `Task.Backgroundable#run()`. 56 | * If an error occurs, show a notification within this function. 57 | * 58 | * @param remote Target remote name 59 | * @param branch Target branch name 60 | * @param repository Git repository 61 | * @param notifyContents Notification content when executing as a series of commands. Add results with this function. 62 | * @param verbose Add `--verbose` option (default: false) 63 | * @return true: Git command successful 64 | */ 65 | fun setUpstreamToRemoteBranch( 66 | remote: String, 67 | branch: String, 68 | repository: GitRepository, 69 | notifyContents: MutableList? = null, 70 | verbose: Boolean = false 71 | ): Boolean { 72 | val command = GitCommand.BRANCH 73 | val options = listOf("--set-upstream-to=$remote/$branch", branch) 74 | 75 | return git(command, options, repository, notifyContents, verbose) 76 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/git/Checkout.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import git4idea.commands.GitCommand 8 | import git4idea.repo.GitRepository 9 | 10 | /** 11 | * git checkout $branch 12 | * 13 | * Must be called from `Task.Backgroundable#run()`. 14 | * If an error occurs, show a notification within this function. 15 | * 16 | * @param branch Target branch name 17 | * @param repository Git repository 18 | * @param notifyContents Notification content when executing as a series of commands. Add results with this function. 19 | * @param verbose Add `--verbose` option (default: false) 20 | * @return true: Git command successful 21 | */ 22 | fun checkout( 23 | branch: String, repository: GitRepository, notifyContents: MutableList? = null, verbose: Boolean = false 24 | ): Boolean { 25 | val command = GitCommand.CHECKOUT 26 | val options = listOf(branch) 27 | 28 | return git(command, options, repository, notifyContents, verbose) 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/git/Commit.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import git4idea.commands.GitCommand 8 | import git4idea.repo.GitRepository 9 | 10 | /** 11 | * git commit --message $message --no-verify 12 | * 13 | * Must be called from `Task.Backgroundable#run()`. 14 | * If an error occurs, show a notification within this function. 15 | * 16 | * @param message commit message 17 | * @param repository Git repository 18 | * @param notifyContents Notification content when executing as a series of commands. Add results with this function. 19 | * @param verbose Add `--verbose` option (default: false) 20 | * @return true: Git command successful 21 | */ 22 | fun commit( 23 | message: String, repository: GitRepository, notifyContents: MutableList? = null, verbose: Boolean = false 24 | ): Boolean { 25 | val command = GitCommand.COMMIT 26 | val options = listOf("--message", "\"$message\"", "--no-verify") 27 | 28 | return git(command, options, repository, notifyContents, verbose) 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/git/Config.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import git4idea.commands.GitCommand 8 | import git4idea.repo.GitRepository 9 | 10 | /** 11 | * git config --get user.name 12 | * 13 | * Must be called from `Task.Backgroundable#run()`. 14 | * If an error occurs, show a notification within this function. 15 | * 16 | * @param repository Git repository 17 | * @param verbose Add `--verbose` option (default: false) 18 | * @return git user name 19 | */ 20 | fun gitUserName(repository: GitRepository, verbose: Boolean = false): String { 21 | val command = GitCommand.CONFIG 22 | val options = listOf("--get", "user.name") 23 | 24 | val output = git(command, options, repository, verbose) 25 | if (output.isNotEmpty()) { 26 | return output[0].trim() 27 | } else { 28 | return String() 29 | } 30 | } 31 | 32 | /** 33 | * git config --get user.email 34 | * 35 | * Must be called from `Task.Backgroundable#run()`. 36 | * If an error occurs, show a notification within this function. 37 | * 38 | * @param repository Git repository 39 | * @param verbose Add `--verbose` option (default: false) 40 | * @return git user name 41 | */ 42 | fun gitUserEmail(repository: GitRepository, verbose: Boolean = false): String { 43 | val command = GitCommand.CONFIG 44 | val options = listOf("--get", "user.email") 45 | 46 | val output = git(command, options, repository, verbose) 47 | if (output.isNotEmpty()) { 48 | return output[0].trim() 49 | } else { 50 | return String() 51 | } 52 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/git/Diff.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import com.nowsprinting.intellij_mob.config.MobProjectSettings 8 | import git4idea.commands.GitCommand 9 | import git4idea.repo.GitRepository 10 | 11 | /** 12 | * git diff $remote/$branch --stat 13 | * 14 | * Must be called from `Task.Backgroundable#run()`. 15 | * If an error occurs, show a notification within this function. 16 | * 17 | * @param remote Target remote name 18 | * @param branch Target branch name 19 | * @param repository Git repository 20 | * @param verbose Add `--verbose` option (default: false) 21 | * @return change list 22 | */ 23 | fun diffFrom(remote: String, branch: String, repository: GitRepository, verbose: Boolean = false): List { 24 | return diffFrom("$remote/$branch", repository, verbose) 25 | } 26 | 27 | /** 28 | * git diff $branch --stat 29 | * 30 | * Must be called from `Task.Backgroundable#run()`. 31 | * If an error occurs, show a notification within this function. 32 | * 33 | * @param branch Target branch name 34 | * @param repository Git repository 35 | * @param verbose Add `--verbose` option (default: false) 36 | * @return change list 37 | */ 38 | fun diffFrom(branch: String, repository: GitRepository, verbose: Boolean = false): List { 39 | val command = GitCommand.DIFF 40 | val options = listOf(branch, "--stat") 41 | 42 | return git(command, options, repository, verbose) 43 | } 44 | 45 | /** 46 | * git diff --cached --stat 47 | * 48 | * Must be called from `Task.Backgroundable#run()`. 49 | * If an error occurs, show a notification within this function. 50 | * 51 | * @param repository Git repository 52 | * @param verbose Add `--verbose` option (default: false) 53 | * @return change list 54 | */ 55 | fun diffCached(repository: GitRepository, verbose: Boolean = false): List { 56 | val command = GitCommand.DIFF 57 | val options = listOf("--cached", "--stat") 58 | 59 | return git(command, options, repository, verbose) 60 | } 61 | 62 | /** 63 | * Check exist unpushed commit(s). 64 | * 65 | * Must be called from `Task.Backgroundable#run()`. 66 | * If an error occurs, show a notification within this function. 67 | * 68 | * @param settings Use remoteName, wipBranch 69 | * @param repository Git repository 70 | * @param verbose Add `--verbose` option (default: false) 71 | * @return true if exist unpushed commit(s) 72 | * 73 | */ 74 | fun hasUnpushedCommit(settings: MobProjectSettings, repository: GitRepository, verbose: Boolean = false): Boolean { 75 | val commits = diffFrom(settings.remoteName, settings.wipBranch, repository, verbose) 76 | return commits.isNotEmpty() 77 | } 78 | 79 | /** 80 | * Check diff from remote/base 81 | * 82 | * Must be called from `Task.Backgroundable#run()`. 83 | * If an error occurs, show a notification within this function. 84 | * 85 | * @param settings Use remoteName, wipBranch 86 | * @param repository Git repository 87 | * @param verbose Add `--verbose` option (default: false) 88 | * @return true if exist unpushed commit(s) 89 | * 90 | */ 91 | fun hasChangesForDone(settings: MobProjectSettings, repository: GitRepository, verbose: Boolean = false): Boolean { 92 | val commits = diffFrom(settings.remoteName, settings.baseBranch, repository, verbose) 93 | return commits.isNotEmpty() 94 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/git/Fetch.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import git4idea.commands.GitCommand 8 | import git4idea.repo.GitRepository 9 | 10 | /** 11 | * git fetch --prune 12 | * 13 | * Must be called from `Task.Backgroundable#run()`. 14 | * If an error occurs, show a notification within this function. 15 | * 16 | * @param repository Git repository 17 | * @param notifyContents Notification content when executing as a series of commands. Add results with this function. 18 | * @param verbose Add `--verbose` option (default: false) 19 | * @return true: Git command successful 20 | */ 21 | fun fetch(repository: GitRepository, notifyContents: MutableList? = null, verbose: Boolean = false): Boolean { 22 | val command = GitCommand.FETCH 23 | val options = listOf("--prune") 24 | 25 | return git(command, options, repository, notifyContents, verbose) 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/git/GitCommandUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import com.intellij.openapi.diagnostic.Logger 8 | import com.nowsprinting.intellij_mob.MobBundle 9 | import com.nowsprinting.intellij_mob.util.notifyError 10 | import git4idea.commands.Git 11 | import git4idea.commands.GitCommand 12 | import git4idea.commands.GitLineHandler 13 | import git4idea.repo.GitRepository 14 | 15 | private val logger = Logger.getInstance("#com.nowsprinting.intellij_mob.git.GitCommandUtilKt") 16 | 17 | /** 18 | * Execute git command. 19 | * 20 | * Must be called from `Task.Backgroundable#run()`. 21 | * If an error occurs, show a notification within this function. 22 | * 23 | * @param command Execute git command 24 | * @param options Git command options 25 | * @param repository Git repository 26 | * @param notifyContents Notification content when executing as a series of commands. Add results with this function. 27 | * @param verbose Add `--verbose` option (default: false) 28 | * @return true: Git command successful 29 | */ 30 | fun git( 31 | command: GitCommand, 32 | options: List = listOf(), 33 | repository: GitRepository, 34 | notifyContents: MutableList? = null, 35 | verbose: Boolean = false 36 | ): Boolean { 37 | val commandFull = StringBuilder("git $command") 38 | for (v in options) { 39 | commandFull.append(" ").append(v) 40 | } 41 | logger.debug(String.format(MobBundle.message("mob.notify_content.begin"), commandFull)) 42 | logCurrentRevision(repository) 43 | 44 | val handler = GitLineHandler(repository.project, repository.root, command) 45 | handler.addParameters(options) 46 | if (verbose) { 47 | handler.addParameters("--verbose") 48 | } 49 | 50 | val result = Git.getInstance().runCommand(handler) 51 | if (result.output.isNotEmpty()) { 52 | logger.debug(result.outputAsJoinedString) 53 | } 54 | 55 | if (!result.success()) { 56 | val gitErrorMessage = result.errorOutputAsJoinedString 57 | notifyError(gitErrorMessage) 58 | 59 | val message = String.format(MobBundle.message("mob.notify_content.failure"), commandFull) 60 | notifyContents?.add(message) 61 | logger.error("$message%n$gitErrorMessage".format()) 62 | return false 63 | } 64 | 65 | val message = String.format(MobBundle.message("mob.notify_content.success"), commandFull) 66 | notifyContents?.add(message) 67 | logger.info(message) 68 | logCurrentRevision(repository) 69 | 70 | return true 71 | } 72 | 73 | /** 74 | * Execute git command and returns output. 75 | * 76 | * Must be called from `Task.Backgroundable#run()`. 77 | * If an error occurs, show a notification within this function. 78 | * 79 | * @param command Execute git command 80 | * @param options Git command options 81 | * @param repository Git repository 82 | * @param verbose Add `--verbose` option (default: false) 83 | * @return output 84 | */ 85 | fun git( 86 | command: GitCommand, 87 | options: List = listOf(), 88 | repository: GitRepository, 89 | verbose: Boolean = false 90 | ): List { 91 | val commandFull = StringBuilder("git $command") 92 | for (v in options) { 93 | commandFull.append(" ").append(v) 94 | } 95 | logger.debug(String.format(MobBundle.message("mob.notify_content.begin"), commandFull)) 96 | logCurrentRevision(repository) 97 | 98 | val handler = GitLineHandler(repository.project, repository.root, command) // maybe with "--no-pager" 99 | handler.addParameters(options) 100 | if (verbose) { 101 | handler.addParameters("--verbose") 102 | } 103 | 104 | val result = Git.getInstance().runCommand(handler) 105 | if (result.output.isNotEmpty()) { 106 | logger.debug(result.outputAsJoinedString) 107 | } 108 | 109 | if (!result.success()) { 110 | val gitErrorMessage = result.errorOutputAsJoinedString 111 | notifyError(gitErrorMessage) 112 | 113 | val message = String.format(MobBundle.message("mob.notify_content.failure"), commandFull) 114 | logger.error("$message%n$gitErrorMessage".format()) 115 | return emptyList() 116 | } 117 | 118 | val message = String.format(MobBundle.message("mob.notify_content.success"), commandFull) 119 | logger.info(message) 120 | logCurrentRevision(repository) 121 | 122 | return result.output 123 | } 124 | 125 | private fun logCurrentRevision(repository: GitRepository) { 126 | if (!repository.isFresh) { 127 | repository.update() 128 | } 129 | logger.debug("repository current revision: ${repository.currentRevision}") 130 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/git/GitLocalBranchExtension.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import git4idea.GitLocalBranch 8 | import git4idea.repo.GitRepository 9 | 10 | fun GitLocalBranch.hasValidUpstream(repository: GitRepository): Boolean { 11 | val trackedBranch = this.findTrackedBranch(repository) ?: return false 12 | return repository.hasRemoteBranch(trackedBranch.name) 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/git/GitRepositoryExtension.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import com.intellij.dvcs.repo.Repository 8 | import com.intellij.openapi.diagnostic.Logger 9 | import com.intellij.openapi.vcs.changes.ChangeListManager 10 | import com.nowsprinting.intellij_mob.MobBundle 11 | import com.nowsprinting.intellij_mob.config.MobProjectSettings 12 | import git4idea.GitLocalBranch 13 | import git4idea.repo.GitRepository 14 | 15 | private val logger = Logger.getInstance("#com.nowsprinting.intellij_mob.git.GitRepositoryExtensionKt") 16 | 17 | fun GitRepository.hasRemote(remoteName: String): Boolean { 18 | for (remote in this.remotes) { 19 | if (remoteName == remote.name) { 20 | return true 21 | } 22 | } 23 | return false 24 | } 25 | 26 | fun GitRepository.hasMobProgrammingBranch(settings: MobProjectSettings): Boolean { 27 | return hasLocalBranch(settings.wipBranch) 28 | } 29 | 30 | fun GitRepository.hasLocalBranch(branchName: String): Boolean { 31 | if (this.getLocalBranch(branchName) != null) { 32 | return true 33 | } 34 | return false 35 | } 36 | 37 | fun GitRepository.getLocalBranch(branchName: String): GitLocalBranch? { 38 | for (branch in this.branches.localBranches) { 39 | if (branch.name == branchName) { 40 | return branch 41 | } 42 | } 43 | return null 44 | } 45 | 46 | fun GitRepository.hasMobProgrammingBranchOrigin(settings: MobProjectSettings): Boolean { 47 | return hasRemoteBranch(settings.remoteName, settings.wipBranch) 48 | } 49 | 50 | fun GitRepository.hasRemoteBranch(remoteName: String, branchName: String): Boolean { 51 | val remoteBranchName = "${remoteName}/${branchName}" 52 | return hasRemoteBranch(remoteBranchName) 53 | } 54 | 55 | fun GitRepository.hasRemoteBranch(remoteBranchName: String): Boolean { 56 | for (branch in this.branches.remoteBranches) { 57 | if (branch.name == remoteBranchName) { 58 | return true 59 | } 60 | } 61 | return false 62 | } 63 | 64 | fun GitRepository.isMobProgramming(settings: MobProjectSettings): Boolean { 65 | return stayBranch(settings.wipBranch) 66 | } 67 | 68 | fun GitRepository.stayBranch(branchName: String): Boolean { 69 | if (this.currentBranchName == branchName) { 70 | return true 71 | } 72 | return false 73 | } 74 | 75 | /** 76 | * Check uncommitted changes with outside `Task.Backgroundable#run()`. 77 | * 78 | * Note: Untracked files are not detected. 79 | */ 80 | fun GitRepository.isNothingToCommit(): Boolean { 81 | if (state != Repository.State.NORMAL) { 82 | logger.info("Repository state is ${state.toString()}") 83 | return false 84 | } 85 | 86 | val changes = ChangeListManager.getInstance(project).allChanges 87 | if (changes.isNotEmpty()) { 88 | logger.info("Has uncommitted changes") 89 | for (v in changes) { 90 | logger.debug(" ${v.type.toString()}: ${v.virtualFile?.path}") 91 | } 92 | return false 93 | } 94 | return true 95 | } 96 | 97 | private fun GitRepository.validateCommon(settings: MobProjectSettings): Pair { 98 | if (!hasRemote(remoteName = settings.remoteName)) { 99 | return Pair(false, MobBundle.message("mob.validate_reason.not_exist_remote_name")) 100 | } 101 | if (!hasRemoteBranch(remoteName = settings.remoteName, branchName = settings.baseBranch)) { 102 | return Pair(false, MobBundle.message("mob.validate_reason.not_exist_base_branch_on_remote")) 103 | } 104 | currentBranch?.let { 105 | if (!it.hasValidUpstream(this)) { 106 | return Pair(false, MobBundle.message("mob.validate_reason.current_branch_has_not_valid_upstream")) 107 | } 108 | } 109 | return Pair(true, null) 110 | } 111 | 112 | /** 113 | * Validate repository for start precondition check and task. 114 | */ 115 | fun GitRepository.validateForStart(settings: MobProjectSettings): Pair { 116 | val (valid, reason) = validateCommon(settings) 117 | if (!valid) { 118 | return Pair(valid, reason) 119 | } 120 | getLocalBranch(settings.baseBranch)?.let { 121 | if (!it.hasValidUpstream(this)) { 122 | return Pair(false, MobBundle.message("mob.validate_reason.base_branch_has_not_valid_upstream")) 123 | } 124 | } 125 | if (!isNothingToCommit()) { 126 | return Pair(false, MobBundle.message("mob.validate_reason.has_uncommitted_changes")) 127 | } 128 | return Pair(true, null) 129 | } 130 | 131 | /** 132 | * Validate repository for next precondition check and task. 133 | * 134 | * Note: Do not check nothing uncommitted changes, because can not detect unpushed commits here. 135 | */ 136 | fun GitRepository.validateForNext(settings: MobProjectSettings): Pair { 137 | val (valid, reason) = validateCommon(settings) 138 | if (!valid) { 139 | return Pair(valid, reason) 140 | } 141 | if (!stayBranch(settings.wipBranch)) { 142 | val message = MobBundle.message("mob.validate_reason.not_stay_wip_branch") 143 | return Pair(false, String.format(message, settings.wipBranch)) 144 | } 145 | // Validate about upstream is passed inside validateCommonPrecondition() 146 | 147 | return Pair(true, null) 148 | } 149 | 150 | /** 151 | * Validate repository for done precondition check and task. 152 | * 153 | * Note: Do not check nothing uncommitted changes, because can not detect unpushed commits here. 154 | */ 155 | fun GitRepository.validateForDone(settings: MobProjectSettings): Pair { 156 | val (valid, reason) = validateCommon(settings) 157 | if (!valid) { 158 | return Pair(valid, reason) 159 | } 160 | if (!stayBranch(settings.wipBranch)) { 161 | val message = MobBundle.message("mob.validate_reason.not_stay_wip_branch") 162 | return Pair(false, String.format(message, settings.wipBranch)) 163 | } 164 | // Validate about upstream is passed inside validateCommonPrecondition() 165 | 166 | return Pair(true, null) 167 | } 168 | 169 | /** 170 | * Validate repository for reset precondition check and task. 171 | */ 172 | fun GitRepository.validateForReset(settings: MobProjectSettings): Pair { 173 | val (valid, reason) = validateCommon(settings) 174 | if (!valid) { 175 | return Pair(valid, reason) 176 | } 177 | if (!stayBranch(settings.wipBranch)) { 178 | val message = MobBundle.message("mob.validate_reason.not_stay_wip_branch") 179 | return Pair(false, String.format(message, settings.wipBranch)) 180 | } 181 | // Validate about upstream is passed inside validateCommonPrecondition() 182 | 183 | return Pair(true, null) 184 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/git/GitRepositoryUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import com.intellij.openapi.diagnostic.Logger 8 | import com.intellij.openapi.project.Project 9 | import com.nowsprinting.intellij_mob.MobBundle 10 | import git4idea.repo.GitRepository 11 | import git4idea.repo.GitRepositoryManager 12 | 13 | private val logger = Logger.getInstance("#com.nowsprinting.intellij_mob.git.GitRepositoryUtilKt") 14 | 15 | sealed class GitRepositoryResult { 16 | class Failure(val reason: String) : GitRepositoryResult() 17 | class Success(val repository: GitRepository) : GitRepositoryResult() 18 | } 19 | 20 | /** 21 | * Get git repository 22 | */ 23 | fun getGitRepository(project: Project): GitRepositoryResult { 24 | val manager = GitRepositoryManager.getInstance(project) 25 | return getGitRepository(manager, logger) 26 | } 27 | 28 | internal fun getGitRepository(manager: GitRepositoryManager, log: Logger): GitRepositoryResult { 29 | val repositories = manager.repositories 30 | if (repositories.count() == 0) { 31 | val result = GitRepositoryResult.Failure(MobBundle.message("mob.start.error.reason.repository_not_found")) 32 | log.error(result.reason) 33 | return result 34 | } 35 | 36 | if (repositories.count() > 1) { 37 | val result = GitRepositoryResult.Failure(MobBundle.message("mob.start.error.reason.has_multiple_repositories")) 38 | // TODO: support multiple repositories 39 | val logMessage = StringBuilder(result.reason) 40 | for (repo in repositories) { 41 | logMessage.append("%nFound repository: $repo.root.path") 42 | } 43 | log.error(logMessage.toString().format()) 44 | return result 45 | } 46 | 47 | log.debug("Found repository: $repositories[0].root.path") 48 | return GitRepositoryResult.Success(repositories[0]) 49 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/git/Log.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import com.intellij.openapi.diagnostic.Logger 8 | import com.nowsprinting.intellij_mob.MobBundle 9 | import com.nowsprinting.intellij_mob.config.MobProjectSettings 10 | import git4idea.commands.GitCommand 11 | import git4idea.repo.GitRepository 12 | 13 | private val logger = Logger.getInstance("#com.nowsprinting.intellij_mob.git.LogKt") 14 | 15 | /** 16 | * git log $baseBranch..$wipBranch --pretty="format:%h %cr <%an>" --abbrev-commit 17 | * 18 | * Must be called from `Task.Backgroundable#run()`. 19 | * If an error occurs, show a notification within this function. 20 | * 21 | * @param settings Use base and wip branch 22 | * @param repository Git repository 23 | * @param verbose Add `--verbose` option (default: false) 24 | * @return commit list 25 | */ 26 | fun logInWip(settings: MobProjectSettings, repository: GitRepository, verbose: Boolean = false): List { 27 | val command = GitCommand.LOG 28 | val from = settings.baseBranch 29 | val to = settings.wipBranch 30 | val options = listOf("$from..$to", "--pretty=format:%h %cr <%an>", "--abbrev-commit") 31 | 32 | return git(command, options, repository, verbose) 33 | } 34 | 35 | /** 36 | * Add (probably) next typist to notification content, if can guess. 37 | * 38 | * Must be called from `Task.Backgroundable#run()`. 39 | * If an error occurs, show a notification within this function. 40 | * 41 | * @param settings Use base and wip branch 42 | * @param repository Git repository 43 | * @param notifyContents Notification content when executing as a series of commands. Add results with this function. 44 | * @param verbose Add `--verbose` option (default: false) 45 | */ 46 | fun showNextTypist( 47 | settings: MobProjectSettings, 48 | repository: GitRepository, 49 | notifyContents: MutableList, 50 | verbose: Boolean = false 51 | ) { 52 | val command = GitCommand.LOG 53 | val from = settings.baseBranch 54 | val to = settings.wipBranch 55 | val options = listOf("$from..$to", "--pretty=format:%an", "--abbrev-commit") 56 | 57 | val authors = git(command, options, repository, verbose) 58 | logger.debug("there have been ${authors.size} changes") 59 | 60 | val gitUserName = gitUserName(repository) 61 | logger.debug("current git user.name is '$gitUserName'") 62 | 63 | var foundAnotherAuthor = false 64 | for (i in 1 until authors.size) { 65 | if (authors[i].trim().equals(gitUserName)) { 66 | if (!foundAnotherAuthor) { 67 | continue 68 | } 69 | val history = StringBuilder() 70 | for (j in (i - 1) downTo 0) { 71 | history.append(authors[j].trim()) 72 | if (j != 0) { 73 | history.append(", ") 74 | } 75 | } 76 | val notifyFormat = MobBundle.message("mob.notify_content.notify") 77 | notifyContents.add(String.format(notifyFormat, "Committers after your last commit: $history")) 78 | notifyContents.add(String.format(notifyFormat, "***${authors[i - 1].trim()}*** is (probably) next.")) 79 | return 80 | } else { 81 | foundAnotherAuthor = true 82 | } 83 | } 84 | } 85 | 86 | /** 87 | * Create co-author list for `Co-authored-by:` trailer. 88 | * 89 | * Must be called from `Task.Backgroundable#run()`. 90 | * If an error occurs, show a notification within this function. 91 | * 92 | * @param settings Use base and wip branch 93 | * @param repository Git repository 94 | * @param verbose Add `--verbose` option (default: false) 95 | */ 96 | fun getCoAuthors( 97 | settings: MobProjectSettings, 98 | repository: GitRepository, 99 | verbose: Boolean = false 100 | ): Set { 101 | val command = GitCommand.LOG 102 | val from = settings.baseBranch 103 | val to = settings.wipBranch 104 | val options = listOf("$from..$to", "--pretty=format:%an <%ae>", "--abbrev-commit") 105 | 106 | val authors = git(command, options, repository, verbose) 107 | logger.debug("there have been ${authors.size} changes") 108 | 109 | val gitUserName = gitUserName(repository) 110 | val gitUserEmail = gitUserEmail(repository) 111 | val gitUser = "$gitUserName <$gitUserEmail>" 112 | logger.debug("current git user is '$gitUser'") 113 | 114 | val uniqueAuthors = mutableSetOf() 115 | for (author in authors) { 116 | if (!author.equals(gitUser)) { 117 | uniqueAuthors.add(author) 118 | } 119 | } 120 | return uniqueAuthors 121 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/git/Merge.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import git4idea.commands.GitCommand 8 | import git4idea.repo.GitRepository 9 | 10 | /** 11 | * git merge $remote/$branch --ff-only 12 | * 13 | * Must be called from `Task.Backgroundable#run()`. 14 | * If an error occurs, show a notification within this function. 15 | * 16 | * @param remote Target remote name 17 | * @param branch Target branch name 18 | * @param repository Git repository 19 | * @param notifyContents Notification content when executing as a series of commands. Add results with this function. 20 | * @param verbose Add `--verbose` option (default: false) 21 | * @return true: Git command successful 22 | */ 23 | fun mergeFastForward( 24 | remote: String, 25 | branch: String, 26 | repository: GitRepository, 27 | notifyContents: MutableList? = null, 28 | verbose: Boolean = false 29 | ): Boolean { 30 | val command = GitCommand.MERGE 31 | val options = listOf("$remote/$branch", "--ff-only") 32 | 33 | return git(command, options, repository, notifyContents, verbose) 34 | } 35 | 36 | /** 37 | * git merge --squash --ff $branch 38 | * 39 | * Must be called from `Task.Backgroundable#run()`. 40 | * If an error occurs, show a notification within this function. 41 | * 42 | * @param branch Target branch name 43 | * @param repository Git repository 44 | * @param notifyContents Notification content when executing as a series of commands. Add results with this function. 45 | * @param verbose Add `--verbose` option (default: false) 46 | * @return true: Git command successful 47 | */ 48 | fun mergeWithSquash( 49 | branch: String, 50 | repository: GitRepository, 51 | notifyContents: MutableList? = null, 52 | verbose: Boolean = false 53 | ): Boolean { 54 | val command = GitCommand.MERGE 55 | val options = listOf("--squash", "--ff", branch) 56 | 57 | return git(command, options, repository, notifyContents, verbose) 58 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/git/Pull.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import git4idea.commands.GitCommand 8 | import git4idea.repo.GitRepository 9 | 10 | /** 11 | * git pull --ff-only 12 | * 13 | * Must be called from `Task.Backgroundable#run()`. 14 | * If an error occurs, show a notification within this function. 15 | * 16 | * @param repository Git repository 17 | * @param notifyContents Notification content when executing as a series of commands. Add results with this function. 18 | * @param verbose Add `--verbose` option (default: false) 19 | * @return true: Git command successful 20 | */ 21 | fun pull(repository: GitRepository, notifyContents: MutableList? = null, verbose: Boolean = false): Boolean { 22 | val command = GitCommand.PULL 23 | val options = listOf("--ff-only") 24 | 25 | return git(command, options, repository, notifyContents, verbose) 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/git/Push.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import git4idea.commands.GitCommand 8 | import git4idea.repo.GitRepository 9 | 10 | /** 11 | * git push --set-upstream $remote $branch 12 | * 13 | * Must be called from `Task.Backgroundable#run()`. 14 | * If an error occurs, show a notification within this function. 15 | * 16 | * @param remote Target remote name 17 | * @param branch Target branch name 18 | * @param repository Git repository 19 | * @param notifyContents Notification content when executing as a series of commands. Add results with this function. 20 | * @param verbose Add `--verbose` option (default: false) 21 | * @return true: Git command successful 22 | */ 23 | fun push( 24 | remote: String, 25 | branch: String, 26 | repository: GitRepository, 27 | notifyContents: MutableList? = null, 28 | verbose: Boolean = false 29 | ): Boolean { 30 | val command = GitCommand.PUSH 31 | val options = listOf("--set-upstream", remote, branch) 32 | 33 | return git(command, options, repository, notifyContents, verbose) 34 | } 35 | 36 | /** 37 | * git push $remote --delete branch 38 | * 39 | * Must be called from `Task.Backgroundable#run()`. 40 | * If an error occurs, show a notification within this function. 41 | * 42 | * @param remote Target remote name 43 | * @param branch Target branch name 44 | * @param repository Git repository 45 | * @param notifyContents Notification content when executing as a series of commands. Add results with this function. 46 | * @param verbose Add `--verbose` option (default: false) 47 | * @return true: Git command successful 48 | */ 49 | fun deleteRemoteBranch( 50 | remote: String, 51 | branch: String, 52 | repository: GitRepository, 53 | notifyContents: MutableList? = null, 54 | verbose: Boolean = false 55 | ): Boolean { 56 | val command = GitCommand.PUSH 57 | val options = listOf(remote, "--delete", branch) 58 | 59 | return git(command, options, repository, notifyContents, verbose) 60 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/git/Status.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import com.nowsprinting.intellij_mob.config.MobProjectSettings 8 | import git4idea.commands.GitCommand 9 | import git4idea.repo.GitRepository 10 | 11 | /** 12 | * Check exist uncommitted changes. 13 | * 14 | * Must be called from `Task.Backgroundable#run()`. 15 | * If an error occurs, show a notification within this function. 16 | * 17 | * @param repository Git repository 18 | * @param verbose Add `--verbose` option (default: false) 19 | * @return true if exist uncommite changes 20 | */ 21 | fun hasUncommittedChanges(repository: GitRepository, verbose: Boolean = false): Boolean { 22 | val command = GitCommand.STATUS 23 | val options = listOf("--short") 24 | 25 | val status = git(command, options, repository, verbose) 26 | return status.isNotEmpty() 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/timer/TimerListener.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.timer 6 | 7 | interface TimerListener { 8 | fun notifyUpdate() 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/timer/TimerService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.timer 6 | 7 | import com.intellij.notification.NotificationGroupManager 8 | import com.intellij.notification.NotificationType 9 | import com.intellij.openapi.components.Service 10 | import com.intellij.openapi.components.service 11 | import com.intellij.openapi.diagnostic.Logger 12 | import com.intellij.openapi.project.Project 13 | import com.nowsprinting.intellij_mob.MobBundle 14 | import com.nowsprinting.intellij_mob.action.done.DoneNotificationAction 15 | import com.nowsprinting.intellij_mob.action.next.NextNotificationAction 16 | import kotlinx.coroutines.GlobalScope 17 | import kotlinx.coroutines.delay 18 | import kotlinx.coroutines.launch 19 | import java.time.LocalDateTime 20 | import java.time.temporal.ChronoUnit 21 | 22 | enum class TimerState { 23 | NOT_RUNNING, 24 | REMAINING_TIME, 25 | OVER_TIME, 26 | ELAPSED_TIME 27 | } 28 | 29 | @Service 30 | class TimerService { 31 | private val logger = Logger.getInstance(javaClass) 32 | private var startTime: LocalDateTime? = null 33 | private var expireTime: LocalDateTime? = null 34 | private var notified = false 35 | private var timerListeners = mutableSetOf() 36 | 37 | init { 38 | timerCoroutine() 39 | } 40 | 41 | fun addListener(listener: TimerListener) { 42 | timerListeners.add(listener) 43 | } 44 | 45 | fun removeListener(listener: TimerListener) { 46 | timerListeners.remove(listener) 47 | } 48 | 49 | private fun notifyUpdate() { 50 | timerListeners.forEach { 51 | it.notifyUpdate() 52 | } 53 | } 54 | 55 | fun start(minutes: Int = 0, now: LocalDateTime = LocalDateTime.now()) { 56 | startTime = now 57 | if (minutes > 0) { 58 | expireTime = startTime?.plusMinutes(minutes.toLong()) 59 | } 60 | notifyUpdate() 61 | logger.info("mob timer started") 62 | } 63 | 64 | fun stop() { 65 | startTime = null 66 | expireTime = null 67 | notified = false 68 | notifyUpdate() 69 | logger.info("mob timer stopped") 70 | } 71 | 72 | fun isRunning(): Boolean { 73 | return startTime != null 74 | } 75 | 76 | fun getState(now: LocalDateTime = LocalDateTime.now()): TimerState { 77 | if (!isRunning()) { 78 | return TimerState.NOT_RUNNING 79 | } 80 | expireTime?.let { 81 | if (it.isAfter(now)) { 82 | return TimerState.REMAINING_TIME 83 | } else { 84 | return TimerState.OVER_TIME 85 | } 86 | } 87 | return TimerState.ELAPSED_TIME 88 | } 89 | 90 | fun getTime(now: LocalDateTime = LocalDateTime.now()): String { 91 | return when (getState(now)) { 92 | TimerState.NOT_RUNNING -> MobBundle.message("mob.timer.not_running_text") 93 | TimerState.REMAINING_TIME -> textFromSec(ChronoUnit.SECONDS.between(now, expireTime)) 94 | TimerState.OVER_TIME -> textFromSec(ChronoUnit.SECONDS.between(expireTime, now)) 95 | TimerState.ELAPSED_TIME -> textFromSec(ChronoUnit.SECONDS.between(startTime, now)) 96 | } 97 | } 98 | 99 | private fun textFromSec(sec: Long): String { 100 | val m = sec / 60 101 | val s = sec - m * 60 102 | return String.format("%02d:%02d", m, s) 103 | } 104 | 105 | private fun isExpired(now: LocalDateTime = LocalDateTime.now()): Boolean { 106 | return getState(now).equals(TimerState.OVER_TIME) 107 | } 108 | 109 | private fun isNeedNotify(now: LocalDateTime = LocalDateTime.now()): Boolean { 110 | return !notified && isExpired(now) 111 | } 112 | 113 | private fun notifyExpire() { 114 | val stickyGroup = NotificationGroupManager.getInstance().getNotificationGroup("Mob Timer") 115 | val notification = stickyGroup.createNotification( 116 | MobBundle.message("mob.timer.expired.title"), 117 | NotificationType.INFORMATION 118 | ) 119 | notification.addAction(NextNotificationAction()) 120 | notification.addAction(DoneNotificationAction()) 121 | notification.notify(null) 122 | } 123 | 124 | private fun timerCoroutine() = GlobalScope.launch { 125 | while (true) { 126 | if (isRunning()) { 127 | notifyUpdate() 128 | } 129 | if (isNeedNotify(LocalDateTime.now())) { 130 | notifyExpire() 131 | notified = true 132 | logger.info("mob timer expired") 133 | } 134 | delay(1000L) 135 | } 136 | } // may it leak??? 137 | 138 | companion object { 139 | @JvmStatic 140 | fun getInstance(project: Project): TimerService? { 141 | return project.service() 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/timer/statusbar/TimerWidget.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.timer.statusbar 6 | 7 | import com.intellij.openapi.diagnostic.Logger 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.ui.popup.ListPopup 10 | import com.intellij.openapi.wm.StatusBar 11 | import com.intellij.openapi.wm.StatusBarWidget.MultipleTextValuesPresentation 12 | import com.intellij.openapi.wm.StatusBarWidget.WidgetPresentation 13 | import com.intellij.openapi.wm.impl.status.EditorBasedWidget 14 | import com.intellij.util.Consumer 15 | import com.intellij.util.concurrency.annotations.RequiresEdt 16 | import com.nowsprinting.intellij_mob.MobBundle 17 | import com.nowsprinting.intellij_mob.timer.TimerListener 18 | import com.nowsprinting.intellij_mob.timer.TimerService 19 | import com.nowsprinting.intellij_mob.timer.TimerState 20 | import java.awt.event.MouseEvent 21 | import javax.swing.Icon 22 | 23 | class TimerWidget(project: Project) : EditorBasedWidget(project), MultipleTextValuesPresentation, TimerListener { 24 | private val logger = Logger.getInstance(javaClass) 25 | private lateinit var timer: TimerService 26 | 27 | init { 28 | TimerService.getInstance(project)?.let { 29 | timer = it 30 | timer.addListener(this) 31 | logger.debug("got a timer service") 32 | } 33 | logger.debug("init mob timer widget completed") 34 | } 35 | 36 | override fun ID(): String { 37 | return ID 38 | } 39 | 40 | override fun install(statusBar: StatusBar) { 41 | super.install(statusBar) 42 | update() 43 | logger.debug("install mob timer widget completed") 44 | } 45 | 46 | override fun dispose() { 47 | super.dispose() 48 | timer.removeListener(this) 49 | logger.debug("dispose mob timer widget completed") 50 | } 51 | 52 | override fun getPresentation(): WidgetPresentation? { 53 | return this 54 | } 55 | 56 | override fun getSelectedValue(): String? { 57 | return timer.getTime() 58 | } 59 | 60 | override fun getTooltipText(): String? { 61 | val state = when (timer.getState()) { 62 | TimerState.NOT_RUNNING -> MobBundle.message("mob.timer_widget.state.not_running") 63 | TimerState.REMAINING_TIME -> MobBundle.message("mob.timer_widget.state.remaining_time") 64 | TimerState.OVER_TIME -> MobBundle.message("mob.timer_widget.state.over_time") 65 | TimerState.ELAPSED_TIME -> MobBundle.message("mob.timer_widget.state.elapsed_time") 66 | } 67 | return "${MobBundle.message("mob.timer_widget.name")}: $state" 68 | } 69 | 70 | override fun getIcon(): Icon? { 71 | return null 72 | } 73 | 74 | override fun getPopupStep(): ListPopup? { 75 | if (project.isDisposed) return null 76 | 77 | // TODO: implements later: timer restart, suspend, resume actions. maybe 78 | return null 79 | } 80 | 81 | override fun getClickConsumer(): Consumer? { 82 | // has no effect since the click opens a list popup, and the consumer is not called for the MultipleTextValuesPresentation 83 | return null 84 | } 85 | 86 | override fun notifyUpdate() { 87 | update() 88 | } 89 | 90 | @RequiresEdt 91 | private fun update() { 92 | if (project.isDisposed) return 93 | myStatusBar.updateWidget(ID()) 94 | } 95 | 96 | companion object { 97 | internal const val ID = "com.nowsprinting.intellij_mob.TimerWidget" 98 | } 99 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/timer/statusbar/TimerWidgetFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.timer.statusbar 6 | 7 | import com.intellij.openapi.diagnostic.Logger 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.util.Disposer 10 | import com.intellij.openapi.wm.StatusBar 11 | import com.intellij.openapi.wm.StatusBarWidget 12 | import com.intellij.openapi.wm.StatusBarWidgetFactory 13 | import com.nowsprinting.intellij_mob.MobBundle 14 | import com.nowsprinting.intellij_mob.timer.TimerService 15 | import org.jetbrains.annotations.Nls 16 | 17 | class TimerWidgetFactory : StatusBarWidgetFactory { 18 | private val logger = Logger.getInstance(javaClass) 19 | private lateinit var myProject: Project 20 | 21 | override fun getId(): String { 22 | return TimerWidget.ID 23 | } 24 | 25 | @Nls 26 | override fun getDisplayName(): String { 27 | return MobBundle.message("mob.timer_widget.name") 28 | } 29 | 30 | override fun isAvailable(project: Project): Boolean { 31 | return true 32 | } 33 | 34 | override fun createWidget(project: Project): TimerWidget { 35 | logger.info("create mob timer widget") 36 | myProject = project 37 | return TimerWidget(project) 38 | } 39 | 40 | override fun disposeWidget(widget: StatusBarWidget) { 41 | logger.info("dispose mob timer widget") 42 | Disposer.dispose(widget) 43 | } 44 | 45 | override fun canBeEnabledOn(statusBar: StatusBar): Boolean { 46 | return true 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/util/Notification.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.util 6 | 7 | import com.intellij.notification.* 8 | import com.intellij.openapi.project.Project 9 | 10 | private val NOTIFICATION_GROUP = NotificationGroupManager.getInstance().getNotificationGroup("Mob") 11 | 12 | /** 13 | * Notify information level message 14 | */ 15 | fun notifyInformation(content: String): Notification? { 16 | return notify(content = content, type = NotificationType.INFORMATION) 17 | } 18 | 19 | /** 20 | * Notify warning level message 21 | */ 22 | fun notifyWarning(content: String): Notification? { 23 | return notify(content = content, type = NotificationType.WARNING) 24 | } 25 | 26 | /** 27 | * Notify error level message 28 | */ 29 | fun notifyError(content: String): Notification? { 30 | return notify(content = content, type = NotificationType.ERROR) 31 | } 32 | 33 | /** 34 | * Notify messages with title 35 | */ 36 | fun notify( 37 | project: Project? = null, 38 | title: String = "", 39 | contents: List, 40 | type: NotificationType 41 | ): Notification? { 42 | var content = StringBuilder() 43 | for (v in contents) { 44 | content.append("%n").append(v) 45 | } 46 | return notify(project = project, title = title, content = content.toString().format(), type = type) 47 | } 48 | 49 | /** 50 | * Notify message with title 51 | */ 52 | fun notify( 53 | project: Project? = null, 54 | title: String = "", 55 | content: String, 56 | type: NotificationType 57 | ): Notification? { 58 | val notification = NOTIFICATION_GROUP.createNotification(title = title, content = content, type = type) 59 | notification.notify(project) 60 | return notification 61 | } 62 | 63 | /** 64 | * Open `EventLog` window 65 | */ 66 | fun openEventLog(project: Project) { 67 | val eventLog = EventLog.getEventLog(project) 68 | if (eventLog != null && !eventLog.isVisible) { 69 | eventLog.activate(Runnable { 70 | val contentName = getContentName(NOTIFICATION_GROUP.displayId) 71 | val content = eventLog.contentManager.findContent(contentName) 72 | if (content != null) { 73 | eventLog.contentManager.setSelectedContent(content) 74 | } 75 | }, true) 76 | } 77 | } 78 | 79 | private fun getContentName(groupId: String): String { 80 | for (category in EventLogCategory.EP_NAME.extensionList) { 81 | if (category.acceptsNotification(groupId)) { 82 | return category.displayName 83 | } 84 | } 85 | return "" 86 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/nowsprinting/intellij_mob/util/Status.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.util 6 | 7 | import com.nowsprinting.intellij_mob.MobBundle 8 | import com.nowsprinting.intellij_mob.config.MobProjectSettings 9 | import com.nowsprinting.intellij_mob.git.logInWip 10 | import com.nowsprinting.intellij_mob.git.isMobProgramming 11 | import git4idea.repo.GitRepository 12 | 13 | /** 14 | * Response `mob status` command 15 | * 16 | * Must be called from `Task.Backgroundable#run()`. 17 | * If an error occurs, show a notification within this function. 18 | * 19 | * @return message for notification content 20 | */ 21 | fun status(repository: GitRepository, settings: MobProjectSettings): String { 22 | val notifyFormat = MobBundle.message("mob.notify_content.notify") 23 | val message = StringBuilder() 24 | 25 | if (repository.isMobProgramming(settings)) { 26 | message.append(String.format(notifyFormat, MobBundle.message("mob.status.is_mob_programming"))) 27 | 28 | val commitsInWip = logInWip(settings, repository) 29 | for (commit in commitsInWip) { 30 | message.append("%n| ").append(commit) 31 | } 32 | } else { 33 | message.append(String.format(notifyFormat, MobBundle.message("mob.status.is_not_mob_programming"))) 34 | } 35 | return message.toString() 36 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | com.nowsprinting.intellij-mob 7 | Mob 8 | Koji Hasegawa 9 | 10 | 12 | com.intellij.modules.platform 13 | Git4Idea 14 | 15 | 17 | 18 | 19 | 22 | 24 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 41 | 42 | 43 | 45 | 46 | 47 | 49 | 50 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | mobo3 9 | 10 | 15 | 21 | 26 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main/resources/com/nowsprinting/intellij_mob/MobBundle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | # 4 | 5 | # Settings (Preferences) 6 | mob.settings.name=Mob 7 | 8 | # Settings (Preferences) dialog labels 9 | mob.settings.section.general=General 10 | mob.settings.label.remote_name=Remote Repository Name 11 | mob.settings.label.base_branch=Base Branch Name 12 | mob.settings.label.wip_branch=WIP Branch Name 13 | mob.settings.section.start_options=Start Options: 14 | mob.settings.label.timer_minutes=Timer [min] 15 | mob.settings.label.start_with_share=Also activates screenshare in Zoom (requires Zoom configuration) 16 | mob.settings.label.start_with_share.tips=This feature uses the Zoom keyboard shortcut "Start/Stop Screen Sharing". This only works if you make the shortcut globally available (Zoom > Preferences > Keyboard Shortcuts), and keep the default shortcut at CMD+SHIFT+S (macOS)/ ALT+S (Windows, Linux). 17 | mob.settings.section.next_options=Next (handover to next typist) Options: 18 | mob.settings.label.wip_commit_message=WIP Commit Message 19 | mob.settings.label.next_stay=Stay in WIP branch after executing 'Next' and checkout base branch 20 | 21 | # Settings (Preferences) dialog default values 22 | mob.settings.default.remote_name=origin 23 | mob.settings.default.base_branch=master 24 | mob.settings.default.wip_branch=mob-session 25 | mob.settings.default.timer_minutes=10 26 | mob.settings.default.start_with_share=false 27 | mob.settings.default.wip_commit_message=mob next [ci-skip] 28 | mob.settings.default.next_stay=true 29 | 30 | # Validations 31 | mob.validate_reason.unset_wip_branch=unset WIP branch name 32 | mob.validate_reason.unset_base_branch=unset base branch name 33 | mob.validate_reason.unset_remote_name=unset remote repository name 34 | mob.validate_reason.unset_wip_commit_message=unset WIP commit message 35 | mob.validate_reason.not_exist_remote_name=remote repository is not exist 36 | mob.validate_reason.base_branch_has_not_valid_upstream=base branch has not valid upstream branch 37 | mob.validate_reason.not_exist_base_branch_on_remote=base branch is not exist on remote 38 | mob.validate_reason.current_branch_has_not_valid_upstream=current branch has not valid upstream branch 39 | mob.validate_reason.has_uncommitted_changes=uncommitted changes present 40 | mob.validate_reason.not_stay_wip_branch=you aren't mob programming, current branch is not %s 41 | 42 | # Start dialog 43 | mob.start.dialog.open_settings=Open Settings / Preferences... 44 | mob.start.error.precondition=Can not start; %s 45 | mob.start.error.reason.repository_not_found=repository not found in this project 46 | mob.start.error.reason.has_multiple_repositories=multiple repositories is not support yet 47 | 48 | # Start task 49 | mob.start.task_successful=Mob Start: successful 50 | mob.start.task_failure=Mob Start: failure 51 | mob.start.rejoining_mob_session=rejoining mob session 52 | mob.start.create_wip_branch_from_base_branch=create %s from %s 53 | mob.start.joining_mob_session=joining mob session 54 | mob.start.purging_local_branch_and_start_new_wip_branch_from_base=purging local branch and start new %s branch from %s 55 | 56 | # Next dialog 57 | mob.next.error.precondition=Can not do next; %s 58 | 59 | # Next task 60 | mob.next.task_successful=Mob Next: successful 61 | mob.next.task_failure=Mob Next: failure 62 | mob.next.task_not_run=Mob Next: do not run 63 | mob.next.error.reason.has_not_changes=nothing was done, so nothing to commit 64 | 65 | # Done dialog 66 | mob.done.confirm1=Brings all commits in the WIP branch back to stage. After execution, 67 | mob.done.confirm2=please commit and push into base branch yourself. 68 | mob.done.error.precondition=Can not do done; %s 69 | 70 | # Done task 71 | mob.done.task_successful=Mob Done: successful 72 | mob.done.task_failure=Mob Done: failure 73 | mob.done.task_not_run=Mob Done: do not run 74 | mob.done.error.reason.nothing_changes_to_squash=nothing was done, so nothing changes to squash in this mob session 75 | mob.done.please_commit_and_push=please commit changes and push into base branch yourself 76 | mob.done.already_ended=someone else already ended your mob session 77 | mob.done.commit_dialog.initial_commit_message=describe the changes here%n 78 | mob.done.commit_dialog.open_start=open git commit dialog... 79 | mob.done.commit_dialog.open_failure=unable to open git commit dialog. cause: %s 80 | mob.done.commit_dialog.closed=git commit dialog closed 81 | 82 | # Reset dialog 83 | mob.reset.confirm=Resets any unfinished mob session. Delete local and remote WIP branch. 84 | mob.reset.foolproof=If you really want to reset, check the this box 85 | mob.reset.error.precondition=Can not do reset; %s 86 | 87 | # Reset task 88 | mob.reset.task_successful=Mob Reset: successful 89 | mob.reset.task_failure=Mob Reset: failure 90 | 91 | # Status 92 | mob.status.is_mob_programming=mob programming in progress 93 | mob.status.is_not_mob_programming=you aren't mob programming right now 94 | 95 | # Timer 96 | mob.timer.start_successful=timer started 97 | mob.timer.start_failure=unable to start timer 98 | mob.timer.not_running_text=Mob 99 | mob.timer.expired.title=Mob Timer: expired 100 | mob.timer.expired.next=Next 101 | mob.timer.expired.done=Done 102 | 103 | # Timer widget 104 | mob.timer_widget.name=Mob Timer 105 | mob.timer_widget.state.not_running=not running 106 | mob.timer_widget.state.remaining_time=remaining time 107 | mob.timer_widget.state.over_time=over time 108 | mob.timer_widget.state.elapsed_time=elapsed time 109 | 110 | # Screen share 111 | mob.screenshare.share_successful=screenshare started in Zoom 112 | mob.screenshare.share_failure=unable to start screenshare in Zoom 113 | mob.screenshare.share_not_supported_os="Also activates screenshare in Zoom" is not supported by your OS 114 | 115 | # Notification content and git command log format 116 | mob.notify_content.notify=📢 %s 117 | mob.notify_content.begin=🏃 %s 118 | mob.notify_content.success=✔ %s 119 | mob.notify_content.warning=⚠ %s 120 | mob.notify_content.failure=❌ %s 121 | 122 | # Logging 123 | mob.logging.save_all_documents=save all documents 124 | mob.logging.refresh=refresh project files 125 | -------------------------------------------------------------------------------- /src/test/kotlin/com/nowsprinting/intellij_mob/action/next/NextPreconditionKtTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.action.next 6 | 7 | import com.intellij.dvcs.repo.Repository 8 | import com.intellij.vcs.log.Hash 9 | import com.nowsprinting.intellij_mob.config.MobProjectSettings 10 | import com.nowsprinting.intellij_mob.testdouble.DummyGitRepository 11 | import com.nowsprinting.intellij_mob.testdouble.DummyHash 12 | import git4idea.GitLocalBranch 13 | import git4idea.GitRemoteBranch 14 | import git4idea.GitStandardRemoteBranch 15 | import git4idea.branch.GitBranchesCollection 16 | import git4idea.repo.GitBranchTrackInfo 17 | import git4idea.repo.GitRemote 18 | import org.junit.jupiter.api.Assertions.* 19 | import org.junit.jupiter.api.Test 20 | 21 | internal class NextPreconditionKtTest { 22 | 23 | private class StubGitRepository( 24 | val remoteSet: MutableCollection?, 25 | val remoteBranches: Collection?, 26 | val localBranches: Collection?, 27 | val trackedRemoteBranches: Collection?, 28 | val current: GitLocalBranch?, 29 | val repositoryState: Repository.State = Repository.State.NORMAL 30 | ) : DummyGitRepository() { 31 | 32 | override fun getRemotes(): MutableCollection { 33 | return remoteSet!! 34 | } 35 | 36 | override fun getBranches(): GitBranchesCollection { 37 | val remoteBranchesMap = hashMapOf() 38 | remoteBranches?.let { 39 | for (branch in it) { 40 | remoteBranchesMap[branch] = DummyHash() 41 | } 42 | } 43 | val localBranchesMap = hashMapOf() 44 | localBranches?.let { 45 | for (branch in it) { 46 | localBranchesMap[branch] = DummyHash() 47 | } 48 | } 49 | return GitBranchesCollection(localBranchesMap, remoteBranchesMap) 50 | } 51 | 52 | override fun getBranchTrackInfo(localBranchName: String): GitBranchTrackInfo? { 53 | trackedRemoteBranches?.let { 54 | for (remoteBranch in it) { 55 | if (remoteBranch.name.endsWith(localBranchName)) { 56 | return GitBranchTrackInfo( 57 | GitLocalBranch(localBranchName), 58 | remoteBranch, 59 | false 60 | ) 61 | } 62 | } 63 | } 64 | return null 65 | } 66 | 67 | override fun getCurrentBranch(): GitLocalBranch? { 68 | return current 69 | } 70 | 71 | override fun getCurrentBranchName(): String? { 72 | return current?.name 73 | } 74 | 75 | override fun getState(): Repository.State { 76 | return repositoryState 77 | } 78 | } 79 | 80 | private fun createSettings(): MobProjectSettings { 81 | val settings = MobProjectSettings() 82 | settings.noStateLoaded() 83 | return settings 84 | } 85 | 86 | @Test 87 | fun checkNextPrecondition_notStayWipBranch_failure() { 88 | val settings = createSettings() 89 | val origin = GitRemote("origin", listOf(), listOf(), listOf(), listOf()) 90 | val remoteMaster = GitStandardRemoteBranch(origin, "master") 91 | val remoteWip = GitStandardRemoteBranch(origin, "mob-session") 92 | val localMaster = GitLocalBranch("master") 93 | val localWip = GitLocalBranch("mob-session") 94 | val repository = StubGitRepository( 95 | remoteSet = mutableSetOf(origin), 96 | remoteBranches = setOf(remoteMaster, remoteWip), 97 | localBranches = setOf(localMaster, localWip), 98 | trackedRemoteBranches = setOf(remoteMaster, remoteWip), 99 | current = localMaster // not stay wip branch 100 | ) 101 | 102 | val (canExecute, errorMessage) = checkNextPrecondition(settings, repository) 103 | assertFalse(canExecute) 104 | assertEquals("you aren't mob programming, current branch is not mob-session", errorMessage) 105 | } 106 | 107 | @Test 108 | fun checkNextPrecondition_stayWipBranch_success() { 109 | val settings = createSettings() 110 | val origin = GitRemote("origin", listOf(), listOf(), listOf(), listOf()) 111 | val remoteMaster = GitStandardRemoteBranch(origin, "master") 112 | val remoteWip = GitStandardRemoteBranch(origin, "mob-session") 113 | val localMaster = GitLocalBranch("master") 114 | val localWip = GitLocalBranch("mob-session") 115 | val repository = StubGitRepository( 116 | remoteSet = mutableSetOf(origin), 117 | remoteBranches = setOf(remoteMaster, remoteWip), 118 | localBranches = setOf(localMaster, localWip), 119 | trackedRemoteBranches = setOf(remoteWip), 120 | current = localWip 121 | ) 122 | 123 | val (canExecute, errorMessage) = checkNextPrecondition(settings, repository) 124 | assertTrue(canExecute) 125 | assertNull(errorMessage) 126 | } 127 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/nowsprinting/intellij_mob/config/MobProjectSettingsExtensionKtTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.config 6 | 7 | import com.nowsprinting.intellij_mob.MobBundle 8 | import org.junit.jupiter.api.Assertions 9 | import org.junit.jupiter.api.Assertions.assertEquals 10 | import org.junit.jupiter.api.Assertions.assertFalse 11 | import org.junit.jupiter.api.Test 12 | 13 | internal class MobProjectSettingsExtensionKtTest { 14 | 15 | private fun createSettings(): MobProjectSettings { 16 | val settings = MobProjectSettings() 17 | settings.remoteName = "remote" 18 | settings.baseBranch = "master" 19 | settings.wipBranch = "mob-session" 20 | settings.wipCommitMessage = "mob next [ci-skip]" 21 | return settings 22 | } 23 | 24 | @Test 25 | fun validateForStartTask_remoteNameIsNull_failure() { 26 | val settings = createSettings() 27 | settings.remoteName = null 28 | val (valid, reason) = settings.validateForStartTask() 29 | assertFalse(valid) 30 | assertEquals(MobBundle.message("mob.validate_reason.unset_remote_name"), reason) 31 | } 32 | 33 | @Test 34 | fun validateForStartTask_remoteNameIsEmpty_failure() { 35 | val settings = createSettings() 36 | settings.remoteName = "" 37 | val (valid, reason) = settings.validateForStartTask() 38 | assertFalse(valid) 39 | assertEquals(MobBundle.message("mob.validate_reason.unset_remote_name"), reason) 40 | } 41 | 42 | @Test 43 | fun validateForStartTask_baseBranchIsNull_failure() { 44 | val settings = createSettings() 45 | settings.baseBranch = null 46 | val (valid, reason) = settings.validateForStartTask() 47 | assertFalse(valid) 48 | assertEquals(MobBundle.message("mob.validate_reason.unset_base_branch"), reason) 49 | } 50 | 51 | @Test 52 | fun validateForStartTask_baseBranchIsEmpty_failure() { 53 | val settings = createSettings() 54 | settings.baseBranch = "" 55 | val (valid, reason) = settings.validateForStartTask() 56 | assertFalse(valid) 57 | assertEquals(MobBundle.message("mob.validate_reason.unset_base_branch"), reason) 58 | } 59 | 60 | @Test 61 | fun validateForStartTask_wipBranchIsNull_failure() { 62 | val settings = createSettings() 63 | settings.wipBranch = null 64 | val (valid, reason) = settings.validateForStartTask() 65 | assertFalse(valid) 66 | assertEquals(MobBundle.message("mob.validate_reason.unset_wip_branch"), reason) 67 | } 68 | 69 | @Test 70 | fun validateForStartTask_wipBranchIsEmpty_failure() { 71 | val settings = createSettings() 72 | settings.wipBranch = "" 73 | val (valid, reason) = settings.validateForStartTask() 74 | assertFalse(valid) 75 | assertEquals(MobBundle.message("mob.validate_reason.unset_wip_branch"), reason) 76 | } 77 | 78 | @Test 79 | fun validateForStartTask_valid_success() { 80 | val settings = createSettings() 81 | settings.wipCommitMessage = null // empty but not validate in start task 82 | val (valid, reason) = settings.validateForStartTask() 83 | Assertions.assertTrue(valid) 84 | Assertions.assertNull(reason) 85 | } 86 | 87 | @Test 88 | fun validateForNextPrecondition_valid_success() { 89 | val settings = createSettings() 90 | settings.wipCommitMessage = null // empty but not validate in next precondition 91 | val (valid, reason) = settings.validateForNextPrecondition() 92 | Assertions.assertTrue(valid) 93 | Assertions.assertNull(reason) 94 | } 95 | 96 | @Test 97 | fun validateForNextTask_wipCommitMessageIsNull_failure() { 98 | val settings = createSettings() 99 | settings.wipCommitMessage = null 100 | val (valid, reason) = settings.validateForNextTask() 101 | assertFalse(valid) 102 | assertEquals(MobBundle.message("mob.validate_reason.unset_wip_commit_message"), reason) 103 | } 104 | 105 | @Test 106 | fun validateForNextTask_wipCommitMessageIsEmpty_failure() { 107 | val settings = createSettings() 108 | settings.wipCommitMessage = "" 109 | val (valid, reason) = settings.validateForNextTask() 110 | assertFalse(valid) 111 | assertEquals(MobBundle.message("mob.validate_reason.unset_wip_commit_message"), reason) 112 | } 113 | 114 | @Test 115 | fun validateForNextTask_valid_success() { 116 | val settings = createSettings() 117 | val (valid, reason) = settings.validateForNextTask() 118 | Assertions.assertTrue(valid) 119 | Assertions.assertNull(reason) 120 | } 121 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/nowsprinting/intellij_mob/config/MobProjectSettingsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.config 6 | 7 | import org.junit.jupiter.api.Assertions 8 | import org.junit.jupiter.api.Test 9 | 10 | class MobProjectSettingsTest { 11 | 12 | @Test 13 | fun noStateLoaded_setDefault() { 14 | val sut = MobProjectSettings() 15 | sut.noStateLoaded() 16 | Assertions.assertEquals(sut.wipBranch, "mob-session") 17 | Assertions.assertEquals(sut.baseBranch, "master") 18 | Assertions.assertEquals(sut.remoteName, "origin") 19 | Assertions.assertEquals(sut.timerMinutes, 10) 20 | Assertions.assertFalse(sut.startWithShare) 21 | Assertions.assertEquals(sut.wipCommitMessage, "mob next [ci-skip]") 22 | Assertions.assertTrue(sut.nextStay) 23 | } 24 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/nowsprinting/intellij_mob/git/GitLocalBranchExtensionKtTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import com.intellij.vcs.log.Hash 8 | import com.nowsprinting.intellij_mob.testdouble.DummyGitRepository 9 | import com.nowsprinting.intellij_mob.testdouble.DummyHash 10 | import git4idea.GitLocalBranch 11 | import git4idea.GitRemoteBranch 12 | import git4idea.GitStandardRemoteBranch 13 | import git4idea.branch.GitBranchesCollection 14 | import git4idea.repo.GitBranchTrackInfo 15 | import git4idea.repo.GitRemote 16 | import org.junit.jupiter.api.Assertions.assertFalse 17 | import org.junit.jupiter.api.Assertions.assertTrue 18 | import org.junit.jupiter.api.Test 19 | 20 | internal class GitLocalBranchExtensionKtTest { 21 | 22 | private class StubGitRepository( 23 | val remoteBranches: MutableCollection?, 24 | val trackedRemoteBranch: GitRemoteBranch? 25 | ) : DummyGitRepository() { 26 | 27 | override fun getBranches(): GitBranchesCollection { 28 | val localBranchesMap = hashMapOf() 29 | val remoteBranchesMap = hashMapOf() 30 | remoteBranches?.let { 31 | for (branch in it) { 32 | remoteBranchesMap[branch] = DummyHash() 33 | } 34 | } 35 | return GitBranchesCollection(localBranchesMap, remoteBranchesMap) 36 | } 37 | 38 | override fun getBranchTrackInfo(localBranchName: String): GitBranchTrackInfo? { 39 | trackedRemoteBranch?.let { 40 | return GitBranchTrackInfo( 41 | GitLocalBranch(localBranchName), 42 | it, 43 | false 44 | ) 45 | } 46 | return null 47 | } 48 | } 49 | 50 | @Test 51 | fun hasValidUpstream_hasNotTrackedBranch_false() { 52 | val origin = GitRemote("origin", listOf(), listOf(), listOf(), listOf()) 53 | val remoteBranch = GitStandardRemoteBranch(origin, "master") 54 | val repository = StubGitRepository( 55 | mutableSetOf(remoteBranch), 56 | null 57 | ) 58 | val sut = GitLocalBranch("has_not_tracked_remote_branch") 59 | 60 | val actual = sut.hasValidUpstream(repository) 61 | assertFalse(actual) 62 | } 63 | 64 | @Test 65 | fun hasValidUpstream_hasNotValidTrackedBranch_false() { 66 | val origin = GitRemote("origin", listOf(), listOf(), listOf(), listOf()) 67 | val remoteBranch = GitStandardRemoteBranch(origin, "master") 68 | val trackedRemoteBranch = GitStandardRemoteBranch(origin, "tracked") 69 | val repository = StubGitRepository( 70 | mutableSetOf(remoteBranch), 71 | trackedRemoteBranch 72 | ) 73 | val sut = GitLocalBranch("has_not_valid_tracked_remote_branch") 74 | 75 | val actual = sut.hasValidUpstream(repository) 76 | assertFalse(actual) 77 | } 78 | 79 | @Test 80 | fun hasValidUpstream_hasValidTrackedBranch_true() { 81 | val origin = GitRemote("origin", listOf(), listOf(), listOf(), listOf()) 82 | val remoteBranch = GitStandardRemoteBranch(origin, "master") 83 | val repository = StubGitRepository( 84 | mutableSetOf(remoteBranch), 85 | remoteBranch 86 | ) 87 | val sut = GitLocalBranch("master") 88 | 89 | val actual = sut.hasValidUpstream(repository) 90 | assertTrue(actual) 91 | } 92 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/nowsprinting/intellij_mob/git/GitRepositoryUtilKtTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.git 6 | 7 | import com.intellij.openapi.vfs.VirtualFile 8 | import com.nowsprinting.intellij_mob.testdouble.DummyGitRepository 9 | import com.nowsprinting.intellij_mob.testdouble.DummyVirtualFile 10 | import com.nowsprinting.intellij_mob.testdouble.FakeLogger 11 | import git4idea.repo.GitRepositoryManager 12 | import io.mockk.every 13 | import io.mockk.mockk 14 | import org.junit.jupiter.api.Assertions 15 | import org.junit.jupiter.api.Test 16 | 17 | internal class GitRepositoryUtilKtTest { 18 | 19 | private class StubGitRepository(val repoPath: String) : DummyGitRepository() { 20 | private class StubVirtualFile(val repoPath: String) : DummyVirtualFile() { 21 | override fun getPath(): String { 22 | return repoPath 23 | } 24 | } 25 | 26 | override fun getRoot(): VirtualFile { 27 | return StubVirtualFile(repoPath) 28 | } 29 | } 30 | 31 | @Test 32 | fun getRepository_success() { 33 | val mockRepositoryManager = mockk() 34 | every { mockRepositoryManager.repositories } returns listOf( 35 | StubGitRepository("/path/to/repository") 36 | ) 37 | 38 | val actual = getGitRepository( 39 | mockRepositoryManager, 40 | FakeLogger() 41 | ) 42 | Assertions.assertTrue(actual is GitRepositoryResult.Success) 43 | } 44 | 45 | @Test 46 | fun getRepository_noRepository_failure() { 47 | val mockRepositoryManager = mockk() 48 | every { mockRepositoryManager.repositories } returns listOf() 49 | 50 | val actual = getGitRepository( 51 | mockRepositoryManager, 52 | FakeLogger() 53 | ) 54 | Assertions.assertTrue(actual is GitRepositoryResult.Failure) 55 | Assertions.assertEquals("repository not found in this project", (actual as GitRepositoryResult.Failure).reason) 56 | } 57 | 58 | @Test 59 | fun getRepository_manyRepository_failure() { 60 | val mockRepositoryManager = mockk() 61 | every { mockRepositoryManager.repositories } returns listOf( 62 | StubGitRepository("/path/to/repository-1"), 63 | StubGitRepository("/path/to/repository-2") 64 | ) 65 | 66 | val actual = getGitRepository( 67 | mockRepositoryManager, 68 | FakeLogger() 69 | ) 70 | Assertions.assertTrue(actual is GitRepositoryResult.Failure) 71 | Assertions.assertEquals( 72 | "multiple repositories is not support yet", 73 | (actual as GitRepositoryResult.Failure).reason 74 | ) 75 | } 76 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/nowsprinting/intellij_mob/testdouble/DummyGitRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.testdouble 6 | 7 | import com.intellij.dvcs.repo.Repository 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.vfs.VirtualFile 10 | import git4idea.GitLocalBranch 11 | import git4idea.GitVcs 12 | import git4idea.branch.GitBranchesCollection 13 | import git4idea.ignore.GitRepositoryIgnoredFilesHolder 14 | import git4idea.repo.* 15 | import git4idea.status.GitStagingAreaHolder 16 | 17 | internal open class DummyGitRepository : GitRepository { 18 | override fun getRepositoryFiles(): GitRepositoryFiles { 19 | throw Exception("Not yet implemented") 20 | } 21 | 22 | override fun getBranches(): GitBranchesCollection { 23 | throw Exception("Not yet implemented") 24 | } 25 | 26 | override fun getInfo(): GitRepoInfo { 27 | throw Exception("Not yet implemented") 28 | } 29 | 30 | override fun getStagingAreaHolder(): GitStagingAreaHolder { 31 | TODO("Not yet implemented") 32 | } 33 | 34 | override fun toLogString(): String { 35 | throw Exception("Not yet implemented") 36 | } 37 | 38 | override fun getBranchTrackInfos(): MutableCollection { 39 | throw Exception("Not yet implemented") 40 | } 41 | 42 | @Deprecated("Deprecated in Java") 43 | override fun getGitDir(): VirtualFile { 44 | throw Exception("Not yet implemented") 45 | } 46 | 47 | override fun update() { 48 | throw Exception("Not yet implemented") 49 | } 50 | 51 | override fun getCurrentBranch(): GitLocalBranch? { 52 | throw Exception("Not yet implemented") 53 | } 54 | 55 | override fun getPresentableUrl(): String { 56 | throw Exception("Not yet implemented") 57 | } 58 | 59 | override fun getVcs(): GitVcs { 60 | throw Exception("Not yet implemented") 61 | } 62 | 63 | override fun getCurrentRevision(): String? { 64 | throw Exception("Not yet implemented") 65 | } 66 | 67 | override fun getState(): Repository.State { 68 | throw Exception("Not yet implemented") 69 | } 70 | 71 | override fun getRemotes(): MutableCollection { 72 | throw Exception("Not yet implemented") 73 | } 74 | 75 | override fun getCurrentBranchName(): String? { 76 | throw Exception("Not yet implemented") 77 | } 78 | 79 | override fun getIgnoredFilesHolder(): GitRepositoryIgnoredFilesHolder { 80 | throw Exception("Not yet implemented") 81 | } 82 | 83 | override fun isFresh(): Boolean { 84 | throw Exception("Not yet implemented") 85 | } 86 | 87 | override fun getBranchTrackInfo(localBranchName: String): GitBranchTrackInfo? { 88 | throw Exception("Not yet implemented") 89 | } 90 | 91 | override fun isOnBranch(): Boolean { 92 | throw Exception("Not yet implemented") 93 | } 94 | 95 | override fun getProject(): Project { 96 | throw Exception("Not yet implemented") 97 | } 98 | 99 | override fun getRoot(): VirtualFile { 100 | throw Exception("Not yet implemented") 101 | } 102 | 103 | override fun getSubmodules(): MutableCollection { 104 | throw Exception("Not yet implemented") 105 | } 106 | 107 | override fun isRebaseInProgress(): Boolean { 108 | throw Exception("Not yet implemented") 109 | } 110 | 111 | override fun getUntrackedFilesHolder(): GitUntrackedFilesHolder { 112 | throw Exception("Not yet implemented") 113 | } 114 | 115 | override fun dispose() { 116 | throw Exception("Not yet implemented") 117 | } 118 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/nowsprinting/intellij_mob/testdouble/DummyHash.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.testdouble 6 | 7 | import com.intellij.vcs.log.Hash 8 | 9 | internal open class DummyHash : Hash { 10 | override fun toShortString(): String { 11 | throw Exception("Not yet implemented") 12 | } 13 | 14 | override fun asString(): String { 15 | throw Exception("Not yet implemented") 16 | } 17 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/nowsprinting/intellij_mob/testdouble/DummyProject.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.testdouble 6 | 7 | import com.intellij.diagnostic.ActivityCategory 8 | import com.intellij.openapi.extensions.PluginDescriptor 9 | import com.intellij.openapi.extensions.PluginId 10 | import com.intellij.openapi.project.Project 11 | import com.intellij.openapi.util.Condition 12 | import com.intellij.openapi.util.Key 13 | import com.intellij.openapi.vfs.VirtualFile 14 | import com.intellij.util.messages.MessageBus 15 | import org.picocontainer.PicoContainer 16 | import java.lang.RuntimeException 17 | 18 | internal open class DummyProject : Project { 19 | override fun isDisposed(): Boolean { 20 | throw Exception("Not yet implemented") 21 | } 22 | 23 | override fun getWorkspaceFile(): VirtualFile? { 24 | throw Exception("Not yet implemented") 25 | } 26 | 27 | override fun getProjectFilePath(): String? { 28 | throw Exception("Not yet implemented") 29 | } 30 | 31 | override fun getName(): String { 32 | throw Exception("Not yet implemented") 33 | } 34 | 35 | override fun getComponent(interfaceClass: Class): T { 36 | throw Exception("Not yet implemented") 37 | } 38 | 39 | @Deprecated("Deprecated in Java") 40 | @Suppress("UnstableApiUsage") 41 | override fun getComponents(baseClass: Class): Array { 42 | TODO("Not yet implemented") 43 | } 44 | 45 | @Deprecated("Deprecated in Java") 46 | override fun getBaseDir(): VirtualFile { 47 | throw Exception("Not yet implemented") 48 | } 49 | 50 | override fun putUserData(key: Key, value: T?) { 51 | throw Exception("Not yet implemented") 52 | } 53 | 54 | override fun isOpen(): Boolean { 55 | throw Exception("Not yet implemented") 56 | } 57 | 58 | override fun save() { 59 | throw Exception("Not yet implemented") 60 | } 61 | 62 | override fun getDisposed(): Condition<*> { 63 | throw Exception("Not yet implemented") 64 | } 65 | 66 | override fun getService(serviceClass: Class): T { 67 | TODO("Not yet implemented") 68 | } 69 | 70 | @Suppress("UnstableApiUsage") 71 | override fun instantiateClassWithConstructorInjection( 72 | aClass: Class, 73 | key: Any, 74 | pluginId: PluginId 75 | ): T { 76 | TODO("Not yet implemented") 77 | } 78 | 79 | @Suppress("UnstableApiUsage") 80 | override fun createError(error: Throwable, pluginId: PluginId): RuntimeException { 81 | TODO("Not yet implemented") 82 | } 83 | 84 | @Suppress("UnstableApiUsage") 85 | override fun createError(message: String, pluginId: PluginId): RuntimeException { 86 | TODO("Not yet implemented") 87 | } 88 | 89 | override fun createError( 90 | message: String, 91 | error: Throwable?, 92 | pluginId: PluginId, 93 | attachments: MutableMap? 94 | ): RuntimeException { 95 | TODO("Not yet implemented") 96 | } 97 | 98 | @Suppress("UnstableApiUsage") 99 | override fun loadClass(className: String, pluginDescriptor: PluginDescriptor): Class { 100 | TODO("Not yet implemented") 101 | } 102 | 103 | override fun getActivityCategory(isExtension: Boolean): ActivityCategory { 104 | TODO("Not yet implemented") 105 | } 106 | 107 | @Suppress("UnstableApiUsage") 108 | override fun getPicoContainer(): PicoContainer { 109 | throw Exception("Not yet implemented") 110 | } 111 | 112 | @Suppress("UnstableApiUsage") 113 | override fun isInjectionForExtensionSupported(): Boolean { 114 | TODO("Not yet implemented") 115 | } 116 | 117 | override fun getProjectFile(): VirtualFile? { 118 | throw Exception("Not yet implemented") 119 | } 120 | 121 | override fun getUserData(key: Key): T? { 122 | throw Exception("Not yet implemented") 123 | } 124 | 125 | override fun isInitialized(): Boolean { 126 | throw Exception("Not yet implemented") 127 | } 128 | 129 | override fun getMessageBus(): MessageBus { 130 | throw Exception("Not yet implemented") 131 | } 132 | 133 | override fun isDefault(): Boolean { 134 | throw Exception("Not yet implemented") 135 | } 136 | 137 | override fun getBasePath(): String? { 138 | throw Exception("Not yet implemented") 139 | } 140 | 141 | override fun getLocationHash(): String { 142 | throw Exception("Not yet implemented") 143 | } 144 | 145 | override fun dispose() { 146 | throw Exception("Not yet implemented") 147 | } 148 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/nowsprinting/intellij_mob/testdouble/DummyVirtualFile.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.testdouble 6 | 7 | import com.intellij.openapi.vfs.VirtualFile 8 | import com.intellij.openapi.vfs.VirtualFileSystem 9 | import java.io.InputStream 10 | import java.io.OutputStream 11 | 12 | internal open class DummyVirtualFile : VirtualFile() { 13 | override fun refresh(asynchronous: Boolean, recursive: Boolean, postRunnable: Runnable?) { 14 | throw Exception("Not yet implemented") 15 | } 16 | 17 | override fun getLength(): Long { 18 | throw Exception("Not yet implemented") 19 | } 20 | 21 | override fun getFileSystem(): VirtualFileSystem { 22 | throw Exception("Not yet implemented") 23 | } 24 | 25 | override fun getPath(): String { 26 | throw Exception("Not yet implemented") 27 | } 28 | 29 | override fun isDirectory(): Boolean { 30 | throw Exception("Not yet implemented") 31 | } 32 | 33 | override fun getTimeStamp(): Long { 34 | throw Exception("Not yet implemented") 35 | } 36 | 37 | override fun getName(): String { 38 | throw Exception("Not yet implemented") 39 | } 40 | 41 | override fun contentsToByteArray(): ByteArray { 42 | throw Exception("Not yet implemented") 43 | } 44 | 45 | override fun isValid(): Boolean { 46 | throw Exception("Not yet implemented") 47 | } 48 | 49 | override fun getInputStream(): InputStream { 50 | throw Exception("Not yet implemented") 51 | } 52 | 53 | override fun getParent(): VirtualFile { 54 | throw Exception("Not yet implemented") 55 | } 56 | 57 | override fun getChildren(): Array { 58 | throw Exception("Not yet implemented") 59 | } 60 | 61 | override fun isWritable(): Boolean { 62 | throw Exception("Not yet implemented") 63 | } 64 | 65 | override fun getOutputStream(requestor: Any?, newModificationStamp: Long, newTimeStamp: Long): OutputStream { 66 | throw Exception("Not yet implemented") 67 | } 68 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/nowsprinting/intellij_mob/testdouble/FakeChange.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.testdouble 6 | 7 | import com.intellij.openapi.vcs.LocalFilePath 8 | import com.intellij.openapi.vcs.changes.Change 9 | import com.intellij.openapi.vcs.changes.ContentRevision 10 | import com.intellij.openapi.vcs.changes.FakeRevision 11 | import com.intellij.openapi.vfs.VirtualFile 12 | 13 | fun createFakeChange(): FakeChange { 14 | val beforeRevision = FakeRevision(LocalFilePath("/path/to/before", false)) 15 | val afterRevision = FakeRevision(LocalFilePath("/path/to/after", false)) 16 | return FakeChange(beforeRevision, afterRevision) 17 | } 18 | 19 | class FakeChange(beforeRevision: ContentRevision, afterRevision: ContentRevision) : 20 | Change(beforeRevision, afterRevision) { 21 | 22 | override fun getType(): Type { 23 | return Type.NEW 24 | } 25 | 26 | override fun getVirtualFile(): VirtualFile? { 27 | return null 28 | } 29 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/nowsprinting/intellij_mob/testdouble/FakeLogger.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.testdouble 6 | 7 | import com.intellij.openapi.diagnostic.LogLevel 8 | import com.intellij.openapi.diagnostic.Logger 9 | import org.apache.log4j.Level 10 | 11 | internal open class FakeLogger : Logger() { 12 | override fun warn(message: String?, t: Throwable?) { 13 | println(message) 14 | println(t?.stackTrace) 15 | } 16 | 17 | @Deprecated("Deprecated in Java", ReplaceWith("throw Exception(\"Not yet implemented\")")) 18 | @Suppress("UnstableApiUsage") 19 | override fun setLevel(level: Level) { 20 | throw Exception("Not yet implemented") 21 | } 22 | 23 | override fun setLevel(level: LogLevel) { 24 | throw Exception("Not yet implemented") 25 | } 26 | 27 | override fun info(message: String?) { 28 | println(message) 29 | } 30 | 31 | override fun info(message: String?, t: Throwable?) { 32 | println(message) 33 | println(t?.stackTrace) 34 | } 35 | 36 | override fun error(message: String?, t: Throwable?, vararg details: String?) { 37 | println(message) 38 | println(t?.stackTrace) 39 | } 40 | 41 | override fun isDebugEnabled(): Boolean { 42 | throw Exception("Not yet implemented") 43 | } 44 | 45 | override fun debug(message: String?) { 46 | println(message) 47 | } 48 | 49 | override fun debug(t: Throwable?) { 50 | println(t?.stackTrace) 51 | } 52 | 53 | override fun debug(message: String?, t: Throwable?) { 54 | println(message) 55 | println(t?.stackTrace) 56 | } 57 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/nowsprinting/intellij_mob/timer/TimerServiceTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2021 Koji Hasegawa. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 3 | */ 4 | 5 | package com.nowsprinting.intellij_mob.timer 6 | 7 | import org.junit.jupiter.api.Assertions.assertEquals 8 | import org.junit.jupiter.api.Assertions.assertFalse 9 | import org.junit.jupiter.api.Disabled 10 | import org.junit.jupiter.api.Test 11 | import java.time.LocalDateTime 12 | import java.time.Month 13 | 14 | @Disabled("ApplicationManager.getApplication() returns null in tests. Since 2020.3") 15 | internal class TimerServiceTest { 16 | private val startTime = LocalDateTime.of(2020, Month.DECEMBER, 31, 23, 55, 30) 17 | private val verifyTime = LocalDateTime.of(2021, Month.JANUARY, 1, 0, 1, 10) 18 | 19 | @Test 20 | fun getTime_notRunning_fixedText() { 21 | val sut = TimerService() 22 | assertFalse(sut.isRunning(), "not running") 23 | assertEquals("Mob", sut.getTime()) 24 | } 25 | 26 | @Test 27 | fun getTime_stopped_fixedText() { 28 | val sut = TimerService() 29 | sut.start() 30 | sut.stop() 31 | assertFalse(sut.isRunning(), "not running") 32 | assertEquals("Mob", sut.getTime()) 33 | } 34 | 35 | @Test 36 | fun getTime_remainingTime_timeText() { 37 | val sut = TimerService() 38 | sut.start(10, startTime) 39 | assertEquals(TimerState.REMAINING_TIME, sut.getState(verifyTime)) 40 | assertEquals("04:20", sut.getTime(verifyTime)) 41 | } 42 | 43 | @Test 44 | fun getTime_overTime_timeText() { 45 | val sut = TimerService() 46 | sut.start(5, startTime) 47 | assertEquals(TimerState.OVER_TIME, sut.getState(verifyTime)) 48 | assertEquals("00:40", sut.getTime(verifyTime)) 49 | } 50 | 51 | @Test 52 | fun getTime_elapsedTime_timeText() { 53 | val sut = TimerService() 54 | sut.start(0, startTime) 55 | assertEquals(TimerState.ELAPSED_TIME, sut.getState(verifyTime)) 56 | assertEquals("05:40", sut.getTime(verifyTime)) 57 | } 58 | 59 | @Test 60 | fun getTime_sameTime_timeTextZero() { 61 | val sameTime = startTime 62 | val sut = TimerService() 63 | sut.start(0, sameTime) 64 | assertEquals("00:00", sut.getTime(sameTime)) 65 | } 66 | 67 | @Test 68 | fun getTime_longerTime_timeText() { 69 | val longerTime = verifyTime.plusHours(2) 70 | val sut = TimerService() 71 | sut.start(0, startTime) 72 | assertEquals("125:40", sut.getTime(longerTime)) 73 | } 74 | // other idea, e.g. "77 minutes passed", "8 hours passed", "500 days passed" 75 | } --------------------------------------------------------------------------------