├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ ├── FEATURE_REQUEST.md │ └── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── dependabot-approve-and-auto-merge.yml │ └── publish.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── .vscodeignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __mocks__ └── vscode.js ├── eslint.config.js ├── extension.js ├── extension.spec.js ├── github.http ├── jest.config.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── resources ├── co-author-suggestion.png ├── git-mob-in-action.gif ├── gitmob.png └── icons │ ├── more.svg │ ├── selected.svg │ ├── unselected.svg │ └── user.svg ├── src ├── build-co-author-groups.js ├── build-co-author-groups.spec.js ├── co-author-tree-provider │ ├── author.js │ ├── co-authors-provider.js │ ├── co-authors-provider.spec.js │ ├── co-authors.js │ ├── count-decorator-provider.js │ ├── count-decorator-provider.spec.js │ ├── error-author.js │ ├── group-item.js │ ├── input-completion-provider.js │ └── repo-authors.js ├── commands │ ├── add-co-author.js │ ├── add-main-author.js │ ├── change-primary-author.js │ ├── co-author-actions.js │ ├── copy-co-author.js │ ├── github-authors.js │ ├── open-git-coauthors.js │ ├── open-settings.js │ ├── reload.js │ ├── search-git-emojis.js │ ├── search-repository-authors.js │ ├── shared │ │ └── input-author-data.js │ └── solo.js ├── constants.js ├── errors │ └── log-issue.js ├── ext-config │ ├── config.js │ └── solo-after-commit.js ├── git-emojis │ └── gitmojis.json ├── git │ └── watch-for-commit.js ├── install │ └── install-git-coauthor-file.js ├── reload-on-save.js ├── reload-on-save.spec.js ├── setup-git-mob.js └── vscode-git-extension │ ├── git-ext.js │ ├── git-ext.spec.js │ └── vs-code-git.js └── test ├── extension.test.js ├── fixture-1 ├── .git-coauthors └── hello.js └── setup ├── after-all.js ├── before-all.js ├── install-test-co-author-file.js ├── mocha-suite.js ├── run-cmd.js └── test-runner.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically normalize line endings. 2 | * text=auto 3 | 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # 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 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: rkotze # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | 10 | 11 | ### Prerequisites 12 | 13 | - [ ] [Checked that your issue isn't already filed][git-mob-vs-code issues] 14 | - [ ] Tried upgrading to the latest git-mob-vs-code and git-mob version (`npm i -g git-mob`) 15 | - [ ] Tried upgrading to the latest git version (`brew upgrade git` if installed with Homebrew/Linuxbrew) 16 | 17 | [git-mob-vs-code issues]: https://github.com/rkotze/git-mob-vs-code/issues?utf8=%E2%9C%93&q=is%3Aissue 18 | 19 | ### Description 20 | 21 | 22 | 23 | ### Steps to Reproduce 24 | 25 | 1. ... 26 | 2. ... 27 | 3. and so on... 28 | 29 | **Expected behaviour:** [What you expect to happen] 30 | 31 | **Actual behaviour:** [What actually happens] 32 | 33 | **Reproduces how often:** [What percentage of the time does it reproduce?] 34 | 35 | ### Versions 36 | 37 | - operating system and version: 38 | - VS Code version 39 | - git-mob-vs-code version (can be found in the Extensions tab) 40 | - git version (`git --version`): 41 | 42 | ### Additional Information 43 | 44 | 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | 13 | 14 | ## Summary 15 | 16 | 17 | 18 | ## Motivation 19 | 20 | 21 | 22 | ## Describe alternatives you've considered 23 | 24 | 25 | 26 | ## Additional context 27 | 28 | 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | ### Description of changes 3 | 4 | ### Screenshot of change 5 | 6 | ### Checklists 7 | 8 | - [] Reference a Github issue 9 | - [] Add unit tests 10 | - [] Ensure CI pipeline is all green. 11 | - linting, unit tests and integration tests all pass 12 | 13 | Follow [code of conduct](https://github.com/rkotze/git-mob-vs-code/blob/master/CODE_OF_CONDUCT.md) and [contributing guidelines](https://github.com/rkotze/git-mob-vs-code/blob/master/CONTRIBUTING.md). -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | open-pull-requests-limit: 10 13 | ignore: 14 | - dependency-name: "@types/vscode" 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the trunk branch 8 | push: 9 | branches: [master] 10 | pull_request: 11 | branches: [master] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | name: Node.js ${{ matrix.node-version }} 21 | # The type of runner that the job will run on 22 | runs-on: ubuntu-latest 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | node-version: 27 | - 22 28 | - 20 29 | - 18 30 | 31 | # Steps represent a sequence of tasks that will be executed as part of the job 32 | steps: 33 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 34 | - name: Check out code 35 | uses: actions/checkout@v4 36 | with: 37 | fetch-depth: "0" 38 | 39 | - name: Set up Node 40 | uses: actions/setup-node@v4 41 | with: 42 | node-version: ${{ matrix.node-version }} 43 | 44 | - name: Install dependencies 45 | run: npm install 46 | 47 | - name: Run lint and tests 48 | run: npm run ci 49 | 50 | - name: UI Tests 51 | uses: coactions/setup-xvfb@v1 52 | with: 53 | run: npm run ui-test 54 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-approve-and-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Pull Request Approve and Merge 2 | 3 | on: pull_request_target 4 | 5 | permissions: 6 | pull-requests: write 7 | contents: write 8 | 9 | jobs: 10 | dependabot: 11 | runs-on: ubuntu-latest 12 | # Checking the actor will prevent your Action run failing on non-Dependabot 13 | # PRs but also ensures that it only does work for Dependabot PRs. 14 | if: ${{ github.actor == 'dependabot[bot]' }} 15 | steps: 16 | # This first step will fail if there's no metadata and so the approval 17 | # will not occur. 18 | - name: Dependabot metadata 19 | id: dependabot-metadata 20 | uses: dependabot/fetch-metadata@v1.1.1 21 | with: 22 | github-token: "${{ secrets.GITHUB_TOKEN }}" 23 | # Finally, this sets the PR to allow auto-merging for patch and minor 24 | # updates if all checks pass 25 | - name: Enable auto-merge for Dependabot PRs 26 | if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }} 27 | run: gh pr merge --auto --squash "$PR_URL" 28 | env: 29 | PR_URL: ${{ github.event.pull_request.html_url }} 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: "Publish" 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out code 11 | uses: actions/checkout@v4 12 | 13 | - name: Set up Node 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: "20" 17 | 18 | - name: Install dependencies 19 | run: npm install 20 | 21 | - name: Publish to Open VSX Registry 22 | uses: HaaLeo/publish-vscode-extension@v1 23 | continue-on-error: true 24 | id: publishToOpenVSX 25 | with: 26 | pat: ${{ secrets.OPEN_VSX_TOKEN }} 27 | 28 | - name: Publish to VS Marketplace 29 | uses: HaaLeo/publish-vscode-extension@v1 30 | continue-on-error: true 31 | with: 32 | pat: ${{ secrets.VS_MARKETPLACE_TOKEN }} 33 | registryUrl: https://marketplace.visualstudio.com 34 | extensionFile: ${{ steps.publishToOpenVSX.outputs.vsixPath }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode-test/ 3 | *.vsix 4 | 5 | git.co-authors -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Jest", 10 | "type": "node", 11 | "request": "launch", 12 | "runtimeArgs": [ 13 | "--inspect-brk", 14 | "${workspaceRoot}/node_modules/jest/bin/jest.js", 15 | "--runInBand", 16 | "-o" 17 | ], 18 | "console": "integratedTerminal", 19 | "internalConsoleOptions": "neverOpen", 20 | "port": 9229 21 | }, 22 | { 23 | "name": "Extension", 24 | "type": "extensionHost", 25 | "request": "launch", 26 | "args": [ 27 | "--extensionDevelopmentPath=${workspaceFolder}", 28 | "--disable-extensions" 29 | ] 30 | }, 31 | { 32 | "name": "Extension Tests", 33 | "type": "extensionHost", 34 | "request": "launch", 35 | "args": [ 36 | "${workspaceFolder}/test/fixture-1", 37 | "--extensionDevelopmentPath=${workspaceFolder}", 38 | "--extensionTestsPath=${workspaceFolder}/test/setup/mocha-suite", 39 | "--disable-extensions" 40 | ] 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.formatOnSave": true, 4 | "[json]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | }, 7 | "[javascript]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | __mocks__/** 2 | .github/** 3 | .vscode/** 4 | test/** 5 | **/*.spec.js 6 | azure-pipelines.yml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Follows [Semantic Versioning](https://semver.org/). 4 | 5 | ## Next version 6 | 7 | ## 1.23.1 8 | 9 | ### Fixed 10 | 11 | Error: command 'gitmob.openSettings' already exists [Issue 418](https://github.com/rkotze/git-mob-vs-code/issues/418). If you have multiple projects open then the setup command would run twice causing the error. 12 | 13 | ## 1.23.0 14 | 15 | ### Added 16 | 17 | - If no primary author set handle error inline and ability to set new author through vscode inputs. [Issue 27](https://github.com/rkotze/git-mob-vs-code/issues/27) 18 | 19 | ## 1.22.0 20 | 21 | ### Added 22 | 23 | - Integrate `git-mob-core v0.10.0` wit new message formatter function. 24 | - Autocomplete co-author feature in SCM textbox by typing plus `+`. Can be enabled/disabled in settings. 25 | 26 | ### Fixed 27 | 28 | - Fix Git Mob start up error with new project and initialising a new Git repo. 29 | 30 | ## 1.21.1 31 | 32 | ### Fixed 33 | 34 | - When executing the solo sequence post-commit the unselected author gets the "plus" action instead a "minus" icon. [Issue 380](https://github.com/rkotze/git-mob-vs-code/issues/380) 35 | 36 | ## 1.21.0 37 | 38 | ### Added 39 | 40 | - Update the list Gitmoji's to latest - thanks @regisbsb 41 | 42 | ### Removed 43 | 44 | - Clean up documentation PAT config not used any more for any commands. 45 | 46 | ## 1.20.1 47 | 48 | ### Added 49 | 50 | - Use `git-mob-core` to search GitHub users replacing what is in the extension. The limitation is core API does not allow a PAT key and won't get authors selected email (will just return GH anonymous email). Less code to maintain across tools. 51 | 52 | ### Fixed 53 | 54 | - On launching vs code error modal appears, when no path set for the commit template, default to the global template. [Issue 302](https://github.com/rkotze/git-mob-vs-code/issues/302) 55 | - Save to commit template on reload or start of vs code if any authors are selected. List object was in wrong format. 56 | 57 | ## 1.20.0 58 | 59 | ### Added 60 | 61 | - Integrate features from `git-mob-core` `v0.8.2` - see [PR 266](https://github.com/rkotze/git-mob-vs-code/pull/266) for details 62 | - Create new `.git-coauthors` file and temp one for UI tests 63 | - Use core `Author` class 64 | - Remove usage of old `gitAuthors` internal function, was moved into test folder as it's needed there. 65 | - Get repo contributors `repoAuthorList` 66 | - Fetch user from GitHub 67 | - Removed old Git commands and rev parse exec files. 68 | - Integrate breaking changes from `git-mob-core` `v0.9.x` 69 | - `getPrimaryAuthor`, `getSelectedCoAuthors`, `setPrimaryAuthor` now use promises 70 | - Remove usage of old `gitAuthors` internal function in UI test folder replace with `git-mob-core` library 71 | 72 | ### Fixed 73 | 74 | - VS code UI not updated after saved changes in `.git-coauthor` file. 75 | - After UI tests run clean up temporary files, `.git-coauthors` and `.gitmessage`. 76 | 77 | ## 1.19.0 78 | 79 | ### Added 80 | 81 | - Refactor approach for managing the co-author group lists by using a memorized and facade approach. There is less logic involved and making it easier to read. 82 | - Performance gain to render the list UI in the SCM panel. 83 | - Removed most of the old `git-mob-api` which is replaced by the new `git-mob-core` API, part of the migration. 84 | 85 | ## 1.18.0 86 | 87 | ### Added 88 | 89 | - Integrated `git-mob-core` v0.5.0 new shared library between Git Mob cli and VS Code. 90 | - New feature: add `.git-coauthors` file to your Git repository will be used instead of global one. 91 | 92 | ## 1.17.2 93 | 94 | ### Fixed 95 | 96 | - Stop the Git Mob list from reloading and making it difficult to update the commit message text input. 97 | - On reload co authors in selected would duplicate in unselected lists 98 | - Ensure counts are correct when selecting a co author 99 | 100 | ## 1.17.1 101 | 102 | ### Fixed 103 | 104 | - Extension breaks if the user's name is empty from GitHub [issue 152](https://github.com/rkotze/git-mob-vs-code/issues/152). Replaced empty name property with `username` (`login` property). 105 | 106 | ### Note 107 | 108 | - CI build step in GitHub actions. 109 | - Add publish step for GitHub actions 110 | - Published to [open VSX registry](https://open-vsx.org/extension/RichardKotze/git-mob). Addresses [issue 122](https://github.com/rkotze/git-mob-vs-code/issues/122). 111 | 112 | ## 1.17.0 113 | 114 | ### Added 115 | 116 | - Global Git Mob means that you will have the same selected co-authors when switching between projects. Read more on [migrating from local commit template](https://github.com/rkotze/git-mob-vs-code/discussions/120). 117 | - Reload icon added to project folder item. 118 | - By default fetch anonymous email from GitHub [issue 101](https://github.com/rkotze/git-mob-vs-code/issues/101) 119 | 120 | ## 1.16.0 -- 2020-03-03 121 | 122 | ### Added 123 | 124 | - Use command palette to select co-authors from unselected and repo contributors [issue 82](https://github.com/rkotze/git-mob-vs-code/issues/82) This also enables adding multiple repo contributors at once with the multi-selector command palette. 125 | - Right-click on co-author item and copy co-author to clipboard [issue 79](https://github.com/rkotze/git-mob-vs-code/issues/79) 126 | 127 | ### Fixes 128 | 129 | - Clean up unused icons and use VS Code provided icons to match the editors UI. 130 | - When selecting a repo contributor from search command palette, fix flow of adding them to selected for co-authoring if setting is enabled. 131 | 132 | ## 1.15.0 -- 2022-01-22 133 | 134 | ### Added 135 | 136 | - Toggle authors in alphabetical order ascending or descending [issue 73](https://github.com/rkotze/git-mob-vs-code/issues/73) - see settings to toggle sort. Thanks to @viperet. 137 | - New core integration tests added using @vscode/test-electron and mocha. Added to Azure CI pipeline. 138 | 139 | ### Fixes 140 | 141 | - Small stability fixes: 142 | - app is ready check for tests 143 | - set git template path on solo function (edge case) 144 | 145 | ## 1.14.1 -- 2021-11-14 146 | 147 | ### Fixed 148 | 149 | - Cannot read property 'inputBox' of undefined for workspaces [issue 80](https://github.com/rkotze/git-mob-vs-code/issues/80) and related [issue 61](https://github.com/rkotze/git-mob-vs-code/issues/61). When there are two or more repositories open and Git Mob **cannot** reliably workout the selected repository it fails. 150 | - Author **not** changing when switching between repositories in workspaces. 151 | - Setting the commit template first time usage on a repository was done incorrectly and essentially failing to write the changes into the template file. This prevented the co-author metadata from being added into the inputBox. See [issue 81](https://github.com/rkotze/git-mob-vs-code/issues/81) 152 | 153 | ## 1.14.0 -- 2021-11-09 154 | 155 | ### Added 156 | 157 | - Sort co-author list by alphabetical order in [issue 11](https://github.com/rkotze/git-mob-vs-code/issues/11) - thanks to @viperet 158 | - Add co-author from "more authors" directly into "selected" [issue 76](https://github.com/rkotze/git-mob-vs-code/issues/76) 159 | 160 | ### Fixed 161 | 162 | - Listen to repo change event to sync Git Mob UI rather than VS Code UI change events, which run more often even if co-authors have not changed. 163 | - Loosen the author email matcher for "more authors" to ensure all contributors are listed. 164 | - Open extension settings correctly by showing only GitMob settings. 165 | 166 | ## 1.13.0 -- 2021-08-29 167 | 168 | ### Added 169 | 170 | - Use Git Mob API [PR 67](https://github.com/rkotze/git-mob-vs-code/pull/67) 171 | - Simplifies the extension and no need to check if CLI is installed including which version. 172 | - Performance, UI responds faster when adding, selecting and listing co-authors. Most notable on Windows. 173 | - Still keeps in sync with Git Mob CLI and can be used in conjunction. 174 | - Addresses the errors of missing "npx git mob" commands "could not determine executable to run NPM ERR!" [PR 49](https://github.com/rkotze/git-mob-vs-code/pull/49) 175 | 176 | ## 1.12.0 -- 2021-06-02 177 | 178 | ### Added 179 | 180 | - Stop support for prepare-commit-msg no longer option in `git-mob v2` [Issue 62](https://github.com/rkotze/git-mob-vs-code/issues/62) 181 | - Stop continuous loading of co-author list when interacting with SCM UI 182 | - Show error message if no GitHub PAT is set [Issue 58](https://github.com/rkotze/git-mob-vs-code/issues/58) 183 | 184 | ## 1.11.0 -- 2021-01-06 185 | 186 | ### Added 187 | 188 | - Add a total count for Selected and Unselected co-authors [Issue 3](https://github.com/rkotze/git-mob-vs-code/issues/3) 189 | - Updated gif demo of Git Mob 190 | 191 | ### Fixed 192 | 193 | - Watch for commit failed ENOENT: no such file or directory `\.git\COMMIT_EDITMSG` [Issue 56](https://github.com/rkotze/git-mob-vs-code/issues/56) 194 | 195 | ## 1.10.1 -- 2020-11-11 196 | 197 | ### Fixed 198 | 199 | - When selecting a co-author error "Cannot read property 'inputBox' of undefined". [Issue 48](https://github.com/rkotze/git-mob-vs-code/issues/48) **Note** related VS Code issues logged in ticket. 200 | 201 | ## 1.10.0 -- 2020-11-01 202 | 203 | ### Added 204 | 205 | - Select multiple CoAuthors to mob program with. [Issue 46](https://github.com/rkotze/git-mob-vs-code/issues/46) 206 | - set `.git-coauthors` language to JSON 207 | - Add linting to CI pipeline 208 | - Make search options for Git repository and GitHub available in context menu 209 | - Open Git Mob settings from context menu 210 | 211 | ### Fixed 212 | 213 | - Pressing ESC on GitHub search input should not trigger a search 214 | 215 | ## [1.9.0] -- 2020-10-18 216 | 217 | ### Added 218 | 219 | - Support using Git Mob CLI as a dev dependency instead of global only using npx. Container will show install options if no cli version detected. [Issue 44](https://github.com/rkotze/git-mob-vs-code/issues/44) 220 | 221 | ## [1.8.0] -- 2020-10-04 222 | 223 | ### Added 224 | 225 | - Enable user to search GitHub for users that don't exist in current repository. [Issue 15](https://github.com/rkotze/git-mob-vs-code/issues/15) 226 | - Minor UI updates, show emails for co-authoring, unselected and author to make it clear which account is being used. 227 | 228 | ## [1.7.0] -- 2020-08-15 229 | 230 | ### Added 231 | 232 | - Toggle setting to move selected co-authors to unselected after a commit, default false. [Issue 43](https://github.com/rkotze/git-mob-vs-code/issues/43) 233 | - Toggle setting to expand "More authors" on start by default true. 234 | 235 | ## [1.6.0] -- 2020-08-01 236 | 237 | ### Added 238 | 239 | - Change primary author from co-author list [Issue 42](https://github.com/rkotze/git-mob-vs-code/issues/42) 240 | - Starting a new Git repo from VS Code editor will initialize Git Mob as well [Issue 41](https://github.com/rkotze/git-mob-vs-code/issues/41) 241 | 242 | ![Change primary author](https://user-images.githubusercontent.com/10452163/89105613-6d86a400-d41a-11ea-88ec-25b34a084598.gif) 243 | 244 | ## [1.5.4] -- 2020-07-04 245 | 246 | ### Fixed 247 | 248 | - On Mac OS users opening workspaces might see an error notification appear when contacting Git Mob CLI [Issue 39](https://github.com/rkotze/git-mob-vs-code/issues/39) 249 | 250 | ## [1.5.3] -- 2020-05-10 251 | 252 | ### Fixed 253 | 254 | - List updates when solo action clicked and "selected" is collapsed [Issue 36](https://github.com/rkotze/git-mob-vs-code/issues/36) 255 | - Code improvements making it easier to reason about and is more idiomatic vscode extension code. [Issue 35](https://github.com/rkotze/git-mob-vs-code/issues/35) 256 | 257 | ## [1.5.2] -- 2020-04-26 258 | 259 | ### Fixed 260 | 261 | - Fix timing issue with dependency on `vscode.git`. Sometimes Git Mob will trigger before the Git repositories are registered in VS code. 262 | 263 | ## [1.5.0] -- 2020-04-23 264 | 265 | ### Added 266 | 267 | - Support workspaces, be able to select one of multiple Git repositories and update with co-author metadata [Issue 31](https://github.com/rkotze/git-mob-vs-code/issues/31) 268 | 269 | Workspace support 270 | 271 | - Gitmoji, search and select from the standardized emojis for commit message [Issue 32](https://github.com/rkotze/git-mob-vs-code/issues/32) 272 | 273 | ![git emojis select list](https://user-images.githubusercontent.com/10452163/79442052-ef6bd200-7fcf-11ea-85c1-82789738add3.png) 274 | 275 | ### Fixed 276 | 277 | - Prevent the main Git Mob action icons from showing on other views like "output" [Issue 34](https://github.com/rkotze/git-mob-vs-code/issues/34) 278 | 279 | ## [1.4.0] -- 2020-04-13 280 | 281 | ### Added 282 | 283 | - Handle broken UI when no Git author or missing Git Mob list command [Issue 29](https://github.com/rkotze/git-mob-vs-code/issues/29) 284 | - Enable users to input a co-author manually using VS code inputs [Issue 4](https://github.com/rkotze/git-mob-vs-code/issues/4) 285 | 286 | ### Fixed 287 | 288 | - Update dependencies: latest vscode engine and security patches in dev dependencies 289 | - Introduce End to End testing using Jest as the test runner 290 | 291 | ## [1.3.0] -- 2019-10-09 292 | 293 | ### Added 294 | 295 | - Handle opening `.git-coauthors` file if users specify the path using environment variable `GITMOB_COAUTHORS_PATH`. 296 | - Tweet action to encourage users to share extension. [Tweet about Git Mob](https://twitter.com/intent/tweet?hashtags=GitHub,WebDev&text=I%20use%20Git%20Mob%20for%20co-authoring%20commits.%20Try%20it%20out:%20https://marketplace.visualstudio.com/items?itemName=RichardKotze.git-mob) 297 | 298 | ### Fixed 299 | 300 | - Open VS Code in sub-directory of a project Git Mob does not show co-authors [Issue 26](https://github.com/rkotze/git-mob-vs-code/issues/26). 301 | - Open git-coauthors file from sidebar doesn't work on Windows [Issue 21](https://github.com/rkotze/git-mob-vs-code/issues/21). 302 | 303 | ## [1.2.0] -- 2019-09-26 304 | 305 | ### Added 306 | 307 | - Support Git Mob feature [overwrite main author](https://github.com/findmypast-oss/git-mob#overwrite-the-main-author) to excluding the main author in unselect or select list. Reduce confusion when you're the main author. 308 | 309 | ### Fixed 310 | 311 | - Git Mob authors fails to show with private emails and special chars in initials key 312 | - Upgrade dependencies to address GitHub security alerts 313 | 314 | ## [1.1.1] -- 2019-05-16 315 | 316 | ### Fixed 317 | 318 | - Keep consistent behaviour of adding more authors to unselected list instead of straight to selected. 319 | 320 | ## [1.1.0] -- 2019-05-15 321 | 322 | ### Added 323 | 324 | - Search for & select an author from the "More Authors" list (all contributors to the repository). 325 | 326 | ### Fixed 327 | 328 | - Fix security alert for dependency 329 | 330 | ## [1.0.2] -- 2019-04-10 331 | 332 | ### Fixed 333 | 334 | - Git Mob title bar actions showing on other extension bars (see issue [#16](https://github.com/rkotze/git-mob-vs-code/issues/16)) 335 | - Add two EOL between co-authors meta data and message for VS Code Git text input field 336 | 337 | ## [1.0.1] -- 2019-02-09 338 | 339 | ### Fixed 340 | 341 | - Install `git-mob` when the extension is installed to make it easier to get going (see issue [#9](https://github.com/rkotze/git-mob-vs-code/issues/9)) 342 | 343 | ## [1.0.0] -- 2019-02-09 344 | 345 | ### Added 346 | 347 | - Add a repo author into co-author file (see issue [#13](https://github.com/rkotze/git-mob-vs-code/issues/13)) 348 | 349 | ### Fixed 350 | 351 | - Allow co-author key to be longer than 3 characters and have other characters 352 | 353 | ## [0.4.0] -- 2019-01-27 354 | 355 | ### Added 356 | 357 | - Show if using git-template or prepare-commit-msg hook in the status bar (see issue [#2](https://github.com/rkotze/git-mob-vs-code/issues/2)) 358 | 359 | ### Fixed 360 | 361 | - Git Mob will not run if a git repo is not open (see issue [#8](https://github.com/rkotze/git-mob-vs-code/issues/8)) 362 | - Entering text into SCM input box removed when added on refocusing of vs code (see issue [#12](https://github.com/rkotze/git-mob-vs-code/issues/12)) 363 | 364 | ## [0.3.0] -- 2019-01-20 365 | 366 | ### Added 367 | 368 | - Add reload action for when `.git-coauthors` changes 369 | - Change `.git-coauthors` in VS Code and the Git Mob UI list will update 370 | - UI updated to include icons for selected and unselected co-authors 371 | 372 | ### Fixed 373 | 374 | - On Mac opening the `.git-coauthors` file failed 375 | - Attached the open `.git-coauthors` button on other extension title bars 376 | 377 | ## [0.2.0] -- 2019-01-12 378 | 379 | ### Added 380 | 381 | - Open the `.git-coauthors` file from Git Mob title bar 382 | - Can action git solo from the "Selected" group 383 | - Icon added to Author and Co-Author items 384 | 385 | ## [0.1.1] -- 2018-12-30 386 | 387 | ### Added 388 | 389 | - Query installed Git Mob terminal app to find co-authors status for commit 390 | - Add and remove co-authors for commit messages 391 | - Supports adding co-authors to SCM input box or via git `prepare-commit-message` hook 392 | - Changing co-authors in the terminal is reflected in the UI when returning to VS code 393 | 394 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 395 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behaviour that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behaviour by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behaviour and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behaviour. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviours that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behaviour may be reported by contacting the project team at richkotze@outlook.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | 👋Thank you for taking the time to read this and I look forward to reviewing your contribution. 4 | 5 | ## Contribution instructions: 6 | 7 | - Create a fork of the project on Github. 8 | - Clone the fork on your local machine. Your remote repo on Github is called `origin`. 9 | - Add the original repository as a remote called `upstream`. 10 | - If you created your fork a while ago be sure to pull upstream changes into your local repository. 11 | - Create a new branch to work on! Branch from `master`. 12 | - Please make an issue to reference and implement/fix your feature. 13 | - Please add unit tests, **jest** is the testing tool. 14 | - Make a pull request (PR) to merge into `master` and make sure it passes the continuous integration pipeline. 15 | - Ensure linting and unit tests all pass before submitting your PR. Run command `npm run ci`. 16 | - Ensure integration tests are all passing `npm run ui-tests`. 17 | - You're PR will be reviewed as soon as possible. 18 | 19 | 20 | ## Getting Started 21 | 22 | 1. Run tests (Uses Jest) 23 | ``` 24 | npm test 25 | npm test -- --watch 26 | ``` 27 | 1. Run integration tests `npm run ui-tests` 28 | 1. Debug extension: 29 | Press F5 in VS Code to launch the extension into a sandbox and you can place break points 30 | 1. Debug tests: In the debug tab you can change to `jest` from the dropdown 31 | 1. Optional - Install latest version of `git-mob`. Useful to check it's keeping in sync with CLI. 32 | ``` 33 | npm i -g git-mob 34 | ``` 35 | 36 | ### List of available [VS Code icons](https://microsoft.github.io/vscode-codicons/dist/codicon.html) 37 | 38 | ## Releasing 39 | 40 | This section is for owner/maintainers with publish access to git-mob-vs-code on the extension marketplace. 41 | 42 | 1. Add release notes at https://github.com/rkotze/git-mob-vs-code/releases and update the CHANGELOG https://github.com/rkotze/git-mob-vs-code/blob/master/CHANGELOG.md 43 | 1. Bump the version at the appropriate level (major, minor, patch); e.g. 44 | ``` 45 | npm version patch 46 | ``` 47 | 1. Push the version commit and tag 48 | ``` 49 | git push --follow-tags 50 | ``` 51 | 1. New releases need to be manually triggered 52 | [GitHub publish action](https://dev.azure.com/TinkerTaylor/VS%20code%20extensions/_release?view=all&_a=releases&definitionId=1) 53 | 54 | You might need to [regenerate PATs](https://dev.azure.com/TinkerTaylor/_usersSettings/tokens) 55 | 56 | 1. **Never to be used** only for reference: Manually release the package 57 | ``` 58 | vsce publish -p 59 | ``` 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Richard Kotze 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VS Code Git Mob logo Co-author commits 2 | 3 | ![Visual Studio Marketplace Installs](https://img.shields.io/visual-studio-marketplace/i/RichardKotze.git-mob?color=brightgreen&label=VSCode%20installs) [![](https://img.shields.io/open-vsx/dt/RichardKotze/git-mob?color=brightgreen&label=VSX%20installs)](https://open-vsx.org/extension/RichardKotze/git-mob) 4 | 5 | Buy Me A Coffee 6 | 7 | > VS Code extension to easily _co-author a commit_ message from the source control panel when **pair programming**. 8 | 9 | ![Git Mob in action](https://user-images.githubusercontent.com/10452163/156645932-8f5629f5-24b6-42cd-b24a-767164364353.gif) 10 | 11 | Git Mob add using plus symbol 12 | 13 | 14 | 1. [Install](#install) 15 | 2. [Quick start](#quick-start) 16 | 3. [Features](#features) 17 | 4. [Notable changes](#notable-changes) 18 | 5. [Settings](#settings) 19 | 6. [Contributing](https://github.com/rkotze/git-mob-vs-code/blob/master/CONTRIBUTING.md) 20 | 21 | ## Install 22 | 23 | Open VS Code and search for **"git-mob"** in **Extensions panel**. 24 | 25 | OR 26 | 27 | Via the terminal `code --install-extension RichardKotze.git-mob` 28 | 29 | Available in [VS Code marketplace](https://marketplace.visualstudio.com/items?itemName=RichardKotze.git-mob) and [open VSX registry](https://open-vsx.org/extension/RichardKotze/git-mob) 30 | 31 | If you also use _[git-mob cli](https://github.com/rkotze/git-mob/#install)_ they work fine together. 32 | 33 | ## Quick start 34 | 35 | ### Key commands 36 | 37 | **Select co-authors** - `CTRL+shift+p` or `⌘+⇧+p` -> "Select co-authors". Use the multi-selector to add and remove co-authors. 38 | 39 | **Solo** - clear all selected co-authors: `CTRL+shift+p` or `⌘+⇧+p` -> "Solo: remove all co-authors" 40 | 41 | Add new co-authors from [repository contributors](#search-repository-contributors-co-authors) or add directly to your co-authors file [`.git-coauthors`](#add-new-co-authors). 42 | 43 | ### Saving your co-authors 44 | 45 | To keep track of co-authors git-mob uses a JSON file called `.git-coauthors`, and will try to find it in the following directories: 46 | 47 | 1. If `GITMOB_COAUTHORS_PATH` environment variable is set this will override any other settings. 48 | 1. If the current Git repository has a `.git-coauthors` file in the root directory. 49 | 1. The default is the users home directory at `~/.git-coauthors`. 50 | 51 | ## Features 52 | 53 | - Append _multiple co-authors_ to a commit message 54 | - Type `+` in the SCM textbox to see autocomplete list of co-authors 55 | - Select multiple co-authors (`shift` or `ctrl` or `⌘`) in SCM panel view 56 | - Easily add co-author data by [searching repository contributors co-authors](#search-repository-contributors-co-authors) from the "More Authors" list 57 | - **After a commit** [remove all selected co-authors](#post-commit---solo) 58 | - [Workspace support](#workspace-support), multiple Git repositories 59 | - **Search and add** co-authors from **GitHub** using anonymous email 60 | - [Change primary author](#change-primary-author) 61 | - Right-click and copy co-author data. 62 | - Add [emojis](#git-emojis) to commits (Gitmojis) 63 | - Configurable see [setting options](#settings) 64 | 65 | ### Notable changes: 66 | 67 | 1. [Global Git Mob](https://github.com/rkotze/git-mob-vs-code/discussions/120) meaning switch between projects will have same co-authors selected. (`v1.17.0`) 68 | 2. `prepare-commit-msg` hook support **removed** (`v1.12.0`) 69 | 70 | ### Add new co-authors 71 | 72 | **Option 1:** `CTRL+shift+p` or `⌘+⇧+p` -> "Add new co-author". Fill in all input fields. 73 | 74 | **Option 2:** `ctrl+shift+p` or `⌘+⇧+p` -> "Open .git-coauthors file" or use UI to open co-authors file `.git-coauthors`. 75 | 76 | ### Add from repository contributors 77 | 78 | Click the **plus +** button on an author in **More Authors** list. 79 | 80 | ### Search repository contributors co-authors 81 | 82 | Click the search icon on the **More Authors** section or `CTRL+shift+p` or `⌘+⇧+p` -> "Search Git repository for co-authors". 83 | 84 | ![image](https://user-images.githubusercontent.com/10452163/57807338-e2f44f00-7758-11e9-8fb1-6d8b8cb9d7ce.png) 85 | 86 | ### Workspace support 87 | 88 | Select one of multiple open Git repositories and add co-author metadata to source control input field. You can have different co-authors applied to all the open Git repositories. 89 | 90 | ### Git Emojis 91 | 92 | Using the **standardised** list from [Gitmoji](https://github.com/carloscuesta/gitmoji). 93 | 94 | Search and select an emoji to add to the Git message input field. 95 | 96 | `CTRL+shift+p` or `⌘+⇧+p` -> "Search Gitmojis - emojis" 97 | 98 | ![git emojis select list](https://user-images.githubusercontent.com/10452163/79442052-ef6bd200-7fcf-11ea-85c1-82789738add3.png) 99 | 100 | ### Change primary author 101 | 102 | `CTRL+shift+p` or `⌘+⇧+p` -> "Change primary author" 103 | 104 | ## Settings 105 | 106 | ### Post commit -> Solo 107 | 108 | After a commit remove selected co-authors. If you commit in the command-line the UI will update as well. 109 | 110 | `Default: false` 111 | 112 | ### Author list -> Expand more authors 113 | 114 | Expand 'More Authors' tree when UI starts. 115 | 116 | `Default: true` 117 | 118 | ### Author list -> More Authors To Co-authoring 119 | 120 | Add a new author directly to 'Co-authoring' from 'More Authors'. 121 | 122 | `Default: true` 123 | 124 | ### Co-authors -> Sort direction 125 | 126 | Co-author list sorting direction. 127 | 128 | `Default: ascending` 129 | 130 | ### SCM Textbox -> Enable input autocomplete 131 | 132 | Enable co-author autocompletion in the commit message SCM input textbox. 133 | 134 | `Default: true` 135 | -------------------------------------------------------------------------------- /__mocks__/vscode.js: -------------------------------------------------------------------------------- 1 | const languages = { 2 | createDiagnosticCollection: jest.fn(), 3 | }; 4 | 5 | const StatusBarAlignment = {}; 6 | 7 | const window = { 8 | createStatusBarItem: jest.fn(() => ({ 9 | show: jest.fn(), 10 | })), 11 | showErrorMessage: jest.fn(), 12 | showWarningMessage: jest.fn(), 13 | createTextEditorDecorationType: jest.fn(), 14 | registerFileDecorationProvider: jest.fn(), 15 | }; 16 | 17 | const workspace = { 18 | getConfiguration: jest.fn(), 19 | workspaceFolders: [], 20 | onDidSaveTextDocument: jest.fn(), 21 | onDidChangeConfiguration: jest.fn(), 22 | }; 23 | 24 | const OverviewRulerLane = { 25 | Left: null, 26 | }; 27 | 28 | const Uri = { 29 | file: (f) => f, 30 | parse: jest.fn(), 31 | }; 32 | const Range = jest.fn(); 33 | const Diagnostic = jest.fn(); 34 | const DiagnosticSeverity = { Error: 0, Warning: 1, Information: 2, Hint: 3 }; 35 | 36 | const debug = { 37 | onDidTerminateDebugSession: jest.fn(), 38 | startDebugging: jest.fn(), 39 | }; 40 | 41 | const commands = { 42 | executeCommand: jest.fn(), 43 | }; 44 | 45 | const TreeItemCollapsibleState = { 46 | None: "None", 47 | Expanded: "Expanded", 48 | Collapsed: "Collapsed", 49 | }; 50 | 51 | class TreeItem {} 52 | 53 | class EventEmitter { 54 | get event() { 55 | return EventEmitter.event; 56 | } 57 | fire() {} 58 | } 59 | 60 | const vscode = { 61 | languages, 62 | StatusBarAlignment, 63 | window, 64 | workspace, 65 | OverviewRulerLane, 66 | Uri, 67 | Range, 68 | Diagnostic, 69 | DiagnosticSeverity, 70 | debug, 71 | commands, 72 | TreeItemCollapsibleState, 73 | TreeItem, 74 | EventEmitter, 75 | }; 76 | 77 | module.exports = vscode; 78 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const js = require("@eslint/js"); 2 | const globals = require("globals"); 3 | 4 | module.exports = [ 5 | { ignores: [".vscode-test/*"] }, 6 | { 7 | files: ["**/*.js"], 8 | languageOptions: { 9 | ecmaVersion: "latest", 10 | sourceType: "commonjs", 11 | globals: { 12 | ...globals.node, 13 | ...globals.jest, 14 | ...globals.mocha, 15 | }, 16 | }, 17 | ...js.configs.recommended, 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /extension.js: -------------------------------------------------------------------------------- 1 | const { setupGitMob } = require("./src/setup-git-mob"); 2 | const { GitExt } = require("./src/vscode-git-extension/git-ext"); 3 | const { 4 | installGitCoAuthorFile, 5 | } = require("./src/install/install-git-coauthor-file"); 6 | const { logIssue } = require("./src/errors/log-issue"); 7 | 8 | let isReady = false; 9 | 10 | async function activate(context) { 11 | try { 12 | await installGitCoAuthorFile(); 13 | } catch (error) { 14 | logIssue( 15 | `Extension Activate: Oops something went wrong creating co-author file. Error: ${error.message}` 16 | ); 17 | } 18 | const gitExt = new GitExt(); 19 | gitExt.gitApi.onDidOpenRepository(async () => { 20 | if (!isReady && gitExt.gitApi.repositories.length <= 1) { 21 | await setupGitMob(context, gitExt); 22 | isReady = true; 23 | } 24 | }); 25 | } 26 | 27 | exports.activate = activate; 28 | 29 | function deactivate() {} 30 | exports.deactivate = deactivate; 31 | 32 | function ready() { 33 | return new Promise((resolve, reject) => { 34 | retry(1, resolve, reject); 35 | }); 36 | } 37 | 38 | function retry(count, resolve, reject) { 39 | if (isReady) { 40 | resolve("git-mob ready"); 41 | } 42 | if (count >= 10) { 43 | reject("git-mob failed"); 44 | } 45 | setTimeout(function () { 46 | retry(count + 1, resolve, reject); 47 | }, 200); 48 | } 49 | 50 | exports.ready = ready; 51 | -------------------------------------------------------------------------------- /extension.spec.js: -------------------------------------------------------------------------------- 1 | const { activate } = require("./extension"); 2 | const { setupGitMob } = require("./src/setup-git-mob"); 3 | const { GitExt } = require("./src/vscode-git-extension/git-ext"); 4 | const { 5 | installGitCoAuthorFile, 6 | } = require("./src/install/install-git-coauthor-file"); 7 | 8 | jest.mock("./src/setup-git-mob"); 9 | jest.mock("./src/vscode-git-extension/git-ext"); 10 | jest.mock("./src/install/install-git-coauthor-file"); 11 | 12 | describe("Activate Git Mob extension", function () { 13 | const onDidOpenRepositoryMock = jest.fn((callback) => { 14 | callback(); 15 | }); 16 | 17 | beforeAll(() => { 18 | GitExt.mockImplementation(() => { 19 | return { 20 | gitApi: { 21 | onDidOpenRepository: onDidOpenRepositoryMock, 22 | repositories: [ 23 | { 24 | rootUri: { fsPath: "/path/to/repo" }, 25 | inputBox: { value: "" }, 26 | }, 27 | ], 28 | }, 29 | }; 30 | }); 31 | }); 32 | 33 | it("confirm execution order", async function () { 34 | await activate(); 35 | const first = installGitCoAuthorFile.mock.invocationCallOrder[0]; 36 | const second = GitExt.mock.invocationCallOrder[0]; 37 | const third = setupGitMob.mock.invocationCallOrder[0]; 38 | 39 | expect(first).toBeLessThan(second); 40 | expect(second).toBeLessThan(third); 41 | expect(installGitCoAuthorFile).toHaveBeenCalledTimes(1); 42 | expect(GitExt).toHaveBeenCalledTimes(1); 43 | expect(onDidOpenRepositoryMock).toHaveBeenCalledTimes(1); 44 | expect(setupGitMob).toHaveBeenCalledTimes(1); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /github.http: -------------------------------------------------------------------------------- 1 | GET https://api.github.com/search/users?q=richard kotze 2 | Accept: application/vnd.github.v3+json 3 | # Authorization: token {{githubkey}} 4 | 5 | ### 6 | 7 | GET https://api.github.com/users/rkotze 8 | Accept: application/vnd.github.v3+json 9 | # Authorization: token {{githubkey}} 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | testPathIgnorePatterns: ["/test"], 4 | }; 5 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "checkJs": false, /* Typecheck .js files. */ 6 | "lib": [ 7 | "es6" 8 | ] 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-mob", 3 | "displayName": "Git Mob co-author commits", 4 | "description": "Manage and add co-author meta data to commits during pair/mob programming from the source control panel.", 5 | "version": "1.23.1", 6 | "publisher": "RichardKotze", 7 | "engines": { 8 | "vscode": "^1.67.0" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/rkotze/git-mob-vs-code.git" 13 | }, 14 | "categories": [ 15 | "Other" 16 | ], 17 | "icon": "resources/gitmob.png", 18 | "galleryBanner": { 19 | "color": "#232323", 20 | "theme": "dark" 21 | }, 22 | "license": "MIT", 23 | "author": { 24 | "name": "Richard Kotze", 25 | "email": "richkotze@outlook.com" 26 | }, 27 | "homepage": "https://github.com/rkotze/git-mob-vs-code/blob/master/README.md", 28 | "bugs": { 29 | "url": "https://github.com/rkotze/git-mob-vs-code/issues" 30 | }, 31 | "extensionDependencies": [ 32 | "vscode.git" 33 | ], 34 | "activationEvents": [ 35 | "onCommand:gitmob.openGitCoauthor", 36 | "onCommand:gitmob.reload", 37 | "onCommand:gitmob.searchRepositoryUsers", 38 | "onCommand:gitmob.searchGithubAuthors", 39 | "onCommand:gitmob.addRepoAuthorToCoAuthors", 40 | "onCommand:gitmob.solo", 41 | "onCommand:gitmob.searchGitEmojis", 42 | "*" 43 | ], 44 | "main": "./extension", 45 | "keywords": [ 46 | "git", 47 | "co-authors", 48 | "git author", 49 | "author", 50 | "annotation", 51 | "github", 52 | "commit", 53 | "emojis", 54 | "pair programming", 55 | "mob programming" 56 | ], 57 | "contributes": { 58 | "languages": [ 59 | { 60 | "id": "json", 61 | "extensions": [ 62 | ".git-coauthors" 63 | ] 64 | } 65 | ], 66 | "configuration": { 67 | "title": "Git Mob", 68 | "properties": { 69 | "gitMob.postCommit.solo": { 70 | "type": "boolean", 71 | "default": false, 72 | "description": "After a commit remove selected co-authors" 73 | }, 74 | "gitMob.authorList.expandMoreAuthors": { 75 | "type": "boolean", 76 | "default": true, 77 | "description": "Expand 'More Authors' tree when UI starts" 78 | }, 79 | "gitMob.authorList.moreAuthorsToCo-authoring": { 80 | "type": "boolean", 81 | "default": true, 82 | "description": "Add a new author directly to 'Co-authoring' from 'More Authors'" 83 | }, 84 | "gitMob.coAuthors.sortDirection": { 85 | "type": "string", 86 | "default": "ascending", 87 | "enum": [ 88 | "ascending", 89 | "descending" 90 | ], 91 | "description": "Co-author list sorting direction" 92 | }, 93 | "gitMob.scmTextbox.enableInputAutocompletion": { 94 | "type": "boolean", 95 | "default": true, 96 | "description": "Enable co-author autocompletion in the commit message SCM input textbox." 97 | } 98 | } 99 | }, 100 | "commands": [ 101 | { 102 | "command": "gitmob.openGitCoauthor", 103 | "title": "Open git-coauthors file", 104 | "category": "Git Mob", 105 | "icon": "$(file)" 106 | }, 107 | { 108 | "command": "gitmob.searchGitEmojis", 109 | "title": "Search Gitmojis - emojis", 110 | "category": "Git Mob", 111 | "icon": "$(smiley)" 112 | }, 113 | { 114 | "command": "gitmob.addFromFavourite", 115 | "title": "Select co-authors", 116 | "category": "Git Mob" 117 | }, 118 | { 119 | "command": "gitmob.reload", 120 | "title": "Reload view", 121 | "category": "Git Mob", 122 | "icon": "$(refresh)" 123 | }, 124 | { 125 | "command": "gitmob.addCoAuthor", 126 | "title": "Select co-author", 127 | "category": "Git Mob", 128 | "icon": "$(add)" 129 | }, 130 | { 131 | "command": "gitmob.addRepoAuthorToCoAuthors", 132 | "title": "Add new co-author", 133 | "category": "Git Mob", 134 | "icon": "$(add)" 135 | }, 136 | { 137 | "command": "gitmob.addMainAuthor", 138 | "title": "New primary author", 139 | "category": "Git Mob", 140 | "icon": "$(add)" 141 | }, 142 | { 143 | "command": "gitmob.searchRepositoryUsers", 144 | "title": "Search Git repository for co-authors", 145 | "category": "Git Mob", 146 | "icon": "$(search)" 147 | }, 148 | { 149 | "command": "gitmob.searchGithubAuthors", 150 | "title": "Search GitHub for co-authors", 151 | "category": "Git Mob" 152 | }, 153 | { 154 | "command": "gitmob.openSettings", 155 | "title": "Settings", 156 | "category": "Git Mob" 157 | }, 158 | { 159 | "command": "gitmob.changePrimaryAuthor", 160 | "title": "Change primary author", 161 | "category": "Git Mob", 162 | "icon": "$(sync)" 163 | }, 164 | { 165 | "command": "gitmob.removeCoAuthor", 166 | "title": "Unselect co-author", 167 | "category": "Git Mob", 168 | "icon": "$(remove)" 169 | }, 170 | { 171 | "command": "gitmob.solo", 172 | "title": "Solo: remove all co-authors", 173 | "category": "Git Mob", 174 | "icon": "$(remove)" 175 | }, 176 | { 177 | "command": "gitmob.copyCoAuthor", 178 | "title": "Copy co-author", 179 | "category": "Git Mob" 180 | } 181 | ], 182 | "menus": { 183 | "view/title": [ 184 | { 185 | "command": "gitmob.searchGitEmojis", 186 | "when": "view == gitmob.CoAuthorsView", 187 | "group": "navigation" 188 | }, 189 | { 190 | "command": "gitmob.openGitCoauthor", 191 | "when": "view == gitmob.CoAuthorsView", 192 | "group": "navigation" 193 | }, 194 | { 195 | "command": "gitmob.reload", 196 | "when": "view == gitmob.CoAuthorsView", 197 | "group": "2_misc@1" 198 | }, 199 | { 200 | "command": "gitmob.openSettings", 201 | "when": "view == gitmob.CoAuthorsView", 202 | "group": "2_misc@2" 203 | }, 204 | { 205 | "command": "gitmob.searchRepositoryUsers", 206 | "when": "view == gitmob.CoAuthorsView", 207 | "group": "1_search@1" 208 | }, 209 | { 210 | "command": "gitmob.searchGithubAuthors", 211 | "when": "view == gitmob.CoAuthorsView", 212 | "group": "1_search@2" 213 | } 214 | ], 215 | "view/item/context": [ 216 | { 217 | "command": "gitmob.addCoAuthor", 218 | "when": "viewItem == add-author", 219 | "group": "inline" 220 | }, 221 | { 222 | "command": "gitmob.reload", 223 | "when": "viewItem == project-folder", 224 | "group": "inline" 225 | }, 226 | { 227 | "command": "gitmob.addRepoAuthorToCoAuthors", 228 | "when": "viewItem == add-repo-author", 229 | "group": "inline" 230 | }, 231 | { 232 | "command": "gitmob.addMainAuthor", 233 | "when": "viewItem == error-author || viewItem == primary-author", 234 | "group": "inline" 235 | }, 236 | { 237 | "command": "gitmob.removeCoAuthor", 238 | "when": "viewItem == remove-author", 239 | "group": "inline" 240 | }, 241 | { 242 | "command": "gitmob.changePrimaryAuthor", 243 | "when": "viewItem == error-author || viewItem == primary-author", 244 | "group": "inline" 245 | }, 246 | { 247 | "command": "gitmob.solo", 248 | "when": "viewItem == selected", 249 | "group": "inline" 250 | }, 251 | { 252 | "command": "gitmob.searchRepositoryUsers", 253 | "when": "viewItem == more-authors", 254 | "group": "inline" 255 | }, 256 | { 257 | "command": "gitmob.copyCoAuthor", 258 | "when": "viewItem == add-author || viewItem == add-repo-author || viewItem == remove-author || viewItem == primary-author", 259 | "group": "navigation" 260 | } 261 | ], 262 | "commandPalette": [ 263 | { 264 | "command": "gitmob.copyCoAuthor", 265 | "when": "false" 266 | }, 267 | { 268 | "command": "gitmob.removeCoAuthor", 269 | "when": "false" 270 | }, 271 | { 272 | "command": "gitmob.openSettings", 273 | "when": "false" 274 | }, 275 | { 276 | "command": "gitmob.addCoAuthor", 277 | "when": "false" 278 | } 279 | ] 280 | }, 281 | "views": { 282 | "scm": [ 283 | { 284 | "id": "gitmob.CoAuthorsView", 285 | "name": "Git Mob Co-authors" 286 | } 287 | ] 288 | }, 289 | "viewsWelcome": [ 290 | { 291 | "view": "gitmob.CoAuthorsView", 292 | "contents": "The current open folder does not have a git repository.\nInitialize one to activate Git Mob.", 293 | "when": "config.git.enabled && git.state == initialized && workbenchState == empty" 294 | } 295 | ] 296 | }, 297 | "scripts": { 298 | "test": "jest", 299 | "ui-test": "node ./test/setup/test-runner", 300 | "lint": "eslint", 301 | "ci": "npm run lint && npm test", 302 | "preversion": "npm run ci", 303 | "postversion": "git push --follow-tags", 304 | "package": "vsce package", 305 | "vsix-publish": "vsce publish", 306 | "ovsx-publish": "ovsx publish" 307 | }, 308 | "devDependencies": { 309 | "@eslint/js": "^9.28.0", 310 | "@types/jest": "^29.5.14", 311 | "@types/vscode": "^1.67.0", 312 | "@vscode/test-electron": "^2.5.2", 313 | "@vscode/vsce": "^3.4.2", 314 | "chai": "^5.2.0", 315 | "eslint": "^9.28.0", 316 | "glob": "^11.0.2", 317 | "globals": "^16.2.0", 318 | "jest": "^29.7.0", 319 | "jest-environment-node": "^29.6.4", 320 | "mocha": "^11.5.0", 321 | "ovsx": "^0.10.3" 322 | }, 323 | "dependencies": { 324 | "git-mob-core": "^0.10.1" 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /resources/co-author-suggestion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rkotze/git-mob-vs-code/54cb22a329bfc240740b7152c9605a44ada7201f/resources/co-author-suggestion.png -------------------------------------------------------------------------------- /resources/git-mob-in-action.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rkotze/git-mob-vs-code/54cb22a329bfc240740b7152c9605a44ada7201f/resources/git-mob-in-action.gif -------------------------------------------------------------------------------- /resources/gitmob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rkotze/git-mob-vs-code/54cb22a329bfc240740b7152c9605a44ada7201f/resources/gitmob.png -------------------------------------------------------------------------------- /resources/icons/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /resources/icons/selected.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /resources/icons/unselected.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /resources/icons/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/build-co-author-groups.js: -------------------------------------------------------------------------------- 1 | const { RepoAuthor } = require("./co-author-tree-provider/repo-authors"); 2 | const { CoAuthor } = require("./co-author-tree-provider/co-authors"); 3 | const { PrimaryAuthor } = require("./co-author-tree-provider/author"); 4 | const { ErrorAuthor } = require("./co-author-tree-provider/error-author"); 5 | const { getSortDirection } = require("./ext-config/config"); 6 | const { 7 | getPrimaryAuthor, 8 | getAllAuthors, 9 | solo, 10 | setCoAuthors, 11 | getSelectedCoAuthors, 12 | updateGitTemplate, 13 | repoAuthorList, 14 | } = require("git-mob-core"); 15 | 16 | exports.buildCoAuthorGroups = async function buildCoAuthorGroups() { 17 | let mainAuthor = null; 18 | let unselected = null; 19 | let selected = null; 20 | 21 | async function resolveAuthorLists() { 22 | mainAuthor = await getPrimaryAuthor(); 23 | const allAuthors = await getAllAuthors(); 24 | const selectedCoAuthors = await getSelectedCoAuthors(allAuthors); 25 | unselected = authorListToMap( 26 | allAuthors.filter( 27 | (author) => mainAuthor === undefined || mainAuthor.email != author.email 28 | ), 29 | (author) => new CoAuthor(author.name, author.email, false, author.key) 30 | ); 31 | selected = authorListToMap( 32 | selectedCoAuthors, 33 | (author) => new CoAuthor(author.name, author.email, true, author.key) 34 | ); 35 | 36 | selected.forEach((_, key) => { 37 | unselected.delete(key); 38 | }); 39 | 40 | await updateGitTemplate(selectedCoAuthors); 41 | } 42 | 43 | await resolveAuthorLists(); 44 | 45 | return { 46 | getMainAuthor() { 47 | if (mainAuthor) { 48 | return new PrimaryAuthor(mainAuthor.name, mainAuthor.email); 49 | } 50 | return new ErrorAuthor("Error: Missing main Git author."); 51 | }, 52 | getUnselected() { 53 | return sortAuthors(Array.from(unselected.values())); 54 | }, 55 | getSelected() { 56 | return sortAuthors(Array.from(selected.values())); 57 | }, 58 | async getGitRepoAuthors() { 59 | const authors = await repoAuthorList(); 60 | const contributorAuthorList = authors.map( 61 | ({ name, email, key }) => new RepoAuthor(name, email, key) 62 | ); 63 | 64 | return sortAuthors( 65 | contributorAuthorList.filter((repoAuthor) => { 66 | if (repoAuthor.email === mainAuthor.email) return false; 67 | 68 | return ![...selected.values(), ...unselected.values()].some( 69 | (coAuthor) => coAuthor.email === repoAuthor.email 70 | ); 71 | }) 72 | ); 73 | }, 74 | select(coAuthors) { 75 | for (const coAuthorIn of coAuthors) { 76 | const key = coAuthorIn.commandKey; 77 | const coAuthor = unselected.get(key); 78 | coAuthor.selected = true; 79 | selected.set(key, coAuthor); 80 | unselected.delete(key); 81 | } 82 | 83 | setCoAuthors(Array.from(selected.keys())); 84 | }, 85 | unselect(coAuthors) { 86 | for (const coAuthorIn of coAuthors) { 87 | const key = coAuthorIn.commandKey; 88 | const coAuthor = selected.get(key); 89 | coAuthor.selected = false; 90 | unselected.set(key, coAuthor); 91 | selected.delete(key); 92 | } 93 | 94 | setCoAuthors(Array.from(selected.keys())); 95 | }, 96 | addNew(coAuthors) { 97 | for (const coAuthor of coAuthors) { 98 | coAuthor.selected = false; 99 | unselected.set(coAuthor.commandKey, coAuthor); 100 | } 101 | }, 102 | async solo() { 103 | selected.forEach((value, key) => { 104 | unselected.set(key, value); 105 | selected.delete(key); 106 | }); 107 | await solo(); 108 | }, 109 | async reloadData() { 110 | return resolveAuthorLists(); 111 | }, 112 | }; 113 | }; 114 | 115 | function authorListToMap(authors, cb) { 116 | const map = new Map(); 117 | for (const author of authors) { 118 | const authorType = cb(author); 119 | map.set(authorType.commandKey, authorType); 120 | } 121 | 122 | return map; 123 | } 124 | 125 | function sortAuthors(authors) { 126 | let sorted = authors.sort((a, b) => 127 | a.name.toLowerCase().localeCompare(b.name.toLowerCase()) 128 | ); 129 | if (getSortDirection() == "descending") { 130 | sorted.reverse(); 131 | } 132 | return sorted; 133 | } 134 | -------------------------------------------------------------------------------- /src/build-co-author-groups.spec.js: -------------------------------------------------------------------------------- 1 | const { workspace } = require("../__mocks__/vscode"); 2 | const { buildCoAuthorGroups } = require("./build-co-author-groups"); 3 | const { CoAuthor } = require("./co-author-tree-provider/co-authors"); 4 | const { ErrorAuthor } = require("./co-author-tree-provider/error-author"); 5 | const { 6 | getAllAuthors, 7 | setCoAuthors, 8 | getSelectedCoAuthors, 9 | getPrimaryAuthor, 10 | repoAuthorList, 11 | } = require("git-mob-core"); 12 | const { Author } = jest.requireActual("git-mob-core"); 13 | 14 | jest.mock("git-mob-core"); 15 | 16 | describe("Co-author list", function () { 17 | const author = new Author("rk", "Richard Kotze", "rkotze@email.com"); 18 | 19 | beforeAll(function () { 20 | workspace.getConfiguration.mockReturnValue({ 21 | get() { 22 | return "ascending"; 23 | }, 24 | }); 25 | getPrimaryAuthor.mockResolvedValue(author); 26 | }); 27 | 28 | beforeEach(function () { 29 | getAllAuthors.mockReset(); 30 | getSelectedCoAuthors.mockReset(); 31 | repoAuthorList.mockReset(); 32 | }); 33 | 34 | it("No main author email should not throw error", async function () { 35 | getPrimaryAuthor.mockResolvedValueOnce(undefined); 36 | getAllAuthors.mockResolvedValueOnce([ 37 | { key: "rk", name: "Richard Kotze", email: "rkotze@email.com" }, 38 | { key: "ts", name: "Tony Stark", email: "tony@stark.com" }, 39 | ]); 40 | getSelectedCoAuthors.mockResolvedValueOnce([]); 41 | 42 | const coAuthorGroups = await buildCoAuthorGroups(); 43 | 44 | expect(coAuthorGroups.getMainAuthor()).toEqual( 45 | new ErrorAuthor("Missing main Git author") 46 | ); 47 | }); 48 | 49 | it("Repo authors should not contain co-authors with same email", async function () { 50 | repoAuthorList.mockResolvedValueOnce([ 51 | new Author("ts", "Tony Stark", "tony@stark.com"), 52 | new Author("CA", "Captian America", "captain@america.com"), 53 | ]); 54 | getAllAuthors.mockResolvedValueOnce([ 55 | { key: "rk", name: "Richard Kotze", email: "rkotze@email.com" }, 56 | { key: "ts", name: "Tony Stark", email: "tony@stark.com" }, 57 | ]); 58 | getSelectedCoAuthors.mockResolvedValueOnce([]); 59 | 60 | const coAuthorGroups = await buildCoAuthorGroups(); 61 | let repoAuthors = await coAuthorGroups.getGitRepoAuthors(); 62 | 63 | expect(repoAuthors).toHaveLength(1); 64 | expect(repoAuthors[0].email).toEqual("captain@america.com"); 65 | }); 66 | 67 | it("Remove author from repo authors", async function () { 68 | getAllAuthors.mockResolvedValueOnce([ 69 | { key: "ts", name: "Tony Stark", email: "tony@stark.com" }, 70 | ]); 71 | 72 | repoAuthorList.mockResolvedValueOnce([ 73 | new Author("rk", "Richard Kotze", "rkotze@email.com"), 74 | new Author("CA", "Black Panther", "black@panther.com"), 75 | ]); 76 | 77 | getSelectedCoAuthors.mockResolvedValueOnce([]); 78 | 79 | const coAuthorGroups = await buildCoAuthorGroups(); 80 | const repoAuthors = await coAuthorGroups.getGitRepoAuthors(); 81 | const repoAuthorEmails = repoAuthors.map((author) => author.email); 82 | 83 | expect(repoAuthorEmails).not.toEqual( 84 | expect.arrayContaining([author.email]) 85 | ); 86 | }); 87 | 88 | it("Set selected co-authors only", async function () { 89 | getAllAuthors.mockResolvedValueOnce([ 90 | { 91 | key: "rk", 92 | name: "Richard Kotze", 93 | email: "rkotze@email.com", 94 | selected: false, 95 | }, 96 | { 97 | key: "ts", 98 | name: "Tony Stark", 99 | email: "tony@stark.com", 100 | selected: true, 101 | }, 102 | { 103 | key: "pp", 104 | name: "Peter Parker", 105 | email: "peter@stark.com", 106 | selected: false, 107 | }, 108 | ]); 109 | 110 | getSelectedCoAuthors.mockResolvedValueOnce([]); 111 | 112 | const coAuthorGroups = await buildCoAuthorGroups(); 113 | const selected = [ 114 | new CoAuthor("Peter Parker", "peter@stark.com", false, "pp"), 115 | ]; 116 | await coAuthorGroups.select(selected); 117 | const all = [ 118 | ...coAuthorGroups.getSelected(), 119 | ...coAuthorGroups.getUnselected(), 120 | ]; 121 | const pp = all.find((author) => author.commandKey == "pp"); 122 | const ts = all.find((author) => author.commandKey == "ts"); 123 | expect(pp.selected).toEqual(true); 124 | expect(ts.selected).toEqual(false); 125 | expect(setCoAuthors).toHaveBeenCalledWith(["pp"]); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /src/co-author-tree-provider/author.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const { None } = vscode.TreeItemCollapsibleState; 3 | 4 | class PrimaryAuthor extends vscode.TreeItem { 5 | constructor(name, email) { 6 | super(name, None); 7 | this.name = name; 8 | this.email = email; 9 | this.contextValue = "primary-author"; 10 | } 11 | 12 | get iconPath() { 13 | return new vscode.ThemeIcon("account"); 14 | } 15 | 16 | get description() { 17 | return this.email; 18 | } 19 | 20 | get tooltip() { 21 | return `Author: ${this.label}\n${this.email}`; 22 | } 23 | } 24 | 25 | exports.PrimaryAuthor = PrimaryAuthor; 26 | -------------------------------------------------------------------------------- /src/co-author-tree-provider/co-authors-provider.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const { 3 | ProjectFolder, 4 | Selected, 5 | Unselected, 6 | MoreAuthors, 7 | } = require("./group-item"); 8 | const { GitExt } = require("../vscode-git-extension/git-ext"); 9 | const { CoAuthor } = require("./co-authors"); 10 | 11 | class CoAuthorProvider { 12 | constructor(coAuthorGroups) { 13 | this.multiSelected = []; 14 | this._onDidChangeTreeData = new vscode.EventEmitter(); 15 | this.onDidChangeTreeData = this._onDidChangeTreeData.event; 16 | this.coAuthorGroups = coAuthorGroups; 17 | this.gitExt = new GitExt(); 18 | this.config = vscode.workspace.getConfiguration("gitMob.authorList"); 19 | } 20 | 21 | async getChildren(element = {}) { 22 | if (element.fetchChildren) { 23 | return element.fetchChildren(); 24 | } 25 | 26 | return [ 27 | new ProjectFolder(this.gitExt.selectedFolderName), 28 | this.coAuthorGroups.getMainAuthor(), 29 | new Selected(() => this.coAuthorGroups.getSelected()), 30 | new Unselected(() => this.coAuthorGroups.getUnselected()), 31 | new MoreAuthors(this.config.get("expandMoreAuthors"), () => 32 | this.coAuthorGroups.getGitRepoAuthors() 33 | ), 34 | ]; 35 | } 36 | 37 | async getTreeItem(element) { 38 | return element; 39 | } 40 | 41 | async toggleCoAuthor(author, selected) { 42 | const selectedCoAuthors = this.multiSelected.filter( 43 | (author) => author instanceof CoAuthor 44 | ); 45 | const coAuthors = this.coAuthorGroups; 46 | if (selectedCoAuthors.length > 1) { 47 | selected 48 | ? coAuthors.select(selectedCoAuthors) 49 | : coAuthors.unselect(selectedCoAuthors); 50 | } else { 51 | selected ? coAuthors.select([author]) : coAuthors.unselect([author]); 52 | } 53 | this.reloadData(); 54 | } 55 | 56 | reloadData() { 57 | this.multiSelected = []; 58 | this._onDidChangeTreeData.fire(); 59 | } 60 | } 61 | 62 | exports.CoAuthorProvider = CoAuthorProvider; 63 | -------------------------------------------------------------------------------- /src/co-author-tree-provider/co-authors-provider.spec.js: -------------------------------------------------------------------------------- 1 | const { CoAuthor } = require("./co-authors"); 2 | const { CoAuthorProvider } = require("./co-authors-provider"); 3 | const { RepoAuthor } = require("./repo-authors"); 4 | 5 | jest.mock("../build-co-author-groups"); 6 | jest.mock("../vscode-git-extension/git-ext"); 7 | 8 | describe("Git author provider", function () { 9 | const jyn = new CoAuthor("Jyn Erso", "jyn@rebel.com", false, "je"); 10 | const galen = new CoAuthor("Galen Erso", "galen@deathstar.com", false, "ge"); 11 | const cassian = new RepoAuthor("Cassian Andor", "cassian@rebel.com", "ca"); 12 | const coAuthorGroups = { 13 | select: jest.fn(), 14 | unselect: jest.fn(), 15 | }; 16 | 17 | this.afterEach(() => { 18 | coAuthorGroups.select.mockReset(); 19 | coAuthorGroups.unselect.mockReset(); 20 | }); 21 | 22 | it("Select one co-author", () => { 23 | const provider = new CoAuthorProvider(coAuthorGroups); 24 | provider.toggleCoAuthor(jyn, true); 25 | expect(provider.coAuthorGroups.select).toBeCalledWith([jyn]); 26 | }); 27 | 28 | it("Unselect one co-author", () => { 29 | const provider = new CoAuthorProvider(coAuthorGroups); 30 | provider.toggleCoAuthor(galen, false); 31 | expect(provider.coAuthorGroups.unselect).toBeCalledWith([galen]); 32 | }); 33 | 34 | it("toggle two co-authors to selected", () => { 35 | const provider = new CoAuthorProvider(coAuthorGroups); 36 | provider.multiSelected = [jyn, galen]; 37 | provider.toggleCoAuthor(jyn, true); 38 | expect(provider.coAuthorGroups.select).toBeCalledWith([jyn, galen]); 39 | }); 40 | 41 | it("when one co-author selected but click add action on different author", () => { 42 | const provider = new CoAuthorProvider(coAuthorGroups); 43 | provider.multiSelected = [jyn]; 44 | provider.toggleCoAuthor(galen, true); 45 | expect(provider.coAuthorGroups.select).toBeCalledWith([galen]); 46 | }); 47 | 48 | it("select RepoAuthor and two CoAuthors then only the CoAuthors should pass through", () => { 49 | const provider = new CoAuthorProvider(coAuthorGroups); 50 | provider.multiSelected = [jyn, galen, cassian]; 51 | provider.toggleCoAuthor(galen, true); 52 | expect(provider.coAuthorGroups.select).toBeCalledWith([jyn, galen]); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/co-author-tree-provider/co-authors.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const vscode = require("vscode"); 3 | const { None } = vscode.TreeItemCollapsibleState; 4 | 5 | class CoAuthor extends vscode.TreeItem { 6 | constructor(name, email, selected = false, commandKey = "") { 7 | super(name, None); 8 | this.name = name; 9 | this.email = email; 10 | this.selected = selected; 11 | this.commandKey = commandKey; 12 | } 13 | 14 | get contextValue() { 15 | return this.selected ? "remove-author" : "add-author"; 16 | } 17 | 18 | get tooltip() { 19 | return `${this.label}\n${this.email}`; 20 | } 21 | 22 | get iconPath() { 23 | return path.join(__dirname, "..", "..", "resources", "icons", "user.svg"); 24 | } 25 | 26 | get description() { 27 | return this.email; 28 | } 29 | 30 | format() { 31 | return `Co-authored-by: ${this.toString()}`; 32 | } 33 | 34 | toString() { 35 | return `${this.label} <${this.email}>`; 36 | } 37 | } 38 | 39 | exports.CoAuthor = CoAuthor; 40 | -------------------------------------------------------------------------------- /src/co-author-tree-provider/count-decorator-provider.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | 3 | class CountDecorationProvider { 4 | constructor(coAuthorProvider) { 5 | this._onDidChangeDecorations = new vscode.EventEmitter(); 6 | this.onDidChangeFileDecorations = this._onDidChangeDecorations.event; 7 | this.coAuthorProvider = coAuthorProvider; 8 | this.handleChange(); 9 | this.disposables = []; 10 | this.disposables.push(vscode.window.registerFileDecorationProvider(this)); 11 | } 12 | 13 | handleChange() { 14 | const changeDecoration = this._onDidChangeDecorations; 15 | this.coAuthorProvider.onDidChangeTreeData(function () { 16 | changeDecoration.fire(); 17 | }); 18 | } 19 | 20 | async provideFileDecoration(uri) { 21 | const unselected = "/unselected"; 22 | const selected = "/selected"; 23 | const moreAuthors = "/more-authors"; 24 | const coAuthors = this.coAuthorProvider.coAuthorGroups; 25 | 26 | if (uri.path === unselected) { 27 | return { 28 | badge: coAuthors.getUnselected().length.toString(), 29 | tooltip: "Available to co-author", 30 | }; 31 | } 32 | if (uri.path === selected) { 33 | return { 34 | badge: coAuthors.getSelected().length.toString(), 35 | tooltip: "Selected co-authors", 36 | }; 37 | } 38 | if (uri.path === moreAuthors) { 39 | const repoAuthors = await coAuthors.getGitRepoAuthors(); 40 | const authorTotal = repoAuthors.length; 41 | const tooltip = `Contributors to this repo (${authorTotal})`; 42 | 43 | if (authorTotal > 99) { 44 | return { 45 | badge: "99", 46 | tooltip, 47 | }; 48 | } 49 | 50 | return { 51 | badge: authorTotal.toString(), 52 | tooltip, 53 | }; 54 | } 55 | } 56 | 57 | dispose() { 58 | this.disposables.forEach((d) => d.dispose()); 59 | } 60 | } 61 | 62 | exports.CountDecorationProvider = CountDecorationProvider; 63 | -------------------------------------------------------------------------------- /src/co-author-tree-provider/count-decorator-provider.spec.js: -------------------------------------------------------------------------------- 1 | const { CountDecorationProvider } = require("./count-decorator-provider"); 2 | 3 | describe("Count co-authors", function () { 4 | let mockCoAuthorProvider = { 5 | coAuthorGroups: { 6 | getUnselected: jest 7 | .fn() 8 | .mockReturnValue(new Array(5).fill({ selected: false })), 9 | getSelected: jest.fn().mockReturnValue(new Array(2)), 10 | getGitRepoAuthors: jest.fn(), 11 | }, 12 | onDidChangeTreeData: jest.fn(), 13 | }; 14 | 15 | it("should be two selected", async function () { 16 | let countDecorator = new CountDecorationProvider(mockCoAuthorProvider); 17 | await expect( 18 | countDecorator.provideFileDecoration({ path: "/selected" }) 19 | ).resolves.toEqual(expect.objectContaining({ badge: "2" })); 20 | }); 21 | 22 | it("should be five unselected", async function () { 23 | let countDecorator = new CountDecorationProvider(mockCoAuthorProvider); 24 | await expect( 25 | countDecorator.provideFileDecoration({ path: "/unselected" }) 26 | ).resolves.toEqual(expect.objectContaining({ badge: "5" })); 27 | }); 28 | 29 | it("should have 30 more authors", async function () { 30 | mockCoAuthorProvider.coAuthorGroups.getGitRepoAuthors.mockResolvedValueOnce( 31 | new Array(30) 32 | ); 33 | let countDecorator = new CountDecorationProvider(mockCoAuthorProvider); 34 | await expect( 35 | countDecorator.provideFileDecoration({ path: "/more-authors" }) 36 | ).resolves.toEqual(expect.objectContaining({ badge: "30" })); 37 | }); 38 | 39 | it("when 100+ show 99 in badge because limit is two characters", async function () { 40 | mockCoAuthorProvider.coAuthorGroups.getGitRepoAuthors.mockResolvedValueOnce( 41 | new Array(101) 42 | ); 43 | let countDecorator = new CountDecorationProvider(mockCoAuthorProvider); 44 | await expect( 45 | countDecorator.provideFileDecoration({ path: "/more-authors" }) 46 | ).resolves.toEqual(expect.objectContaining({ badge: "99" })); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/co-author-tree-provider/error-author.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const { None } = vscode.TreeItemCollapsibleState; 3 | 4 | class ErrorAuthor extends vscode.TreeItem { 5 | constructor(name, contextValue = "error-author") { 6 | super(name || "Error: missing author!", None); 7 | this.email = "e@r.ror"; 8 | this.selected = false; 9 | this.commandKey = ""; 10 | this.contextValue = contextValue; 11 | } 12 | 13 | get tooltip() { 14 | return "Error: missing author!"; 15 | } 16 | } 17 | 18 | exports.ErrorAuthor = ErrorAuthor; 19 | -------------------------------------------------------------------------------- /src/co-author-tree-provider/group-item.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const vscode = require("vscode"); 3 | 4 | const { Expanded, None, Collapsed } = vscode.TreeItemCollapsibleState; 5 | 6 | class Group extends vscode.TreeItem { 7 | constructor(label, context, iconFile, collapsibleState = Expanded) { 8 | super(label, collapsibleState); 9 | this.contextValue = context; 10 | this.iconFile = iconFile; 11 | } 12 | 13 | get iconPath() { 14 | if (this.iconFile instanceof vscode.ThemeIcon) { 15 | return this.iconFile; 16 | } 17 | 18 | return path.join( 19 | __dirname, 20 | "..", 21 | "..", 22 | "resources", 23 | "icons", 24 | this.iconFile 25 | ); 26 | } 27 | } 28 | 29 | class ProjectFolder extends Group { 30 | constructor(label) { 31 | super(label, "project-folder", new vscode.ThemeIcon("folder-active"), None); 32 | } 33 | 34 | get tooltip() { 35 | return "Project folder: " + this.label; 36 | } 37 | } 38 | 39 | class Selected extends Group { 40 | constructor(fetchChildren = () => {}) { 41 | super("Co-authoring", "selected", "selected.svg"); 42 | this.fetchChildren = fetchChildren; 43 | } 44 | 45 | get tooltip() { 46 | return "Selected co-authors"; 47 | } 48 | 49 | get resourceUri() { 50 | return vscode.Uri.file(this.contextValue); 51 | } 52 | } 53 | 54 | class Unselected extends Group { 55 | constructor(fetchChildren = () => {}) { 56 | super("Unselected", "unselected", "unselected.svg"); 57 | this.fetchChildren = fetchChildren; 58 | } 59 | 60 | get tooltip() { 61 | return "Available co-authors"; 62 | } 63 | 64 | get resourceUri() { 65 | return vscode.Uri.file(this.contextValue); 66 | } 67 | } 68 | 69 | class MoreAuthors extends Group { 70 | constructor(expand = true, fetchChildren = () => {}) { 71 | super( 72 | "More Authors", 73 | "more-authors", 74 | "more.svg", 75 | expand ? Expanded : Collapsed 76 | ); 77 | 78 | this.fetchChildren = fetchChildren; 79 | } 80 | 81 | get tooltip() { 82 | return "Contributors to this repo"; 83 | } 84 | 85 | get resourceUri() { 86 | return vscode.Uri.file(this.contextValue); 87 | } 88 | } 89 | 90 | exports.ProjectFolder = ProjectFolder; 91 | exports.Selected = Selected; 92 | exports.Unselected = Unselected; 93 | exports.MoreAuthors = MoreAuthors; 94 | -------------------------------------------------------------------------------- /src/co-author-tree-provider/input-completion-provider.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const { 3 | getInputCompletionConfig, 4 | ENABLE_INPUT_AUTOCOMPLETION, 5 | } = require("../ext-config/config"); 6 | 7 | class InputCompletionProvider { 8 | constructor(coAuthorProvider) { 9 | this.coAuthorProvider = coAuthorProvider; 10 | this.disposables = []; 11 | this.registerProvider(); 12 | 13 | vscode.workspace.onDidChangeConfiguration((e) => { 14 | if (e.affectsConfiguration(ENABLE_INPUT_AUTOCOMPLETION)) { 15 | this.registerProvider(); 16 | } 17 | }); 18 | } 19 | 20 | registerProvider() { 21 | this.dispose(); 22 | if (getInputCompletionConfig()) { 23 | this.disposables.push( 24 | vscode.languages.registerCompletionItemProvider("scminput", this, "+") 25 | ); 26 | } 27 | } 28 | 29 | provideCompletionItems(_doc, position) { 30 | return buildCompletionItems( 31 | this.coAuthorProvider.coAuthorGroups.getUnselected(), 32 | position 33 | ); 34 | } 35 | 36 | resolveCompletionItem(item) { 37 | item.command = { 38 | command: "gitmob.addCoAuthor", 39 | arguments: [item.author], 40 | title: "Add co-author", 41 | }; 42 | return item; 43 | } 44 | 45 | dispose() { 46 | this.disposables.forEach((d) => d.dispose()); 47 | this.disposables = []; 48 | } 49 | } 50 | 51 | function buildCompletionItems(unselectAuthors, position) { 52 | const items = unselectAuthors.map((author) => { 53 | const item = new vscode.CompletionItem( 54 | author.toString(), 55 | vscode.CompletionItemKind.User 56 | ); 57 | 58 | const removeTriggerChar = new vscode.Range( 59 | position.line, 60 | position.character - 1, 61 | position.line, 62 | position.character 63 | ); 64 | item.additionalTextEdits = [vscode.TextEdit.delete(removeTriggerChar)]; 65 | 66 | item.author = author; 67 | item.insertText = author.format(); 68 | return item; 69 | }); 70 | return items; 71 | } 72 | 73 | exports.InputCompletionProvider = InputCompletionProvider; 74 | -------------------------------------------------------------------------------- /src/co-author-tree-provider/repo-authors.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const vscode = require("vscode"); 3 | const { None } = vscode.TreeItemCollapsibleState; 4 | 5 | class RepoAuthor extends vscode.TreeItem { 6 | constructor(name, email, commandKey) { 7 | super(name, None); 8 | this.name = name; 9 | this.email = email; 10 | this.commandKey = commandKey; 11 | } 12 | 13 | get iconPath() { 14 | return path.join(__dirname, "..", "..", "resources", "icons", "user.svg"); 15 | } 16 | 17 | get description() { 18 | return this.email; 19 | } 20 | 21 | get tooltip() { 22 | return `${this.label}\n${this.email}`; 23 | } 24 | 25 | get contextValue() { 26 | return "add-repo-author"; 27 | } 28 | } 29 | 30 | exports.RepoAuthor = RepoAuthor; 31 | -------------------------------------------------------------------------------- /src/commands/add-co-author.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const { CoAuthor } = require("../co-author-tree-provider/co-authors"); 3 | const { moveToCoAuthoring } = require("../ext-config/config"); 4 | const { saveNewCoAuthors } = require("git-mob-core"); 5 | const { inputAuthorData } = require("./shared/input-author-data"); 6 | 7 | function addRepoAuthorToCoauthors({ coAuthorProvider }) { 8 | return vscode.commands.registerCommand( 9 | "gitmob.addRepoAuthorToCoAuthors", 10 | async function (author) { 11 | const moveToSelected = moveToCoAuthoring(); 12 | 13 | if (author) { 14 | await saveNewCoAuthors([{ ...author, key: author.commandKey }]); 15 | const coAuthor = new CoAuthor( 16 | author.name, 17 | author.email, 18 | false, 19 | author.commandKey 20 | ); 21 | coAuthorProvider.coAuthorGroups.addNew([coAuthor]); 22 | if (moveToSelected) { 23 | await coAuthorProvider.toggleCoAuthor(coAuthor, true); 24 | } 25 | } else { 26 | const newAuthor = await inputAuthorData([ 27 | { key: "name", label: "Author name" }, 28 | { key: "email", label: "Author email" }, 29 | { key: "key", label: "Author initials" }, 30 | ]); 31 | if (newAuthor) { 32 | await saveNewCoAuthors([newAuthor]); 33 | const coAuthor = new CoAuthor( 34 | newAuthor.name, 35 | newAuthor.email, 36 | false, 37 | newAuthor.key 38 | ); 39 | coAuthorProvider.coAuthorGroups.addNew([coAuthor]); 40 | if (moveToSelected) { 41 | await coAuthorProvider.toggleCoAuthor(coAuthor, true); 42 | } 43 | } 44 | } 45 | 46 | if (!moveToSelected) { 47 | coAuthorProvider.reloadData(); 48 | } 49 | } 50 | ); 51 | } 52 | 53 | exports.addRepoAuthorToCoauthors = addRepoAuthorToCoauthors; 54 | -------------------------------------------------------------------------------- /src/commands/add-main-author.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const { Author, setPrimaryAuthor } = require("git-mob-core"); 3 | const { inputAuthorData } = require("./shared/input-author-data"); 4 | 5 | function addMainAuthor() { 6 | return vscode.commands.registerCommand( 7 | "gitmob.addMainAuthor", 8 | async function () { 9 | const newAuthor = await inputAuthorData([ 10 | { key: "name", label: "Author name" }, 11 | { key: "email", label: "Author email" }, 12 | ]); 13 | if (newAuthor) { 14 | await setPrimaryAuthor( 15 | new Author("author", newAuthor.name, newAuthor.email) 16 | ); 17 | await vscode.commands.executeCommand("gitmob.reload"); 18 | } 19 | } 20 | ); 21 | } 22 | 23 | exports.addMainAuthor = addMainAuthor; 24 | -------------------------------------------------------------------------------- /src/commands/change-primary-author.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const { setPrimaryAuthor } = require("git-mob-core"); 3 | 4 | function changePrimaryAuthor({ coAuthorProvider }) { 5 | const { coAuthorGroups } = coAuthorProvider; 6 | return vscode.commands.registerCommand( 7 | "gitmob.changePrimaryAuthor", 8 | async function () { 9 | const allCoAuthors = [ 10 | ...coAuthorGroups.getSelected(), 11 | ...coAuthorGroups.getUnselected(), 12 | ]; 13 | const selectedAuthor = await quickPickFromCoAuthors(allCoAuthors); 14 | if (selectedAuthor) { 15 | await setPrimaryAuthor(selectedAuthor); 16 | await vscode.commands.executeCommand("gitmob.reload"); 17 | } 18 | } 19 | ); 20 | } 21 | 22 | exports.changePrimaryAuthor = changePrimaryAuthor; 23 | 24 | async function quickPickFromCoAuthors(repoAuthors) { 25 | const authorTextArray = repoAuthors.map((author) => ({ 26 | label: author.name, 27 | description: author.email, 28 | authorKey: author.commandKey, 29 | })); 30 | const selected = await vscode.window.showQuickPick(authorTextArray); 31 | if (selected) { 32 | return repoAuthors.find( 33 | (author) => selected.authorKey === author.commandKey 34 | ); 35 | } 36 | return null; 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/co-author-actions.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const { CoAuthor } = require("../co-author-tree-provider/co-authors"); 3 | const { saveNewCoAuthors } = require("git-mob-core"); 4 | const { GitExt } = require("../vscode-git-extension/git-ext"); 5 | 6 | async function quickPickAuthors(repoAuthors) { 7 | const gitExt = new GitExt(); 8 | const authorTextArray = repoAuthors.map((author) => { 9 | let icon = ""; 10 | let type = "RepoAuthor"; 11 | if (author instanceof CoAuthor) { 12 | icon = "$(star-full)"; 13 | type = "CoAuthor"; 14 | } 15 | return { 16 | label: `${icon}${author.name} <${author.email}>`, 17 | description: gitExt.selectedFolderName, 18 | repoAuthor: { ...author, type, key: author.commandKey }, 19 | picked: author.selected, 20 | }; 21 | }); 22 | return vscode.window.showQuickPick(authorTextArray, { canPickMany: true }); 23 | } 24 | 25 | function addCoAuthor({ coAuthorProvider }) { 26 | return vscode.commands.registerCommand( 27 | "gitmob.addCoAuthor", 28 | async function (author) { 29 | return coAuthorProvider.toggleCoAuthor(author, true); 30 | } 31 | ); 32 | } 33 | 34 | function removeCoAuthor({ coAuthorProvider }) { 35 | return vscode.commands.registerCommand( 36 | "gitmob.removeCoAuthor", 37 | function (author) { 38 | return coAuthorProvider.toggleCoAuthor(author, false); 39 | } 40 | ); 41 | } 42 | 43 | function addFromFavourite({ coAuthorProvider }) { 44 | const { coAuthorGroups } = coAuthorProvider; 45 | return vscode.commands.registerCommand( 46 | "gitmob.addFromFavourite", 47 | async function () { 48 | const contributors = await coAuthorGroups.getGitRepoAuthors(); 49 | 50 | const selectedAuthors = await quickPickAuthors([ 51 | ...coAuthorGroups.getSelected(), 52 | ...coAuthorGroups.getUnselected(), 53 | ...contributors, 54 | ]); 55 | 56 | if (selectedAuthors && selectedAuthors.length > 0) { 57 | const authors = selectedAuthors.map((author) => author.repoAuthor); 58 | const anyRepoAuthors = authors.filter( 59 | (author) => author.type === "RepoAuthor" 60 | ); 61 | if (anyRepoAuthors.length > 0) { 62 | await saveNewCoAuthors(anyRepoAuthors); 63 | coAuthorGroups.addNew( 64 | anyRepoAuthors.map( 65 | (author) => 66 | new CoAuthor(author.name, author.email, false, author.key) 67 | ) 68 | ); 69 | } 70 | coAuthorGroups.select(authors); 71 | coAuthorProvider.reloadData(); 72 | } 73 | } 74 | ); 75 | } 76 | 77 | exports.addCoAuthor = addCoAuthor; 78 | exports.removeCoAuthor = removeCoAuthor; 79 | exports.addFromFavourite = addFromFavourite; 80 | -------------------------------------------------------------------------------- /src/commands/copy-co-author.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | 3 | function copyCoAuthor() { 4 | return vscode.commands.registerCommand( 5 | "gitmob.copyCoAuthor", 6 | async function (author) { 7 | const { email, name } = author; 8 | await vscode.env.clipboard.writeText(`${name} <${email}>`); 9 | } 10 | ); 11 | } 12 | 13 | exports.copyCoAuthor = copyCoAuthor; 14 | -------------------------------------------------------------------------------- /src/commands/github-authors.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const { 3 | saveNewCoAuthors, 4 | searchGitHubAuthors: gmSearchGhAuthors, 5 | } = require("git-mob-core"); 6 | 7 | function searchGithubAuthors() { 8 | return vscode.commands.registerCommand( 9 | "gitmob.searchGithubAuthors", 10 | async function () { 11 | const searchText = await vscode.window.showInputBox({ 12 | placeHolder: "Try the name of person, email or username", 13 | validateInput(value) { 14 | if (value.length === 0) { 15 | return "Enter at least one character"; 16 | } 17 | return null; 18 | }, 19 | }); 20 | 21 | if (typeof searchText === "undefined") return null; 22 | let users = []; 23 | try { 24 | users = await gmSearchGhAuthors(searchText, "vs-code-git-mob"); 25 | } catch (error) { 26 | vscode.window.showErrorMessage(error.message); 27 | return; 28 | } 29 | 30 | if (users.length === 0) { 31 | vscode.window.showInformationMessage("No users found!"); 32 | return; 33 | } 34 | 35 | const messageUnder30 = `Git Mob: Found ${users.length} GitHub users.`; 36 | const messageOver30 = `Git Mob: More GitHub users found but limited to show 30 GitHub users.`; 37 | vscode.window.showInformationMessage( 38 | users.length === 30 ? messageOver30 : messageUnder30 39 | ); 40 | 41 | const selectedAuthor = await quickPickAuthors(users); 42 | if (selectedAuthor) { 43 | if (!selectedAuthor.repoAuthor.email) { 44 | vscode.window.showErrorMessage("No email! Can't be added."); 45 | } else { 46 | await saveNewCoAuthors([selectedAuthor.repoAuthor]); 47 | await vscode.commands.executeCommand("gitmob.reload"); 48 | } 49 | } 50 | } 51 | ); 52 | } 53 | 54 | async function quickPickAuthors(repoAuthors) { 55 | const authorTextArray = repoAuthors.map((mobAuthor) => { 56 | return { 57 | label: `${mobAuthor.name} ${mobAuthor.key}`, 58 | description: `<${mobAuthor.email}>`, 59 | repoAuthor: mobAuthor, 60 | }; 61 | }); 62 | return await vscode.window.showQuickPick(authorTextArray, { 63 | matchOnDescription: true, 64 | }); 65 | } 66 | 67 | exports.searchGithubAuthors = searchGithubAuthors; 68 | -------------------------------------------------------------------------------- /src/commands/open-git-coauthors.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const { logIssue } = require("../errors/log-issue"); 3 | const { pathToCoAuthors } = require("git-mob-core"); 4 | 5 | function openGitCoAuthor() { 6 | return vscode.commands.registerCommand( 7 | "gitmob.openGitCoauthor", 8 | async function () { 9 | const { openTextDocument } = vscode.workspace; 10 | const { showTextDocument } = vscode.window; 11 | 12 | try { 13 | const coauthorsFile = vscode.Uri.file(await pathToCoAuthors()); 14 | const doc = await openTextDocument(coauthorsFile); 15 | showTextDocument(doc); 16 | } catch (err) { 17 | logIssue("GitMob error: " + err); 18 | } 19 | } 20 | ); 21 | } 22 | 23 | exports.openGitCoAuthor = openGitCoAuthor; 24 | -------------------------------------------------------------------------------- /src/commands/open-settings.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | 3 | function openSettings() { 4 | return vscode.commands.registerCommand("gitmob.openSettings", function () { 5 | vscode.commands.executeCommand( 6 | "workbench.action.openSettings", 7 | "@ext:richardkotze.git-mob" 8 | ); 9 | }); 10 | } 11 | 12 | exports.openSettings = openSettings; 13 | -------------------------------------------------------------------------------- /src/commands/reload.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | 3 | function reloadCommand({ coAuthorProvider }) { 4 | return vscode.commands.registerCommand("gitmob.reload", async function () { 5 | await coAuthorProvider.coAuthorGroups.reloadData(); 6 | coAuthorProvider.reloadData(); 7 | }); 8 | } 9 | 10 | exports.reloadCommand = reloadCommand; 11 | -------------------------------------------------------------------------------- /src/commands/search-git-emojis.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const { GitExt } = require("../vscode-git-extension/git-ext"); 3 | const gitEmojisJson = require("../git-emojis/gitmojis.json"); 4 | const { logIssue } = require("../errors/log-issue"); 5 | 6 | function searchGitEmojis() { 7 | return vscode.commands.registerCommand( 8 | "gitmob.searchGitEmojis", 9 | async function () { 10 | const emoji = await quickPickEmojis(); 11 | if (emoji) { 12 | try { 13 | const gitExt = new GitExt(); 14 | gitExt.updateSelectedInput(function (value) { 15 | return emoji.code + " " + value; 16 | }); 17 | } catch (err) { 18 | logIssue("Failed to add emoji: " + err.message); 19 | } 20 | } 21 | } 22 | ); 23 | } 24 | 25 | exports.searchGitEmojis = searchGitEmojis; 26 | 27 | async function quickPickEmojis() { 28 | const emojiList = gitEmojisJson.gitmojis.map((emoji) => ({ 29 | label: `${emoji.emoji} ${emoji.name}`, 30 | description: emoji.description, 31 | code: emoji.code, 32 | })); 33 | return await vscode.window.showQuickPick(emojiList, { 34 | matchOnDescription: true, 35 | placeHolder: "Select emoji to add to source control input box", 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/search-repository-authors.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const { moveToCoAuthoring } = require("../ext-config/config"); 3 | const { saveNewCoAuthors } = require("git-mob-core"); 4 | const { GitExt } = require("../vscode-git-extension/git-ext"); 5 | const { CoAuthor } = require("../co-author-tree-provider/co-authors"); 6 | 7 | function searchRepositoryUsers({ coAuthorProvider }) { 8 | const { coAuthorGroups } = coAuthorProvider; 9 | return vscode.commands.registerCommand( 10 | "gitmob.searchRepositoryUsers", 11 | async function () { 12 | const repoAuthors = await coAuthorGroups.getGitRepoAuthors(); 13 | const authorItem = await quickPickAuthors(repoAuthors); 14 | if (authorItem) { 15 | await saveNewCoAuthors([authorItem.repoAuthor]); 16 | const rAuthor = authorItem.repoAuthor; 17 | const coAuthor = new CoAuthor( 18 | rAuthor.name, 19 | rAuthor.email, 20 | false, 21 | rAuthor.commandKey 22 | ); 23 | coAuthorProvider.coAuthorGroups.addNew([coAuthor]); 24 | if (moveToCoAuthoring()) { 25 | await coAuthorProvider.toggleCoAuthor(coAuthor, true); 26 | } 27 | } 28 | } 29 | ); 30 | } 31 | 32 | exports.searchRepositoryUsers = searchRepositoryUsers; 33 | 34 | async function quickPickAuthors(repoAuthors) { 35 | const gitExt = new GitExt(); 36 | const authorTextArray = repoAuthors.map((author) => ({ 37 | label: `${author.name} <${author.email}>`, 38 | description: gitExt.selectedFolderName, 39 | repoAuthor: { ...author, key: author.commandKey }, 40 | })); 41 | return await vscode.window.showQuickPick(authorTextArray); 42 | } 43 | -------------------------------------------------------------------------------- /src/commands/shared/input-author-data.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | 3 | async function inputAuthorData(fields) { 4 | const authorData = {}; 5 | 6 | for (const field of fields) { 7 | const value = await vscode.window.showInputBox({ 8 | prompt: `${field.label} (Required)`, 9 | validateInput: isRequired(field.label), 10 | }); 11 | if (typeof value === "undefined") return null; 12 | authorData[field.key] = value; 13 | } 14 | 15 | return authorData; 16 | } 17 | 18 | function isRequired(fieldName) { 19 | return (value) => { 20 | if (value.length === 0) return `${fieldName} is required.`; 21 | }; 22 | } 23 | 24 | exports.inputAuthorData = inputAuthorData; 25 | -------------------------------------------------------------------------------- /src/commands/solo.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | 3 | function soloCommand({ coAuthorProvider }) { 4 | return vscode.commands.registerCommand("gitmob.solo", async function () { 5 | await coAuthorProvider.coAuthorGroups.solo(); 6 | await vscode.commands.executeCommand("gitmob.reload"); 7 | }); 8 | } 9 | 10 | exports.soloCommand = soloCommand; 11 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | const CONSTANTS = { 2 | GIT_COAUTHORS_FILE: ".git-coauthors", 3 | }; 4 | 5 | exports.CONSTANTS = CONSTANTS; 6 | -------------------------------------------------------------------------------- /src/errors/log-issue.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | 3 | const LOG_ISSUE = "Log issue"; 4 | 5 | async function logIssue(errorMessage) { 6 | const errorAction = await vscode.window.showErrorMessage( 7 | errorMessage, 8 | LOG_ISSUE, 9 | "Dismiss" 10 | ); 11 | if (errorAction === LOG_ISSUE) 12 | vscode.env.openExternal( 13 | vscode.Uri.parse("https://github.com/rkotze/git-mob-vs-code/issues") 14 | ); 15 | } 16 | 17 | exports.logIssue = logIssue; 18 | -------------------------------------------------------------------------------- /src/ext-config/config.js: -------------------------------------------------------------------------------- 1 | const { workspace, commands } = require("vscode"); 2 | 3 | function getConfigSolo() { 4 | const config = workspace.getConfiguration("gitMob.postCommit"); 5 | return config.get("solo"); 6 | } 7 | 8 | function getSortDirection() { 9 | const config = workspace.getConfiguration("gitMob.coAuthors"); 10 | return config.get("sortDirection"); 11 | } 12 | 13 | function moveToCoAuthoring() { 14 | const authorListConfig = workspace.getConfiguration("gitMob.authorList"); 15 | return authorListConfig.get("moreAuthorsToCo-authoring"); 16 | } 17 | 18 | const ENABLE_INPUT_AUTOCOMPLETION = 19 | "gitMob.scmTextbox.enableInputAutocompletion"; 20 | 21 | function getInputCompletionConfig() { 22 | const config = workspace.getConfiguration("gitMob.scmTextbox"); 23 | return config.get("enableInputAutocompletion"); 24 | } 25 | 26 | workspace.onDidChangeConfiguration((evt) => { 27 | if (evt.affectsConfiguration("gitMob.coAuthors.sortDirection")) { 28 | commands.executeCommand("gitmob.reload"); 29 | } 30 | }); 31 | 32 | exports.getConfigSolo = getConfigSolo; 33 | exports.getSortDirection = getSortDirection; 34 | exports.moveToCoAuthoring = moveToCoAuthoring; 35 | exports.ENABLE_INPUT_AUTOCOMPLETION = ENABLE_INPUT_AUTOCOMPLETION; 36 | exports.getInputCompletionConfig = getInputCompletionConfig; 37 | -------------------------------------------------------------------------------- /src/ext-config/solo-after-commit.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const { watchForCommit } = require("../git/watch-for-commit"); 3 | const { getConfigSolo } = require("./config"); 4 | 5 | exports.soloAfterCommit = function soloAfterCommit(coAuthorProvider) { 6 | vscode.workspace.onDidChangeConfiguration((evt) => { 7 | if (evt.affectsConfiguration("gitMob.postCommit")) { 8 | soloSwitch(coAuthorProvider, getConfigSolo(coAuthorProvider)); 9 | } 10 | }); 11 | 12 | soloSwitch(coAuthorProvider, getConfigSolo(coAuthorProvider)); 13 | }; 14 | 15 | let watch = null; 16 | function soloSwitch(coAuthorProvider, afterCommitOn) { 17 | if (afterCommitOn) { 18 | watch = watchForCommit(async function () { 19 | await coAuthorProvider.coAuthorGroups.solo(); 20 | await vscode.commands.executeCommand("gitmob.reload"); 21 | }); 22 | } else { 23 | if (watch) { 24 | watch.close(); 25 | watch = null; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/git-emojis/gitmojis.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitmojis": [ 3 | { 4 | "emoji": "🎨", 5 | "entity": "🎨", 6 | "code": ":art:", 7 | "description": "Improving structure / format of the code.", 8 | "name": "art" 9 | }, 10 | { 11 | "emoji": "⚡️", 12 | "entity": "⚡", 13 | "code": ":zap:", 14 | "description": "Improving performance.", 15 | "name": "zap" 16 | }, 17 | { 18 | "emoji": "🔥", 19 | "entity": "🔥", 20 | "code": ":fire:", 21 | "description": "Removing code or files.", 22 | "name": "fire" 23 | }, 24 | { 25 | "emoji": "🐛", 26 | "entity": "🐛", 27 | "code": ":bug:", 28 | "description": "Fixing a bug.", 29 | "name": "bug" 30 | }, 31 | { 32 | "emoji": "🚑", 33 | "entity": "🚑", 34 | "code": ":ambulance:", 35 | "description": "Critical hotfix.", 36 | "name": "ambulance" 37 | }, 38 | { 39 | "emoji": "✨", 40 | "entity": "✨", 41 | "code": ":sparkles:", 42 | "description": "Introducing new features.", 43 | "name": "sparkles" 44 | }, 45 | { 46 | "emoji": "📝", 47 | "entity": "📝", 48 | "code": ":pencil:", 49 | "description": "Writing docs.", 50 | "name": "pencil" 51 | }, 52 | { 53 | "emoji": "🚀", 54 | "entity": "🚀", 55 | "code": ":rocket:", 56 | "description": "Deploying stuff.", 57 | "name": "rocket" 58 | }, 59 | { 60 | "emoji": "💄", 61 | "entity": "&#ff99cc;", 62 | "code": ":lipstick:", 63 | "description": "Updating the UI and style files.", 64 | "name": "lipstick" 65 | }, 66 | { 67 | "emoji": "🎉", 68 | "entity": "🎉", 69 | "code": ":tada:", 70 | "description": "Initial commit.", 71 | "name": "tada" 72 | }, 73 | { 74 | "emoji": "✅", 75 | "entity": "✅", 76 | "code": ":white_check_mark:", 77 | "description": "Updating tests.", 78 | "name": "white-check-mark" 79 | }, 80 | { 81 | "emoji": "🔒", 82 | "entity": "🔒", 83 | "code": ":lock:", 84 | "description": "Fixing security issues.", 85 | "name": "lock" 86 | }, 87 | { 88 | "emoji": "🍎", 89 | "entity": "🍎", 90 | "code": ":apple:", 91 | "description": "Fixing something on macOS.", 92 | "name": "apple" 93 | }, 94 | { 95 | "emoji": "🐧", 96 | "entity": "🐧", 97 | "code": ":penguin:", 98 | "description": "Fixing something on Linux.", 99 | "name": "penguin" 100 | }, 101 | { 102 | "emoji": "🏁", 103 | "entity": "🏁", 104 | "code": ":checkered_flag:", 105 | "description": "Fixing something on Windows.", 106 | "name": "checkered-flag" 107 | }, 108 | { 109 | "emoji": "🤖", 110 | "entity": "🤖", 111 | "code": ":robot:", 112 | "description": "Fixing something on Android.", 113 | "name": "robot" 114 | }, 115 | { 116 | "emoji": "🍏", 117 | "entity": "🍏", 118 | "code": ":green_apple:", 119 | "description": "Fixing something on iOS.", 120 | "name": "green-apple" 121 | }, 122 | { 123 | "emoji": "🔖", 124 | "entity": "🔖", 125 | "code": ":bookmark:", 126 | "description": "Releasing / Version tags.", 127 | "name": "bookmark" 128 | }, 129 | { 130 | "emoji": "🚨", 131 | "entity": "🚨", 132 | "code": ":rotating_light:", 133 | "description": "Removing linter warnings.", 134 | "name": "rotating-light" 135 | }, 136 | { 137 | "emoji": "🚧", 138 | "entity": "🚧", 139 | "code": ":construction:", 140 | "description": "Work in progress.", 141 | "name": "construction" 142 | }, 143 | { 144 | "emoji": "💚", 145 | "entity": "💚", 146 | "code": ":green_heart:", 147 | "description": "Fixing CI Build.", 148 | "name": "green-heart" 149 | }, 150 | { 151 | "emoji": "⬇️", 152 | "entity": "⬇️", 153 | "code": ":arrow_down:", 154 | "description": "Downgrading dependencies.", 155 | "name": "arrow-down" 156 | }, 157 | { 158 | "emoji": "⬆️", 159 | "entity": "⬆️", 160 | "code": ":arrow_up:", 161 | "description": "Upgrading dependencies.", 162 | "name": "arrow-up" 163 | }, 164 | { 165 | "emoji": "📌", 166 | "entity": "📌", 167 | "code": ":pushpin:", 168 | "description": "Pinning dependencies to specific versions.", 169 | "name": "pushpin" 170 | }, 171 | { 172 | "emoji": "👷", 173 | "entity": "👷", 174 | "code": ":construction_worker:", 175 | "description": "Adding CI build system.", 176 | "name": "construction-worker" 177 | }, 178 | { 179 | "emoji": "📈", 180 | "code": ":chart_with_upwards_trend:", 181 | "description": "Adding analytics or tracking code.", 182 | "name": "chart-with-upwards-trend" 183 | }, 184 | { 185 | "emoji": "♻️", 186 | "entity": "♲", 187 | "code": ":recycle:", 188 | "description": "Refactoring code.", 189 | "name": "recycle" 190 | }, 191 | { 192 | "emoji": "🐳", 193 | "entity": "🐳", 194 | "code": ":whale:", 195 | "description": "Work about Docker.", 196 | "name": "whale" 197 | }, 198 | { 199 | "emoji": "➕", 200 | "entity": "➕", 201 | "code": ":heavy_plus_sign:", 202 | "description": "Adding a dependency.", 203 | "name": "heavy-plus-sign" 204 | }, 205 | { 206 | "emoji": "➖", 207 | "entity": "➖", 208 | "code": ":heavy_minus_sign:", 209 | "description": "Removing a dependency.", 210 | "name": "heavy-minus-sign" 211 | }, 212 | { 213 | "emoji": "🔧", 214 | "entity": "🔧", 215 | "code": ":wrench:", 216 | "description": "Changing configuration files.", 217 | "name": "wrench" 218 | }, 219 | { 220 | "emoji": "🌐", 221 | "entity": "🌐", 222 | "code": ":globe_with_meridians:", 223 | "description": "Internationalization and localization.", 224 | "name": "globe-with-meridians" 225 | }, 226 | { 227 | "emoji": "✏️", 228 | "entity": "", 229 | "code": ":pencil2:", 230 | "description": "Fixing typos.", 231 | "name": "pencil" 232 | }, 233 | { 234 | "emoji": "💩", 235 | "entity": "", 236 | "code": ":poop:", 237 | "description": "Writing bad code that needs to be improved.", 238 | "name": "poop" 239 | }, 240 | { 241 | "emoji": "⏪", 242 | "entity": "⏪", 243 | "code": ":rewind:", 244 | "description": "Reverting changes.", 245 | "name": "rewind" 246 | }, 247 | { 248 | "emoji": "🔀", 249 | "entity": "🔀", 250 | "code": ":twisted_rightwards_arrows:", 251 | "description": "Merging branches.", 252 | "name": "twisted-rightwards-arrows" 253 | }, 254 | { 255 | "emoji": "📦", 256 | "entity": "F4E6;", 257 | "code": ":package:", 258 | "description": "Updating compiled files or packages.", 259 | "name": "package" 260 | }, 261 | { 262 | "emoji": "👽", 263 | "entity": "F47D;", 264 | "code": ":alien:", 265 | "description": "Updating code due to external API changes.", 266 | "name": "alien" 267 | }, 268 | { 269 | "emoji": "🚚", 270 | "entity": "F69A;", 271 | "code": ":truck:", 272 | "description": "Moving or renaming files.", 273 | "name": "truck" 274 | }, 275 | { 276 | "emoji": "📄", 277 | "entity": "F4C4;", 278 | "code": ":page_facing_up:", 279 | "description": "Adding or updating license.", 280 | "name": "page-facing-up" 281 | }, 282 | { 283 | "emoji": "💥", 284 | "entity": "💥", 285 | "code": ":boom:", 286 | "description": "Introducing breaking changes.", 287 | "name": "boom" 288 | }, 289 | { 290 | "emoji": "🍱", 291 | "entity": "F371", 292 | "code": ":bento:", 293 | "description": "Adding or updating assets.", 294 | "name": "bento" 295 | }, 296 | { 297 | "emoji": "👌", 298 | "entity": "👌", 299 | "code": ":ok_hand:", 300 | "description": "Updating code due to code review changes.", 301 | "name": "ok-hand" 302 | }, 303 | { 304 | "emoji": "♿️", 305 | "entity": "♿", 306 | "code": ":wheelchair:", 307 | "description": "Improving accessibility.", 308 | "name": "wheelchair" 309 | }, 310 | { 311 | "emoji": "💡", 312 | "entity": "💡", 313 | "code": ":bulb:", 314 | "description": "Documenting source code.", 315 | "name": "bulb" 316 | }, 317 | { 318 | "emoji": "🍻", 319 | "entity": "🍻", 320 | "code": ":beers:", 321 | "description": "Writing code drunkenly.", 322 | "name": "beers" 323 | }, 324 | { 325 | "emoji": "💬", 326 | "entity": "💬", 327 | "code": ":speech_balloon:", 328 | "description": "Updating text and literals.", 329 | "name": "speech-balloon" 330 | }, 331 | { 332 | "emoji": "🗃", 333 | "entity": "🗃", 334 | "code": ":card_file_box:", 335 | "description": "Performing database related changes.", 336 | "name": "card-file-box" 337 | }, 338 | { 339 | "emoji": "🔊", 340 | "entity": "🔊", 341 | "code": ":loud_sound:", 342 | "description": "Adding logs.", 343 | "name": "loud-sound" 344 | }, 345 | { 346 | "emoji": "🔇", 347 | "entity": "🔇", 348 | "code": ":mute:", 349 | "description": "Removing logs.", 350 | "name": "mute" 351 | }, 352 | { 353 | "emoji": "👥", 354 | "entity": "👥", 355 | "code": ":busts_in_silhouette:", 356 | "description": "Adding contributor(s).", 357 | "name": "busts-in-silhouette" 358 | }, 359 | { 360 | "emoji": "🚸", 361 | "entity": "🚸", 362 | "code": ":children_crossing:", 363 | "description": "Improving user experience / usability.", 364 | "name": "children-crossing" 365 | }, 366 | { 367 | "emoji": "🏗", 368 | "entity": "f3d7;", 369 | "code": ":building_construction:", 370 | "description": "Making architectural changes.", 371 | "name": "building-construction" 372 | }, 373 | { 374 | "emoji": "📱", 375 | "entity": "📱", 376 | "code": ":iphone:", 377 | "description": "Working on responsive design.", 378 | "name": "iphone" 379 | }, 380 | { 381 | "emoji": "🤡", 382 | "entity": "🤡", 383 | "code": ":clown_face:", 384 | "description": "Mocking things.", 385 | "name": "clown-face" 386 | }, 387 | { 388 | "emoji": "🥚", 389 | "entity": "🥚", 390 | "code": ":egg:", 391 | "description": "Adding an easter egg.", 392 | "name": "egg" 393 | }, 394 | { 395 | "emoji": "🙈", 396 | "entity": "bdfe7;", 397 | "code": ":see_no_evil:", 398 | "description": "Adding or updating a .gitignore file", 399 | "name": "see-no-evil" 400 | }, 401 | { 402 | "emoji": "📸", 403 | "entity": "📸", 404 | "code": ":camera_flash:", 405 | "description": "Adding or updating snapshots", 406 | "name": "camera-flash" 407 | }, 408 | { 409 | "emoji": "⚗", 410 | "entity": "📸", 411 | "code": ":alembic:", 412 | "description": "Experimenting new things", 413 | "name": "alembic" 414 | }, 415 | { 416 | "emoji": "🔍", 417 | "entity": "🔍", 418 | "code": ":mag:", 419 | "description": "Improving SEO", 420 | "name": "mag" 421 | }, 422 | { 423 | "emoji": "☸️", 424 | "entity": "☸", 425 | "code": ":wheel_of_dharma:", 426 | "description": "Work about Kubernetes", 427 | "name": "wheel-of-dharma" 428 | }, 429 | { 430 | "emoji": "🏷️", 431 | "entity": "🏷", 432 | "code": ":label:", 433 | "description": "Adding or updating types (Flow, TypeScript)", 434 | "name": "label" 435 | }, 436 | { 437 | "emoji": "🌱", 438 | "entity": "🌱", 439 | "code": ":seedling:", 440 | "description": "Adding or updating seed files", 441 | "name": "seedling" 442 | }, 443 | { 444 | "emoji": "🚩", 445 | "entity": "🚩", 446 | "code": ":triangular_flag_on_post:", 447 | "description": "Adding, updating, or removing feature flags", 448 | "name": "triangular-flag-on-post" 449 | }, 450 | { 451 | "emoji": "🥅", 452 | "entity": "🥅", 453 | "code": ":goal_net:", 454 | "description": "Catching errors", 455 | "name": "goal-net" 456 | }, 457 | { 458 | "emoji": "💫", 459 | "entity": "💫", 460 | "code": ":dizzy:", 461 | "description": "Adding or updating animations and transitions", 462 | "name": "animation" 463 | }, 464 | { 465 | "emoji": "🗑", 466 | "entity": "🗑", 467 | "code": ":wastebasket:", 468 | "description": "Deprecating code that needs to be cleaned up.", 469 | "name": "wastebasket" 470 | }, 471 | { 472 | "emoji": "🛂", 473 | "entity": "🛂", 474 | "code": ":passport_control:", 475 | "description": "Work on code related to authorization, roles and permissions.", 476 | "name": "passport-control" 477 | }, 478 | { 479 | "emoji": "🩹", 480 | "entity": "🩹", 481 | "code": ":adhesive_bandage:", 482 | "description": "Simple fix for a non-critical issue.", 483 | "name": "adhesive-bandage" 484 | }, 485 | { 486 | "emoji": "🧐", 487 | "entity": "🧐", 488 | "code": ":monocle_face:", 489 | "description": "Data exploration/inspection.", 490 | "name": "monocle-face" 491 | }, 492 | { 493 | "emoji": "⚰️", 494 | "entity": "⚰", 495 | "code": ":coffin:", 496 | "description": "Remove dead code.", 497 | "name": "coffin" 498 | }, 499 | { 500 | "emoji": "🧪", 501 | "entity": "🧪", 502 | "code": ":test_tube:", 503 | "description": "Add a failing test.", 504 | "name": "test-tube" 505 | }, 506 | { 507 | "emoji": "👔", 508 | "entity": "👔", 509 | "code": ":necktie:", 510 | "description": "Add or update business logic.", 511 | "name": "necktie" 512 | }, 513 | { 514 | "emoji": "🩺", 515 | "entity": "🩺", 516 | "code": ":stethoscope:", 517 | "description": "Add or update healthcheck.", 518 | "name": "stethoscope" 519 | }, 520 | { 521 | "emoji": "🧱", 522 | "entity": "🧱", 523 | "code": ":bricks:", 524 | "description": "Infrastructure related changes.", 525 | "name": "bricks" 526 | }, 527 | { 528 | "emoji": "🧑‍💻", 529 | "entity": "🧑‍💻", 530 | "code": ":technologist:", 531 | "description": "Improve developer experience.", 532 | "name": "technologist" 533 | }, 534 | { 535 | "emoji": "💸", 536 | "entity": "💸", 537 | "code": ":money_with_wings:", 538 | "description": "Add sponsorships or money related infrastructure.", 539 | "name": "money-with-wings" 540 | }, 541 | { 542 | "emoji": "🧵", 543 | "entity": "🧵", 544 | "code": ":thread:", 545 | "description": "Add or update code related to multithreading or concurrency.", 546 | "name": "thread" 547 | }, 548 | { 549 | "emoji": "🦺", 550 | "entity": "🦺", 551 | "code": ":safety_vest:", 552 | "description": "Add or update code related to validation.", 553 | "name": "safety-vest" 554 | } 555 | ] 556 | } 557 | -------------------------------------------------------------------------------- /src/git/watch-for-commit.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const { logIssue } = require("../errors/log-issue"); 4 | const { GitExt } = require("../vscode-git-extension/git-ext"); 5 | 6 | exports.watchForCommit = function watchForCommit(cb) { 7 | const gitExt = new GitExt(); 8 | const gitCommit = path.join(gitExt.rootPath, ".git"); 9 | 10 | try { 11 | return fs.watch(gitCommit, function (evt, filename) { 12 | if (filename && filename === "COMMIT_EDITMSG") { 13 | if (debounceFsWatch()) return; 14 | cb(evt); 15 | } 16 | }); 17 | } catch (err) { 18 | logIssue("Watch for commit failed!: " + err.message); 19 | } 20 | }; 21 | 22 | let fsWait = false; 23 | function debounceFsWatch() { 24 | if (fsWait) return true; 25 | fsWait = setTimeout(() => { 26 | fsWait = false; 27 | }, 1000); // windows is a bit slower 28 | } 29 | -------------------------------------------------------------------------------- /src/install/install-git-coauthor-file.js: -------------------------------------------------------------------------------- 1 | const { createCoAuthorsFile } = require("git-mob-core"); 2 | 3 | async function installGitCoAuthorFile() { 4 | try { 5 | if (await createCoAuthorsFile()) { 6 | console.log("Co-authors file created!"); 7 | } 8 | } catch (error) { 9 | if (!error.message.includes("file exists")) { 10 | throw new Error( 11 | "Error:Something went wrong creating new .git-coauthors file." 12 | ); 13 | } 14 | } 15 | } 16 | 17 | module.exports = { 18 | installGitCoAuthorFile, 19 | }; 20 | -------------------------------------------------------------------------------- /src/reload-on-save.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const { CONSTANTS } = require("./constants"); 3 | 4 | function reloadOnSave() { 5 | const { onDidSaveTextDocument } = vscode.workspace; 6 | 7 | onDidSaveTextDocument(async function (textDocument) { 8 | if (textDocument.fileName.includes(CONSTANTS.GIT_COAUTHORS_FILE)) { 9 | await vscode.commands.executeCommand("gitmob.reload"); 10 | } 11 | }); 12 | } 13 | 14 | exports.reloadOnSave = reloadOnSave; 15 | -------------------------------------------------------------------------------- /src/reload-on-save.spec.js: -------------------------------------------------------------------------------- 1 | const { reloadOnSave } = require("./reload-on-save"); 2 | const { CONSTANTS } = require("./constants"); 3 | const vscode = require("../__mocks__/vscode"); 4 | 5 | const coAuthorTextDocStub = { 6 | fileName: `file/Path/to/${CONSTANTS.GIT_COAUTHORS_FILE}`, 7 | }; 8 | const otherTextDocStub = { 9 | fileName: `file/Path/to/other.js`, 10 | }; 11 | 12 | let saveTrigger = null; 13 | beforeEach(() => { 14 | vscode.workspace.onDidSaveTextDocument.mockImplementation((cb) => { 15 | saveTrigger = cb; 16 | }); 17 | }); 18 | 19 | afterEach(() => { 20 | saveTrigger = null; 21 | }); 22 | 23 | test("Reload co-author list when git-coauthors file saved", async () => { 24 | reloadOnSave(); 25 | expect(vscode.workspace.onDidSaveTextDocument).toHaveBeenCalledWith( 26 | expect.any(Function) 27 | ); 28 | await saveTrigger(coAuthorTextDocStub); 29 | expect(vscode.commands.executeCommand).toHaveBeenCalledTimes(1); 30 | expect(vscode.commands.executeCommand).toHaveBeenCalledWith("gitmob.reload"); 31 | }); 32 | 33 | test("Do NOT reload co-author list when other files are saved", async () => { 34 | reloadOnSave(); 35 | expect(vscode.workspace.onDidSaveTextDocument).toHaveBeenCalledWith( 36 | expect.any(Function) 37 | ); 38 | await saveTrigger(otherTextDocStub); 39 | expect(vscode.commands.executeCommand).not.toHaveBeenCalled(); 40 | expect(vscode.commands.executeCommand).not.toHaveBeenCalledWith( 41 | "gitmob.reload" 42 | ); 43 | }); 44 | -------------------------------------------------------------------------------- /src/setup-git-mob.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const { 3 | CoAuthorProvider, 4 | } = require("./co-author-tree-provider/co-authors-provider"); 5 | const { reloadOnSave } = require("./reload-on-save"); 6 | const { reloadCommand } = require("./commands/reload"); 7 | const { openGitCoAuthor } = require("./commands/open-git-coauthors"); 8 | const { soloCommand } = require("./commands/solo"); 9 | const { 10 | addCoAuthor, 11 | removeCoAuthor, 12 | addFromFavourite, 13 | } = require("./commands/co-author-actions"); 14 | const { addRepoAuthorToCoauthors } = require("./commands/add-co-author"); 15 | const { 16 | searchRepositoryUsers, 17 | } = require("./commands/search-repository-authors"); 18 | const { changePrimaryAuthor } = require("./commands/change-primary-author"); 19 | const { searchGitEmojis } = require("./commands/search-git-emojis"); 20 | const { openSettings } = require("./commands/open-settings"); 21 | const { searchGithubAuthors } = require("./commands/github-authors"); 22 | const { copyCoAuthor } = require("./commands/copy-co-author"); 23 | const { soloAfterCommit } = require("./ext-config/solo-after-commit"); 24 | const { logIssue } = require("./errors/log-issue"); 25 | const { 26 | CountDecorationProvider, 27 | } = require("./co-author-tree-provider/count-decorator-provider"); 28 | const { updateConfig, messageFormatter } = require("git-mob-core"); 29 | const { buildCoAuthorGroups } = require("./build-co-author-groups"); 30 | const { 31 | InputCompletionProvider, 32 | } = require("./co-author-tree-provider/input-completion-provider"); 33 | const { addMainAuthor } = require("./commands/add-main-author"); 34 | 35 | async function setupGitMob(context, gitExt) { 36 | return bootGitMob(context, gitExt); 37 | } 38 | 39 | async function bootGitMob(context, gitExt) { 40 | updateConfig("processCwd", gitExt.rootPath); 41 | try { 42 | const coAuthorProvider = new CoAuthorProvider(await buildCoAuthorGroups()); 43 | 44 | coAuthorProvider.onDidChangeTreeData(function () { 45 | try { 46 | gitExt.updateSelectedInput((text) => 47 | messageFormatter(text, coAuthorProvider.coAuthorGroups.getSelected()) 48 | ); 49 | } catch (err) { 50 | logIssue("Failed to update input: " + err.message); 51 | } 52 | }); 53 | 54 | const disposables = [ 55 | openSettings(), 56 | reloadCommand({ coAuthorProvider }), 57 | addCoAuthor({ coAuthorProvider }), 58 | removeCoAuthor({ coAuthorProvider }), 59 | addFromFavourite({ coAuthorProvider }), 60 | addRepoAuthorToCoauthors({ coAuthorProvider }), 61 | searchRepositoryUsers({ coAuthorProvider }), 62 | openGitCoAuthor({ coAuthorProvider }), 63 | soloCommand({ coAuthorProvider }), 64 | searchGitEmojis(), 65 | changePrimaryAuthor({ coAuthorProvider }), 66 | searchGithubAuthors({ coAuthorProvider }), 67 | new CountDecorationProvider(coAuthorProvider), 68 | new InputCompletionProvider(coAuthorProvider), 69 | copyCoAuthor(), 70 | addMainAuthor({ coAuthorProvider }), 71 | ]; 72 | 73 | disposables.forEach((dispose) => context.subscriptions.push(dispose)); 74 | 75 | reloadOnSave(); 76 | soloAfterCommit(coAuthorProvider); 77 | 78 | gitExt.onDidChangeUiState(async function () { 79 | if (gitExt.repositories.length === 1) return; 80 | if (this.ui.selected) { 81 | gitExt.selectedRepositoryPath = this.rootUri.path; 82 | updateConfig("processCwd", gitExt.rootPath); 83 | await vscode.commands.executeCommand("gitmob.reload"); 84 | } 85 | }); 86 | 87 | const mobList = vscode.window.createTreeView("gitmob.CoAuthorsView", { 88 | treeDataProvider: coAuthorProvider, 89 | canSelectMany: true, 90 | }); 91 | 92 | mobList.onDidChangeSelection(function (evt) { 93 | coAuthorProvider.multiSelected = evt.selection; 94 | }); 95 | } catch (error) { 96 | logIssue(error.message); 97 | console.log("SETUP ERROR: ", error); 98 | } 99 | } 100 | 101 | exports.setupGitMob = setupGitMob; 102 | -------------------------------------------------------------------------------- /src/vscode-git-extension/git-ext.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { vsCodeGit } = require("./vs-code-git"); 3 | let selectedRepoPath = null; 4 | 5 | class GitExt { 6 | constructor() { 7 | this.gitApi = vsCodeGit(); 8 | } 9 | 10 | get hasRepositories() { 11 | return this.repositories.length > 0; 12 | } 13 | 14 | get repositories() { 15 | return this.gitApi ? this.gitApi.repositories : []; 16 | } 17 | 18 | get rootPath() { 19 | if (!this.hasRepositories) return null; 20 | 21 | return this.selectedRepository.rootUri.fsPath; 22 | } 23 | 24 | get selectedFolderName() { 25 | const segments = this.rootPath.split(path.sep); 26 | if (segments.length > 0) { 27 | return segments.pop(); 28 | } 29 | return ""; 30 | } 31 | 32 | get selectedRepository() { 33 | if (this.hasRepositories && this.repositories.length < 2) 34 | return this.repositories[0]; 35 | 36 | const repo = this.repositories.find( 37 | (repo) => repo.rootUri.path === selectedRepoPath 38 | ); 39 | if (!repo) return this.repositories[0]; 40 | return repo; 41 | } 42 | 43 | set selectedRepositoryPath(val) { 44 | selectedRepoPath = val; 45 | } 46 | 47 | updateSelectedInput(value) { 48 | const valueIsFunction = typeof value === "function"; 49 | const repo = this.selectedRepository; 50 | if (valueIsFunction) { 51 | repo.inputBox.value = value(repo.inputBox.value); 52 | } else { 53 | repo.inputBox.value = value; 54 | } 55 | } 56 | 57 | onDidChangeUiState(stateChangeCallback) { 58 | const trackRepos = []; 59 | 60 | for (let repo of this.repositories) { 61 | if (!trackRepos.includes(repo.rootUri.path)) { 62 | trackRepos.push(repo.rootUri.path); 63 | repo.ui.onDidChange(stateChangeCallback.bind(repo)); 64 | } 65 | } 66 | 67 | this.gitApi.onDidOpenRepository(function (repo) { 68 | if (!trackRepos.includes(repo.rootUri.path)) { 69 | trackRepos.push(repo.rootUri.path); 70 | repo.ui.onDidChange(stateChangeCallback.bind(repo)); 71 | } 72 | }); 73 | 74 | this.gitApi.onDidCloseRepository(function (repo) { 75 | const index = trackRepos.indexOf(repo.rootUri.path); 76 | if (index > -1) { 77 | trackRepos.splice(index, 1); 78 | } 79 | }); 80 | } 81 | } 82 | 83 | exports.GitExt = GitExt; 84 | -------------------------------------------------------------------------------- /src/vscode-git-extension/git-ext.spec.js: -------------------------------------------------------------------------------- 1 | const { GitExt } = require("./git-ext"); 2 | const { vsCodeGit } = require("./vs-code-git"); 3 | 4 | jest.mock("./vs-code-git"); 5 | 6 | test("returns selected repository file path", () => { 7 | vsCodeGitRepo([ 8 | { 9 | ui: { 10 | selected: true, 11 | }, 12 | rootUri: { 13 | fsPath: "project/fspath", 14 | }, 15 | }, 16 | ]); 17 | const gitExt = new GitExt(); 18 | expect(gitExt.rootPath).toEqual("project/fspath"); 19 | }); 20 | 21 | test("returns first file path when there are repositories but none are selected", () => { 22 | vsCodeGitRepo([ 23 | { 24 | ui: { 25 | selected: false, 26 | }, 27 | rootUri: { 28 | fsPath: "first/project/fspath", 29 | }, 30 | }, 31 | { 32 | ui: { 33 | selected: false, 34 | }, 35 | rootUri: { 36 | fsPath: "should/not/find/me", 37 | }, 38 | }, 39 | ]); 40 | const gitExt = new GitExt(); 41 | expect(gitExt.rootPath).toEqual("first/project/fspath"); 42 | }); 43 | 44 | test("returns selected repo by matching rootUri.path", () => { 45 | vsCodeGitRepo([ 46 | { 47 | ui: { 48 | selected: false, 49 | }, 50 | rootUri: { 51 | fsPath: "first/project/fspath", 52 | path: "first/path", 53 | }, 54 | }, 55 | { 56 | ui: { 57 | selected: false, 58 | }, 59 | rootUri: { 60 | fsPath: "second/project/find/me", 61 | path: "second/path", 62 | }, 63 | }, 64 | ]); 65 | const selectedPath = "second/path"; 66 | const gitExt = new GitExt(); 67 | gitExt.selectedRepositoryPath = selectedPath; 68 | expect(gitExt.rootPath).toEqual("second/project/find/me"); 69 | }); 70 | 71 | test("returns null when no repositories", () => { 72 | vsCodeGitRepo([]); 73 | const gitExt = new GitExt(); 74 | expect(gitExt.rootPath).toEqual(null); 75 | }); 76 | 77 | function vsCodeGitRepo(repositories) { 78 | vsCodeGit.mockImplementation(() => { 79 | return { 80 | repositories, 81 | }; 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /src/vscode-git-extension/vs-code-git.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | 3 | function vsCodeGit() { 4 | const ext = vscode.extensions.getExtension("vscode.git"); 5 | return ext.isActive && ext.exports.getAPI(1); 6 | } 7 | 8 | exports.vsCodeGit = vsCodeGit; 9 | -------------------------------------------------------------------------------- /test/extension.test.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const { getAllAuthors, getSelectedCoAuthors } = require("git-mob-core"); 3 | const { CoAuthor } = require("../src/co-author-tree-provider/co-authors"); 4 | const { RepoAuthor } = require("../src/co-author-tree-provider/repo-authors"); 5 | const { GitExt } = require("../src/vscode-git-extension/git-ext"); 6 | 7 | function wait(timeMs) { 8 | return new Promise((resolve) => { 9 | setTimeout(resolve, timeMs); 10 | }); 11 | } 12 | 13 | describe("GitMob core tests", function () { 14 | let allAuthors = []; 15 | let author0, author1, author2; 16 | let gitExt; 17 | let expect; 18 | before(async function () { 19 | // chai using ESM, needs dynamic import 20 | const chai = await import("chai"); 21 | expect = chai.expect; 22 | gitExt = new GitExt(); 23 | allAuthors = await getAllAuthors(); 24 | author0 = allAuthors[0]; 25 | author1 = allAuthors[1]; 26 | author2 = allAuthors[2]; 27 | }); 28 | 29 | this.afterEach(async function () { 30 | await vscode.commands.executeCommand("gitmob.solo"); 31 | await wait(20); // needed for solo to complete 32 | }); 33 | 34 | it("should have 3 co-authors", function () { 35 | expect(allAuthors).to.have.lengthOf(3); 36 | }); 37 | 38 | it("add a co-author to commit and metadata is appended to SCM input", async function () { 39 | const coAuthor = new CoAuthor( 40 | author2.name, 41 | author2.email, 42 | false, 43 | author2.key 44 | ); 45 | await vscode.commands.executeCommand("gitmob.addCoAuthor", coAuthor); 46 | await wait(200); 47 | const selected = await getSelectedCoAuthors(allAuthors); 48 | expect(selected[0].key).to.equal(coAuthor.commandKey); 49 | expect(selected).to.have.lengthOf(1); 50 | expect(gitExt.selectedRepository.inputBox.value).to.contain( 51 | `Co-authored-by: ${coAuthor.name} <${coAuthor.email}>` 52 | ); 53 | }); 54 | 55 | it("remove one of the two co-authors from commit message", async function () { 56 | const addCoAuthor = new CoAuthor( 57 | author1.name, 58 | author1.email, 59 | false, 60 | author1.key 61 | ); 62 | const removeCoAuthor = new CoAuthor( 63 | author2.name, 64 | author2.email, 65 | false, 66 | author2.key 67 | ); 68 | 69 | await vscode.commands.executeCommand("gitmob.addCoAuthor", addCoAuthor); 70 | await wait(300); 71 | await vscode.commands.executeCommand("gitmob.addCoAuthor", removeCoAuthor); 72 | await wait(300); 73 | await vscode.commands.executeCommand( 74 | "gitmob.removeCoAuthor", 75 | removeCoAuthor 76 | ); 77 | await wait(300); 78 | 79 | const selected = await getSelectedCoAuthors(allAuthors); 80 | expect(selected).to.have.lengthOf(1); 81 | expect(selected[0].key).to.equal(addCoAuthor.commandKey); 82 | }); 83 | 84 | it("run solo no co-authors should be selected", async function () { 85 | const coAuthor = new CoAuthor( 86 | author0.name, 87 | author0.email, 88 | false, 89 | author0.key 90 | ); 91 | await vscode.commands.executeCommand("gitmob.addCoAuthor", coAuthor); 92 | await wait(100); 93 | await vscode.commands.executeCommand("gitmob.solo"); 94 | await wait(100); 95 | 96 | expect(gitExt.selectedRepository.inputBox.value).to.not.contain( 97 | "Co-authored-by" 98 | ); 99 | const selected = await getSelectedCoAuthors(allAuthors); 100 | expect(selected).to.have.lengthOf(0); 101 | }); 102 | 103 | it("add a contributor from repo as a new selected co-author by default", async function () { 104 | const coAuthor = new RepoAuthor( 105 | "Jessica Jones", 106 | "jessica-j@gitmob.com", 107 | "jj" 108 | ); 109 | await vscode.commands.executeCommand( 110 | "gitmob.addRepoAuthorToCoAuthors", 111 | coAuthor 112 | ); 113 | await wait(200); 114 | const allAuthors = await getAllAuthors(); 115 | const selected = await getSelectedCoAuthors(allAuthors); 116 | 117 | expect(allAuthors).to.have.lengthOf(4); 118 | console.log(JSON.stringify(allAuthors)); 119 | expect(selected[0].key).to.equal(coAuthor.commandKey); 120 | expect(selected).to.have.lengthOf(1); 121 | expect(gitExt.selectedRepository.inputBox.value).to.contain( 122 | `Co-authored-by: ${coAuthor.name} <${coAuthor.email}>` 123 | ); 124 | }); 125 | 126 | it("Use local .git-coauthors file instead of global", async function () { 127 | const coauthorPath = process.env.GITMOB_COAUTHORS_PATH; 128 | delete process.env.GITMOB_COAUTHORS_PATH; 129 | const expectedAuthor = { 130 | key: "jf", 131 | name: "Jango Fett", 132 | email: "jango@fetts.com", 133 | trailer: "Co-authored-by:", 134 | }; 135 | const coAuthor = new CoAuthor( 136 | expectedAuthor.name, 137 | expectedAuthor.email, 138 | false, 139 | expectedAuthor.key 140 | ); 141 | 142 | await vscode.commands.executeCommand("gitmob.reload"); 143 | await vscode.commands.executeCommand("gitmob.addCoAuthor", coAuthor); 144 | await wait(200); 145 | 146 | const allAuthors = await getAllAuthors(); 147 | expect(allAuthors).to.have.lengthOf(2); 148 | const selected = await getSelectedCoAuthors(allAuthors); 149 | expect(selected[0].key).to.equal(coAuthor.commandKey); 150 | expect(selected).to.have.lengthOf(1); 151 | expect(gitExt.selectedRepository.inputBox.value).to.contain( 152 | `Co-authored-by: ${coAuthor.name} <${coAuthor.email}>` 153 | ); 154 | 155 | process.env.GITMOB_COAUTHORS_PATH = coauthorPath; 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /test/fixture-1/.git-coauthors: -------------------------------------------------------------------------------- 1 | { 2 | "coauthors": { 3 | "rd": { 4 | "name": "Richard random", 5 | "email": "richrand@email.com" 6 | }, 7 | "jf": { 8 | "name": "Jango Fett", 9 | "email": "jango@fetts.com" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixture-1/hello.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rkotze/git-mob-vs-code/54cb22a329bfc240740b7152c9605a44ada7201f/test/fixture-1/hello.js -------------------------------------------------------------------------------- /test/setup/after-all.js: -------------------------------------------------------------------------------- 1 | const { runCmd } = require("./run-cmd"); 2 | 3 | exports.afterAll = async function () { 4 | const { GITMOB_MESSAGE_PATH, GITMOB_COAUTHORS_PATH } = process.env; 5 | runCmd("rm -rf .git"); 6 | runCmd(`rm ${GITMOB_COAUTHORS_PATH}`); 7 | runCmd(`rm ${GITMOB_MESSAGE_PATH}`); 8 | console.log("** After all finished."); 9 | }; 10 | -------------------------------------------------------------------------------- /test/setup/before-all.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const myExtension = require("../../extension"); 3 | const { installTestCoAuthorFile } = require("./install-test-co-author-file"); 4 | const { runCmd } = require("./run-cmd"); 5 | 6 | exports.beforeAll = async function () { 7 | process.env.GITMOB_COAUTHORS_PATH = path.resolve( 8 | __dirname, 9 | "../git.co-authors" 10 | ); 11 | process.env.GITMOB_MESSAGE_PATH = path.resolve(__dirname, "../.gitmessage"); 12 | 13 | runCmd("git init"); 14 | runCmd("git config --local user.name Richard"); 15 | runCmd("git config --local user.email rich.kotze@gitmob.com"); 16 | 17 | await installTestCoAuthorFile(); 18 | const ready = await myExtension.ready(); 19 | console.log("** Before all finished: ", ready); 20 | }; 21 | -------------------------------------------------------------------------------- /test/setup/install-test-co-author-file.js: -------------------------------------------------------------------------------- 1 | const { createCoAuthorsFile, Author } = require("git-mob-core"); 2 | 3 | const authorList = [ 4 | new Author("rk", "Richard Kotze", "richk@gitmob.com"), 5 | new Author("di", "Dennis", "dennis@gitmob.com"), 6 | new Author("sw", "Scarlet Witch", "scalet-w@gitmob.com"), 7 | ]; 8 | 9 | async function installTestCoAuthorFile() { 10 | await createCoAuthorsFile(authorList); 11 | } 12 | 13 | exports.installTestCoAuthorFile = installTestCoAuthorFile; 14 | -------------------------------------------------------------------------------- /test/setup/mocha-suite.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const Mocha = require("mocha"); 3 | const { glob } = require("glob"); 4 | const { beforeAll } = require("./before-all"); 5 | const { afterAll } = require("./after-all"); 6 | 7 | exports.run = function run() { 8 | const testsRoot = path.resolve(__dirname, "../"); 9 | const mocha = new Mocha({ 10 | ui: "bdd", 11 | color: true, 12 | rootHooks: { 13 | beforeAll: beforeAll, 14 | afterAll: afterAll, 15 | }, 16 | // timeout: 5000, 17 | }); 18 | 19 | return new Promise((c, e) => { 20 | glob("**/**.test.js", { cwd: testsRoot }).then(function (files) { 21 | try { 22 | // Add files to the test suite 23 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 24 | // Run the mocha test 25 | mocha.run((failures) => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | e(err); 34 | } 35 | }); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /test/setup/run-cmd.js: -------------------------------------------------------------------------------- 1 | const { spawnSync } = require("child_process"); 2 | const path = require("path"); 3 | 4 | function runCmd(command) { 5 | console.log("runCmd: ", command); 6 | let spawn = spawnSync(command, { 7 | shell: true, 8 | encoding: "utf8", 9 | cwd: path.resolve(__dirname, "../fixture-1"), 10 | }); 11 | return spawn; 12 | } 13 | 14 | exports.runCmd = runCmd; 15 | -------------------------------------------------------------------------------- /test/setup/test-runner.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const { runTests } = require("@vscode/test-electron"); 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, "../../"); 10 | 11 | // The path to the extension test runner script 12 | // Passed to --extensionTestsPath 13 | // const extensionTestsPath = path.resolve(__dirname, "./index"); 14 | const extensionTestsPath = path.resolve(__dirname, "./mocha-suite.js"); 15 | const testWorkspace = path.resolve(__dirname, "../fixture-1"); 16 | // Download VS Code, unzip it and run the integration test 17 | await runTests({ 18 | version: "insiders", 19 | extensionDevelopmentPath, 20 | extensionTestsPath, 21 | launchArgs: [testWorkspace, "--disable-extensions"], 22 | }); 23 | } catch (err) { 24 | console.error(err); 25 | console.error("Failed to run tests"); 26 | process.exit(1); 27 | } 28 | } 29 | 30 | main(); 31 | --------------------------------------------------------------------------------