├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
6 |
7 |
8 |
9 |
12 |
15 |
16 |
17 | true
18 | true
19 | false
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.run/Run IDE with Plugin.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
17 |
18 |
19 | true
20 | true
21 | false
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.run/Run Plugin Tests.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | true
20 | true
21 | false
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.run/Run Verifications.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 | 
4 | [](https://sonarcloud.io/summary/new_code?id=remotemobprogramming_intellij-mob)
5 | [](https://plugins.jetbrains.com/plugin/14266-mob)
6 | [](https://plugins.jetbrains.com/plugin/14266-mob)
7 | [](https://plugins.jetbrains.com/plugin/14266-mob)
8 |
9 | 
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 | 
53 |
54 | 
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 | 
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 | 
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 | 
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 |
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 |
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 | }
--------------------------------------------------------------------------------