├── .all-contributorsrc ├── .conventional-changelog-lintrc ├── .cz-config.js ├── .editorconfig ├── .github ├── stale.yml └── workflows │ ├── ci.yml │ └── duplicate.yml ├── .gitignore ├── .npmrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── images ├── create-pull-request.png ├── github-personal-access-token.png ├── github-personal-access-token2.png ├── octocat.png └── set-personal-access-token.png ├── package.json ├── renovate.json ├── src ├── command-manager.ts ├── command.ts ├── commands │ ├── browse.ts │ ├── pull-requests.ts │ ├── token.ts │ └── user.ts ├── configuration.ts ├── extension.ts ├── git.ts ├── helper.ts ├── issues.ts ├── main.ts ├── provider │ ├── client.ts │ ├── github │ │ ├── api.ts │ │ ├── client.ts │ │ ├── issue.ts │ │ ├── pull-request.ts │ │ ├── repository.ts │ │ └── user.ts │ ├── gitlab │ │ ├── api.ts │ │ ├── client.ts │ │ ├── issue.ts │ │ ├── merge-request.ts │ │ ├── repository.ts │ │ └── user.ts │ ├── issue.ts │ ├── pull-request.ts │ ├── repository.ts │ └── user.ts ├── status-bar-manager.ts ├── tokens.ts └── workflow-manager.ts ├── test ├── extension.test.ts └── index.ts ├── tsconfig.json ├── tslint.json ├── typings ├── execa.d.ts ├── fetch-extras.d.ts └── sander.d.ts └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "vscode-github", 3 | "projectOwner": "KnisterPeter", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "angular", 12 | "contributors": [ 13 | { 14 | "login": "KnisterPeter", 15 | "name": "Markus Wolf", 16 | "avatar_url": "https://avatars.githubusercontent.com/u/327445?v=4", 17 | "profile": "https://about.me/knisterpeter", 18 | "contributions": [ 19 | "code" 20 | ] 21 | }, 22 | { 23 | "login": "RossValler", 24 | "name": "Ross Valler", 25 | "avatar_url": "https://avatars.githubusercontent.com/u/3588000?v=4", 26 | "profile": "https://valler.dev/", 27 | "contributions": [ 28 | "code" 29 | ] 30 | }, 31 | { 32 | "login": "jeveleth", 33 | "name": "Josh Eveleth", 34 | "avatar_url": "https://avatars.githubusercontent.com/u/305137?v=4", 35 | "profile": "https://github.com/jeveleth", 36 | "contributions": [ 37 | "code" 38 | ] 39 | }, 40 | { 41 | "login": "sasial-dev", 42 | "name": "sasial", 43 | "avatar_url": "https://avatars.githubusercontent.com/u/44125644?v=4", 44 | "profile": "https://github.com/sasial-dev", 45 | "contributions": [ 46 | "code" 47 | ] 48 | }, 49 | { 50 | "login": "timhaintz", 51 | "name": "timhaintz", 52 | "avatar_url": "https://avatars.githubusercontent.com/u/19178488?v=4", 53 | "profile": "https://www.timhaintz.com.au/", 54 | "contributions": [ 55 | "doc" 56 | ] 57 | }, 58 | { 59 | "login": "pzelnip", 60 | "name": "Adam Parkin", 61 | "avatar_url": "https://avatars.githubusercontent.com/u/414933?v=4", 62 | "profile": "https://www.codependentcodr.com/", 63 | "contributions": [ 64 | "doc" 65 | ] 66 | }, 67 | { 68 | "login": "dukky", 69 | "name": "Andreas Holley", 70 | "avatar_url": "https://avatars.githubusercontent.com/u/3122655?v=4", 71 | "profile": "http://duk.im/", 72 | "contributions": [ 73 | "code" 74 | ] 75 | }, 76 | { 77 | "login": "angristan", 78 | "name": "Stanislas", 79 | "avatar_url": "https://avatars.githubusercontent.com/u/11699655?v=4", 80 | "profile": "https://stanislas.blog/", 81 | "contributions": [ 82 | "doc" 83 | ] 84 | }, 85 | { 86 | "login": "Drarig29", 87 | "name": "Corentin Girard", 88 | "avatar_url": "https://avatars.githubusercontent.com/u/9317502?v=4", 89 | "profile": "https://github.com/Drarig29", 90 | "contributions": [ 91 | "code" 92 | ] 93 | }, 94 | { 95 | "login": "mmorhun", 96 | "name": "Mykola Morhun", 97 | "avatar_url": "https://avatars.githubusercontent.com/u/15607393?v=4", 98 | "profile": "https://github.com/mmorhun", 99 | "contributions": [ 100 | "code" 101 | ] 102 | }, 103 | { 104 | "login": "NicholasMata", 105 | "name": "Nicholas Mata", 106 | "avatar_url": "https://avatars.githubusercontent.com/u/8304095?v=4", 107 | "profile": "https://github.com/NicholasMata", 108 | "contributions": [ 109 | "code" 110 | ] 111 | }, 112 | { 113 | "login": "ishitatsuyuki", 114 | "name": "Tatsuyuki Ishi", 115 | "avatar_url": "https://avatars.githubusercontent.com/u/12389383?v=4", 116 | "profile": "https://github.com/ishitatsuyuki", 117 | "contributions": [ 118 | "code" 119 | ] 120 | }, 121 | { 122 | "login": "vincentvanderweele", 123 | "name": "Vincent van der Weele", 124 | "avatar_url": "https://avatars.githubusercontent.com/u/9002093?v=4", 125 | "profile": "https://github.com/vincentvanderweele", 126 | "contributions": [ 127 | "code" 128 | ] 129 | }, 130 | { 131 | "login": "ivanmilov", 132 | "name": "ivanmilov", 133 | "avatar_url": "https://avatars.githubusercontent.com/u/522518?v=4", 134 | "profile": "https://github.com/ivanmilov", 135 | "contributions": [ 136 | "code" 137 | ] 138 | }, 139 | { 140 | "login": "KyleMit", 141 | "name": "Kyle", 142 | "avatar_url": "https://avatars.githubusercontent.com/u/4307307?v=4", 143 | "profile": "https://stackoverflow.com/users/1366033", 144 | "contributions": [ 145 | "doc" 146 | ] 147 | }, 148 | { 149 | "login": "lihuanshuai", 150 | "name": "Li Huanshuai", 151 | "avatar_url": "https://avatars.githubusercontent.com/u/4586647?v=4", 152 | "profile": "https://github.com/lihuanshuai", 153 | "contributions": [ 154 | "code" 155 | ] 156 | }, 157 | { 158 | "login": "shaikathaque", 159 | "name": "Shaikat Haque", 160 | "avatar_url": "https://avatars.githubusercontent.com/u/9042881?v=4", 161 | "profile": "https://github.com/shaikathaque", 162 | "contributions": [ 163 | "code" 164 | ] 165 | } 166 | ], 167 | "contributorsPerLine": 7 168 | } 169 | -------------------------------------------------------------------------------- /.conventional-changelog-lintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["angular"] 3 | } 4 | -------------------------------------------------------------------------------- /.cz-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 5 | types: [ 6 | {value: 'feat', name: 'feat: A new feature'}, 7 | {value: 'fix', name: 'fix: A bug fix'}, 8 | {value: 'docs', name: 'docs: Documentation only changes'}, 9 | {value: 'style', name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)'}, 10 | {value: 'refactor', name: 'refactor: A code change that neither fixes a bug nor adds a feature'}, 11 | {value: 'perf', name: 'perf: A code change that improves performance'}, 12 | {value: 'test', name: 'test: Adding missing tests'}, 13 | {value: 'chore', name: 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation'}, 14 | {value: 'revert', name: 'revert: Revert to a commit'}, 15 | {value: 'WIP', name: 'WIP: Work in progress'} 16 | ], 17 | 18 | scopes: [ 19 | {}, 20 | {name: 'vscode-github'}, 21 | {name: 'extension'} 22 | ], 23 | 24 | allowCustomScopes: false, 25 | allowBreakingChanges: ['feat'] 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | charset = utf-8 13 | end_of_line = lf 14 | indent_size = 2 15 | indent_style = space 16 | insert_final_newline = true 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Comment to post when marking an issue as stale. Set to `false` to disable 10 | markComment: > 11 | This issue has been automatically marked as stale because it has not had 12 | recent activity. It will be closed if no further activity occurs. Thank you 13 | for your contributions. 14 | # Comment to post when closing a stale issue. Set to `false` to disable 15 | closeComment: false 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - id: yarn-cache-dir-path 20 | run: echo "::set-output name=dir::$(yarn cache dir)" 21 | - uses: actions/cache@v3 22 | id: yarn-cache 23 | with: 24 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 25 | key: ${{ runner.os }}-${{ matrix.node-version }}-yarn-${{ hashFiles('**/yarn.lock') }} 26 | restore-keys: | 27 | ${{ runner.os }}-${{ matrix.node-version }}-yarn- 28 | - run: | 29 | sudo apt-get install xvfb libsecret-1-0 30 | export DISPLAY=:99.0 31 | Xvfb -ac :99 -screen 0 1280x1024x16 > /dev/null 2>&1 & 32 | mkdir /tmp/test-workspace 33 | (cd /tmp/test-workspace && git init) 34 | yarn install 35 | yarn vscode:prepublish 36 | - run: | 37 | yarn linter 38 | - run: | 39 | xvfb-run --auto-servernum yarn test 40 | env: 41 | CODE_TESTS_WORKSPACE: /tmp/test-workspace 42 | -------------------------------------------------------------------------------- /.github/workflows/duplicate.yml: -------------------------------------------------------------------------------- 1 | name: "duplicate issue" 2 | 3 | on: [issues] 4 | 5 | jobs: 6 | flag: 7 | if: (github.event.action == 'opened' || github.event.action == 'edited') && github.event.issue.title == 'Extension causes high cpu load' 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Comment and close as duplicate 14 | run: | 15 | gh issue comment "$NUMBER" --body "Duplicate of #443" 16 | gh issue close "$NUMBER" 17 | env: 18 | NUMBER: ${{github.event.issue.number}} 19 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test 4 | 5 | npm-debug.log 6 | vscode-github-*.vsix 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "KnisterPeter.vscode-github", 4 | "KnisterPeter.vscode-commitizen", 5 | "eg2.tslint", 6 | "EditorConfig.EditorConfig" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Run Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "--extensionDevelopmentPath=${workspaceRoot}" 12 | ], 13 | "stopOnEntry": false, 14 | "sourceMaps": true, 15 | "outFiles": [ 16 | "${workspaceRoot}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "npm: watch" 19 | }, 20 | { 21 | "name": "Run Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "runtimeExecutable": "${execPath}", 25 | "args": [ 26 | "--extensionDevelopmentPath=${workspaceRoot}", 27 | "--extensionTestsPath=${workspaceRoot}/out/test" 28 | ], 29 | "stopOnEntry": false, 30 | "sourceMaps": true, 31 | "outFiles": [ 32 | "${workspaceRoot}/out/test/**/*.js" 33 | ], 34 | "preLaunchTask": "npm: watch" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "out": false 4 | }, 5 | "search.exclude": { 6 | "out": true 7 | }, 8 | "typescript.tsdk": "./node_modules/typescript/lib", 9 | "editor.rulers": [ 10 | 120 11 | ], 12 | "github.autoPublish": true, 13 | "editor.formatOnSave": true 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "group": { 10 | "kind": "build", 11 | "isDefault": true 12 | }, 13 | "presentation": { 14 | "echo": true, 15 | "reveal": "silent", 16 | "focus": false, 17 | "panel": "shared" 18 | }, 19 | "isBackground": true, 20 | "problemMatcher": [ 21 | "$tsc-watch", 22 | "$tslint5" 23 | ] 24 | }, 25 | { 26 | "type": "npm", 27 | "script": "test", 28 | "group": { 29 | "kind": "test", 30 | "isDefault": true 31 | } 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | test/** 5 | src/** 6 | **/*.map 7 | .gitignore 8 | tsconfig.json 9 | vsc-extension-quickstart.md 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.30.7](https://github.com/KnisterPeter/vscode-github/compare/v0.30.6...v0.30.7) (2021-10-22) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * 'browse file' url encoding issue ([07e2306](https://github.com/KnisterPeter/vscode-github/commit/07e230682e9f171b684e44ae5dea5e62c481e25b)), closes [#729](https://github.com/KnisterPeter/vscode-github/issues/729) 11 | * **deps:** update dependency execa to v5.0.1 ([55a1dc8](https://github.com/KnisterPeter/vscode-github/commit/55a1dc88d3ac3edf5dd3ab8c3bd646c5f7f3d1d2)) 12 | * **deps:** update dependency execa to v5.1.1 ([b526763](https://github.com/KnisterPeter/vscode-github/commit/b52676366e0b5374e0c97c4062e5098b1c32f9e6)) 13 | * **deps:** update dependency vscode-extension-telemetry to v0.2.0 ([1d00448](https://github.com/KnisterPeter/vscode-github/commit/1d00448d841c5fe151a7db2a59abf9eadc7c2f29)) 14 | * **deps:** update dependency vscode-extension-telemetry to v0.2.1 ([8e0a7ca](https://github.com/KnisterPeter/vscode-github/commit/8e0a7caddf93d58d8626c0b4de2cb0710d8bccb5)) 15 | * **deps:** update dependency vscode-extension-telemetry to v0.2.2 ([c93d4a2](https://github.com/KnisterPeter/vscode-github/commit/c93d4a2301dffb4f71b48575ec1141ceea7a9a3e)) 16 | * **deps:** update dependency vscode-extension-telemetry to v0.2.4 ([612d765](https://github.com/KnisterPeter/vscode-github/commit/612d76581efe8a171687d211fc019766a5767602)) 17 | * **deps:** update dependency vscode-extension-telemetry to v0.2.6 ([90da04c](https://github.com/KnisterPeter/vscode-github/commit/90da04c49cd92c2f5b2c0718a6e78815c62c2942)) 18 | * **deps:** update dependency vscode-extension-telemetry to v0.2.7 ([7b58940](https://github.com/KnisterPeter/vscode-github/commit/7b589407e457ae042ee5ea7d46d1c8ffc409de38)) 19 | * **deps:** update dependency vscode-extension-telemetry to v0.2.8 ([481a1a9](https://github.com/KnisterPeter/vscode-github/commit/481a1a982339f748b6d4e116470c6f90e2444e3f)) 20 | * **deps:** update dependency vscode-extension-telemetry to v0.2.9 ([08e6226](https://github.com/KnisterPeter/vscode-github/commit/08e62261d21386e8438ea17c7451010cff3814f3)) 21 | * **deps:** update dependency vscode-extension-telemetry to v0.3.2 ([cd673c3](https://github.com/KnisterPeter/vscode-github/commit/cd673c3019aae3d1059c62152ba322bd70f0c540)) 22 | * **deps:** update dependency vscode-extension-telemetry to v0.4.1 ([4133755](https://github.com/KnisterPeter/vscode-github/commit/413375550d27028f8568d25e0ab7df9e0d287f0e)) 23 | * **deps:** update dependency vscode-extension-telemetry to v0.4.2 ([11f7ca3](https://github.com/KnisterPeter/vscode-github/commit/11f7ca395c7b4e89db1f165c134b160e2c8004e3)) 24 | 25 | ### [0.30.6](https://github.com/KnisterPeter/vscode-github/compare/v0.30.5...v0.30.6) (2021-04-26) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * revert [#774](https://github.com/KnisterPeter/vscode-github/issues/774) and change user message ([7fa3742](https://github.com/KnisterPeter/vscode-github/commit/7fa3742ca493808bb28a067fa3976e6b0d9b9cfb)) 31 | 32 | ### [0.30.5](https://github.com/KnisterPeter/vscode-github/compare/v0.30.4...v0.30.5) (2021-03-19) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * change remote regex for zsh ([f2a3047](https://github.com/KnisterPeter/vscode-github/commit/f2a30471fa315d1628f366d5cffb9c83e6cc1057)) 38 | * **deps:** update dependency vscode-extension-telemetry to v0.1.7 ([f22090e](https://github.com/KnisterPeter/vscode-github/commit/f22090e905067770bd9406d513996281a2288dbd)) 39 | * do not use abstract with async ([0911cf8](https://github.com/KnisterPeter/vscode-github/commit/0911cf894c5347deb9b9a1765390704432955bad)) 40 | * **deps:** update dependency execa to v4.1.0 ([1b2fbb2](https://github.com/KnisterPeter/vscode-github/commit/1b2fbb20625a7802df9d6784bf982d21cc782aa7)) 41 | * **deps:** update dependency execa to v5 ([7ac486a](https://github.com/KnisterPeter/vscode-github/commit/7ac486af121b33a8d0c9436bc966b6171e9467ea)) 42 | * **deps:** update dependency isomorphic-fetch to v3 ([d3fef69](https://github.com/KnisterPeter/vscode-github/commit/d3fef6946e5aa8cf0ebe6c539e10a339eb7fd7e4)) 43 | * **deps:** update dependency tsdi to v0.24.1 ([4109a7f](https://github.com/KnisterPeter/vscode-github/commit/4109a7f01201eccf3722e1e848a0f62d7d4cc688)) 44 | * **deps:** update dependency tsdi to v0.25.0 ([c717d4a](https://github.com/KnisterPeter/vscode-github/commit/c717d4af79242420aa5dc27ae8bbda001406c804)) 45 | 46 | ### [0.30.4](https://github.com/KnisterPeter/vscode-github/compare/v0.30.3...v0.30.4) (2020-09-21) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * use startLine instead of line in browse cmd ([72a34f9](https://github.com/KnisterPeter/vscode-github/commit/72a34f9d705795a139f47369f78aa035d0717021)) 52 | * **deps:** update dependency execa to v3.3.0 ([1a2c724](https://github.com/KnisterPeter/vscode-github/commit/1a2c72420f3b905ea7922f19f78a65b07ebac251)) 53 | * **deps:** update dependency execa to v3.4.0 ([02785e0](https://github.com/KnisterPeter/vscode-github/commit/02785e05eb779889e810d62e7f5deb435a7ec095)) 54 | * **deps:** update dependency execa to v4.0.3 ([e65e20b](https://github.com/KnisterPeter/vscode-github/commit/e65e20b1dc6d6f23ab79df5b4d059c2006c95a49)) 55 | * **deps:** update dependency lru-cache to v6 ([c3089c8](https://github.com/KnisterPeter/vscode-github/commit/c3089c812f7d3ba2bbba5c3560418f3988e20a44)) 56 | * **deps:** update dependency pretend to v3.1.0 ([0a6f922](https://github.com/KnisterPeter/vscode-github/commit/0a6f922ec40588cd2503dc537a530e95c4029657)) 57 | * **deps:** update dependency tsdi to v0.23.0 ([71fd40a](https://github.com/KnisterPeter/vscode-github/commit/71fd40afb0fa70bb47753bbf310d0c9a95fd1c54)) 58 | 59 | ### [0.30.3](https://github.com/KnisterPeter/vscode-github/compare/v0.30.2...v0.30.3) (2019-10-27) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * **deps:** update dependency execa to v2 ([17c54b2](https://github.com/KnisterPeter/vscode-github/commit/17c54b2ad3070bbddac60bd7bea137ff3834c1d0)) 65 | * **deps:** update dependency execa to v2.0.1 ([bf6e46c](https://github.com/KnisterPeter/vscode-github/commit/bf6e46c7f96e80b8a3eba150d63bff5031dde6ca)) 66 | * **deps:** update dependency execa to v2.0.2 ([99b63bf](https://github.com/KnisterPeter/vscode-github/commit/99b63bf6ff214cdbd93fd73ce10c4e09f5bc19ce)) 67 | * **deps:** update dependency execa to v2.0.3 ([ddf5f2d](https://github.com/KnisterPeter/vscode-github/commit/ddf5f2d6041ab6b42ab72871a1a89fd3928dc2af)) 68 | * **deps:** update dependency execa to v2.1.0 ([acc7729](https://github.com/KnisterPeter/vscode-github/commit/acc7729165479b8e573001b9c17d77cfc5644a2d)) 69 | * **deps:** update dependency execa to v3 ([4c8bd15](https://github.com/KnisterPeter/vscode-github/commit/4c8bd15625d82b4c3eeaa5a1859e3cd3fa12043c)) 70 | * **deps:** update dependency execa to v3.1.0 ([8fb1985](https://github.com/KnisterPeter/vscode-github/commit/8fb198552bf205a0d5df164fd338aa3975a52605)) 71 | * **deps:** update dependency execa to v3.2.0 ([64c0119](https://github.com/KnisterPeter/vscode-github/commit/64c0119e60680646fea706a40edfb18b494ef6cf)) 72 | * **deps:** update dependency pretend to v3 ([fc96d28](https://github.com/KnisterPeter/vscode-github/commit/fc96d28c442e6dd1ea0fb9a92ddd35157a37a5ba)) 73 | * **deps:** update dependency tsdi to v0.21.0 ([44eaf49](https://github.com/KnisterPeter/vscode-github/commit/44eaf492c449fcda8ec9a0ec232a736d54933f42)) 74 | * **deps:** update dependency vscode-extension-telemetry to v0.1.2 ([9fc4689](https://github.com/KnisterPeter/vscode-github/commit/9fc468908c0c0336b57424112936d411e0848681)) 75 | * add missing generic parameter ([d8c53d7](https://github.com/KnisterPeter/vscode-github/commit/d8c53d7d47be9cb2c6a13f9200a7d0f17ed441ca)) 76 | 77 | 78 | # [](https://github.com/KnisterPeter/vscode-github/compare/v0.30.1...v) (2019-03-27) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * disble issue scanning for now ([ce8a84d](https://github.com/KnisterPeter/vscode-github/commit/ce8a84d)) 84 | * **deps:** update dependency vscode-extension-telemetry to v0.1.1 ([b8a5d35](https://github.com/KnisterPeter/vscode-github/commit/b8a5d35)) 85 | 86 | 87 | 88 | 89 | ## [0.30.1](https://github.com/KnisterPeter/vscode-github/compare/v0.30.0...v0.30.1) (2018-11-28) 90 | 91 | 92 | ### Bug Fixes 93 | 94 | * **deps:** update dependency execa to v0.11.0 ([1cb7884](https://github.com/KnisterPeter/vscode-github/commit/1cb7884)) 95 | * **deps:** update dependency execa to v1 ([27c331d](https://github.com/KnisterPeter/vscode-github/commit/27c331d)) 96 | * **deps:** update dependency lru-cache to v5 ([8c0b74b](https://github.com/KnisterPeter/vscode-github/commit/8c0b74b)) 97 | * **deps:** update dependency pretend to v2 ([c1fe692](https://github.com/KnisterPeter/vscode-github/commit/c1fe692)) 98 | * **deps:** update dependency vscode-extension-telemetry to v0.0.18 ([a13873a](https://github.com/KnisterPeter/vscode-github/commit/a13873a)) 99 | * **deps:** update dependency vscode-extension-telemetry to v0.0.20 ([be0b081](https://github.com/KnisterPeter/vscode-github/commit/be0b081)) 100 | * **deps:** update dependency vscode-extension-telemetry to v0.0.22 ([7d553d2](https://github.com/KnisterPeter/vscode-github/commit/7d553d2)) 101 | * **deps:** update dependency vscode-extension-telemetry to v0.1.0 ([3910d20](https://github.com/KnisterPeter/vscode-github/commit/3910d20)) 102 | * create instance of LRUCache properly ([5de0679](https://github.com/KnisterPeter/vscode-github/commit/5de0679)) 103 | * use correct type for header foreach ([934e043](https://github.com/KnisterPeter/vscode-github/commit/934e043)) 104 | 105 | 106 | 107 | 108 | # [0.30.0](https://github.com/KnisterPeter/vscode-github/compare/v0.29.0...v0.30.0) (2018-06-26) 109 | 110 | 111 | ### Bug Fixes 112 | 113 | * allow tokens to be updated during runtime ([d9aeb7f](https://github.com/KnisterPeter/vscode-github/commit/d9aeb7f)), closes [#395](https://github.com/KnisterPeter/vscode-github/issues/395) 114 | * uncolored statusbar ([f166cfd](https://github.com/KnisterPeter/vscode-github/commit/f166cfd)), closes [#301](https://github.com/KnisterPeter/vscode-github/issues/301) 115 | 116 | 117 | ### Features 118 | 119 | * calculate git remote name ([a5b18cb](https://github.com/KnisterPeter/vscode-github/commit/a5b18cb)), closes [#407](https://github.com/KnisterPeter/vscode-github/issues/407) 120 | * detect external pull request and allow to pull it ([bf407f7](https://github.com/KnisterPeter/vscode-github/commit/bf407f7)), closes [#258](https://github.com/KnisterPeter/vscode-github/issues/258) 121 | * show and explain error for invalid git remotes ([4309190](https://github.com/KnisterPeter/vscode-github/commit/4309190)), closes [#402](https://github.com/KnisterPeter/vscode-github/issues/402) 122 | 123 | 124 | 125 | 126 | # [0.29.0](https://github.com/KnisterPeter/vscode-github/compare/v0.28.1...v0.29.0) (2018-06-15) 127 | 128 | 129 | ### Bug Fixes 130 | 131 | * **deps:** update dependency common-tags to v1.8.0 ([4f77c3e](https://github.com/KnisterPeter/vscode-github/commit/4f77c3e)) 132 | * **deps:** update dependency lru-cache to v4.1.3 ([6e3af89](https://github.com/KnisterPeter/vscode-github/commit/6e3af89)) 133 | * **deps:** update dependency pretend to v1.5.1 ([2601b3b](https://github.com/KnisterPeter/vscode-github/commit/2601b3b)) 134 | * **deps:** update dependency tsdi to v0.20.3 ([baee7c6](https://github.com/KnisterPeter/vscode-github/commit/baee7c6)) 135 | 136 | 137 | ### Features 138 | 139 | * add custom title when creating a new pull request ([34432ce](https://github.com/KnisterPeter/vscode-github/commit/34432ce)) 140 | 141 | 142 | 143 | 144 | ## [0.28.1](https://github.com/KnisterPeter/vscode-github/compare/v0.28.0...v0.28.1) (2018-05-09) 145 | 146 | 147 | ### Bug Fixes 148 | 149 | * browser current file respect git root ([481f68c](https://github.com/KnisterPeter/vscode-github/commit/481f68c)), closes [#354](https://github.com/KnisterPeter/vscode-github/issues/354) 150 | 151 | 152 | 153 | 154 | # [0.28.0](https://github.com/KnisterPeter/vscode-github/compare/v0.27.2...v0.28.0) (2018-05-08) 155 | 156 | 157 | ### Bug Fixes 158 | 159 | * set statusbar item priority ([12af194](https://github.com/KnisterPeter/vscode-github/commit/12af194)), closes [#336](https://github.com/KnisterPeter/vscode-github/issues/336) 160 | 161 | 162 | ### Features 163 | 164 | * allow to enable/disable statusbar integration ([099b5df](https://github.com/KnisterPeter/vscode-github/commit/099b5df)), closes [#345](https://github.com/KnisterPeter/vscode-github/issues/345) 165 | 166 | 167 | 168 | 169 | ## [0.27.2](https://github.com/KnisterPeter/vscode-github/compare/v0.27.1...v0.27.2) (2018-05-02) 170 | 171 | 172 | ### Bug Fixes 173 | 174 | * **deps:** update dependency common-tags to v1.7.2 ([33c164b](https://github.com/KnisterPeter/vscode-github/commit/33c164b)) 175 | * **deps:** update dependency execa to ^0.10.0 ([b4f4f18](https://github.com/KnisterPeter/vscode-github/commit/b4f4f18)) 176 | * add types to callbacks ([df8d702](https://github.com/KnisterPeter/vscode-github/commit/df8d702)) 177 | * allow definite assignment check ([22b114b](https://github.com/KnisterPeter/vscode-github/commit/22b114b)) 178 | * **deps:** update dependency tsdi to v0.20.0 ([e8a0e60](https://github.com/KnisterPeter/vscode-github/commit/e8a0e60)) 179 | * **deps:** update dependency tsdi to v0.20.1 ([27456c2](https://github.com/KnisterPeter/vscode-github/commit/27456c2)) 180 | * **deps:** update dependency tsdi to v0.20.2 ([3cb1f50](https://github.com/KnisterPeter/vscode-github/commit/3cb1f50)) 181 | * **deps:** update dependency vscode-extension-telemetry to v0.0.16 ([710dd76](https://github.com/KnisterPeter/vscode-github/commit/710dd76)) 182 | * **deps:** update dependency vscode-extension-telemetry to v0.0.17 ([1d11c12](https://github.com/KnisterPeter/vscode-github/commit/1d11c12)) 183 | * cache repository data per workspace and gitconfig ([b2736c6](https://github.com/KnisterPeter/vscode-github/commit/b2736c6)), closes [#356](https://github.com/KnisterPeter/vscode-github/issues/356) 184 | 185 | 186 | 187 | 188 | ## [0.27.1](https://github.com/KnisterPeter/vscode-github/compare/v0.27.0...v0.27.1) (2018-02-28) 189 | 190 | 191 | ### Bug Fixes 192 | 193 | * update package-lock ([02caa02](https://github.com/KnisterPeter/vscode-github/commit/02caa02)) 194 | 195 | 196 | 197 | 198 | # [0.27.0](https://github.com/KnisterPeter/vscode-github/compare/v0.26.0...v0.27.0) (2018-02-28) 199 | 200 | 201 | ### Bug Fixes 202 | 203 | * **package:** update tsdi to version 0.19.2 ([d4a3b92](https://github.com/KnisterPeter/vscode-github/commit/d4a3b92)), closes [#303](https://github.com/KnisterPeter/vscode-github/issues/303) 204 | * **package:** update vscode-extension-telemetry to version 0.0.11 ([cf6d3be](https://github.com/KnisterPeter/vscode-github/commit/cf6d3be)) 205 | * **package:** update vscode-extension-telemetry to version 0.0.12 ([b99e27b](https://github.com/KnisterPeter/vscode-github/commit/b99e27b)) 206 | * **package:** update vscode-extension-telemetry to version 0.0.13 ([9a7497d](https://github.com/KnisterPeter/vscode-github/commit/9a7497d)) 207 | * **package:** update vscode-extension-telemetry to version 0.0.14 ([974898e](https://github.com/KnisterPeter/vscode-github/commit/974898e)) 208 | * **package:** update vscode-extension-telemetry to version 0.0.15 ([5cc6485](https://github.com/KnisterPeter/vscode-github/commit/5cc6485)) 209 | * add agent to global RequestInit interface ([fc8a875](https://github.com/KnisterPeter/vscode-github/commit/fc8a875)), closes [#306](https://github.com/KnisterPeter/vscode-github/issues/306) [#306](https://github.com/KnisterPeter/vscode-github/issues/306) 210 | 211 | 212 | ### Features 213 | 214 | * add allowUnsafeSSL for self-signed certificates ([74949ca](https://github.com/KnisterPeter/vscode-github/commit/74949ca)) 215 | 216 | 217 | 218 | 219 | # [0.26.0](https://github.com/KnisterPeter/vscode-github/compare/v0.25.0...v0.26.0) (2018-01-18) 220 | 221 | 222 | ### Bug Fixes 223 | 224 | * use uri fspath ([883a33d](https://github.com/KnisterPeter/vscode-github/commit/883a33d)) 225 | * **package:** update common-tags to version 1.7.0 ([af3826f](https://github.com/KnisterPeter/vscode-github/commit/af3826f)) 226 | * **package:** update execa to version 0.9.0 ([8cc2549](https://github.com/KnisterPeter/vscode-github/commit/8cc2549)) 227 | * **package:** update tsdi to version 0.18.2 ([ad0a916](https://github.com/KnisterPeter/vscode-github/commit/ad0a916)) 228 | * **package:** update vscode-extension-telemetry to version 0.0.9 ([57ffe73](https://github.com/KnisterPeter/vscode-github/commit/57ffe73)) 229 | 230 | 231 | ### Features 232 | 233 | * add parameters to user commands ([35b468a](https://github.com/KnisterPeter/vscode-github/commit/35b468a)) 234 | 235 | 236 | 237 | 238 | # [0.25.0](https://github.com/KnisterPeter/vscode-github/compare/v0.24.1...v0.25.0) (2017-12-28) 239 | 240 | 241 | ### Bug Fixes 242 | 243 | * **package:** update common-tags to version 1.6.0 ([49c484e](https://github.com/KnisterPeter/vscode-github/commit/49c484e)) 244 | 245 | 246 | ### Features 247 | 248 | * add user list to gitlab api ([f07aa80](https://github.com/KnisterPeter/vscode-github/commit/f07aa80)) 249 | 250 | 251 | 252 | 253 | ## [0.24.1](https://github.com/KnisterPeter/vscode-github/compare/v0.24.0...v0.24.1) (2017-12-19) 254 | 255 | 256 | ### Bug Fixes 257 | 258 | * **package:** update common-tags to version 1.5.1 ([333d77f](https://github.com/KnisterPeter/vscode-github/commit/333d77f)), closes [#270](https://github.com/KnisterPeter/vscode-github/issues/270) 259 | * **package:** update tsdi to version 0.18.0 ([2304d3a](https://github.com/KnisterPeter/vscode-github/commit/2304d3a)) 260 | * **package:** update tsdi to version 0.18.1 ([6e5f0af](https://github.com/KnisterPeter/vscode-github/commit/6e5f0af)) 261 | * do not assume github error ([15b7c0f](https://github.com/KnisterPeter/vscode-github/commit/15b7c0f)), closes [#278](https://github.com/KnisterPeter/vscode-github/issues/278) 262 | 263 | 264 | 265 | 266 | # [0.24.0](https://github.com/KnisterPeter/vscode-github/compare/v0.23.0...v0.24.0) (2017-11-17) 267 | 268 | 269 | ### Bug Fixes 270 | 271 | * **package:** update pretend to version 1.0.0 ([2b9a6b0](https://github.com/KnisterPeter/vscode-github/commit/2b9a6b0)) 272 | * **package:** update tsdi to version 0.16.0 ([0b79af2](https://github.com/KnisterPeter/vscode-github/commit/0b79af2)) 273 | * **package:** update tsdi to version 0.17.0 ([601c72d](https://github.com/KnisterPeter/vscode-github/commit/601c72d)) 274 | * unable to determine first commit ([d821930](https://github.com/KnisterPeter/vscode-github/commit/d821930)) 275 | * update status for single workspace ([0789682](https://github.com/KnisterPeter/vscode-github/commit/0789682)) 276 | 277 | 278 | ### Features 279 | 280 | * add current user api ([bd08db9](https://github.com/KnisterPeter/vscode-github/commit/bd08db9)) 281 | 282 | 283 | 284 | 285 | # [0.23.0](https://github.com/KnisterPeter/vscode-github/compare/v0.22.0...v0.23.0) (2017-10-24) 286 | 287 | 288 | ### Bug Fixes 289 | 290 | * correct usage of lru typings ([39b0977](https://github.com/KnisterPeter/vscode-github/commit/39b0977)) 291 | * only list open issues ([55d11c8](https://github.com/KnisterPeter/vscode-github/commit/55d11c8)) 292 | * parse response codes above 200 ([d32b080](https://github.com/KnisterPeter/vscode-github/commit/d32b080)) 293 | * update logging ([e363349](https://github.com/KnisterPeter/vscode-github/commit/e363349)) 294 | * **package:** update tsdi to version 0.15.0 ([e3a4f75](https://github.com/KnisterPeter/vscode-github/commit/e3a4f75)) 295 | 296 | 297 | ### Features 298 | 299 | * add multi folder support ([e103165](https://github.com/KnisterPeter/vscode-github/commit/e103165)) 300 | * add request logging ([4bbffb6](https://github.com/KnisterPeter/vscode-github/commit/4bbffb6)) 301 | * add test connect per provider ([d3583dd](https://github.com/KnisterPeter/vscode-github/commit/d3583dd)) 302 | * add update pull request command ([f17eb3c](https://github.com/KnisterPeter/vscode-github/commit/f17eb3c)) 303 | * hover issue ([ae9606c](https://github.com/KnisterPeter/vscode-github/commit/ae9606c)), closes [#241](https://github.com/KnisterPeter/vscode-github/issues/241) 304 | * link to issues from source ([968dabf](https://github.com/KnisterPeter/vscode-github/commit/968dabf)), closes [#241](https://github.com/KnisterPeter/vscode-github/issues/241) 305 | * provide list of assignees ([33b4742](https://github.com/KnisterPeter/vscode-github/commit/33b4742)), closes [#84](https://github.com/KnisterPeter/vscode-github/issues/84) 306 | * run test on connect ([60468ef](https://github.com/KnisterPeter/vscode-github/commit/60468ef)) 307 | * support merging in gitlab ([66ff9ee](https://github.com/KnisterPeter/vscode-github/commit/66ff9ee)) 308 | 309 | 310 | 311 | 312 | # [0.22.0](https://github.com/KnisterPeter/vscode-github/compare/v0.21.0...v0.22.0) (2017-10-06) 313 | 314 | 315 | ### Bug Fixes 316 | 317 | * parse hostname ([2e78e01](https://github.com/KnisterPeter/vscode-github/commit/2e78e01)), closes [#208](https://github.com/KnisterPeter/vscode-github/issues/208) 318 | * remove duplicate slash in url ([e409d10](https://github.com/KnisterPeter/vscode-github/commit/e409d10)) 319 | * reschedule status update ([e1159cb](https://github.com/KnisterPeter/vscode-github/commit/e1159cb)) 320 | 321 | 322 | ### Features 323 | 324 | * add remove token command ([f34b116](https://github.com/KnisterPeter/vscode-github/commit/f34b116)), closes [#193](https://github.com/KnisterPeter/vscode-github/issues/193) 325 | * add support for gitlab issues ([a87f4c5](https://github.com/KnisterPeter/vscode-github/commit/a87f4c5)) 326 | * add support for remove_source_branch ([ac60cd1](https://github.com/KnisterPeter/vscode-github/commit/ac60cd1)) 327 | * add/remove assignees for gitlab ([c7d69e1](https://github.com/KnisterPeter/vscode-github/commit/c7d69e1)) 328 | * publish branch automatically ([68b13ba](https://github.com/KnisterPeter/vscode-github/commit/68b13ba)) 329 | * quick pick branches for pull request ([53aa27d](https://github.com/KnisterPeter/vscode-github/commit/53aa27d)), closes [#223](https://github.com/KnisterPeter/vscode-github/issues/223) 330 | 331 | 332 | 333 | 334 | # [0.21.0](https://github.com/KnisterPeter/vscode-github/compare/v0.20.2...v0.21.0) (2017-10-02) 335 | 336 | 337 | ### Features 338 | 339 | * add configuration for git executable ([d737302](https://github.com/KnisterPeter/vscode-github/commit/d737302)), closes [#210](https://github.com/KnisterPeter/vscode-github/issues/210) 340 | * allow parameters when calling commands ([293891e](https://github.com/KnisterPeter/vscode-github/commit/293891e)) 341 | * customizable statusbar ([f099c56](https://github.com/KnisterPeter/vscode-github/commit/f099c56)), closes [#202](https://github.com/KnisterPeter/vscode-github/issues/202) 342 | * prepare for pull request by command ([49c6c68](https://github.com/KnisterPeter/vscode-github/commit/49c6c68)) 343 | 344 | 345 | 346 | 347 | ## [0.20.2](https://github.com/KnisterPeter/vscode-github/compare/v0.20.1...v0.20.2) (2017-09-27) 348 | 349 | 350 | ### Bug Fixes 351 | 352 | * readd github enterpise token command ([464538c](https://github.com/KnisterPeter/vscode-github/commit/464538c)), closes [#203](https://github.com/KnisterPeter/vscode-github/issues/203) 353 | 354 | 355 | 356 | 357 | ## [0.20.1](https://github.com/KnisterPeter/vscode-github/compare/v0.20.0...v0.20.1) (2017-09-26) 358 | 359 | 360 | 361 | 362 | # [0.20.0](https://github.com/KnisterPeter/vscode-github/compare/v0.19.0...v0.20.0) (2017-09-25) 363 | 364 | 365 | ### Bug Fixes 366 | 367 | * corrected test and opened api ([c9c8a46](https://github.com/KnisterPeter/vscode-github/commit/c9c8a46)) 368 | * currect lookup of first commit on branch ([f79b2dd](https://github.com/KnisterPeter/vscode-github/commit/f79b2dd)) 369 | * extra log output ([31059c1](https://github.com/KnisterPeter/vscode-github/commit/31059c1)), closes [#114](https://github.com/KnisterPeter/vscode-github/issues/114) 370 | * protocol includes colon ([929dc8f](https://github.com/KnisterPeter/vscode-github/commit/929dc8f)) 371 | 372 | 373 | ### Features 374 | 375 | * add browse project for gitlab ([43fc258](https://github.com/KnisterPeter/vscode-github/commit/43fc258)) 376 | * add interfaces and stubs for gitlab ([802623f](https://github.com/KnisterPeter/vscode-github/commit/802623f)) 377 | * add support for http with github enterprise ([f7f9d42](https://github.com/KnisterPeter/vscode-github/commit/f7f9d42)), closes [#158](https://github.com/KnisterPeter/vscode-github/issues/158) 378 | * connect to different providers ([9f5bda1](https://github.com/KnisterPeter/vscode-github/commit/9f5bda1)) 379 | * enable gitlab support for creating merge request ([569515e](https://github.com/KnisterPeter/vscode-github/commit/569515e)) 380 | * list merge requests on a project ([7423018](https://github.com/KnisterPeter/vscode-github/commit/7423018)) 381 | * load single merge request ([84d04d6](https://github.com/KnisterPeter/vscode-github/commit/84d04d6)) 382 | * migrate token storage to support providers ([f79beb1](https://github.com/KnisterPeter/vscode-github/commit/f79beb1)) 383 | * remove command prefix for telemetry data ([34293f7](https://github.com/KnisterPeter/vscode-github/commit/34293f7)) 384 | 385 | 386 | 387 | 388 | # [0.19.0](https://github.com/KnisterPeter/vscode-github/compare/v0.18.1...v0.19.0) (2017-09-06) 389 | 390 | 391 | ### Bug Fixes 392 | 393 | * **package:** update tsdi to version 0.12.2 ([6d8fbc9](https://github.com/KnisterPeter/vscode-github/commit/6d8fbc9)) 394 | * **package:** update tsdi to version 0.12.3 ([9e3fda4](https://github.com/KnisterPeter/vscode-github/commit/9e3fda4)) 395 | * **package:** update tsdi to version 0.13.0 ([075678b](https://github.com/KnisterPeter/vscode-github/commit/075678b)) 396 | * **package:** update tsdi to version 0.14.0 ([25c102d](https://github.com/KnisterPeter/vscode-github/commit/25c102d)), closes [#170](https://github.com/KnisterPeter/vscode-github/issues/170) 397 | 398 | 399 | ### Features 400 | 401 | * add configurable status bar ([19a9e01](https://github.com/KnisterPeter/vscode-github/commit/19a9e01)), closes [#140](https://github.com/KnisterPeter/vscode-github/issues/140) 402 | * add telemetry tool ([540e7e8](https://github.com/KnisterPeter/vscode-github/commit/540e7e8)) 403 | * check the existence of git executable ([6eaa948](https://github.com/KnisterPeter/vscode-github/commit/6eaa948)) 404 | * checkout pull request by number ([a61c044](https://github.com/KnisterPeter/vscode-github/commit/a61c044)) 405 | * send a bit more telemetry data ([40c2506](https://github.com/KnisterPeter/vscode-github/commit/40c2506)) 406 | * use eager component pattern ([8e7a45f](https://github.com/KnisterPeter/vscode-github/commit/8e7a45f)) 407 | 408 | 409 | 410 | 411 | ## [0.18.1](https://github.com/KnisterPeter/vscode-github/compare/v0.18.0...v0.18.1) (2017-08-11) 412 | 413 | 414 | ### Bug Fixes 415 | 416 | * use proper remote name default ([7dec182](https://github.com/KnisterPeter/vscode-github/commit/7dec182)) 417 | 418 | 419 | 420 | 421 | # [0.18.0](https://github.com/KnisterPeter/vscode-github/compare/v0.17.2...v0.18.0) (2017-08-10) 422 | 423 | 424 | ### Bug Fixes 425 | 426 | * **package:** update execa to version 0.8.0 ([6f59339](https://github.com/KnisterPeter/vscode-github/commit/6f59339)) 427 | * do not error in empty workspaces ([5b1769c](https://github.com/KnisterPeter/vscode-github/commit/5b1769c)), closes [#142](https://github.com/KnisterPeter/vscode-github/issues/142) 428 | 429 | 430 | ### Features 431 | 432 | * allow to configure remote name ([04006da](https://github.com/KnisterPeter/vscode-github/commit/04006da)), closes [#145](https://github.com/KnisterPeter/vscode-github/issues/145) 433 | 434 | 435 | 436 | 437 | ## [0.17.2](https://github.com/KnisterPeter/vscode-github/compare/v0.17.1...v0.17.2) (2017-07-27) 438 | 439 | 440 | ### Bug Fixes 441 | 442 | * eager activation of extension commands ([638bb75](https://github.com/KnisterPeter/vscode-github/commit/638bb75)), closes [#136](https://github.com/KnisterPeter/vscode-github/issues/136) 443 | 444 | 445 | 446 | 447 | ## [0.17.1](https://github.com/KnisterPeter/vscode-github/compare/v0.17.0...v0.17.1) (2017-07-14) 448 | 449 | 450 | ### Bug Fixes 451 | 452 | * update for vscode 1.14 ([c99b103](https://github.com/KnisterPeter/vscode-github/commit/c99b103)) 453 | 454 | 455 | 456 | 457 | # [0.17.0](https://github.com/KnisterPeter/vscode-github/compare/v0.16.2...v0.17.0) (2017-06-21) 458 | 459 | 460 | ### Bug Fixes 461 | 462 | * activate extension by setup commands ([9eeeaa5](https://github.com/KnisterPeter/vscode-github/commit/9eeeaa5)), closes [#126](https://github.com/KnisterPeter/vscode-github/issues/126) 463 | 464 | 465 | ### Features 466 | 467 | * add action to status bar ([043e504](https://github.com/KnisterPeter/vscode-github/commit/043e504)), closes [#104](https://github.com/KnisterPeter/vscode-github/issues/104) 468 | * allow to select branch for pull request ([a7b720f](https://github.com/KnisterPeter/vscode-github/commit/a7b720f)), closes [#125](https://github.com/KnisterPeter/vscode-github/issues/125) 469 | * extend github api ([094feaa](https://github.com/KnisterPeter/vscode-github/commit/094feaa)) 470 | * fetching comments of a pull request ([58efb4b](https://github.com/KnisterPeter/vscode-github/commit/58efb4b)) 471 | 472 | 473 | 474 | 475 | ## [0.16.2](https://github.com/KnisterPeter/vscode-github/compare/v0.16.1...v0.16.2) (2017-06-12) 476 | 477 | 478 | ### Bug Fixes 479 | 480 | * **package:** update execa to version 0.7.0 ([dea0f57](https://github.com/KnisterPeter/vscode-github/commit/dea0f57)) 481 | 482 | 483 | ### Features 484 | 485 | * error logging ([54d95f4](https://github.com/KnisterPeter/vscode-github/commit/54d95f4)) 486 | 487 | 488 | 489 | 490 | ## [0.16.1](https://github.com/KnisterPeter/vscode-github/compare/v0.16.0...v0.16.1) (2017-06-07) 491 | 492 | 493 | ### Bug Fixes 494 | 495 | * unclickable status bar buttons ([961914a](https://github.com/KnisterPeter/vscode-github/commit/961914a)) 496 | 497 | 498 | 499 | 500 | # [0.16.0](https://github.com/KnisterPeter/vscode-github/compare/v0.15.1...v0.16.0) (2017-05-22) 501 | 502 | 503 | ### Bug Fixes 504 | 505 | * issue order should be latest change on top ([0aba3f1](https://github.com/KnisterPeter/vscode-github/commit/0aba3f1)) 506 | * parsing git url ([0af2d3e](https://github.com/KnisterPeter/vscode-github/commit/0af2d3e)), closes [#107](https://github.com/KnisterPeter/vscode-github/issues/107) 507 | 508 | 509 | ### Features 510 | 511 | * browsing current file ([710a032](https://github.com/KnisterPeter/vscode-github/commit/710a032)), closes [#106](https://github.com/KnisterPeter/vscode-github/issues/106) 512 | 513 | 514 | 515 | 516 | ## [0.15.1](https://github.com/KnisterPeter/vscode-github/compare/v0.15.0...v0.15.1) (2017-05-07) 517 | 518 | 519 | ### Bug Fixes 520 | 521 | * respect protcol in hostnames ([149b430](https://github.com/KnisterPeter/vscode-github/commit/149b430)), closes [#100](https://github.com/KnisterPeter/vscode-github/issues/100) 522 | 523 | 524 | 525 | 526 | # [0.15.0](https://github.com/KnisterPeter/vscode-github/compare/v0.14.0...v0.15.0) (2017-05-06) 527 | 528 | 529 | ### Features 530 | 531 | * browse open issues ([ab1bff8](https://github.com/KnisterPeter/vscode-github/commit/ab1bff8)), closes [#32](https://github.com/KnisterPeter/vscode-github/issues/32) 532 | 533 | 534 | 535 | 536 | # [0.14.0](https://github.com/KnisterPeter/vscode-github/compare/v0.13.0...v0.14.0) (2017-05-06) 537 | 538 | 539 | ### Features 540 | 541 | * add multi token support ([0cc8bd6](https://github.com/KnisterPeter/vscode-github/commit/0cc8bd6)) 542 | * github enterprise support ([94d1cf9](https://github.com/KnisterPeter/vscode-github/commit/94d1cf9)), closes [#59](https://github.com/KnisterPeter/vscode-github/issues/59) 543 | 544 | 545 | 546 | 547 | # [0.13.0](https://github.com/KnisterPeter/vscode-github/compare/v0.12.1...v0.13.0) (2017-05-05) 548 | 549 | 550 | ### Features 551 | 552 | * add vscode progress-ui to long running operations ([7b26790](https://github.com/KnisterPeter/vscode-github/commit/7b26790)), closes [#94](https://github.com/KnisterPeter/vscode-github/issues/94) 553 | * open pull request of current branch ([3d15385](https://github.com/KnisterPeter/vscode-github/commit/3d15385)), closes [#91](https://github.com/KnisterPeter/vscode-github/issues/91) 554 | * use vscode git extension to checkout treeish ([3998f50](https://github.com/KnisterPeter/vscode-github/commit/3998f50)), closes [#18](https://github.com/KnisterPeter/vscode-github/issues/18) 555 | 556 | 557 | 558 | 559 | ## [0.12.1](https://github.com/KnisterPeter/vscode-github/compare/v0.12.0...v0.12.1) (2017-03-29) 560 | 561 | 562 | ### Bug Fixes 563 | 564 | * handle local only branches ([619522e](https://github.com/KnisterPeter/vscode-github/commit/619522e)), closes [#61](https://github.com/KnisterPeter/vscode-github/issues/61) 565 | 566 | 567 | 568 | 569 | # [0.12.0](https://github.com/KnisterPeter/vscode-github/compare/v0.11.0...v0.12.0) (2017-03-28) 570 | 571 | 572 | ### Features 573 | 574 | * **vscode-github:** allow to manage PR assignees ([2229bf2](https://github.com/KnisterPeter/vscode-github/commit/2229bf2)), closes [#71](https://github.com/KnisterPeter/vscode-github/issues/71) 575 | * **vscode-github:** manage pull request reviews ([9ac2a9e](https://github.com/KnisterPeter/vscode-github/commit/9ac2a9e)), closes [#83](https://github.com/KnisterPeter/vscode-github/issues/83) 576 | 577 | 578 | 579 | 580 | # [0.11.0](https://github.com/KnisterPeter/vscode-github/compare/v0.10.0...v0.11.0) (2017-03-22) 581 | 582 | 583 | ### Bug Fixes 584 | 585 | * update dependencies ([af38214](https://github.com/KnisterPeter/vscode-github/commit/af38214)) 586 | * **package:** update execa to version 0.6.1 ([c371fa7](https://github.com/KnisterPeter/vscode-github/commit/c371fa7)) 587 | * **package:** update execa to version 0.6.2 ([8020305](https://github.com/KnisterPeter/vscode-github/commit/8020305)) 588 | * **package:** update execa to version 0.6.3 ([6513ee5](https://github.com/KnisterPeter/vscode-github/commit/6513ee5)) 589 | * **vscode-github:** update pretend ([e775b9e](https://github.com/KnisterPeter/vscode-github/commit/e775b9e)) 590 | 591 | 592 | ### Features 593 | 594 | * **vscode-github:** add command to open github page ([b409cd2](https://github.com/KnisterPeter/vscode-github/commit/b409cd2)), closes [#70](https://github.com/KnisterPeter/vscode-github/issues/70) 595 | * **vscode-github:** allow to open pull request ([b59cab6](https://github.com/KnisterPeter/vscode-github/commit/b59cab6)), closes [#69](https://github.com/KnisterPeter/vscode-github/issues/69) 596 | 597 | 598 | 599 | 600 | # [0.10.0](https://github.com/KnisterPeter/vscode-github/compare/v0.9.1...v0.10.0) (2017-02-15) 601 | 602 | 603 | ### Features 604 | 605 | * create PR for fork ([d1e3794](https://github.com/KnisterPeter/vscode-github/commit/d1e3794)) 606 | * select upstream repo and create PR ([1e4fb30](https://github.com/KnisterPeter/vscode-github/commit/1e4fb30)) 607 | 608 | 609 | 610 | 611 | ## [0.9.1](https://github.com/KnisterPeter/vscode-github/compare/v0.9.0...v0.9.1) (2017-02-10) 612 | 613 | 614 | 615 | 616 | 617 | # [0.9.0](https://github.com/KnisterPeter/vscode-github/compare/v0.8.0...v0.9.0) (2017-01-09) 618 | 619 | 620 | ### Bug Fixes 621 | 622 | * **package:** update execa to version 0.5.1 ([#49](https://github.com/KnisterPeter/vscode-github/issues/49)) ([1e3aec1](https://github.com/KnisterPeter/vscode-github/commit/1e3aec1)) 623 | * **package:** update execa to version 0.6.0 ([#50](https://github.com/KnisterPeter/vscode-github/issues/50)) ([8a70e5c](https://github.com/KnisterPeter/vscode-github/commit/8a70e5c)) 624 | 625 | 626 | ### Features 627 | 628 | * add pull request body ([#39](https://github.com/KnisterPeter/vscode-github/issues/39)) ([af07d40](https://github.com/KnisterPeter/vscode-github/commit/af07d40)) 629 | * allow pr body input via dialog or git editor ([#48](https://github.com/KnisterPeter/vscode-github/issues/48)) ([f3e126e](https://github.com/KnisterPeter/vscode-github/commit/f3e126e)) 630 | 631 | 632 | 633 | 634 | # [0.8.0](https://github.com/KnisterPeter/vscode-github/compare/v0.7.1...v0.8.0) (2016-11-29) 635 | 636 | 637 | ### Features 638 | 639 | * allow setting of upstream repo (#35) ([418f9c4](https://github.com/KnisterPeter/vscode-github/commit/418f9c4)), closes [#31](https://github.com/KnisterPeter/vscode-github/issues/31) 640 | 641 | 642 | 643 | 644 | ## [0.7.1](https://github.com/KnisterPeter/vscode-github/compare/v0.7.0...v0.7.1) (2016-11-29) 645 | 646 | 647 | ### Bug Fixes 648 | 649 | * correctly obtain Git remote and parse it (#38) ([adb243b](https://github.com/KnisterPeter/vscode-github/commit/adb243b)) 650 | 651 | 652 | 653 | 654 | # [0.7.0](https://github.com/KnisterPeter/vscode-github/compare/v0.6.0...v0.7.0) (2016-11-18) 655 | 656 | 657 | ### Bug Fixes 658 | 659 | * do not create pull request on default branch ([d6e1a8a](https://github.com/KnisterPeter/vscode-github/commit/d6e1a8a)) 660 | * leverage github default branch ([#22](https://github.com/KnisterPeter/vscode-github/issues/22)) ([693da3f](https://github.com/KnisterPeter/vscode-github/commit/693da3f)) 661 | 662 | 663 | ### Features 664 | 665 | * display only enabled merge methods ([#23](https://github.com/KnisterPeter/vscode-github/issues/23)) ([b1ef991](https://github.com/KnisterPeter/vscode-github/commit/b1ef991)) 666 | 667 | 668 | 669 | 670 | # [0.6.0](https://github.com/KnisterPeter/vscode-github/compare/v0.5.0...v0.6.0) (2016-11-17) 671 | 672 | 673 | ### Bug Fixes 674 | 675 | * handling of headers ([589ba74](https://github.com/KnisterPeter/vscode-github/commit/589ba74)) 676 | 677 | ### Features 678 | 679 | * add configuration for default branch ([12867be](https://github.com/KnisterPeter/vscode-github/commit/12867be)) 680 | * add configuration for prefered merge method ([5b826a4](https://github.com/KnisterPeter/vscode-github/commit/5b826a4)) 681 | * add configuration for refresh interval ([911fbd6](https://github.com/KnisterPeter/vscode-github/commit/911fbd6)) 682 | * enable support for squash and rebase merges ([8409d61](https://github.com/KnisterPeter/vscode-github/commit/8409d61)) 683 | 684 | 685 | 686 | 687 | # [0.5.0](https://github.com/KnisterPeter/vscode-github/compare/v0.4.0...v0.5.0) (2016-11-15) 688 | 689 | 690 | ### Features 691 | 692 | * add merge button (currently no squash and rebase) ([12ead05](https://github.com/KnisterPeter/vscode-github/commit/12ead05)) 693 | 694 | 695 | 696 | 697 | # [0.4.0](https://github.com/KnisterPeter/vscode-github/compare/v0.3.0...v0.4.0) (2016-11-15) 698 | 699 | 700 | ### Features 701 | 702 | * add information message on first startup ([f336239](https://github.com/KnisterPeter/vscode-github/commit/f336239)) 703 | 704 | 705 | 706 | 707 | # [0.3.0](https://github.com/KnisterPeter/vscode-github/compare/v0.2.2...v0.3.0) (2016-11-15) 708 | 709 | 710 | ### Bug Fixes 711 | 712 | * **vscode-github:** detect current branch (#14) ([7c0724b](https://github.com/KnisterPeter/vscode-github/commit/7c0724b)) 713 | * **vscode-github:** show status bar only if github project (#10) ([42f6abb](https://github.com/KnisterPeter/vscode-github/commit/42f6abb)) 714 | * **vscode-github:** update command texts ([8649cc0](https://github.com/KnisterPeter/vscode-github/commit/8649cc0)) 715 | 716 | ### Features 717 | 718 | * **vscode-github:** add status-bar icon for pull-request (#9) ([260cf5f](https://github.com/KnisterPeter/vscode-github/commit/260cf5f)) 719 | * **vscode-github:** allow https-origin for github detection (#16) ([e764751](https://github.com/KnisterPeter/vscode-github/commit/e764751)) 720 | * **vscode-github:** lazy initialize extension (#7) ([12cd5b7](https://github.com/KnisterPeter/vscode-github/commit/12cd5b7)) 721 | * **vscode-github:** refresh pull-request status every 5 seconds (#15) ([51a81b1](https://github.com/KnisterPeter/vscode-github/commit/51a81b1)) 722 | * **vscode-github:** respect etag header from github (#12) ([62f1e54](https://github.com/KnisterPeter/vscode-github/commit/62f1e54)) 723 | * **vscode-github:** show notification on success (#8) ([4359f6c](https://github.com/KnisterPeter/vscode-github/commit/4359f6c)) 724 | * **vscode-github:** show status of current pull request (#17) ([18f9535](https://github.com/KnisterPeter/vscode-github/commit/18f9535)) 725 | 726 | 727 | 728 | 729 | ## [0.2.2](https://github.com/KnisterPeter/vscode-github/compare/v0.2.1...v0.2.2) (2016-11-09) 730 | 731 | 732 | 733 | 734 | 735 | ## [0.2.1](https://github.com/KnisterPeter/vscode-github/compare/v0.2.0...v0.2.1) (2016-11-09) 736 | 737 | 738 | 739 | 740 | 741 | # [0.2.0](https://github.com/KnisterPeter/vscode-github/compare/v0.1.1...v0.2.0) (2016-11-09) 742 | 743 | 744 | ### Bug Fixes 745 | 746 | * **vscode-github:** current branch detection ([719e908](https://github.com/KnisterPeter/vscode-github/commit/719e908)) 747 | * **vscode-github:** stabilized input of personal access token (#3) ([f7735fc](https://github.com/KnisterPeter/vscode-github/commit/f7735fc)) 748 | 749 | ### Features 750 | 751 | * **vscode-github:** add debug/error channel ([9c20ffe](https://github.com/KnisterPeter/vscode-github/commit/9c20ffe)) 752 | 753 | 754 | 755 | 756 | ## [0.1.1](https://github.com/knisterpeter/vscode-github/compare/v0.1.0...v0.1.1) (2016-11-09) 757 | 758 | 759 | 760 | 761 | 762 | # 0.1.0 (2016-11-09) 763 | 764 | 765 | ### Features 766 | 767 | * **vscode-github:** create pull-requests and checkout pull-requests ([d350488](https://github.com/knisterpeter/vscode-github/commit/d350488)) 768 | * **vscode-github:** list pull-requests ([db195b7](https://github.com/knisterpeter/vscode-github/commit/db195b7)) 769 | * **vscode-github:** select a pull-request ([3574016](https://github.com/knisterpeter/vscode-github/commit/3574016)) 770 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Markus Wolf 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 | # vscode-github README 2 | 3 | [![Marketplace Version](https://vsmarketplacebadge.apphb.com/version/knisterpeter.vscode-github.svg)](https://marketplace.visualstudio.com/items?itemName=KnisterPeter.vscode-github) 4 | [![Installs](https://vsmarketplacebadge.apphb.com/installs/knisterpeter.vscode-github.svg)](https://marketplace.visualstudio.com/items?itemName=KnisterPeter.vscode-github) 5 | [![Travis](https://img.shields.io/travis/KnisterPeter/vscode-github.svg)](https://travis-ci.org/KnisterPeter/vscode-github) 6 | [![renovate badge](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovateapp.com/) 7 | 8 | This vscode extension integrates with GitHub. 9 | 10 | **Note: I recommend to use [GitHub Pull Requests](https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github) instead of this, because most usecases are supported and there is a team at Microsoft/GitHub supporting development** 11 | 12 | ## Features 13 | 14 | Currently it is possible to do the following: 15 | 16 | - Checkout one of the open pull requests 17 | - Open github page for the current project in your default browser 18 | - Browse one of the open pull requests in your default browser 19 | - Browse the pull requests of your current branch 20 | - Display pull request and current status (e.g. mergeable, travis build done, ...) in the StatusBar (if enabled) 21 | - Create a new pull request based on the current branch and the last commit 22 | The current branch will be requested to merge into master and the pull request title is the commit message summary, or a custom message if configured that way. 23 | - Create a pull request in forked repositories 24 | - Allow to select a branch to create a pull request for 25 | - Merge current pull request with either of 'merge', 'squash' or 'rebase' method. 26 | - Configure default branch, merge method and refresh interval. 27 | - Allow to manage assignees for pull requests 28 | - Assign or unassign a user (currently only one) 29 | - Allow to create and cancel pull request reviews 30 | - Support for GitHub Enterprise (on-premise installations) 31 | - Browse open issues 32 | - Browse the current open file (including current cursor position) 33 | - Configure the statusbar behaviour by setting the `github.statusBarCommand` configuration value. 34 | - Specify a GitLab private access token and connect to a GitLab server 35 | - Support multi folder setup 36 | 37 | ![Create pull request](images/create-pull-request.png) 38 | 39 | ## Setup Personal Access Token 40 | 41 | To use this extension one needs to create a new GitHub Personal Access Token and registers it in the extension. 42 | The 'GitHub: Set Personal Access Token' should be executed for that. 43 | To execute the 'GitHub: Set Personal Access Token' type Ctrl+Shift+p in VSCode to open the command palette and type 'GitHub: Set Personal Access Token'. You will then be prompted to enter the token generated from GitHub. 44 | 45 | ![GitHub Personal Access Token](images/github-personal-access-token.png) 46 | 47 | ![GitHub Personal Access Token](images/github-personal-access-token2.png) 48 | 49 | ![Set GitHub Personal Access Token](images/set-personal-access-token.png) 50 | 51 | Additionally, by default this extension assumes your remote for a checked out repo is named "origin". If 52 | you wish to use a remote with a different name, you can control this by the `github.remoteName` setting. 53 | 54 | There are additional settings for this extension as well, enter `github.` in the User Settings pane of 55 | VS Code to see them all. 56 | 57 | ## Usage 58 | 59 | ### Create a new pull request 60 | 61 | 1. Create a new local branch from the commit you wanted to start developing with 62 | 1. Do you code changes 63 | 1. Commit your changes 64 | 1. Push your changes to your remote 65 | 1. Then execute `Create pull request from current branch in current repository (quick)` 66 | 1. In the status bar you can then see the status of your created pull request and if you'd like to open it 67 | 68 | ### Create a new pull request from a forked repository 69 | 70 | 1. Fork a repository and clone it afterwards 71 | 1. Create a new local branch from the commit you wanted to start developing with 72 | 1. Do you code changes 73 | 1. Commit your changes 74 | 1. Push your changes to your remote 75 | 1. Then execute `Create pull request...` 76 | 1. Select the upstream repository you want to create the pull requests for 77 | **Note**: The status bar will not reflect the pull request status in this case 78 | 79 | ### Checkout pull request 80 | 81 | 1. Execute `Checkout open pull request...` 82 | 1. Select a pull request from the list 83 | 1. The pull request is checked out and your working copy switches to that branch 84 | 85 | ### Browser pull request 86 | 87 | 1. Execute `Browse open pull request...` 88 | 1. Select a pull request from the list 89 | 1. Your default browser opens the pull request on github 90 | 91 | ### Merge pull request 92 | 93 | 1. Execute `Merge pull request (current branch)...` 94 | 1. Select your merge strategy from the shown list (merge, squash, rebase) 95 | 1. The pull request associated with your current branch is then merged 96 | 97 | ### Telemetry data (extension usage) 98 | 99 | This extension collects telemetry data to track and improve usage. 100 | The collection of data could be disabled as described here . 101 | 102 | ## Contributors ✨ 103 | 104 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 |

Markus Wolf

💻

Ross Valler

💻

Josh Eveleth

💻

sasial

💻

timhaintz

📖

Adam Parkin

📖

Andreas Holley

💻

Stanislas

📖

Corentin Girard

💻

Mykola Morhun

💻

Nicholas Mata

💻

Tatsuyuki Ishi

💻

Vincent van der Weele

💻

ivanmilov

💻

Kyle

📖

Li Huanshuai

💻

Shaikat Haque

💻
134 | 135 | 136 | 137 | 138 | 139 | 140 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 141 | -------------------------------------------------------------------------------- /images/create-pull-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnisterPeter/vscode-github/8bb803966ebd2959eca7ecf3fc3d917d3aaa41fd/images/create-pull-request.png -------------------------------------------------------------------------------- /images/github-personal-access-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnisterPeter/vscode-github/8bb803966ebd2959eca7ecf3fc3d917d3aaa41fd/images/github-personal-access-token.png -------------------------------------------------------------------------------- /images/github-personal-access-token2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnisterPeter/vscode-github/8bb803966ebd2959eca7ecf3fc3d917d3aaa41fd/images/github-personal-access-token2.png -------------------------------------------------------------------------------- /images/octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnisterPeter/vscode-github/8bb803966ebd2959eca7ecf3fc3d917d3aaa41fd/images/octocat.png -------------------------------------------------------------------------------- /images/set-personal-access-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnisterPeter/vscode-github/8bb803966ebd2959eca7ecf3fc3d917d3aaa41fd/images/set-personal-access-token.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-github", 3 | "displayName": "GitHub", 4 | "description": "Integrates github and its workflows into vscode", 5 | "icon": "images/octocat.png", 6 | "galleryBanner": { 7 | "color": "#dddddd", 8 | "theme": "light" 9 | }, 10 | "version": "0.30.7", 11 | "publisher": "KnisterPeter", 12 | "engines": { 13 | "vscode": "^1.18.0" 14 | }, 15 | "categories": [ 16 | "Other" 17 | ], 18 | "keywords": [ 19 | "git", 20 | "github", 21 | "gitlab", 22 | "pull request" 23 | ], 24 | "activationEvents": [ 25 | "*" 26 | ], 27 | "main": "./out/src/main", 28 | "contributes": { 29 | "configuration": { 30 | "title": "GitHub configuration", 31 | "type": "object", 32 | "properties": { 33 | "github.gitCommand": { 34 | "type": "string", 35 | "description": "Path to the git executable. If it is in the OS path, this should not be necessary to set.", 36 | "scope": "window" 37 | }, 38 | "github.preferedMergeMethod": { 39 | "type": "string", 40 | "description": "By default the extension asks the user during merge process. This could be set to on of 'merge', 'squash' or 'rebase' to define the prefered method and not to be asked every time.", 41 | "scope": "resource" 42 | }, 43 | "github.remoteName": { 44 | "type": "string", 45 | "description": "Defines the name of the git remote. Defaults to undefined which automatically tries to determine the proper remote name.", 46 | "scope": "resource" 47 | }, 48 | "github.upstream": { 49 | "type": "string", 50 | "description": "By default the extension get the repository and user from .git/config. For forks where upstream is a different repository this could be configured here (e.g. microsoft/typescript).", 51 | "scope": "resource" 52 | }, 53 | "github.customPullRequestTitle": { 54 | "type": "boolean", 55 | "default": false, 56 | "description": "Ask the user for a custom title when creating the PR. If false, defaults to the last commit message.", 57 | "scope": "resource" 58 | }, 59 | "github.customPullRequestDescription": { 60 | "type": "string", 61 | "enum": [ 62 | "off", 63 | "singleLine", 64 | "gitEditor" 65 | ], 66 | "default": "off", 67 | "description": "By default the pull request description is the first commit message. When this property is set, the user is asked for a description when creating the PR. This can be a single-line description via an input dialog ('singleLine') or a multi-line markdown description via the editor configured in git ('gitEditor').", 68 | "scope": "resource" 69 | }, 70 | "github.autoPublish": { 71 | "type": "boolean", 72 | "description": "Automatically publish the current branch before creating a pull request. Defaults to false", 73 | "default": false, 74 | "scope": "resource" 75 | }, 76 | "github.allowUnsafeSSL": { 77 | "type": "boolean", 78 | "description": "Allow SSL connnection with unauthorized self-signed certificates. Defaults to false", 79 | "default": false, 80 | "scope": "resource" 81 | }, 82 | "github.statusbar.enabled": { 83 | "type": "boolean", 84 | "description": "True if the statusbar integration should be enabled. Defaults to true", 85 | "default": true, 86 | "scope": "window" 87 | }, 88 | "github.statusbar.command": { 89 | "type": [ 90 | "string", 91 | "null" 92 | ], 93 | "description": "Defines which command is executed when clicking the status bar item. Default behaviour is the create-pull-request-then-merge cycle (different command based on state). For example to open the current pull request set this to 'vscode-github.browserSimplePullRequest'", 94 | "default": null, 95 | "scope": "window" 96 | }, 97 | "github.statusbar.refresh": { 98 | "type": "number", 99 | "default": 5, 100 | "description": "Interval in seconds to refresh the current pull request status.", 101 | "scope": "window" 102 | }, 103 | "github.statusbar.color": { 104 | "type": "boolean", 105 | "default": true, 106 | "description": "Enables or disables the coloring of the status bar based on the mergeable state.", 107 | "scope": "window" 108 | }, 109 | "github.statusbar.successText": { 110 | "type": "string", 111 | "description": "Overrides the default success text.", 112 | "scope": "window" 113 | }, 114 | "github.statusbar.pendingText": { 115 | "type": "string", 116 | "description": "Overrides the default pending text.", 117 | "scope": "window" 118 | }, 119 | "github.statusbar.failureText": { 120 | "type": "string", 121 | "description": "Overrides the default failure text.", 122 | "scope": "window" 123 | }, 124 | "gitlab.removeSourceBranch": { 125 | "type": "boolean", 126 | "description": "GitLab setting to 'remove_source_branch' per MergeRequest.", 127 | "default": false, 128 | "scope": "resource" 129 | } 130 | } 131 | }, 132 | "commands": [ 133 | { 134 | "command": "vscode-github.setGitHubToken", 135 | "title": "Set Personal Access Token...", 136 | "category": "GitHub" 137 | }, 138 | { 139 | "command": "vscode-github.setGitHubEnterpriseToken", 140 | "title": "Setup GitHub Enterprise Token...", 141 | "category": "GitHub" 142 | }, 143 | { 144 | "command": "vscode-github.setGitlabToken", 145 | "title": "Setup Gitlab Token...", 146 | "category": "GitHub" 147 | }, 148 | { 149 | "command": "vscode-github.clearToken", 150 | "title": "Remove Token...", 151 | "category": "GitHub" 152 | }, 153 | { 154 | "command": "vscode-github.browseProject", 155 | "title": "Browse project", 156 | "category": "GitHub" 157 | }, 158 | { 159 | "command": "vscode-github.createPullRequest", 160 | "title": "Create pull request...", 161 | "category": "GitHub" 162 | }, 163 | { 164 | "command": "vscode-github.createSimplePullRequest", 165 | "title": "Create pull request from current branch in current repository (quick)", 166 | "category": "GitHub" 167 | }, 168 | { 169 | "command": "vscode-github.checkoutPullRequests", 170 | "title": "Checkout open pull request...", 171 | "category": "GitHub" 172 | }, 173 | { 174 | "command": "vscode-github.browserSimplePullRequest", 175 | "title": "Browse pull request of current branch", 176 | "category": "GitHub" 177 | }, 178 | { 179 | "command": "vscode-github.browserPullRequest", 180 | "title": "Browse open pull request...", 181 | "category": "GitHub" 182 | }, 183 | { 184 | "command": "vscode-github.mergePullRequest", 185 | "title": "Merge pull request (current branch)...", 186 | "category": "GitHub" 187 | }, 188 | { 189 | "command": "vscode-github.addAssignee", 190 | "title": "Add assignee to pull request...", 191 | "category": "GitHub" 192 | }, 193 | { 194 | "command": "vscode-github.removeAssignee", 195 | "title": "Remove assignee from pull request...", 196 | "category": "GitHub" 197 | }, 198 | { 199 | "command": "vscode-github.requestReview", 200 | "title": "Request a review for the current pull request...", 201 | "category": "GitHub" 202 | }, 203 | { 204 | "command": "vscode-github.deleteReviewRequest", 205 | "title": "Cancel a pull request review...", 206 | "category": "GitHub" 207 | }, 208 | { 209 | "command": "vscode-github.browseOpenIssue", 210 | "title": "Browse open issue...", 211 | "category": "GitHub" 212 | }, 213 | { 214 | "command": "vscode-github.browseCurrentFile", 215 | "title": "Browse current file", 216 | "category": "GitHub" 217 | } 218 | ] 219 | }, 220 | "scripts": { 221 | "linter": "tslint --project ./tsconfig.json", 222 | "pretest": "rm -rf /tmp/test-workspace && mkdir /tmp/test-workspace && cd /tmp/test-workspace && git init && cd -", 223 | "test": "CODE_TESTS_WORKSPACE=/tmp/test-workspace node ./node_modules/vscode/bin/test", 224 | "vscode:prepublish": "npm run compile", 225 | "compile": "tsc -p ./", 226 | "watch": "tsc -watch -p ./", 227 | "postinstall": "node ./node_modules/vscode/bin/install", 228 | "prerelease": "git checkout master && git pull origin master && npm run vscode:prepublish", 229 | "release": "standard-version", 230 | "postrelease": "git push --follow-tags origin master", 231 | "publish-extension": "vsce publish $(node -e \"console.log(require('./package.json').version)\")" 232 | }, 233 | "devDependencies": { 234 | "@knisterpeter/standard-tslint": "1.7.2", 235 | "@types/common-tags": "1.8.1", 236 | "@types/lru-cache": "5.1.1", 237 | "@types/mocha": "10.0.1", 238 | "@types/node": "18.11.17", 239 | "conventional-changelog-lint": "2.1.1", 240 | "cz-customizable": "7.0.0", 241 | "husky": "8.0.2", 242 | "mocha": "10.2.0", 243 | "standard-version": "9.5.0", 244 | "tslint": "6.1.3", 245 | "typescript": "4.9.4", 246 | "vscode": "1.1.37" 247 | }, 248 | "dependencies": { 249 | "common-tags": "1.8.2", 250 | "conventional-changelog-lint-config-angular": "1.0.0", 251 | "decko": "1.2.0", 252 | "execa": "5.1.1", 253 | "https": "1.0.0", 254 | "isomorphic-fetch": "3.0.0", 255 | "lru-cache": "7.4.0", 256 | "pretend": "3.1.1", 257 | "sander": "0.6.0", 258 | "tsdi": "0.25.0", 259 | "vscode-extension-telemetry": "0.4.5" 260 | }, 261 | "repository": { 262 | "type": "git", 263 | "url": "https://github.com/KnisterPeter/vscode-github.git" 264 | }, 265 | "author": { 266 | "name": "Markus Wolf", 267 | "email": "knister.peter@shadowrun-clan.de" 268 | }, 269 | "license": "MIT", 270 | "bugs": { 271 | "url": "https://github.com/KnisterPeter/vscode-github/issues" 272 | }, 273 | "homepage": "https://github.com/KnisterPeter/vscode-github", 274 | "config": { 275 | "commitizen": { 276 | "path": "./node_modules/cz-customizable" 277 | }, 278 | "cz-customizable": { 279 | "config": "./.cz-config.js" 280 | } 281 | }, 282 | "prettier": { 283 | "singleQuote": true 284 | }, 285 | "husky": { 286 | "hooks": { 287 | "commit-msg": "conventional-changelog-lint -e" 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/command-manager.ts: -------------------------------------------------------------------------------- 1 | import { TSDI, LifecycleListener, component, inject, initialize } from 'tsdi'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { Command } from './command'; 5 | import './commands/browse'; 6 | import './commands/pull-requests'; 7 | import './commands/token'; 8 | import './commands/user'; 9 | 10 | @component 11 | export class CommandManager implements LifecycleListener { 12 | 13 | @inject 14 | private readonly tsdi!: TSDI; 15 | 16 | @inject('vscode.ExtensionContext') 17 | private readonly context!: vscode.ExtensionContext; 18 | 19 | @initialize 20 | protected init(): void { 21 | this.tsdi.addLifecycleListener(this); 22 | } 23 | 24 | public onCreate(component: any): void { 25 | if (component instanceof Command) { 26 | this.context.subscriptions.push( 27 | vscode.commands.registerCommand(component.id, (...args: any[]) => component.run(...args)) 28 | ); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/command.ts: -------------------------------------------------------------------------------- 1 | import { inject } from 'tsdi'; 2 | import * as vscode from 'vscode'; 3 | import TelemetryReporter from 'vscode-extension-telemetry'; 4 | 5 | import { GitHubError } from './provider/github/api'; 6 | import { WorkflowManager } from './workflow-manager'; 7 | 8 | export abstract class Command { 9 | @inject private readonly reporter!: TelemetryReporter; 10 | 11 | public abstract get id(): string; 12 | 13 | public abstract run(...args: any[]): void; 14 | 15 | protected track(message: string): void { 16 | const properties = { 17 | id: this.id.replace('vscode-github.', ''), 18 | message, 19 | }; 20 | this.reporter.sendTelemetryEvent('vscode-github.command', properties); 21 | } 22 | 23 | protected async getProjectFolder(): Promise { 24 | const folders = vscode.workspace.workspaceFolders; 25 | if (!folders) { 26 | return undefined; 27 | } 28 | if (folders.length === 1) { 29 | return folders[0].uri; 30 | } 31 | const folder = await vscode.window.showWorkspaceFolderPick(); 32 | if (!folder) { 33 | return undefined; 34 | } 35 | return folder.uri; 36 | } 37 | } 38 | 39 | export abstract class TokenCommand extends Command { 40 | @inject protected readonly workflowManager!: WorkflowManager; 41 | 42 | @inject('vscode.OutputChannel') 43 | private readonly channel!: vscode.OutputChannel; 44 | 45 | protected requireProjectFolder = true; 46 | 47 | protected uri?: vscode.Uri; 48 | 49 | public async run(...args: any[]): Promise { 50 | if (this.requireProjectFolder) { 51 | const uri = await this.getProjectFolder(); 52 | if (!uri) { 53 | return; 54 | } 55 | this.uri = uri; 56 | if ( 57 | !this.workflowManager || 58 | !Boolean(await this.workflowManager.canConnect(this.uri)) 59 | ) { 60 | this.track('execute without token'); 61 | vscode.window.showWarningMessage( 62 | 'Please setup your Github Personal Access Token ' + 63 | 'and open a GitHub project in your workspace' 64 | ); 65 | return; 66 | } 67 | } 68 | try { 69 | this.track('execute'); 70 | try { 71 | await this.runWithToken(...args); 72 | } catch (e) { 73 | this.logAndShowError(e as Error); 74 | } 75 | } finally { 76 | this.uri = undefined as any; 77 | } 78 | } 79 | 80 | protected abstract runWithToken(...args: any[]): Promise; 81 | 82 | private logAndShowError(e: Error): void { 83 | this.track('failed'); 84 | if (this.channel) { 85 | this.channel.appendLine(e.message); 86 | if (e.stack) { 87 | e.stack.split('\n').forEach((line) => this.channel.appendLine(line)); 88 | } 89 | } 90 | if (e instanceof GitHubError) { 91 | console.error(e.response); 92 | vscode.window.showErrorMessage('GitHub error: ' + e.message); 93 | } else { 94 | console.error(e); 95 | vscode.window.showErrorMessage('Error: ' + e.message); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/commands/browse.ts: -------------------------------------------------------------------------------- 1 | import { component, inject } from 'tsdi'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { TokenCommand } from '../command'; 5 | import { Git } from '../git'; 6 | import { showProgress } from '../helper'; 7 | 8 | @component({eager: true}) 9 | export class BrowseProject extends TokenCommand { 10 | 11 | public id = 'vscode-github.browseProject'; 12 | 13 | @showProgress 14 | protected async runWithToken(): Promise { 15 | if (!this.uri) { 16 | throw new Error('uri is undefined'); 17 | } 18 | const url = await this.workflowManager.getRepositoryUrl(this.uri); 19 | await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(url)); 20 | } 21 | 22 | } 23 | 24 | @component({eager: true}) 25 | export class BrowseOpenIssues extends TokenCommand { 26 | 27 | public id = 'vscode-github.browseOpenIssue'; 28 | 29 | @showProgress 30 | protected async runWithToken(): Promise { 31 | if (!this.uri) { 32 | throw new Error('uri is undefined'); 33 | } 34 | const issues = await this.workflowManager.issues(this.uri, 'open'); 35 | if (issues.length > 0) { 36 | const selected = await vscode.window.showQuickPick(issues.map(issue => ({ 37 | label: `${issue.title}`, 38 | description: `#${issue.number}`, 39 | issue 40 | }))); 41 | if (selected) { 42 | vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(selected.issue.url)); 43 | } 44 | } else { 45 | vscode.window.showInformationMessage(`No open issues found`); 46 | } 47 | } 48 | 49 | } 50 | 51 | @component({eager: true}) 52 | export class BrowseCurrentFile extends TokenCommand { 53 | 54 | public id = 'vscode-github.browseCurrentFile'; 55 | 56 | @inject 57 | private readonly git!: Git; 58 | 59 | protected requireProjectFolder = false; 60 | 61 | @showProgress 62 | protected async runWithToken(): Promise { 63 | const editor = vscode.window.activeTextEditor; 64 | if (vscode.workspace.workspaceFolders && editor) { 65 | const folder = vscode.workspace.getWorkspaceFolder(editor.document.uri); 66 | if (!folder) { 67 | return; 68 | } 69 | const root = await this.git.getGitRoot(folder.uri); 70 | const file = editor.document.fileName.substring(root.length); 71 | const startLine = editor.selection.start.line; 72 | const endLine = editor.selection.end.line; 73 | const uri = vscode.Uri.parse(await this.workflowManager.getGithubFileUrl(folder.uri, file, startLine, endLine)); 74 | vscode.commands.executeCommand('vscode.open', uri); 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/commands/pull-requests.ts: -------------------------------------------------------------------------------- 1 | import { component, inject } from 'tsdi'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { TokenCommand } from '../command'; 5 | import { Git } from '../git'; 6 | import { getConfiguration, showProgress } from '../helper'; 7 | import { MergeMethod, PullRequest } from '../provider/pull-request'; 8 | import { StatusBarManager } from '../status-bar-manager'; 9 | 10 | abstract class PullRequestCommand extends TokenCommand { 11 | @inject protected git!: Git; 12 | 13 | protected async selectPullRequest(): Promise { 14 | if (!this.uri) { 15 | throw new Error('uri is undefined'); 16 | } 17 | const pullRequests = await this.workflowManager.listPullRequests(this.uri); 18 | const items = pullRequests.map(pullRequest => ({ 19 | label: pullRequest.title, 20 | description: `#${pullRequest.number}`, 21 | pullRequest 22 | })); 23 | const selected = await vscode.window.showQuickPick(items, { 24 | matchOnDescription: true 25 | }); 26 | return selected ? selected.pullRequest : undefined; 27 | } 28 | 29 | private async hasRemoteTrackingBranch(uri: vscode.Uri): Promise { 30 | const localBranch = await this.git.getCurrentBranch(uri); 31 | if (!localBranch) { 32 | return false; 33 | } 34 | return Boolean(await this.git.getRemoteTrackingBranch(localBranch, uri)); 35 | } 36 | 37 | protected async requireRemoteTrackingBranch( 38 | uri: vscode.Uri 39 | ): Promise { 40 | const hasBranch = await this.hasRemoteTrackingBranch(uri); 41 | if (!hasBranch) { 42 | if (getConfiguration('github', uri).autoPublish) { 43 | await vscode.commands.executeCommand('git.publish'); 44 | return true; 45 | } else { 46 | vscode.window.showWarningMessage( 47 | `Cannot create pull request without remote branch. ` + 48 | `Please push your local branch before creating pull request.` 49 | ); 50 | } 51 | } 52 | return hasBranch; 53 | } 54 | 55 | protected async showPullRequestNotification( 56 | pullRequest: PullRequest 57 | ): Promise { 58 | const result = await vscode.window.showInformationMessage( 59 | `Successfully created #${pullRequest.number}`, 60 | 'Open on Github' 61 | ); 62 | if (result) { 63 | vscode.commands.executeCommand( 64 | 'vscode.open', 65 | vscode.Uri.parse(pullRequest.url) 66 | ); 67 | } 68 | } 69 | } 70 | 71 | @component({ eager: true }) 72 | export class BrowsePullRequest extends PullRequestCommand { 73 | public id = 'vscode-github.browserPullRequest'; 74 | 75 | @showProgress 76 | protected async runWithToken(): Promise { 77 | const selected = await this.selectPullRequest(); 78 | if (selected) { 79 | vscode.commands.executeCommand( 80 | 'vscode.open', 81 | vscode.Uri.parse(selected.url) 82 | ); 83 | } 84 | } 85 | } 86 | 87 | @component({ eager: true }) 88 | export class BrowseSimpleRequest extends PullRequestCommand { 89 | public id = 'vscode-github.browserSimplePullRequest'; 90 | 91 | @showProgress 92 | protected async runWithToken(): Promise { 93 | if (!this.uri) { 94 | throw new Error('uri is undefined'); 95 | } 96 | const pullRequest = await this.workflowManager.getPullRequestForCurrentBranch( 97 | this.uri 98 | ); 99 | if (pullRequest) { 100 | await vscode.commands.executeCommand( 101 | 'vscode.open', 102 | vscode.Uri.parse(pullRequest.url) 103 | ); 104 | } else { 105 | vscode.window.showInformationMessage( 106 | 'No pull request for current branch found' 107 | ); 108 | } 109 | } 110 | } 111 | 112 | @component({ eager: true }) 113 | export class CheckoutPullRequest extends PullRequestCommand { 114 | public id = 'vscode-github.checkoutPullRequests'; 115 | 116 | @inject private readonly statusBarManager!: StatusBarManager; 117 | 118 | @showProgress 119 | protected async runWithToken(): Promise { 120 | const selected = await this.selectPullRequest(); 121 | if (selected) { 122 | if (selected.base.repository.url !== selected.head.repository.url) { 123 | const question = await vscode.window.showInformationMessage( 124 | 'External pull request selected. Should it be cloned into a local branch?', 125 | 'Yes', 126 | 'No' 127 | ); 128 | if (question !== 'Yes') { 129 | return; 130 | } 131 | 132 | if (!this.uri) { 133 | throw new Error('uri is undefined'); 134 | } 135 | 136 | await this.git.branch( 137 | selected.sourceBranch, 138 | selected.targetBranch, 139 | this.uri 140 | ); 141 | await this.git.pullExternal( 142 | selected.head.repository.cloneUrl, 143 | selected.sourceBranch, 144 | this.uri 145 | ); 146 | } else { 147 | await vscode.commands.executeCommand( 148 | 'git.checkout', 149 | selected.sourceBranch 150 | ); 151 | } 152 | await this.statusBarManager.updateStatus(); 153 | } 154 | } 155 | } 156 | 157 | @component({ eager: true }) 158 | export class CreatePullRequestWithParameters extends PullRequestCommand { 159 | public id = 'vscode-github.createPullRequestWithParameters'; 160 | 161 | @inject private readonly statusBarManager!: StatusBarManager; 162 | 163 | @showProgress 164 | protected async runWithToken( 165 | sourceBranch: string, 166 | targetBranch: string, 167 | title: string, 168 | body?: string 169 | ): Promise { 170 | if (!this.uri) { 171 | throw new Error('uri is undefined'); 172 | } 173 | if (!this.requireRemoteTrackingBranch(this.uri)) { 174 | return; 175 | } 176 | const pullRequest = await this.workflowManager.createPullRequestFromData( 177 | { 178 | sourceBranch, 179 | targetBranch, 180 | title, 181 | body 182 | }, 183 | this.uri 184 | ); 185 | if (pullRequest) { 186 | await this.statusBarManager.updateStatus(); 187 | await this.showPullRequestNotification(pullRequest); 188 | } 189 | } 190 | } 191 | 192 | @component({ eager: true }) 193 | export class CreateSimplePullRequest extends PullRequestCommand { 194 | public id = 'vscode-github.createSimplePullRequest'; 195 | 196 | @inject private readonly statusBarManager!: StatusBarManager; 197 | 198 | @showProgress 199 | protected async runWithToken(): Promise { 200 | if (!this.uri) { 201 | throw new Error('uri is undefined'); 202 | } 203 | if (!this.requireRemoteTrackingBranch(this.uri)) { 204 | return; 205 | } 206 | const pullRequest = await this.workflowManager.createPullRequest(this.uri); 207 | if (pullRequest) { 208 | await this.statusBarManager.updateStatus(); 209 | await this.showPullRequestNotification(pullRequest); 210 | } 211 | } 212 | } 213 | 214 | @component({ eager: true }) 215 | export class CreatePullRequest extends PullRequestCommand { 216 | public id = 'vscode-github.createPullRequest'; 217 | 218 | @inject private readonly statusBarManager!: StatusBarManager; 219 | 220 | @showProgress 221 | protected async runWithToken(): Promise { 222 | if (!this.uri) { 223 | throw new Error('uri is undefined'); 224 | } 225 | if (!this.requireRemoteTrackingBranch(this.uri)) { 226 | return; 227 | } 228 | let [owner, repo] = await this.git.getGitProviderOwnerAndRepository( 229 | this.uri 230 | ); 231 | const selectedRepository = await this.getRepository(this.uri); 232 | if (!selectedRepository) { 233 | return; 234 | } 235 | [owner, repo] = selectedRepository.label.split('/'); 236 | const branch = await this.getTargetBranch( 237 | selectedRepository.repo.defaultBranch, 238 | this.uri 239 | ); 240 | if (!branch) { 241 | return; 242 | } 243 | const pullRequest = await this.workflowManager.createPullRequest(this.uri, { 244 | owner, 245 | repository: repo, 246 | branch 247 | }); 248 | if (pullRequest) { 249 | await this.statusBarManager.updateStatus(); 250 | await this.showPullRequestNotification(pullRequest); 251 | } 252 | } 253 | 254 | private async getRepository( 255 | uri: vscode.Uri 256 | ): Promise<{ label: string; repo: { defaultBranch: string } } | undefined> { 257 | const repository = await this.workflowManager.getRepository(uri); 258 | const items = [ 259 | { 260 | label: repository.name, 261 | description: '', 262 | repo: repository as { defaultBranch: string } 263 | } 264 | ]; 265 | if (repository.parent) { 266 | items.push({ 267 | label: repository.parent.name, 268 | description: '', 269 | repo: repository.parent as { defaultBranch: string } 270 | }); 271 | } 272 | if (items.length === 1) { 273 | return items[0]; 274 | } 275 | return vscode.window.showQuickPick(items, { 276 | placeHolder: 'Select a repository to create the pull request in' 277 | }); 278 | } 279 | 280 | private async getTargetBranch( 281 | defaultBranch: string, 282 | uri: vscode.Uri 283 | ): Promise { 284 | // sort default branch up 285 | const currentBranch = await this.git.getCurrentBranch(uri); 286 | const picks = (await this.git.getRemoteBranches(uri)).sort((b1, b2) => { 287 | if (b1 === defaultBranch) { 288 | return -1; 289 | } else if (b2 === defaultBranch) { 290 | return 1; 291 | } 292 | return b1.localeCompare(b2); 293 | }).filter(branch => branch !== currentBranch); 294 | return vscode.window.showQuickPick(picks, { 295 | ignoreFocusOut: true, 296 | placeHolder: `Select the branch you would like to merge ${currentBranch} into` 297 | }); 298 | } 299 | } 300 | 301 | @component({ eager: true }) 302 | export class MergePullRequest extends PullRequestCommand { 303 | public id = 'vscode-github.mergePullRequest'; 304 | 305 | @inject private readonly statusBarManager!: StatusBarManager; 306 | 307 | @showProgress 308 | private async getMergeMethdod( 309 | uri: vscode.Uri 310 | ): Promise { 311 | const config = getConfiguration('github', uri); 312 | if (config.preferedMergeMethod) { 313 | return config.preferedMergeMethod; 314 | } 315 | const items: { 316 | label: string; 317 | description: string; 318 | method: MergeMethod; 319 | }[] = []; 320 | const enabledMethods = await this.workflowManager.getEnabledMergeMethods( 321 | uri 322 | ); 323 | if (enabledMethods.has('merge')) { 324 | items.push({ 325 | label: 'Create merge commit', 326 | description: '', 327 | method: 'merge' 328 | }); 329 | } 330 | if (enabledMethods.has('squash')) { 331 | items.push({ 332 | label: 'Squash and merge', 333 | description: '', 334 | method: 'squash' 335 | }); 336 | } 337 | if (enabledMethods.has('rebase')) { 338 | items.push({ 339 | label: 'Rebase and merge', 340 | description: '', 341 | method: 'rebase' 342 | }); 343 | } 344 | const selected = await vscode.window.showQuickPick(items); 345 | return selected ? selected.method : undefined; 346 | } 347 | 348 | @showProgress 349 | protected async runWithToken(): Promise { 350 | if (!this.uri) { 351 | throw new Error('uri is undefined'); 352 | } 353 | const pullRequest = await this.workflowManager.getPullRequestForCurrentBranch( 354 | this.uri 355 | ); 356 | if (pullRequest && pullRequest.mergeable) { 357 | const method = await this.getMergeMethdod(this.uri); 358 | if (method) { 359 | if (await this.workflowManager.mergePullRequest(pullRequest, method)) { 360 | await this.statusBarManager.updateStatus(); 361 | vscode.window.showInformationMessage(`Successfully merged`); 362 | } else { 363 | vscode.window.showInformationMessage( 364 | `Merge failed for unknown reason` 365 | ); 366 | } 367 | } 368 | } else { 369 | vscode.window.showWarningMessage( 370 | 'Either no pull request for current branch, or the pull request is not mergable' 371 | ); 372 | } 373 | } 374 | } 375 | 376 | @component({ eager: true }) 377 | export class UpdatePullRequest extends PullRequestCommand { 378 | public id = 'vscode-github.updatePullRequest'; 379 | 380 | @showProgress 381 | protected async runWithToken(): Promise { 382 | if (!this.uri) { 383 | throw new Error('uri is undefined'); 384 | } 385 | const pullRequest = await this.workflowManager.getPullRequestForCurrentBranch( 386 | this.uri 387 | ); 388 | if (pullRequest) { 389 | await this.workflowManager.updatePullRequest(pullRequest, this.uri); 390 | } else { 391 | vscode.window.showInformationMessage( 392 | 'No pull request for current branch found' 393 | ); 394 | } 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /src/commands/token.ts: -------------------------------------------------------------------------------- 1 | import { component, inject } from 'tsdi'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { Command } from '../command'; 5 | import { getHostname } from '../helper'; 6 | import { getTokens, listTokenHosts, removeToken } from '../tokens'; 7 | import { WorkflowManager } from '../workflow-manager'; 8 | 9 | @component({ eager: true }) 10 | export class SetGithubToken extends Command { 11 | public id = 'vscode-github.setGitHubToken'; 12 | 13 | @inject('vscode.ExtensionContext') 14 | private readonly context!: vscode.ExtensionContext; 15 | 16 | @inject private readonly workflowManager!: WorkflowManager; 17 | 18 | public async run(): Promise { 19 | this.track('execute'); 20 | const options = { 21 | ignoreFocusOut: true, 22 | password: true, 23 | placeHolder: 'GitHub Personal Access Token' 24 | }; 25 | const input = await vscode.window.showInputBox(options); 26 | if (input) { 27 | const tokens = getTokens(this.context.globalState); 28 | tokens['github.com'] = { 29 | token: input, 30 | provider: 'github' 31 | }; 32 | this.context.globalState.update('tokens', tokens); 33 | this.workflowManager.resetProviders(); 34 | } 35 | } 36 | } 37 | 38 | @component({ eager: true }) 39 | export class SetGithubEnterpriseToken extends Command { 40 | public id = 'vscode-github.setGitHubEnterpriseToken'; 41 | 42 | @inject('vscode.ExtensionContext') 43 | private readonly context!: vscode.ExtensionContext; 44 | 45 | @inject private readonly workflowManager!: WorkflowManager; 46 | 47 | public async run(): Promise { 48 | this.track('execute'); 49 | const hostInput = await vscode.window.showInputBox({ 50 | ignoreFocusOut: true, 51 | placeHolder: 'GitHub Enterprise Hostname' 52 | }); 53 | if (hostInput) { 54 | const tokenInput = await vscode.window.showInputBox({ 55 | ignoreFocusOut: true, 56 | password: true, 57 | placeHolder: 'GitHub Enterprise Token' 58 | }); 59 | if (tokenInput) { 60 | const tokens = getTokens(this.context.globalState); 61 | tokens[getHostname(hostInput)] = { 62 | token: tokenInput, 63 | provider: 'github' 64 | }; 65 | this.context.globalState.update('tokens', tokens); 66 | this.workflowManager.resetProviders(); 67 | } 68 | } 69 | } 70 | } 71 | 72 | @component({ eager: true }) 73 | export class SetGitLabToken extends Command { 74 | public id = 'vscode-github.setGitlabToken'; 75 | 76 | @inject('vscode.ExtensionContext') 77 | private readonly context!: vscode.ExtensionContext; 78 | 79 | @inject private readonly workflowManager!: WorkflowManager; 80 | 81 | public async run(): Promise { 82 | this.track('execute'); 83 | const hostInput = await vscode.window.showInputBox({ 84 | ignoreFocusOut: true, 85 | placeHolder: 'GitLab Hostname' 86 | }); 87 | if (hostInput) { 88 | const tokenInput = await vscode.window.showInputBox({ 89 | ignoreFocusOut: true, 90 | password: true, 91 | placeHolder: 'GitLab Token (Personal Access Tokens)' 92 | }); 93 | if (tokenInput) { 94 | const tokens = getTokens(this.context.globalState); 95 | tokens[getHostname(hostInput)] = { 96 | token: tokenInput, 97 | provider: 'gitlab' 98 | }; 99 | this.context.globalState.update('tokens', tokens); 100 | this.workflowManager.resetProviders(); 101 | } 102 | } 103 | } 104 | } 105 | 106 | @component({ eager: true }) 107 | export class ClearToken extends Command { 108 | public id = 'vscode-github.clearToken'; 109 | 110 | @inject('vscode.ExtensionContext') 111 | private readonly context!: vscode.ExtensionContext; 112 | 113 | @inject private readonly workflowManager!: WorkflowManager; 114 | 115 | public async run(): Promise { 116 | this.track('execute'); 117 | const host = await vscode.window.showQuickPick( 118 | listTokenHosts(this.context.globalState), 119 | { 120 | placeHolder: 'Token to remove' 121 | } 122 | ); 123 | if (host) { 124 | removeToken(this.context.globalState, host); 125 | this.workflowManager.resetProviders(); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/commands/user.ts: -------------------------------------------------------------------------------- 1 | import { component } from 'tsdi'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { TokenCommand } from '../command'; 5 | import { showProgress } from '../helper'; 6 | 7 | abstract class UserCommand extends TokenCommand { 8 | 9 | protected async selectUser(uri: vscode.Uri): Promise { 10 | const assignees = await this.workflowManager.getAssignees(uri); 11 | const picks = assignees.map(assignee => ({ 12 | label: assignee.username, 13 | description: '', 14 | assignee 15 | })); 16 | picks.push({ 17 | label: 'Other', 18 | description: '', 19 | assignee: undefined as any 20 | }); 21 | const selected = picks.length > 1 22 | ? await vscode.window.showQuickPick(picks, { 23 | ignoreFocusOut: true 24 | }) 25 | : picks[0]; 26 | if (selected) { 27 | let username: string | undefined; 28 | if (!selected.assignee) { 29 | username = await this.getUser(); 30 | } else { 31 | username = selected.assignee.username; 32 | } 33 | return username; 34 | } 35 | return undefined; 36 | } 37 | 38 | protected async getUser(): Promise { 39 | return vscode.window.showInputBox({ 40 | ignoreFocusOut: true, 41 | placeHolder: 'username, email or fullname' 42 | }); 43 | } 44 | } 45 | 46 | @component({eager: true}) 47 | export class AddAssignee extends UserCommand { 48 | 49 | public id = 'vscode-github.addAssignee'; 50 | 51 | @showProgress 52 | protected async runWithToken(user?: string): Promise { 53 | if (!this.uri) { 54 | throw new Error('uri is undefined'); 55 | } 56 | const pullRequest = await this.workflowManager.getPullRequestForCurrentBranch(this.uri); 57 | if (pullRequest) { 58 | if (!user) { 59 | user = await this.selectUser(this.uri); 60 | } 61 | if (user) { 62 | await this.workflowManager.addAssignee(pullRequest, user, this.uri); 63 | vscode.window.showInformationMessage(`Successfully assigned ${user} to the pull request`); 64 | } 65 | } else { 66 | vscode.window.showWarningMessage('No pull request for current branch'); 67 | } 68 | } 69 | 70 | } 71 | 72 | @component({eager: true}) 73 | export class RemoveAssignee extends UserCommand { 74 | 75 | public id = 'vscode-github.removeAssignee'; 76 | 77 | @showProgress 78 | protected async runWithToken(): Promise { 79 | if (!this.uri) { 80 | throw new Error('uri is undefined'); 81 | } 82 | const pullRequest = await this.workflowManager.getPullRequestForCurrentBranch(this.uri); 83 | if (pullRequest) { 84 | await this.workflowManager.removeAssignee(pullRequest); 85 | vscode.window.showInformationMessage(`Successfully unassigned the pull request`); 86 | } else { 87 | vscode.window.showWarningMessage('No pull request for current brach'); 88 | } 89 | } 90 | 91 | } 92 | 93 | @component({eager: true}) 94 | export class RequestReview extends UserCommand { 95 | 96 | public id = 'vscode-github.requestReview'; 97 | 98 | @showProgress 99 | protected async runWithToken(user?: string): Promise { 100 | if (!this.uri) { 101 | throw new Error('uri is undefined'); 102 | } 103 | const pullRequest = await this.workflowManager.getPullRequestForCurrentBranch(this.uri); 104 | if (pullRequest) { 105 | if (!user) { 106 | user = await this.selectUser(this.uri); 107 | } 108 | if (user) { 109 | await this.workflowManager.requestReview(pullRequest.number, user, this.uri); 110 | vscode.window.showInformationMessage(`Successfully requested review from ${user}`); 111 | } 112 | } else { 113 | vscode.window.showWarningMessage('No pull request for current brach'); 114 | } 115 | } 116 | 117 | } 118 | 119 | @component({eager: true}) 120 | export class DeleteReviewRequest extends UserCommand { 121 | 122 | public id = 'vscode-github.deleteReviewRequest'; 123 | 124 | @showProgress 125 | protected async runWithToken(user?: string): Promise { 126 | if (!this.uri) { 127 | throw new Error('uri is undefined'); 128 | } 129 | const pullRequest = await this.workflowManager.getPullRequestForCurrentBranch(this.uri); 130 | if (pullRequest) { 131 | if (!user) { 132 | user = await this.selectUser(this.uri); 133 | } 134 | if (user) { 135 | await this.workflowManager.deleteReviewRequest(pullRequest.number, user, this.uri); 136 | vscode.window.showInformationMessage(`Successfully canceled review request from ${user}`); 137 | } 138 | } else { 139 | vscode.window.showWarningMessage('No pull request for current brach'); 140 | } 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/configuration.ts: -------------------------------------------------------------------------------- 1 | import { MergeMethod } from './provider/pull-request'; 2 | 3 | export interface Configuration { 4 | gitCommand?: string; 5 | preferedMergeMethod?: MergeMethod; 6 | refreshPullRequestStatus: number; 7 | remoteName?: string; 8 | upstream?: string; 9 | customPullRequestTitle: boolean; 10 | customPullRequestDescription: 'off' | 'singleLine' | 'gitEditor'; 11 | autoPublish?: boolean; 12 | allowUnsafeSSL?: boolean; 13 | statusBarCommand: string | null; 14 | statusbar: { 15 | enabled: boolean; 16 | refresh: number; 17 | command: string | null; 18 | color: boolean; 19 | successText?: string; 20 | pendingText?: string; 21 | failureText?: string; 22 | }; 23 | } 24 | 25 | export interface GitLabConfiguration { 26 | removeSourceBranch?: boolean; 27 | } 28 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import * as sander from 'sander'; 3 | import { TSDI, component, inject, initialize } from 'tsdi'; 4 | import * as vscode from 'vscode'; 5 | import TelemetryReporter from 'vscode-extension-telemetry'; 6 | 7 | import { CommandManager } from './command-manager'; 8 | import { Git } from './git'; 9 | import { HoverProvider } from './issues'; 10 | import { GitHubError } from './provider/github/api'; 11 | import { StatusBarManager } from './status-bar-manager'; 12 | import { migrateToken, Tokens } from './tokens'; 13 | 14 | @component 15 | export class Extension { 16 | @inject private readonly tsdi!: TSDI; 17 | 18 | @inject private readonly reporter!: TelemetryReporter; 19 | 20 | @inject('vscode.ExtensionContext') 21 | private readonly context!: vscode.ExtensionContext; 22 | 23 | @inject('vscode.OutputChannel') 24 | private readonly channel!: vscode.OutputChannel; 25 | 26 | @inject private readonly git!: Git; 27 | 28 | @initialize 29 | protected async init(): Promise { 30 | this.reporter.sendTelemetryEvent('start'); 31 | try { 32 | migrateToken(this.context.globalState); 33 | this.channel.appendLine('Visual Studio Code GitHub Extension'); 34 | const tokens = this.context.globalState.get('tokens'); 35 | await this.checkVersionAndToken(this.context, tokens); 36 | 37 | this.tsdi.get(CommandManager); 38 | this.tsdi.get(StatusBarManager); 39 | this.tsdi.get(HoverProvider); 40 | 41 | if (!vscode.workspace.workspaceFolders) { 42 | return; 43 | } 44 | if (!(await this.git.checkExistence(vscode.Uri.file(process.cwd())))) { 45 | vscode.window.showWarningMessage( 46 | 'No git executable found. Please install git ' + 47 | "and if required set it in your path. You may also set 'gitCommand'" 48 | ); 49 | } 50 | } catch (e) { 51 | this.logAndShowError(e as Error); 52 | throw e; 53 | } 54 | } 55 | 56 | private async checkVersionAndToken( 57 | context: vscode.ExtensionContext, 58 | tokens: Tokens | undefined 59 | ): Promise { 60 | const content = await sander.readFile( 61 | join(context.extensionPath, 'package.json') 62 | ); 63 | const version = JSON.parse(content.toString()).version as string; 64 | const storedVersion = context.globalState.get( 65 | 'version-test' 66 | ); 67 | if ( 68 | version !== storedVersion && 69 | (!tokens || Object.keys(tokens).length === 0) 70 | ) { 71 | context.globalState.update('version-test', version); 72 | vscode.window.showInformationMessage( 73 | 'To enable the Visual Studio Code GitHub Support, please set a Personal Access Token' 74 | ); 75 | } 76 | } 77 | 78 | private logAndShowError(e: Error): void { 79 | if (this.channel) { 80 | this.channel.appendLine(e.message); 81 | if (e.stack) { 82 | e.stack.split('\n').forEach((line) => this.channel.appendLine(line)); 83 | } 84 | } 85 | if (e instanceof GitHubError) { 86 | console.error(e.response); 87 | vscode.window.showErrorMessage('GitHub error: ' + e.message); 88 | } else { 89 | console.error(e); 90 | vscode.window.showErrorMessage('Error: ' + e.message); 91 | } 92 | } 93 | 94 | public dispose(): void { 95 | // 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/git.ts: -------------------------------------------------------------------------------- 1 | import * as execa from 'execa'; 2 | import { resolve } from 'path'; 3 | import { readFile, unlink } from 'sander'; 4 | import { component, inject } from 'tsdi'; 5 | import * as vscode from 'vscode'; 6 | 7 | import { getConfiguration } from './helper'; 8 | 9 | @component 10 | export class Git { 11 | @inject('vscode.OutputChannel') 12 | private readonly channel!: vscode.OutputChannel; 13 | 14 | private async calculateRemoteName( 15 | uri: vscode.Uri 16 | ): Promise { 17 | const ref = ( 18 | await this.execute(`git symbolic-ref -q HEAD`, uri) 19 | ).stdout.trim(); 20 | const upstreamName = ( 21 | await this.execute( 22 | `git for-each-ref --format='%(upstream)' '${ref}'`, 23 | uri 24 | ) 25 | ).stdout.trim(); 26 | const match = upstreamName.match(/refs\/remotes\/([^/]+)\/.*/); 27 | if (match) { 28 | return match[1]; 29 | } 30 | return undefined; 31 | } 32 | 33 | private getExplicitRemoteName(uri: vscode.Uri): string | undefined { 34 | return getConfiguration('github', uri).remoteName; 35 | } 36 | 37 | private async getRemoteName(uri: vscode.Uri): Promise { 38 | let remoteName = this.getExplicitRemoteName(uri); 39 | if (remoteName) { 40 | return remoteName; 41 | } 42 | remoteName = await this.calculateRemoteName(uri); 43 | if (remoteName) { 44 | return remoteName; 45 | } 46 | // fallback to origin which is a sane default 47 | return 'origin'; 48 | } 49 | 50 | private async getRemoteNames(uri: vscode.Uri): Promise { 51 | const remotes = ( 52 | await this.execute(`git config --local --get-regexp ^remote.*.url`, uri) 53 | ).stdout.trim(); 54 | return remotes 55 | .split('\n') 56 | .map((line) => new RegExp('^remote.([^.]+).url.*').exec(line)) 57 | .map((match) => match && match[1]) 58 | .filter((name) => Boolean(name)) as string[]; 59 | } 60 | 61 | private async execute( 62 | cmd: string, 63 | uri: vscode.Uri 64 | ): Promise<{ stdout: string; stderr: string }> { 65 | const [git, ...args] = cmd.split(' '); 66 | const gitCommand = getConfiguration('github', uri).gitCommand; 67 | this.channel.appendLine(`${gitCommand || git} ${args.join(' ')}`); 68 | return execa(gitCommand || git, args, { cwd: uri.fsPath }); 69 | } 70 | 71 | public async checkExistence(uri: vscode.Uri): Promise { 72 | try { 73 | await this.execute('git --version', uri); 74 | return true; 75 | } catch (e) { 76 | return false; 77 | } 78 | } 79 | 80 | public async getGitRoot(uri: vscode.Uri): Promise { 81 | const response = await this.execute('git rev-parse --show-toplevel', uri); 82 | return response.stdout.trim(); 83 | } 84 | 85 | public async getRemoteBranches(uri: vscode.Uri): Promise { 86 | const response = await this.execute( 87 | 'git branch --list --remotes --no-color', 88 | uri 89 | ); 90 | const remoteName = await this.getRemoteName(uri); 91 | return response.stdout 92 | .split('\n') 93 | .filter((line) => !line.match('->')) 94 | .map((line) => line.replace(`${remoteName}/`, '')) 95 | .map((line) => line.trim()); 96 | } 97 | 98 | /** 99 | * Check config for a default upstream and if none found look in .git/config for a remote origin and 100 | * parses it to get username and repository. 101 | * 102 | * @return {Promise} A tuple of username and repository (e.g. KnisterPeter/vscode-github) 103 | * @throws Throws if the could not be parsed as a github url 104 | */ 105 | public async getGitProviderOwnerAndRepository( 106 | uri: vscode.Uri 107 | ): Promise { 108 | const defaultUpstream = getConfiguration('github', uri).upstream; 109 | if (defaultUpstream) { 110 | return Promise.resolve(defaultUpstream.split('/')); 111 | } 112 | return ( 113 | await this.getGitProviderOwnerAndRepositoryFromGitConfig(uri) 114 | ).slice(2, 4); 115 | } 116 | 117 | public async getGitHostname(uri: vscode.Uri): Promise { 118 | return (await this.getGitProviderOwnerAndRepositoryFromGitConfig(uri))[1]; 119 | } 120 | 121 | public async getGitProtocol(uri: vscode.Uri): Promise { 122 | return (await this.getGitProviderOwnerAndRepositoryFromGitConfig(uri))[0]; 123 | } 124 | 125 | private async getGitProviderOwnerAndRepositoryFromGitConfig( 126 | uri: vscode.Uri 127 | ): Promise { 128 | const remoteName = await this.getRemoteName(uri); 129 | try { 130 | const remote = ( 131 | await this.execute( 132 | `git config --local --get remote.${remoteName}.url`, 133 | uri 134 | ) 135 | ).stdout.trim(); 136 | if (!remote.length) { 137 | throw new Error('Git remote is empty!'); 138 | } 139 | return this.parseGitUrl(remote); 140 | } catch (e) { 141 | if (e instanceof Error) { 142 | const remotes = await this.getRemoteNames(uri); 143 | if (!remotes.includes(remoteName)) { 144 | this.logAndShowError(e); 145 | 146 | this.channel.appendLine( 147 | '\n\nYour configuration contains an invalid remoteName. Consider setting the `github.remoteName` setting with one of these:\n' 148 | ); 149 | this.channel.appendLine(remotes.join('\n') + '\n'); 150 | } 151 | } 152 | throw e; 153 | } 154 | } 155 | 156 | public parseGitUrl(remote: string): string[] { 157 | // git protocol remotes, may be git@github:username/repo.git 158 | // or git://github/user/repo.git, domain names are not case-sensetive 159 | if (remote.startsWith('git@') || remote.startsWith('git://')) { 160 | return this.parseGitProviderUrl(remote); 161 | } 162 | 163 | return this.getGitProviderOwnerAndRepositoryFromHttpUrl(remote); 164 | } 165 | 166 | public parseGitProviderUrl(remote: string): string[] { 167 | const match = new RegExp( 168 | '^git(?:@|://)([^:/]+)(?::|:/|/)([^/]+)/(.+?)(?:.git)?$', 169 | 'i' 170 | ).exec(remote); 171 | if (!match) { 172 | throw new Error( 173 | `'${remote}' does not seem to be a valid git provider url.` 174 | ); 175 | } 176 | return ['git:', ...match.slice(1, 4)]; 177 | } 178 | 179 | private getGitProviderOwnerAndRepositoryFromHttpUrl( 180 | remote: string 181 | ): string[] { 182 | // it must be http or https based remote 183 | const { protocol = 'https:', hostname, pathname } = new URL(remote); 184 | // domain names are not case-sensetive 185 | if (!hostname || !pathname) { 186 | throw new Error('Not a Provider remote!'); 187 | } 188 | const match = pathname.match(/\/(.*?)\/(.*?)(?:.git)?$/); 189 | if (!match) { 190 | throw new Error('Not a Provider remote!'); 191 | } 192 | return [protocol, hostname, ...match.slice(1, 3)]; 193 | } 194 | 195 | public async getCurrentBranch(uri: vscode.Uri): Promise { 196 | const branch = (await this.execute('git rev-parse --abbrev-ref HEAD', uri)) 197 | .stdout; 198 | return branch ? branch : undefined; 199 | } 200 | 201 | public async getCommitMessage(sha: string, uri: vscode.Uri): Promise { 202 | return ( 203 | await this.execute(`git log -n 1 --format=%s ${sha}`, uri) 204 | ).stdout.trim(); 205 | } 206 | 207 | public async getFirstCommitOnBranch( 208 | branch: string, 209 | defaultBranch: string, 210 | uri: vscode.Uri 211 | ): Promise { 212 | const sha = ( 213 | await this.execute(`git rev-list ^${defaultBranch} ${branch}`, uri) 214 | ).stdout 215 | .trim() 216 | .split('\n')[0]; 217 | if (!sha) { 218 | return 'master'; 219 | } 220 | return sha; 221 | } 222 | 223 | private async getCommitBody(sha: string, uri: vscode.Uri): Promise { 224 | return ( 225 | await this.execute(`git log --format=%b -n 1 ${sha}`, uri) 226 | ).stdout.trim(); 227 | } 228 | 229 | public async getPullRequestBody( 230 | sha: string, 231 | uri: vscode.Uri 232 | ): Promise { 233 | const bodyMethod = getConfiguration( 234 | 'github', 235 | uri 236 | ).customPullRequestDescription; 237 | 238 | switch (bodyMethod) { 239 | case 'singleLine': 240 | return this.getSingleLinePullRequestBody(); 241 | case 'gitEditor': 242 | return this.getGitEditorPullRequestBody(uri); 243 | case 'off': 244 | default: 245 | return this.getCommitBody(sha, uri); 246 | } 247 | } 248 | 249 | private async getSingleLinePullRequestBody(): Promise { 250 | return vscode.window.showInputBox({ prompt: 'Pull request description' }); 251 | } 252 | 253 | private async getGitEditorPullRequestBody(uri: vscode.Uri): Promise { 254 | const path = resolve(uri.fsPath, 'PR_EDITMSG'); 255 | 256 | const [editorName, ...params] = ( 257 | await execa('git', ['config', '--get', 'core.editor']) 258 | ).stdout.split(' '); 259 | await execa(editorName, [...params, path]); 260 | 261 | const fileContents = (await readFile(path)).toString(); 262 | 263 | await unlink(path); 264 | 265 | return fileContents; 266 | } 267 | 268 | public async getRemoteTrackingBranch( 269 | branch: string, 270 | uri: vscode.Uri 271 | ): Promise { 272 | try { 273 | return ( 274 | await this.execute(`git config --get branch.${branch}.merge`, uri) 275 | ).stdout 276 | .trim() 277 | .split('\n')[0]; 278 | } catch (e) { 279 | return undefined; 280 | } 281 | } 282 | 283 | public async branch( 284 | name: string, 285 | base: string, 286 | uri: vscode.Uri 287 | ): Promise { 288 | await this.execute(`git checkout -b ${name} ${base}`, uri); 289 | } 290 | 291 | public async pullExternal( 292 | repositoryUrl: string, 293 | branch: string, 294 | uri: vscode.Uri 295 | ): Promise { 296 | await this.execute(`git pull ${repositoryUrl} ${branch}`, uri); 297 | } 298 | 299 | private logAndShowError(e: Error): void { 300 | if (this.channel) { 301 | this.channel.appendLine(e.message); 302 | if (e.stack) { 303 | this.channel.appendLine(e.stack); 304 | } 305 | this.channel.show(); 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/helper.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { Configuration, GitLabConfiguration } from './configuration'; 4 | 5 | export function showProgress(_target: object, _propertyKey: string | symbol, 6 | descriptor: PropertyDescriptor): PropertyDescriptor { 7 | const fn = descriptor.value; 8 | descriptor.value = async function(...args: any[]): Promise { 9 | const options: vscode.ProgressOptions = { 10 | location: vscode.ProgressLocation.Window, 11 | title: 'GitHub' 12 | }; 13 | return vscode.window.withProgress(options, async() => { 14 | return fn.call(this, ...args); 15 | }); 16 | }; 17 | return descriptor; 18 | } 19 | 20 | export function getConfiguration(section: 'github', uri: vscode.Uri): Configuration; 21 | export function getConfiguration(section: 'gitlab', uri: vscode.Uri): GitLabConfiguration; 22 | export function getConfiguration(section: string, uri: vscode.Uri): any { 23 | const config = vscode.workspace.getConfiguration(undefined, uri).get(section); 24 | if (!config) { 25 | throw new Error('Empty configuration. This is likely a bug.'); 26 | } 27 | return config; 28 | } 29 | 30 | export function getHostname(input: string): string { 31 | const match = input.match(/.*?(?::\/\/|@)([^:\/]+)/); 32 | if (match) { 33 | return match[1]; 34 | } 35 | return input; 36 | } 37 | -------------------------------------------------------------------------------- /src/issues.ts: -------------------------------------------------------------------------------- 1 | import { stripIndents } from 'common-tags'; 2 | import { component, initialize, inject } from 'tsdi'; 3 | import * as vscode from 'vscode'; 4 | import { WorkflowManager } from './workflow-manager'; 5 | 6 | @component 7 | export class HoverProvider 8 | implements vscode.DocumentLinkProvider, vscode.HoverProvider { 9 | @inject('vscode.ExtensionContext') 10 | private readonly context!: vscode.ExtensionContext; 11 | 12 | @inject 13 | private readonly workflowManager!: WorkflowManager; 14 | 15 | private hoverContent: { [target: string]: string } = {}; 16 | 17 | @initialize 18 | protected init(): void { 19 | // fixme #443: This provider causes high cpu load and 20 | // is annoying to users. This need to be reimplemented 21 | return; 22 | this.context.subscriptions.push( 23 | vscode.languages.registerDocumentLinkProvider('*', this), 24 | vscode.languages.registerHoverProvider('*', this) 25 | ); 26 | vscode.window.onDidChangeActiveTextEditor(() => (this.hoverContent = {})); 27 | } 28 | 29 | public async provideDocumentLinks( 30 | document: vscode.TextDocument 31 | ): Promise { 32 | const folder = vscode.workspace.getWorkspaceFolder(document.uri); 33 | if (!folder) { 34 | return []; 35 | } 36 | const lines = document.getText().split('\n'); 37 | return (await Promise.all( 38 | lines.map(async (line, no) => this.getMatchesOnLine(folder.uri, line, no)) 39 | )).reduce((akku, links) => [...akku, ...links], []); 40 | } 41 | 42 | private async getMatchesOnLine( 43 | uri: vscode.Uri, 44 | line: string, 45 | lineNo: number 46 | ): Promise { 47 | const expr = new RegExp(`#\\d+`, 'gi'); 48 | let match; 49 | const matches = []; 50 | while (true) { 51 | match = expr.exec(line); 52 | if (match === null) { 53 | break; 54 | } 55 | const range = new vscode.Range( 56 | new vscode.Position(lineNo, match.index), 57 | new vscode.Position(lineNo, match.index + match[0].length) 58 | ); 59 | const url = await this.workflowManager.getIssueUrl( 60 | uri, 61 | match[0].substr(1) 62 | ); 63 | if (url) { 64 | matches.push({ 65 | range, 66 | target: vscode.Uri.parse(url) 67 | }); 68 | } 69 | } 70 | return matches; 71 | } 72 | 73 | public async provideHover( 74 | document: vscode.TextDocument, 75 | position: vscode.Position 76 | ): Promise { 77 | const folder = vscode.workspace.getWorkspaceFolder(document.uri); 78 | if (!folder) { 79 | return undefined; 80 | } 81 | const links = await this.provideDocumentLinks(document); 82 | const link = links.find(link => link.range.contains(position)); 83 | if (!link || !link.target) { 84 | return undefined; 85 | } 86 | const target = link.target.toString(); 87 | if (this.hoverContent[target]) { 88 | return new vscode.Hover(this.hoverContent[target], link.range); 89 | } 90 | const issues = await this.workflowManager.issues(folder.uri); 91 | const issue = issues.find(issue => issue.url === target); 92 | if (!issue) { 93 | return undefined; 94 | } 95 | const comments = await this.workflowManager.getIssueComments(issue); 96 | const content = stripIndents` 97 | 98 | ## ${issue.title} 99 | 100 | ${issue.body} 101 | 102 | --- 103 | 104 | ${comments.map(comment => comment.body).join('\n\n---\n\n')} 105 | `; 106 | this.hoverContent[target] = content; 107 | return new vscode.Hover(content, link.range); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import 'isomorphic-fetch'; 2 | import { TSDI, factory } from 'tsdi'; 3 | import * as vscode from 'vscode'; 4 | import TelemetryReporter from 'vscode-extension-telemetry'; 5 | 6 | import { CommandManager } from './command-manager'; 7 | import { Extension } from './extension'; 8 | 9 | let tsdi: TSDI; 10 | 11 | export function activate(context: vscode.ExtensionContext): void { 12 | class ComponentFactory { 13 | @factory({name: 'vscode.ExtensionContext'}) 14 | public extensionContext(): vscode.ExtensionContext { 15 | return context; 16 | } 17 | @factory({name: 'vscode.OutputChannel'}) 18 | public outputChannel(): vscode.OutputChannel { 19 | const channel = vscode.window.createOutputChannel('GitHub'); 20 | context.subscriptions.push(channel); 21 | return channel; 22 | } 23 | @factory 24 | public telemetryReporter(): TelemetryReporter { 25 | const extensionId = 'vscode-github'; 26 | const extensionVersion = vscode.extensions.getExtension('KnisterPeter.vscode-github')!.packageJSON.version; 27 | const key = '67a6da7f-d420-47bd-97d0-d1fd4b76ac55'; 28 | const reporter = new TelemetryReporter(extensionId, extensionVersion, key); 29 | context.subscriptions.push(reporter); 30 | return reporter; 31 | } 32 | } 33 | tsdi = new TSDI(); 34 | tsdi.enableComponentScanner(); 35 | tsdi.register(ComponentFactory); 36 | // note: trigger CommandManager creating for now 37 | // this could be removed when tsdi is able to defer eager creation 38 | tsdi.get(CommandManager); 39 | context.subscriptions.push(tsdi.get(Extension)); 40 | } 41 | 42 | export function deactivate(): void { 43 | tsdi.close(); 44 | } 45 | -------------------------------------------------------------------------------- /src/provider/client.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Git } from '../git'; 3 | import { getConfiguration } from '../helper'; 4 | import { Tokens } from '../tokens'; 5 | import { Repository } from './repository'; 6 | import { User } from './user'; 7 | 8 | import { GithubClient } from './github/client'; 9 | import { GitLabClient } from './gitlab/client'; 10 | 11 | export async function createClient( 12 | git: Git, 13 | tokens: Tokens, 14 | uri: vscode.Uri, 15 | logger: (message: string) => void 16 | ): Promise { 17 | const gitProtocol = await git.getGitProtocol(uri); 18 | const protocol = gitProtocol.startsWith('http') ? gitProtocol : 'https:'; 19 | const hostname = await git.getGitHostname(uri); 20 | const tokenInfo = tokens[hostname]; 21 | const allowUnsafeSSL = Boolean( 22 | getConfiguration('github', uri).allowUnsafeSSL 23 | ); 24 | if (!tokenInfo) { 25 | throw new Error(`No token found for host ${hostname}`); 26 | } 27 | switch (tokenInfo.provider) { 28 | case 'github': 29 | return new GithubClient( 30 | protocol, 31 | hostname, 32 | tokenInfo.token, 33 | logger, 34 | allowUnsafeSSL 35 | ); 36 | case 'gitlab': 37 | return new GitLabClient( 38 | protocol, 39 | hostname, 40 | tokenInfo.token, 41 | logger, 42 | allowUnsafeSSL 43 | ); 44 | default: 45 | throw new Error(`Unknown git provider '${tokenInfo.provider}'`); 46 | } 47 | } 48 | 49 | export interface Client { 50 | name: string; 51 | 52 | test(): Promise; 53 | 54 | getCurrentUser(): Promise>; 55 | 56 | getRepository(uri: vscode.Uri, rid: string): Promise>; 57 | 58 | getUserByUsername(username: string): Promise>; 59 | } 60 | 61 | export interface Response { 62 | body: T; 63 | } 64 | -------------------------------------------------------------------------------- /src/provider/github/api.ts: -------------------------------------------------------------------------------- 1 | import * as https from 'https'; 2 | import * as LRUCache from 'lru-cache'; 3 | import { 4 | Delete, 5 | Get, 6 | Headers as Header, 7 | Interceptor, 8 | IPretendDecoder, 9 | IPretendRequestInterceptor, 10 | Patch, 11 | Post, 12 | Pretend, 13 | Put, 14 | } from 'pretend'; 15 | 16 | export interface GitHub { 17 | getRepositories(): Promise>; 18 | 19 | getRepository( 20 | owner: string, 21 | repo: string 22 | ): Promise>; 23 | 24 | getPullRequest( 25 | owner: string, 26 | repo: string, 27 | number: number 28 | ): Promise>; 29 | 30 | listPullRequests( 31 | owner: string, 32 | repo: string, 33 | parameters?: ListPullRequestsParameters 34 | ): Promise>; 35 | 36 | createPullRequest( 37 | owner: string, 38 | repo: string, 39 | body: CreatePullRequestBody 40 | ): Promise>; 41 | 42 | updatePullRequest( 43 | owner: string, 44 | repo: string, 45 | number: number, 46 | body: UpdatePullRequestBody 47 | ): Promise; 48 | 49 | getStatusForRef( 50 | owner: string, 51 | repo: string, 52 | ref: string 53 | ): Promise>; 54 | 55 | mergePullRequest( 56 | owner: string, 57 | repo: string, 58 | number: number, 59 | body: Merge 60 | ): Promise>; 61 | 62 | addAssignees( 63 | owner: string, 64 | repo: string, 65 | numer: number, 66 | body: Assignees 67 | ): Promise; 68 | 69 | removeAssignees( 70 | owner: string, 71 | repo: string, 72 | numer: number, 73 | body: Assignees 74 | ): Promise; 75 | 76 | requestReview( 77 | owner: string, 78 | repo: string, 79 | numer: number, 80 | body: Reviewers 81 | ): Promise; 82 | 83 | deleteReviewRequest( 84 | owner: string, 85 | repo: string, 86 | numer: number, 87 | body: Reviewers 88 | ): Promise; 89 | 90 | issues( 91 | owner: string, 92 | repo: string, 93 | parameters?: IssuesParameters 94 | ): Promise>; 95 | 96 | getPullRequestComments( 97 | owner: string, 98 | repo: string, 99 | number: number 100 | ): Promise>; 101 | 102 | editIssue( 103 | owner: string, 104 | repo: string, 105 | number: number, 106 | body: EditIssueBody 107 | ): Promise>; 108 | 109 | getAuthenticatedUser(): Promise>; 110 | 111 | getUser(username: string): Promise>; 112 | 113 | listAssignees( 114 | owner: string, 115 | repo: string 116 | ): Promise>; 117 | 118 | getIssueComments( 119 | owner: string, 120 | repo: string, 121 | number: number 122 | ): Promise>; 123 | } 124 | 125 | export interface GitHubResponse { 126 | status: number; 127 | headers: { [name: string]: string[] }; 128 | body: T; 129 | } 130 | 131 | export interface IssueComment { 132 | body: string; 133 | user: UserResponse; 134 | } 135 | 136 | export interface UpdatePullRequestBody { 137 | title?: string; 138 | body?: string; 139 | state?: 'open' | 'closed'; 140 | } 141 | 142 | export interface UserResponse { 143 | id: number; 144 | login: string; 145 | } 146 | 147 | export interface EditIssueBody { 148 | state?: 'open' | 'closed'; 149 | assignees?: string[]; 150 | } 151 | 152 | export interface EditIssueResponse { 153 | number: number; 154 | state: 'open' | 'closed'; 155 | } 156 | 157 | export interface PullRequestComment { 158 | diff_hunk: string; 159 | path: string; 160 | position: number; 161 | body: string; 162 | } 163 | 164 | export interface Issue { 165 | html_url: string; 166 | number: number; 167 | state: 'open'; 168 | title: string; 169 | body: string; 170 | pull_request?: object; 171 | } 172 | 173 | export interface IssuesParameters { 174 | milestone?: string | number; 175 | state?: 'closed' | 'all' | 'open'; 176 | assignee?: string; 177 | sort?: 'created' | 'updated' | 'comments'; 178 | direction?: 'asc' | 'desc'; 179 | } 180 | 181 | export interface Assignees { 182 | assignees: string[]; 183 | } 184 | 185 | export interface Reviewers { 186 | reviewers: string[]; 187 | } 188 | 189 | export interface GithubRepositoryStruct { 190 | owner: { 191 | login: string; 192 | }; 193 | name: string; 194 | full_name: string; 195 | default_branch: string; 196 | allow_rebase_merge?: boolean; 197 | allow_squash_merge?: boolean; 198 | allow_merge_commit?: boolean; 199 | html_url: string; 200 | clone_url: string; 201 | parent?: GithubRepositoryStruct; 202 | } 203 | 204 | export type MergeMethod = 'merge' | 'squash' | 'rebase'; 205 | 206 | export interface Merge { 207 | commit_title?: string; 208 | commit_message?: string; 209 | sha?: string; 210 | merge_method?: MergeMethod; 211 | } 212 | 213 | export interface MergeResult { 214 | sha?: string; 215 | merged?: boolean; 216 | message: string; 217 | documentation_url?: string; 218 | } 219 | 220 | export type PullRequestStatus = 'failure' | 'pending' | 'success'; 221 | 222 | export interface CombinedStatus { 223 | state: PullRequestStatus; 224 | total_count: number; 225 | statuses: any[]; 226 | } 227 | 228 | export interface ListPullRequestsParameters { 229 | state?: 'open' | 'close' | 'all'; 230 | head?: string; 231 | base?: string; 232 | sort?: 'created' | 'updated' | 'popularity' | 'long-running'; 233 | direction?: 'asc' | 'desc'; 234 | } 235 | 236 | export interface CreatePullRequestBody { 237 | title: string; 238 | head: string; 239 | base: string; 240 | body?: string; 241 | } 242 | 243 | export interface PullRequestStruct { 244 | id: number; 245 | number: number; 246 | state: 'open' | 'closed'; 247 | title: string; 248 | body: string; 249 | html_url: string; 250 | head: { 251 | label: string; 252 | ref: string; 253 | repo: GithubRepositoryStruct; 254 | }; 255 | base: { 256 | label: string; 257 | ref: string; 258 | repo: GithubRepositoryStruct; 259 | }; 260 | mergeable?: boolean | null; 261 | } 262 | 263 | export function getClient( 264 | endpoint: string, 265 | token: string, 266 | logger: (message: string) => void, 267 | allowUnsafeSSL = false 268 | ): GitHub { 269 | return Pretend.builder() 270 | .interceptor(impl.githubCache()) 271 | .requestInterceptor(impl.githubTokenAuthenticator(token)) 272 | .requestInterceptor(impl.githubHttpsAgent(!allowUnsafeSSL)) 273 | .interceptor(impl.logger(logger)) 274 | .decode(impl.githubDecoder()) 275 | .target(impl.GitHubBlueprint, endpoint); 276 | } 277 | 278 | export class GitHubError extends Error { 279 | public readonly response: Response; 280 | 281 | constructor(message: string, response: Response) { 282 | super(message); 283 | this.response = response; 284 | } 285 | } 286 | 287 | namespace impl { 288 | export function logger(logger: (message: string) => void): Interceptor { 289 | return async (chain, request) => { 290 | try { 291 | logger(`${request.options.method} ${request.url}`); 292 | // console.log('github-request: ', request); 293 | const response = await chain(request); 294 | // console.log('response', response); 295 | return response; 296 | } catch (e) { 297 | if ((e as GitHubError).response) { 298 | logger( 299 | `${(e as GitHubError).response.status} ${(e as Error).message}` 300 | ); 301 | } 302 | throw e; 303 | } 304 | }; 305 | } 306 | 307 | export function githubCache(): Interceptor { 308 | // cache at most 100 requests 309 | const cache = new LRUCache(100); 310 | return async (chain, request) => { 311 | const entry = cache.get(request.url); 312 | if (entry) { 313 | // when we have a cache hit, send etag 314 | request.options.headers = new Headers(request.options.headers); 315 | request.options.headers.set('If-None-Match', entry.etag); 316 | } 317 | const response = await chain(request); 318 | if (!entry || response.status !== 304) { 319 | // if no cache hit or response modified, cache and respond 320 | cache.set(request.url, { 321 | etag: response.headers.etag, 322 | response, 323 | }); 324 | return response; 325 | } 326 | // respond from cache 327 | return entry.response; 328 | }; 329 | } 330 | 331 | export function githubTokenAuthenticator( 332 | token: string 333 | ): IPretendRequestInterceptor { 334 | return (request) => { 335 | request.options.headers = new Headers(request.options.headers); 336 | request.options.headers.set('Authorization', `token ${token}`); 337 | return request; 338 | }; 339 | } 340 | 341 | export function githubHttpsAgent( 342 | rejectUnauthorized: boolean 343 | ): IPretendRequestInterceptor { 344 | return (request) => { 345 | if (!request.url.startsWith('https://')) { 346 | return request; 347 | } 348 | request.options.agent = new https.Agent({ rejectUnauthorized }); 349 | return request; 350 | }; 351 | } 352 | 353 | export function githubDecoder(): IPretendDecoder { 354 | return async (response) => { 355 | if (response.status >= 400) { 356 | const body = await response.json(); 357 | throw new GitHubError( 358 | `${body.message || response.statusText}`, 359 | response 360 | ); 361 | } 362 | const headers = {}; 363 | response.headers.forEach((value, key) => { 364 | headers[key] = [...(headers[key] || []), value]; 365 | }); 366 | return { 367 | status: response.status, 368 | headers, 369 | body: 370 | response.status >= 200 && response.status <= 300 371 | ? await response.json() 372 | : undefined, 373 | }; 374 | }; 375 | } 376 | 377 | export class GitHubBlueprint implements GitHub { 378 | @Get('/user') 379 | public getAuthenticatedUser(): any { 380 | /* */ 381 | } 382 | 383 | @Get('/user/repos') 384 | public getRepositories(): any { 385 | /* */ 386 | } 387 | 388 | @Header('Accept: application/vnd.github.polaris-preview') 389 | @Get('/repos/:owner/:repo') 390 | public getRepository(): any { 391 | /* */ 392 | } 393 | 394 | @Get('/repos/:owner/:repo/pulls/:number') 395 | public getPullRequest(): any { 396 | /* */ 397 | } 398 | 399 | @Get('/repos/:owner/:repo/pulls', true) 400 | public listPullRequests(): any { 401 | /* */ 402 | } 403 | 404 | @Post('/repos/:owner/:repo/pulls') 405 | public createPullRequest(): any { 406 | /* */ 407 | } 408 | 409 | @Patch('/repos/:owner/:repo/pulls/:number') 410 | public updatePullRequest(): any { 411 | /* */ 412 | } 413 | 414 | @Get('/repos/:owner/:repo/commits/:ref/status') 415 | public getStatusForRef(): any { 416 | /* */ 417 | } 418 | 419 | @Header('Accept: application/vnd.github.polaris-preview+json') 420 | @Put('/repos/:owner/:repo/pulls/:number/merge') 421 | public mergePullRequest(): any { 422 | /* */ 423 | } 424 | 425 | @Post('/repos/:owner/:repo/issues/:number/assignees') 426 | public addAssignees(): any { 427 | /* */ 428 | } 429 | 430 | @Delete('/repos/:owner/:repo/issues/:number/assignees', true) 431 | public removeAssignees(): any { 432 | /* */ 433 | } 434 | 435 | @Post('/repos/:owner/:repo/pulls/:number/requested_reviewers') 436 | public requestReview(): any { 437 | /* */ 438 | } 439 | 440 | @Delete('/repos/:owner/:repo/pulls/:number/requested_reviewers', true) 441 | public deleteReviewRequest(): any { 442 | /* */ 443 | } 444 | 445 | @Get('/repos/:owner/:repo/issues', true) 446 | public issues(): any { 447 | /* */ 448 | } 449 | 450 | @Get('/repos/:owner/:repo/pulls/:number/comments') 451 | public getPullRequestComments(): any { 452 | /* */ 453 | } 454 | 455 | @Patch('/repos/:owner/:repo/issues/:number') 456 | public editIssue(): any { 457 | /* */ 458 | } 459 | 460 | @Get('/users/:username') 461 | public getUser(): any { 462 | /* */ 463 | } 464 | 465 | @Get('/repos/:owner/:repo/assignees') 466 | public listAssignees(): any { 467 | /* */ 468 | } 469 | 470 | @Get('/repos/:owner/:repo/issues/:number/comments') 471 | public getIssueComments(): any { 472 | /* */ 473 | } 474 | } 475 | } 476 | -------------------------------------------------------------------------------- /src/provider/github/client.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Client, Response } from '../client'; 3 | 4 | import { GitHub, getClient, GithubRepositoryStruct } from './api'; 5 | import { GithubRepository } from './repository'; 6 | import { GithubUser } from './user'; 7 | 8 | export class GithubClient implements Client { 9 | private readonly client: GitHub; 10 | 11 | public name = 'GitHub Client'; 12 | 13 | private readonly repositories = new Map(); 14 | 15 | constructor( 16 | protocol: string, 17 | hostname: string, 18 | token: string, 19 | logger: (message: string) => void, 20 | allowUnsafeSSL = false 21 | ) { 22 | this.client = getClient( 23 | this.getApiEndpoint(protocol, hostname), 24 | token, 25 | logger, 26 | allowUnsafeSSL 27 | ); 28 | } 29 | 30 | private getApiEndpoint(protocol: string, hostname: string): string { 31 | if (hostname === 'github.com') { 32 | return 'https://api.github.com'; 33 | } 34 | if (hostname.startsWith('http')) { 35 | return `${hostname}/api/v3`; 36 | } 37 | return `${protocol}//${hostname}/api/v3`; 38 | } 39 | 40 | public async test(): Promise { 41 | await this.client.getRepositories(); 42 | } 43 | 44 | public async getCurrentUser(): Promise> { 45 | const response = await this.client.getAuthenticatedUser(); 46 | return { 47 | body: new GithubUser(this.client, response.body) 48 | }; 49 | } 50 | 51 | public async getRepository( 52 | uri: vscode.Uri, 53 | rid: string 54 | ): Promise> { 55 | const [owner, repository] = rid.split('/'); 56 | const cacheKey = `${uri.toString()}$$${rid}`; 57 | const cacheHit = this.repositories.get(cacheKey); 58 | if (cacheHit) { 59 | return { 60 | body: new GithubRepository( 61 | uri, 62 | this.client, 63 | owner, 64 | repository, 65 | cacheHit 66 | ) 67 | }; 68 | } 69 | const response = await this.client.getRepository(owner, repository); 70 | this.repositories.set(cacheKey, response.body); 71 | return { 72 | body: new GithubRepository( 73 | uri, 74 | this.client, 75 | owner, 76 | repository, 77 | response.body 78 | ) 79 | }; 80 | } 81 | 82 | public async getUserByUsername( 83 | username: string 84 | ): Promise> { 85 | const response = await this.client.getUser(username); 86 | return { 87 | body: new GithubUser(this.client, response.body) 88 | }; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/provider/github/issue.ts: -------------------------------------------------------------------------------- 1 | import { Response } from '../client'; 2 | import { Issue, IssueComment } from '../issue'; 3 | import { GitHub, Issue as IssueStruct } from './api'; 4 | import { GithubRepository } from './repository'; 5 | import { GithubUser } from './user'; 6 | 7 | export class GithubIssue implements Issue { 8 | 9 | private readonly client: GitHub; 10 | private readonly repository: GithubRepository; 11 | private readonly struct: IssueStruct; 12 | 13 | public get number(): number { 14 | return this.struct.number; 15 | } 16 | 17 | public get title(): string { 18 | return this.struct.title; 19 | } 20 | 21 | public get url(): string { 22 | return this.struct.html_url; 23 | } 24 | 25 | public get body(): string { 26 | return this.struct.body; 27 | } 28 | 29 | constructor(client: GitHub, repository: GithubRepository, struct: IssueStruct) { 30 | this.client = client; 31 | this.repository = repository; 32 | this.struct = struct; 33 | } 34 | 35 | public async comments(): Promise> { 36 | const response = await this.client.getIssueComments( 37 | this.repository.owner, 38 | this.repository.repository, 39 | this.number 40 | ); 41 | return { 42 | body: response.body.map(comment => ({ 43 | body: comment.body, 44 | user: new GithubUser(this.client, comment.user) 45 | })) 46 | }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/provider/github/pull-request.ts: -------------------------------------------------------------------------------- 1 | import { Response } from '../client'; 2 | import { 3 | PullRequest, 4 | MergeBody, 5 | MergeResult, 6 | RequestReviewBody, 7 | CancelReviewBody, 8 | Comment, 9 | UpdateBody 10 | } from '../pull-request'; 11 | import { GitHub, PullRequestStruct } from './api'; 12 | import { GithubRepository } from './repository'; 13 | import { GithubUser } from './user'; 14 | 15 | export class GithubPullRequest implements PullRequest { 16 | private readonly client: GitHub; 17 | private readonly repository: GithubRepository; 18 | private readonly struct: PullRequestStruct; 19 | 20 | get id(): number { 21 | return this.struct.id; 22 | } 23 | 24 | get number(): number { 25 | return this.struct.number; 26 | } 27 | 28 | get state(): 'open' | 'closed' { 29 | return this.struct.state; 30 | } 31 | 32 | get title(): string { 33 | return this.struct.title; 34 | } 35 | 36 | get body(): string { 37 | return this.struct.body; 38 | } 39 | 40 | get url(): string { 41 | return this.struct.html_url; 42 | } 43 | 44 | get sourceBranch(): string { 45 | return this.struct.head.ref; 46 | } 47 | 48 | get targetBranch(): string { 49 | return this.struct.base.ref; 50 | } 51 | 52 | get mergeable(): boolean | null | undefined { 53 | return this.struct.mergeable; 54 | } 55 | 56 | get head(): { repository: GithubRepository } { 57 | return { 58 | repository: new GithubRepository( 59 | undefined, 60 | this.client, 61 | this.struct.head.repo.owner.login, 62 | this.struct.head.repo.name, 63 | this.struct.head.repo 64 | ) 65 | }; 66 | } 67 | 68 | get base(): { repository: GithubRepository } { 69 | return { 70 | repository: new GithubRepository( 71 | undefined, 72 | this.client, 73 | this.struct.base.repo.owner.login, 74 | this.struct.base.repo.name, 75 | this.struct.base.repo 76 | ) 77 | }; 78 | } 79 | 80 | constructor( 81 | client: GitHub, 82 | repository: GithubRepository, 83 | struct: PullRequestStruct 84 | ) { 85 | this.client = client; 86 | this.repository = repository; 87 | this.struct = struct; 88 | } 89 | 90 | public async update(body: UpdateBody): Promise { 91 | await this.client.updatePullRequest( 92 | this.repository.owner, 93 | this.repository.repository, 94 | this.number, 95 | { 96 | title: body.title, 97 | body: body.body, 98 | state: body.state 99 | } 100 | ); 101 | } 102 | 103 | public async getComments(): Promise> { 104 | const response = await this.client.getPullRequestComments( 105 | this.repository.owner, 106 | this.repository.repository, 107 | this.number 108 | ); 109 | return { 110 | body: response.body.map(comment => { 111 | return { 112 | file: comment.path, 113 | line: comment.position, 114 | body: comment.body 115 | }; 116 | }) 117 | }; 118 | } 119 | 120 | public async merge(body: MergeBody): Promise> { 121 | const response = await this.client.mergePullRequest( 122 | this.repository.owner, 123 | this.repository.repository, 124 | this.number, 125 | { 126 | merge_method: body.mergeMethod 127 | } 128 | ); 129 | return { 130 | body: { 131 | merged: response.body.merged, 132 | message: response.body.message, 133 | sha: response.body.sha 134 | } 135 | }; 136 | } 137 | 138 | public async assign(assignees: GithubUser[]): Promise { 139 | await this.client.editIssue( 140 | this.repository.owner, 141 | this.repository.repository, 142 | this.number, 143 | { 144 | assignees: assignees.map(assignee => assignee.username) 145 | } 146 | ); 147 | } 148 | 149 | public async unassign(): Promise { 150 | await this.client.editIssue( 151 | this.repository.owner, 152 | this.repository.repository, 153 | this.number, 154 | { 155 | assignees: [] 156 | } 157 | ); 158 | } 159 | 160 | public async requestReview(body: RequestReviewBody): Promise { 161 | await this.client.requestReview( 162 | this.repository.owner, 163 | this.repository.repository, 164 | this.number, 165 | { 166 | reviewers: body.reviewers 167 | } 168 | ); 169 | } 170 | 171 | public async cancelReview(body: CancelReviewBody): Promise { 172 | await this.client.deleteReviewRequest( 173 | this.repository.owner, 174 | this.repository.repository, 175 | this.number, 176 | { 177 | reviewers: body.reviewers 178 | } 179 | ); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/provider/github/repository.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Response } from '../client'; 3 | import { Issue } from '../issue'; 4 | import { 5 | Repository, 6 | ListPullRequestsParameters, 7 | CreatePullRequestBody, 8 | IssuesParameters 9 | } from '../repository'; 10 | import { GitHub, GithubRepositoryStruct } from './api'; 11 | import { GithubIssue } from './issue'; 12 | import { GithubPullRequest } from './pull-request'; 13 | import { GithubUser } from './user'; 14 | 15 | export class GithubRepository implements Repository { 16 | public readonly uri: vscode.Uri | undefined; 17 | 18 | private readonly client: GitHub; 19 | 20 | private readonly struct: GithubRepositoryStruct; 21 | 22 | public owner: string; 23 | public repository: string; 24 | 25 | public get slug(): string { 26 | return `${this.owner}/${this.repository}`; 27 | } 28 | 29 | public get name(): string { 30 | return this.struct.full_name; 31 | } 32 | 33 | public get defaultBranch(): string { 34 | return this.struct.default_branch; 35 | } 36 | 37 | public get allowMergeCommits(): boolean { 38 | return Boolean(this.struct.allow_merge_commit); 39 | } 40 | 41 | public get allowSquashCommits(): boolean { 42 | return Boolean(this.struct.allow_squash_merge); 43 | } 44 | 45 | public get allowRebaseCommits(): boolean { 46 | return Boolean(this.struct.allow_rebase_merge); 47 | } 48 | 49 | public get parent(): Repository | undefined { 50 | if (!this.struct.parent) { 51 | return undefined; 52 | } 53 | return new GithubRepository( 54 | undefined, 55 | this.client, 56 | this.struct.parent.owner.login, 57 | this.struct.parent.name, 58 | this.struct.parent 59 | ); 60 | } 61 | 62 | public get url(): string { 63 | return this.struct.html_url; 64 | } 65 | 66 | public get cloneUrl(): string { 67 | return this.struct.clone_url; 68 | } 69 | 70 | constructor( 71 | uri: vscode.Uri | undefined, 72 | client: GitHub, 73 | owner: string, 74 | repository: string, 75 | struct: GithubRepositoryStruct 76 | ) { 77 | this.uri = uri; 78 | this.client = client; 79 | this.owner = owner; 80 | this.repository = repository; 81 | this.struct = struct; 82 | } 83 | 84 | public async getPullRequests( 85 | parameters?: ListPullRequestsParameters | undefined 86 | ): Promise> { 87 | const response = await this.client.listPullRequests( 88 | this.owner, 89 | this.repository, 90 | parameters 91 | ); 92 | const body = response.body.map( 93 | pr => new GithubPullRequest(this.client, this, pr) 94 | ); 95 | return { 96 | body 97 | }; 98 | } 99 | 100 | public async getPullRequest( 101 | id: number 102 | ): Promise> { 103 | const response = await this.client.getPullRequest( 104 | this.owner, 105 | this.repository, 106 | id 107 | ); 108 | return { 109 | body: new GithubPullRequest(this.client, this, response.body) 110 | }; 111 | } 112 | 113 | public async createPullRequest( 114 | body: CreatePullRequestBody 115 | ): Promise> { 116 | const result = await this.client.createPullRequest( 117 | this.owner, 118 | this.repository, 119 | { 120 | head: `${this.owner}:${body.sourceBranch}`, 121 | base: `${body.targetBranch}`, 122 | title: body.title, 123 | body: body.body 124 | } 125 | ); 126 | const expr = new RegExp(`https?://[^/:]+/repos/[^/]+/[^/]+/pulls/([0-9]+)`); 127 | const number = expr.exec(result.headers['location'][0]) as RegExpMatchArray; 128 | const response = await this.client.getPullRequest( 129 | this.owner, 130 | this.repository, 131 | parseInt(number[1], 10) 132 | ); 133 | return { 134 | body: new GithubPullRequest(this.client, this, response.body) 135 | }; 136 | } 137 | 138 | public async getIssues( 139 | parameters: IssuesParameters = {} 140 | ): Promise> { 141 | const response = await this.client.issues(this.owner, this.repository, { 142 | direction: parameters.direction, 143 | sort: parameters.sort, 144 | state: parameters.state || 'all' 145 | }); 146 | return { 147 | body: response.body 148 | .filter(issue => !Boolean(issue.pull_request)) 149 | .map(issue => new GithubIssue(this.client, this, issue)) 150 | }; 151 | } 152 | 153 | public async getUsers(): Promise> { 154 | const response = await this.client.listAssignees( 155 | this.owner, 156 | this.repository 157 | ); 158 | return { 159 | body: response.body.map(user => new GithubUser(this.client, user)) 160 | }; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/provider/github/user.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../user'; 2 | import { GitHub, UserResponse } from './api'; 3 | 4 | export class GithubUser implements User { 5 | 6 | // private client: GitHub; 7 | 8 | private readonly struct: UserResponse; 9 | 10 | public get id(): number { 11 | return this.struct.id; 12 | } 13 | 14 | public get username(): string { 15 | return this.struct.login; 16 | } 17 | 18 | constructor(_client: GitHub, struct: UserResponse) { 19 | // this.client = client; 20 | this.struct = struct; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/provider/gitlab/api.ts: -------------------------------------------------------------------------------- 1 | import * as https from 'https'; 2 | import { 3 | Get, 4 | Interceptor, 5 | IPretendDecoder, 6 | IPretendRequestInterceptor, 7 | Post, 8 | Pretend, 9 | Put, 10 | } from 'pretend'; 11 | 12 | export interface GitLab { 13 | getProjects(): Promise>; 14 | 15 | getProject(id: string): Promise>; 16 | 17 | getMergeRequests( 18 | id: string, 19 | parameters?: GetMergeRequestParameters 20 | ): Promise>; 21 | 22 | getMergeRequest( 23 | id: string, 24 | mr_iid: number 25 | ): Promise>; 26 | 27 | createMergeRequest( 28 | id: string, 29 | body: CreateMergeRequestBody 30 | ): Promise>; 31 | 32 | updateMergeRequest( 33 | id: string, 34 | mr_iid: number, 35 | body: UpdateMergeRequestBody 36 | ): Promise>; 37 | 38 | acceptMergeRequest( 39 | id: string, 40 | mr_iid: number, 41 | body: AcceptMergeRequestBody 42 | ): Promise>; 43 | 44 | getProjectIssues( 45 | id: string, 46 | body: ProjectIssuesBody 47 | ): Promise>; 48 | 49 | getAuthenticatedUser(): Promise>; 50 | 51 | searchUser( 52 | parameters?: SearchUsersParameters 53 | ): Promise>; 54 | 55 | getIssueNotes( 56 | id: string, 57 | issue_iid: number 58 | ): Promise>; 59 | } 60 | 61 | export interface GitLabResponse { 62 | status: number; 63 | headers: { [name: string]: string[] }; 64 | body: T; 65 | } 66 | 67 | export interface IssueNote { 68 | body: string; 69 | author: UserResponse; 70 | } 71 | 72 | export interface AcceptMergeRequestBody { 73 | should_remove_source_branch?: boolean; 74 | } 75 | 76 | export interface AcceptMergeRequestResponse { 77 | title: string; 78 | state: 'merged'; 79 | sha: string; 80 | } 81 | 82 | export interface SearchUsersParameters { 83 | username?: string; 84 | } 85 | 86 | export interface UserResponse { 87 | id: number; 88 | username: string; 89 | } 90 | 91 | export interface ProjectIssuesBody { 92 | state?: 'opened' | 'closed'; 93 | order_by?: 'created_at' | 'updated_at'; 94 | sort?: 'asc' | 'desc'; 95 | } 96 | 97 | export interface Issue { 98 | iid: number; 99 | title: string; 100 | web_url: string; 101 | description: string; 102 | } 103 | 104 | export interface CreateMergeRequestBody { 105 | source_branch: string; 106 | target_branch: string; 107 | title: string; 108 | description?: string; 109 | remove_source_branch?: boolean; 110 | } 111 | 112 | export interface UpdateMergeRequestBody { 113 | target_branch?: string; 114 | title?: string; 115 | description?: string; 116 | state_event?: 'close' | 'reopen'; 117 | assignee_id?: number; 118 | remove_source_branch?: boolean; 119 | squash?: boolean; 120 | } 121 | 122 | export interface GetMergeRequestParameters { 123 | state?: 'opened' | 'closed' | 'merged'; 124 | order_by?: 'created_at' | 'updated_at'; 125 | sort?: 'asc' | 'desc'; 126 | } 127 | 128 | export interface MergeRequest { 129 | id: number; 130 | iid: number; 131 | state: 'opened' | 'closed' | 'merged'; 132 | title: string; 133 | description: string; 134 | web_url: string; 135 | target_branch: string; 136 | source_branch: string; 137 | merge_status: 'can_be_merged'; 138 | } 139 | 140 | export interface Project { 141 | id: number; 142 | path_with_namespace: string; 143 | name: string; 144 | default_branch: string; 145 | web_url: string; 146 | merge_requests_enabled: boolean; 147 | } 148 | 149 | export function getClient( 150 | endpoint: string, 151 | token: string, 152 | logger: (message: string) => void, 153 | allowUnsafeSSL = false 154 | ): GitLab { 155 | return Pretend.builder() 156 | .requestInterceptor(impl.gitlabTokenAuthenticator(token)) 157 | .requestInterceptor(impl.gitlabHttpsAgent(!allowUnsafeSSL)) 158 | .requestInterceptor(impl.formEncoding()) 159 | .interceptor(impl.logger(logger)) 160 | .decode(impl.gitlabDecoder()) 161 | .target(impl.GitLabBlueprint, endpoint); 162 | } 163 | 164 | export class GitLabError extends Error { 165 | public readonly response: Response; 166 | 167 | constructor(message: string, response: Response) { 168 | super(message); 169 | this.response = response; 170 | } 171 | } 172 | 173 | namespace impl { 174 | export function logger(logger: (message: string) => void): Interceptor { 175 | return async (chain, request) => { 176 | try { 177 | logger(`${request.options.method} ${request.url}`); 178 | // console.log('gitlab-request: ', request); 179 | const response = await chain(request); 180 | // console.log('response', response); 181 | return response; 182 | } catch (e) { 183 | logger(`${(e as GitLabError).response.status} ${(e as Error).message}`); 184 | throw e; 185 | } 186 | }; 187 | } 188 | 189 | export function gitlabTokenAuthenticator( 190 | token: string 191 | ): IPretendRequestInterceptor { 192 | return (request) => { 193 | request.options.headers = new Headers(request.options.headers); 194 | request.options.headers.set('PRIVATE-TOKEN', `${token}`); 195 | return request; 196 | }; 197 | } 198 | 199 | export function gitlabHttpsAgent( 200 | rejectUnauthorized: boolean 201 | ): IPretendRequestInterceptor { 202 | return (request) => { 203 | if (!request.url.startsWith('https://')) { 204 | return request; 205 | } 206 | request.options.agent = new https.Agent({ rejectUnauthorized }); 207 | return request; 208 | }; 209 | } 210 | 211 | export function formEncoding(): IPretendRequestInterceptor { 212 | return (request) => { 213 | if (request.options.method !== 'GET') { 214 | request.options.headers = new Headers(request.options.headers); 215 | request.options.headers.set( 216 | 'Content-Type', 217 | 'application/x-www-form-urlencoded' 218 | ); 219 | if (request.options.body) { 220 | const body = JSON.parse(request.options.body.toString()); 221 | const encodedBody = Object.keys(body) 222 | .reduce((query, name) => { 223 | return `${query}&${name}=${encodeURIComponent(body[name])}`; 224 | }, '') 225 | .replace(/^&/, ''); 226 | request.options.body = encodedBody; 227 | } 228 | } 229 | return request; 230 | }; 231 | } 232 | 233 | export function gitlabDecoder(): IPretendDecoder { 234 | return async (response) => { 235 | if (response.status >= 400) { 236 | const body = await response.json(); 237 | throw new GitLabError(`${body.error || response.statusText}`, response); 238 | } 239 | const headers = {}; 240 | response.headers.forEach((value, key) => { 241 | headers[key] = [...(headers[key] || []), value]; 242 | }); 243 | return { 244 | status: response.status, 245 | headers, 246 | body: 247 | response.status >= 200 && response.status <= 300 248 | ? await response.json() 249 | : undefined, 250 | }; 251 | }; 252 | } 253 | 254 | export class GitLabBlueprint implements GitLab { 255 | @Get('/projects') 256 | public getProjects(): any { 257 | /* */ 258 | } 259 | 260 | @Get('/user', true) 261 | public getAuthenticatedUser(): any { 262 | /* */ 263 | } 264 | 265 | @Get('/users', true) 266 | public searchUser(): any { 267 | /* */ 268 | } 269 | 270 | @Get('/projects/:id') 271 | public getProject(): any { 272 | /* */ 273 | } 274 | 275 | @Get('/projects/:id/merge_requests', true) 276 | public getMergeRequests(): any { 277 | /* */ 278 | } 279 | 280 | @Get('/projects/:id/merge_requests/:merge_request_iid') 281 | public getMergeRequest(): any { 282 | /* */ 283 | } 284 | 285 | @Post('/projects/:id/merge_requests') 286 | public createMergeRequest(): any { 287 | /* */ 288 | } 289 | 290 | @Put('/projects/:id/merge_requests/:merge_request_iid') 291 | public updateMergeRequest(): any { 292 | /* */ 293 | } 294 | 295 | @Put('/projects/:id/merge_requests/:merge_request_iid/merge') 296 | public acceptMergeRequest(): any { 297 | /* */ 298 | } 299 | 300 | @Get('/projects/:id/issues') 301 | public getProjectIssues(): any { 302 | /* */ 303 | } 304 | 305 | @Get('/projects/:id/issues/:issue_iid/notes') 306 | public getIssueNotes(): any { 307 | /* */ 308 | } 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/provider/gitlab/client.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Client, Response } from '../client'; 3 | import { getClient, GitLab } from './api'; 4 | import { GitLabRepository } from './repository'; 5 | import { GitLabUser } from './user'; 6 | 7 | export class GitLabClient implements Client { 8 | private readonly client: GitLab; 9 | 10 | public name = 'GitLab Client'; 11 | 12 | constructor( 13 | protocol: string, 14 | hostname: string, 15 | token: string, 16 | logger: (message: string) => void, 17 | allowUnsafeSSL: boolean 18 | ) { 19 | this.client = getClient( 20 | this.getApiEndpoint(protocol, hostname), 21 | token, 22 | logger, 23 | allowUnsafeSSL 24 | ); 25 | } 26 | 27 | private getApiEndpoint(protocol: string, hostname: string): string { 28 | return `${protocol}//${hostname}/api/v4`; 29 | } 30 | 31 | public async test(): Promise { 32 | await this.client.getProjects(); 33 | } 34 | 35 | public async getCurrentUser(): Promise> { 36 | const response = await this.client.getAuthenticatedUser(); 37 | return { 38 | body: new GitLabUser(this.client, response.body) 39 | }; 40 | } 41 | 42 | public async getRepository( 43 | uri: vscode.Uri, 44 | rid: string 45 | ): Promise> { 46 | const response = (await this.client.getProject(encodeURIComponent(rid))) 47 | .body; 48 | return { 49 | body: new GitLabRepository(uri, this.client, response) 50 | }; 51 | } 52 | 53 | public async getUserByUsername( 54 | username: string 55 | ): Promise> { 56 | const response = await this.client.searchUser({ 57 | username 58 | }); 59 | return { 60 | body: new GitLabUser(this.client, response.body[0]) 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/provider/gitlab/issue.ts: -------------------------------------------------------------------------------- 1 | import { Response } from '../client'; 2 | import { Issue, IssueComment } from '../issue'; 3 | import { GitLab, Issue as IssueStruct } from './api'; 4 | import { GitLabRepository } from './repository'; 5 | import { GitLabUser } from './user'; 6 | 7 | export class GitLabIssue implements Issue { 8 | 9 | private readonly client: GitLab; 10 | private readonly repository: GitLabRepository; 11 | private readonly struct: IssueStruct; 12 | 13 | public get number(): number { 14 | return this.struct.iid; 15 | } 16 | 17 | public get title(): string { 18 | return this.struct.title; 19 | } 20 | 21 | public get url(): string { 22 | return this.struct.web_url; 23 | } 24 | 25 | public get body(): string { 26 | return this.struct.description; 27 | } 28 | 29 | constructor(client: GitLab, repository: GitLabRepository, struct: IssueStruct) { 30 | this.client = client; 31 | this.repository = repository; 32 | this.struct = struct; 33 | } 34 | 35 | public async comments(): Promise> { 36 | const response = await this.client.getIssueNotes( 37 | encodeURIComponent(this.repository.pathWithNamespace), 38 | this.number 39 | ); 40 | return { 41 | body: response.body.map(note => ({ 42 | body: note.body, 43 | user: new GitLabUser(this.client, note.author) 44 | })) 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/provider/gitlab/merge-request.ts: -------------------------------------------------------------------------------- 1 | import { getConfiguration } from '../../helper'; 2 | import { Response } from '../client'; 3 | import { 4 | PullRequest, 5 | MergeBody, 6 | MergeResult, 7 | RequestReviewBody, 8 | CancelReviewBody, 9 | Comment, 10 | UpdateBody 11 | } from '../pull-request'; 12 | import { GitLab, MergeRequest, UpdateMergeRequestBody } from './api'; 13 | import { GitLabRepository } from './repository'; 14 | import { GitLabUser } from './user'; 15 | 16 | export class GitLabMergeRequest implements PullRequest { 17 | private readonly client: GitLab; 18 | private readonly repository: GitLabRepository; 19 | private readonly mergeRequest: MergeRequest; 20 | 21 | public get id(): number { 22 | return this.mergeRequest.id; 23 | } 24 | 25 | public get number(): number { 26 | return this.mergeRequest.iid; 27 | } 28 | 29 | public get state(): 'open' | 'closed' { 30 | switch (this.mergeRequest.state) { 31 | case 'opened': 32 | return 'open'; 33 | case 'closed': 34 | case 'merged': 35 | return 'closed'; 36 | } 37 | } 38 | 39 | public get title(): string { 40 | return this.mergeRequest.title; 41 | } 42 | 43 | public get body(): string { 44 | return this.mergeRequest.description; 45 | } 46 | 47 | public get url(): string { 48 | return this.mergeRequest.web_url; 49 | } 50 | 51 | public get sourceBranch(): string { 52 | return this.mergeRequest.source_branch; 53 | } 54 | 55 | public get targetBranch(): string { 56 | return this.mergeRequest.target_branch; 57 | } 58 | 59 | public get mergeable(): boolean { 60 | switch (this.mergeRequest.merge_status) { 61 | case 'can_be_merged': 62 | return true; 63 | } 64 | } 65 | 66 | public get head(): never { 67 | throw new Error('not implemented'); 68 | } 69 | 70 | public get base(): never { 71 | throw new Error('not implemented'); 72 | } 73 | 74 | constructor( 75 | client: GitLab, 76 | repository: GitLabRepository, 77 | mergeRequest: MergeRequest 78 | ) { 79 | this.client = client; 80 | this.repository = repository; 81 | this.mergeRequest = mergeRequest; 82 | } 83 | 84 | public async update(body: UpdateBody): Promise { 85 | const gitlabBody: UpdateMergeRequestBody = {}; 86 | if (body.title) { 87 | gitlabBody.title = body.title; 88 | } 89 | if (body.body) { 90 | gitlabBody.description = body.body; 91 | } 92 | if (body.state) { 93 | const mapState = ( 94 | state: UpdateBody['state'] 95 | ): UpdateMergeRequestBody['state_event'] => { 96 | switch (state) { 97 | case 'open': 98 | return 'reopen'; 99 | case 'closed': 100 | return 'close'; 101 | default: 102 | return undefined; 103 | } 104 | }; 105 | gitlabBody.state_event = mapState(body.state); 106 | } 107 | await this.client.updateMergeRequest( 108 | encodeURIComponent(this.repository.pathWithNamespace), 109 | this.mergeRequest.iid, 110 | gitlabBody 111 | ); 112 | } 113 | 114 | public async getComments(): Promise> { 115 | throw new Error('Method not implemented.'); 116 | } 117 | 118 | public async merge(_body: MergeBody): Promise> { 119 | const removeSourceBranch = this.repository.uri 120 | ? getConfiguration('gitlab', this.repository.uri).removeSourceBranch 121 | : false; 122 | const response = await this.client.acceptMergeRequest( 123 | encodeURIComponent(this.repository.pathWithNamespace), 124 | this.mergeRequest.iid, 125 | { 126 | should_remove_source_branch: removeSourceBranch 127 | } 128 | ); 129 | return { 130 | body: { 131 | message: response.body.title, 132 | merged: response.body.state === 'merged', 133 | sha: response.body.sha 134 | } 135 | }; 136 | } 137 | 138 | public async assign(assignees: GitLabUser[]): Promise { 139 | await this.client.updateMergeRequest( 140 | encodeURIComponent(this.repository.pathWithNamespace), 141 | this.mergeRequest.iid, 142 | { 143 | assignee_id: assignees[0].id 144 | } 145 | ); 146 | } 147 | 148 | public async unassign(): Promise { 149 | // note: assign to '0' 150 | await this.client.updateMergeRequest( 151 | encodeURIComponent(this.repository.pathWithNamespace), 152 | this.mergeRequest.iid, 153 | { 154 | assignee_id: 0 155 | } 156 | ); 157 | } 158 | 159 | public async requestReview(_body: RequestReviewBody): Promise { 160 | throw new Error('Method not implemented.'); 161 | } 162 | 163 | public async cancelReview(_body: CancelReviewBody): Promise { 164 | throw new Error('Method not implemented.'); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/provider/gitlab/repository.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { getConfiguration } from '../../helper'; 3 | import { Response } from '../client'; 4 | import { Issue } from '../issue'; 5 | import { 6 | Repository, 7 | ListPullRequestsParameters, 8 | CreatePullRequestBody, 9 | IssuesParameters 10 | } from '../repository'; 11 | import { 12 | GitLab, 13 | Project, 14 | GetMergeRequestParameters, 15 | CreateMergeRequestBody, 16 | ProjectIssuesBody 17 | } from './api'; 18 | import { GitLabIssue } from './issue'; 19 | import { GitLabMergeRequest } from './merge-request'; 20 | import { GitLabUser } from './user'; 21 | 22 | export class GitLabRepository implements Repository { 23 | public readonly uri: vscode.Uri | undefined; 24 | 25 | private readonly client: GitLab; 26 | private readonly project: Project; 27 | 28 | public get name(): string { 29 | return this.project.name; 30 | } 31 | 32 | public get pathWithNamespace(): string { 33 | return this.project.path_with_namespace; 34 | } 35 | 36 | public get defaultBranch(): string { 37 | return this.project.default_branch; 38 | } 39 | 40 | public get allowMergeCommits(): boolean { 41 | return this.project.merge_requests_enabled; 42 | } 43 | 44 | public get allowSquashCommits(): boolean { 45 | return false; 46 | } 47 | 48 | public get allowRebaseCommits(): boolean { 49 | return false; 50 | } 51 | 52 | public get parent(): Repository | undefined { 53 | return undefined; 54 | } 55 | 56 | public get url(): string { 57 | return this.project.web_url; 58 | } 59 | 60 | public get cloneUrl(): string { 61 | throw new Error('not implemented'); 62 | } 63 | 64 | constructor(uri: vscode.Uri | undefined, client: GitLab, project: Project) { 65 | this.uri = uri; 66 | this.client = client; 67 | this.project = project; 68 | } 69 | 70 | public async getPullRequests( 71 | parameters: ListPullRequestsParameters = {} 72 | ): Promise> { 73 | function getState( 74 | state: ListPullRequestsParameters['state'] 75 | ): GetMergeRequestParameters['state'] { 76 | switch (state) { 77 | case 'open': 78 | return 'opened'; 79 | case 'close': 80 | return 'closed'; 81 | default: 82 | return undefined; 83 | } 84 | } 85 | function getOrderBy( 86 | orderBy: ListPullRequestsParameters['sort'] 87 | ): GetMergeRequestParameters['order_by'] { 88 | switch (orderBy) { 89 | case 'created': 90 | return 'created_at'; 91 | case 'updated': 92 | return 'updated_at'; 93 | default: 94 | return undefined; 95 | } 96 | } 97 | 98 | const params: GetMergeRequestParameters = {}; 99 | if (parameters.state) { 100 | params.state = getState(parameters.state); 101 | } 102 | if (parameters.sort) { 103 | params.order_by = getOrderBy(parameters.sort); 104 | } 105 | if (parameters.direction) { 106 | params.sort = parameters.direction; 107 | } 108 | const respose = await this.client.getMergeRequests( 109 | encodeURIComponent(this.project.path_with_namespace), 110 | params 111 | ); 112 | return { 113 | body: respose.body.map( 114 | mergeRequest => new GitLabMergeRequest(this.client, this, mergeRequest) 115 | ) 116 | }; 117 | } 118 | 119 | public async getPullRequest( 120 | id: number 121 | ): Promise> { 122 | const response = await this.client.getMergeRequest( 123 | encodeURIComponent(this.project.path_with_namespace), 124 | id 125 | ); 126 | return { 127 | body: new GitLabMergeRequest(this.client, this, response.body) 128 | }; 129 | } 130 | 131 | public async createPullRequest( 132 | body: CreatePullRequestBody 133 | ): Promise> { 134 | const removeSourceBranch = this.uri 135 | ? getConfiguration('gitlab', this.uri).removeSourceBranch 136 | : false; 137 | const gitlabBody: CreateMergeRequestBody = { 138 | source_branch: body.sourceBranch, 139 | target_branch: body.targetBranch, 140 | title: body.title, 141 | remove_source_branch: removeSourceBranch 142 | }; 143 | if (body.body) { 144 | gitlabBody.description = body.body; 145 | } 146 | const response = await this.client.createMergeRequest( 147 | encodeURIComponent(this.project.path_with_namespace), 148 | gitlabBody 149 | ); 150 | return { 151 | body: new GitLabMergeRequest(this.client, this, response.body) 152 | }; 153 | } 154 | 155 | public async getIssues( 156 | parameters?: IssuesParameters | undefined 157 | ): Promise> { 158 | function getState( 159 | state: IssuesParameters['state'] 160 | ): ProjectIssuesBody['state'] { 161 | switch (state) { 162 | case 'open': 163 | return 'opened'; 164 | case 'closed': 165 | return 'closed'; 166 | } 167 | return undefined; 168 | } 169 | function getOrderBy( 170 | orderBy: IssuesParameters['sort'] 171 | ): ProjectIssuesBody['order_by'] { 172 | switch (orderBy) { 173 | case 'created': 174 | return 'created_at'; 175 | case 'updated': 176 | return 'updated_at'; 177 | default: 178 | return undefined; 179 | } 180 | } 181 | 182 | const body: ProjectIssuesBody = {}; 183 | if (parameters) { 184 | if (parameters.state && parameters.state !== 'all') { 185 | body.state = getState(parameters.state); 186 | } 187 | if (parameters.sort) { 188 | body.order_by = getOrderBy(parameters.sort); 189 | } 190 | if (parameters.direction) { 191 | body.sort = parameters.direction; 192 | } 193 | } 194 | const response = await this.client.getProjectIssues( 195 | encodeURIComponent(this.project.path_with_namespace), 196 | body 197 | ); 198 | return { 199 | body: response.body.map( 200 | issue => new GitLabIssue(this.client, this, issue) 201 | ) 202 | }; 203 | } 204 | 205 | public async getUsers(): Promise> { 206 | const response = await this.client.searchUser(); 207 | return { 208 | body: response.body.map(user => new GitLabUser(this.client, user)) 209 | }; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/provider/gitlab/user.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../user'; 2 | import { GitLab, UserResponse } from './api'; 3 | 4 | export class GitLabUser implements User { 5 | 6 | // private client: GitLab; 7 | 8 | private readonly struct: UserResponse; 9 | 10 | public get id(): number { 11 | return this.struct.id; 12 | } 13 | 14 | public get username(): string { 15 | return this.struct.username; 16 | } 17 | 18 | constructor(_client: GitLab, struct: UserResponse) { 19 | // this.client = client; 20 | this.struct = struct; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/provider/issue.ts: -------------------------------------------------------------------------------- 1 | import { Response } from './client'; 2 | import { User } from './user'; 3 | 4 | export interface Issue { 5 | number: number; 6 | title: string; 7 | url: string; 8 | body: string; 9 | 10 | comments(): Promise>; 11 | } 12 | 13 | export interface IssueComment { 14 | user: User; 15 | body: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/provider/pull-request.ts: -------------------------------------------------------------------------------- 1 | import { Response } from './client'; 2 | import { Repository } from './repository'; 3 | import { User } from './user'; 4 | 5 | export interface PullRequest { 6 | id: number; 7 | number: number; 8 | state: 'open' | 'closed'; 9 | title: string; 10 | body: string; 11 | url: string; 12 | sourceBranch: string; 13 | targetBranch: string; 14 | mergeable?: boolean | null; 15 | head: { 16 | repository: Repository; 17 | }; 18 | base: { 19 | repository: Repository; 20 | }; 21 | 22 | update(body: UpdateBody): Promise; 23 | getComments(): Promise>; 24 | merge(body: MergeBody): Promise>; 25 | assign(assignees: User[]): Promise; 26 | unassign(): Promise; 27 | requestReview(body: RequestReviewBody): Promise; 28 | cancelReview(body: CancelReviewBody): Promise; 29 | } 30 | 31 | export interface MergeBody { 32 | mergeMethod: MergeMethod; 33 | } 34 | 35 | export type MergeMethod = 'merge' | 'squash' | 'rebase'; 36 | 37 | export interface MergeResult { 38 | sha?: string; 39 | merged?: boolean; 40 | message: string; 41 | } 42 | 43 | export interface UpdateBody { 44 | title?: string; 45 | body?: string; 46 | state?: 'open' | 'closed'; 47 | } 48 | 49 | export interface RequestReviewBody { 50 | reviewers: string[]; 51 | } 52 | 53 | export interface CancelReviewBody { 54 | reviewers: string[]; 55 | } 56 | 57 | export interface Comment { 58 | file: string; 59 | line: number; 60 | body: string; 61 | } 62 | -------------------------------------------------------------------------------- /src/provider/repository.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Response } from './client'; 3 | import { Issue } from './issue'; 4 | import { PullRequest } from './pull-request'; 5 | import { User } from './user'; 6 | 7 | export interface Repository { 8 | uri: vscode.Uri | undefined; 9 | name: string; 10 | defaultBranch: string; 11 | allowMergeCommits: boolean; 12 | allowSquashCommits: boolean; 13 | allowRebaseCommits: boolean; 14 | parent: Repository | undefined; 15 | url: string; 16 | cloneUrl: string; 17 | 18 | getPullRequests( 19 | parameters?: ListPullRequestsParameters 20 | ): Promise>; 21 | getPullRequest(id: number): Promise>; 22 | createPullRequest( 23 | body: CreatePullRequestBody 24 | ): Promise>; 25 | getIssues(parameters?: IssuesParameters): Promise>; 26 | getUsers(): Promise>; 27 | } 28 | 29 | export interface ListPullRequestsParameters { 30 | state?: 'open' | 'close' | 'all'; 31 | sort?: 'created' | 'updated'; 32 | direction?: 'asc' | 'desc'; 33 | } 34 | 35 | export interface CreatePullRequestBody { 36 | sourceBranch: string; 37 | targetBranch: string; 38 | title: string; 39 | body?: string; 40 | } 41 | 42 | export interface IssuesParameters { 43 | state?: 'closed' | 'all' | 'open'; 44 | sort?: 'created' | 'updated'; 45 | direction?: 'asc' | 'desc'; 46 | } 47 | -------------------------------------------------------------------------------- /src/provider/user.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number; 3 | username: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/status-bar-manager.ts: -------------------------------------------------------------------------------- 1 | import { component, inject, initialize } from 'tsdi'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { Git } from './git'; 5 | import { getConfiguration } from './helper'; 6 | import { GitHubError, PullRequestStatus } from './provider/github/api'; 7 | import { PullRequest } from './provider/pull-request'; 8 | import { WorkflowManager } from './workflow-manager'; 9 | 10 | const colors = { 11 | none: '#ffffff', 12 | success: '#56e39f', 13 | failure: '#f24236', 14 | pending: '#f6f5ae' 15 | }; 16 | 17 | const githubPullRequestIcon = '$(git-pull-request)'; 18 | 19 | @component 20 | export class StatusBarManager { 21 | private get enabled(): boolean { 22 | const uri = this.getActiveWorkspaceFolder(); 23 | if (!uri) { 24 | return true; 25 | } 26 | return getConfiguration('github', uri).statusbar.enabled; 27 | } 28 | 29 | private get customStatusBarCommand(): string | null { 30 | // #202: migrate from statusBarCommand to statusbar.command 31 | const uri = this.getActiveWorkspaceFolder(); 32 | if (!uri) { 33 | return null; 34 | } 35 | return ( 36 | getConfiguration('github', uri).statusBarCommand || 37 | getConfiguration('github', uri).statusbar.command 38 | ); 39 | } 40 | 41 | private get refreshInterval(): number { 42 | // #202: migrate from refreshPullRequestStatus to statusbar.refresh 43 | const uri = this.getActiveWorkspaceFolder(); 44 | if (!uri) { 45 | return 0; 46 | } 47 | return ( 48 | (getConfiguration('github', uri).refreshPullRequestStatus || 49 | getConfiguration('github', uri).statusbar.refresh) * 1000 50 | ); 51 | } 52 | 53 | private get colored(): boolean { 54 | const uri = this.getActiveWorkspaceFolder(); 55 | if (!uri) { 56 | return true; 57 | } 58 | return getConfiguration('github', uri).statusbar.color; 59 | } 60 | 61 | private get successText(): string | undefined { 62 | const uri = this.getActiveWorkspaceFolder(); 63 | if (!uri) { 64 | return undefined; 65 | } 66 | return getConfiguration('github', uri).statusbar.successText; 67 | } 68 | 69 | private get pendingText(): string | undefined { 70 | const uri = this.getActiveWorkspaceFolder(); 71 | if (!uri) { 72 | return undefined; 73 | } 74 | return getConfiguration('github', uri).statusbar.pendingText; 75 | } 76 | 77 | private get failureText(): string | undefined { 78 | const uri = this.getActiveWorkspaceFolder(); 79 | if (!uri) { 80 | return undefined; 81 | } 82 | return getConfiguration('github', uri).statusbar.failureText; 83 | } 84 | 85 | @inject('vscode.ExtensionContext') 86 | private readonly context!: vscode.ExtensionContext; 87 | 88 | @inject private readonly git!: Git; 89 | 90 | private statusBar!: vscode.StatusBarItem; 91 | 92 | @inject private readonly workflowManager!: WorkflowManager; 93 | 94 | @inject('vscode.OutputChannel') 95 | private readonly channel!: vscode.OutputChannel; 96 | 97 | @initialize 98 | protected init(): void { 99 | this.statusBar = vscode.window.createStatusBarItem( 100 | vscode.StatusBarAlignment.Left, 101 | 10 102 | ); 103 | this.statusBar.command = this.customStatusBarCommand || ''; 104 | this.statusBar.text = `${githubPullRequestIcon} ...`; 105 | if (this.colored) { 106 | this.statusBar.color = colors.none; 107 | } else { 108 | this.statusBar.color = undefined; 109 | } 110 | this.context.subscriptions.push(this.statusBar); 111 | 112 | this.refreshStatus().catch(() => { 113 | /* drop error (handled in refreshStatus) */ 114 | }); 115 | 116 | if (!this.enabled) { 117 | this.statusBar.hide(); 118 | } 119 | } 120 | 121 | private async refreshStatus(): Promise { 122 | try { 123 | const uri = this.getActiveWorkspaceFolder(); 124 | if (uri) { 125 | const connect = await this.workflowManager.canConnect(uri); 126 | if (!connect) { 127 | throw connect; 128 | } 129 | await this.updateStatus(); 130 | setTimeout(() => { 131 | this.refreshStatus().catch(() => { 132 | /* drop error (handled below) */ 133 | }); 134 | }, this.refreshInterval); 135 | } 136 | } catch (e) { 137 | if (e instanceof GitHubError) { 138 | console.log(e); 139 | this.channel.appendLine('Failed to update pull request status:'); 140 | this.channel.appendLine(JSON.stringify(e.response, undefined, ' ')); 141 | } else { 142 | throw e; 143 | } 144 | } 145 | } 146 | 147 | private getActiveWorkspaceFolder(): vscode.Uri | undefined { 148 | if (!vscode.workspace.workspaceFolders) { 149 | // no workspace open 150 | return undefined; 151 | } 152 | if (vscode.workspace.workspaceFolders.length === 1) { 153 | // just one workspace open 154 | return vscode.workspace.workspaceFolders[0].uri; 155 | } 156 | // check which workspace status should be visible 157 | const editor = vscode.window.activeTextEditor; 158 | if (!editor) { 159 | return undefined; 160 | } 161 | const folder = vscode.workspace.getWorkspaceFolder(editor.document.uri); 162 | if (!folder) { 163 | return undefined; 164 | } 165 | return folder.uri; 166 | } 167 | 168 | public async updateStatus(): Promise { 169 | if (!this.enabled) { 170 | return; 171 | } 172 | const uri = this.getActiveWorkspaceFolder(); 173 | if (uri) { 174 | const branch = await this.git.getCurrentBranch(uri); 175 | if (branch !== (await this.workflowManager.getDefaultBranch(uri))) { 176 | return this.updatePullRequestStatus(uri); 177 | } 178 | } 179 | this.statusBar.show(); 180 | if (this.colored) { 181 | this.statusBar.color = colors.none; 182 | } else { 183 | this.statusBar.color = undefined; 184 | } 185 | this.statusBar.text = `${githubPullRequestIcon}`; 186 | if (!this.customStatusBarCommand) { 187 | this.statusBar.tooltip = 188 | 'Not on a pull request branch. Click to checkout pull request'; 189 | this.statusBar.command = 'vscode-github.checkoutPullRequests'; 190 | } 191 | } 192 | 193 | private async updatePullRequestStatus(uri: vscode.Uri): Promise { 194 | try { 195 | const pullRequest = await this.workflowManager.getPullRequestForCurrentBranch( 196 | uri 197 | ); 198 | this.statusBar.show(); 199 | if (pullRequest) { 200 | await this.showPullRequestStauts(pullRequest); 201 | } else { 202 | this.showCreatePullRequestStatus(); 203 | } 204 | } catch (e) { 205 | if (e instanceof GitHubError) { 206 | console.log(e); 207 | this.channel.appendLine('Update pull request status error:'); 208 | this.channel.appendLine(JSON.stringify(e.response, undefined, ' ')); 209 | } 210 | throw e; 211 | } 212 | } 213 | 214 | private async showPullRequestStauts(pullRequest: PullRequest): Promise { 215 | const status = await this.calculateMergableStatus(pullRequest); 216 | if (this.colored) { 217 | this.statusBar.color = colors[status]; 218 | } else { 219 | this.statusBar.color = undefined; 220 | } 221 | this.statusBar.text = this.getPullRequestStautsText(pullRequest, status); 222 | if (!this.customStatusBarCommand) { 223 | this.statusBar.tooltip = 224 | status === 'success' ? `Merge pull-request #${pullRequest.number}` : ''; 225 | this.statusBar.command = 226 | status === 'success' ? 'vscode-github.mergePullRequest' : ''; 227 | } 228 | } 229 | 230 | // tslint:disable-next-line:cyclomatic-complexity 231 | private getPullRequestStautsText( 232 | pullRequest: PullRequest, 233 | status: PullRequestStatus 234 | ): string { 235 | let text = '${icon} #${prNumber} ${status}'; 236 | switch (status) { 237 | case 'success': 238 | text = this.successText || text; 239 | break; 240 | case 'pending': 241 | text = this.pendingText || text; 242 | break; 243 | case 'failure': 244 | text = this.failureText || text; 245 | break; 246 | } 247 | return text 248 | .replace('${icon}', githubPullRequestIcon) 249 | .replace('${prNumber}', String(pullRequest.number)) 250 | .replace('${status}', status); 251 | } 252 | 253 | private showCreatePullRequestStatus(): void { 254 | if (this.colored) { 255 | this.statusBar.color = colors.none; 256 | } else { 257 | this.statusBar.color = undefined; 258 | } 259 | this.statusBar.text = `${githubPullRequestIcon} Create PR`; 260 | if (!this.customStatusBarCommand) { 261 | this.statusBar.tooltip = 'Create pull-request for current branch'; 262 | this.statusBar.command = 'vscode-github.createPullRequest'; 263 | } 264 | } 265 | 266 | private async calculateMergableStatus( 267 | pullRequest: PullRequest 268 | ): Promise { 269 | let status: PullRequestStatus = 'pending'; 270 | if (typeof pullRequest.mergeable === 'undefined') { 271 | status = 'failure'; 272 | } else { 273 | if (pullRequest.mergeable) { 274 | status = 'success'; 275 | } else { 276 | status = 'failure'; 277 | } 278 | } 279 | return status; 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/tokens.ts: -------------------------------------------------------------------------------- 1 | import { Memento } from 'vscode'; 2 | 3 | export interface Tokens { 4 | [host: string]: { 5 | token: string; 6 | provider: 'github' | 'gitlab'; 7 | }; 8 | } 9 | 10 | export function migrateToken(memento: Memento): void { 11 | const token = memento.get('token'); 12 | if (token) { 13 | const tokens = memento.get('tokens', {}); 14 | tokens['github.com'] = { 15 | token, 16 | provider: 'github' 17 | }; 18 | memento.update('tokens', tokens); 19 | memento.update(token, undefined); 20 | } 21 | let migrated = false; 22 | const tokens = memento.get<{ [host: string]: string }>('tokens', {}); 23 | const struct = Object.keys(tokens).reduce((akku: Tokens, host) => { 24 | if (typeof tokens[host] === 'string') { 25 | migrated = true; 26 | akku[host] = { 27 | token: tokens[host], 28 | provider: 'github' 29 | }; 30 | } else { 31 | akku[host] = tokens[host] as any; 32 | } 33 | return akku; 34 | }, {}); 35 | if (migrated) { 36 | memento.update('tokens', struct); 37 | } 38 | } 39 | 40 | export function getTokens(memento: Memento): Tokens { 41 | return memento.get('tokens', {}); 42 | } 43 | 44 | export function listTokenHosts(memento: Memento): string[] { 45 | const tokens: Tokens | undefined = memento.get('tokens'); 46 | if (!tokens) { 47 | return []; 48 | } 49 | return Object.keys(tokens); 50 | } 51 | 52 | export function removeToken(memento: Memento, host: string): void { 53 | const tokens: Tokens | undefined = memento.get('tokens'); 54 | if (tokens) { 55 | delete tokens[host]; 56 | memento.update('tokens', tokens); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/workflow-manager.ts: -------------------------------------------------------------------------------- 1 | import { component, inject } from 'tsdi'; 2 | import * as vscode from 'vscode'; 3 | import { Git } from './git'; 4 | import { getConfiguration } from './helper'; 5 | import { Client, createClient } from './provider/client'; 6 | import { GitHubError } from './provider/github/api'; 7 | import { Issue, IssueComment } from './provider/issue'; 8 | import { 9 | Comment, 10 | MergeBody, 11 | MergeMethod, 12 | PullRequest, 13 | } from './provider/pull-request'; 14 | import { 15 | CreatePullRequestBody, 16 | ListPullRequestsParameters, 17 | Repository, 18 | } from './provider/repository'; 19 | import { User } from './provider/user'; 20 | import { getTokens } from './tokens'; 21 | 22 | @component 23 | export class WorkflowManager { 24 | @inject({ name: 'vscode.ExtensionContext' }) 25 | private readonly context!: vscode.ExtensionContext; 26 | 27 | @inject('vscode.OutputChannel') 28 | private readonly channel!: vscode.OutputChannel; 29 | 30 | @inject private readonly git!: Git; 31 | 32 | private providers: { [cwd: string]: Client } = {}; 33 | 34 | private async connect(uri: vscode.Uri): Promise { 35 | const logger = (message: string) => this.log(message); 36 | const provider = await createClient( 37 | this.git, 38 | getTokens(this.context.globalState), 39 | uri, 40 | logger 41 | ); 42 | try { 43 | await provider.test(); 44 | } catch (e) { 45 | throw new Error( 46 | `Connection with ${provider.name} failed. Please make sure your git executable` + 47 | `is setup correct, and your token has enought access rights.` 48 | ); 49 | } 50 | this.log(`Connected with provider ${provider.name}`); 51 | return provider; 52 | } 53 | 54 | private async getProvider(uri: vscode.Uri): Promise { 55 | if (!this.providers[uri.fsPath]) { 56 | this.providers[uri.fsPath] = await this.connect(uri); 57 | } 58 | return this.providers[uri.fsPath]; 59 | } 60 | 61 | public resetProviders(): void { 62 | this.providers = {}; 63 | } 64 | 65 | private log(message: string, obj?: any): void { 66 | const formatted = 67 | `${message} ` + (obj ? JSON.stringify(obj, undefined, ' ') : ''); 68 | this.channel.appendLine(formatted); 69 | console.log(formatted); 70 | } 71 | 72 | public async canConnect(uri: vscode.Uri): Promise { 73 | try { 74 | await this.getProvider(uri); 75 | return true; 76 | } catch (e) { 77 | this.log(`Failed to connect to provider`, (e as Error).message); 78 | return e as Error; 79 | } 80 | } 81 | 82 | public async getRepository(uri: vscode.Uri): Promise { 83 | const [owner, repository] = await this.git.getGitProviderOwnerAndRepository( 84 | uri 85 | ); 86 | const provider = await this.getProvider(uri); 87 | return (await provider.getRepository(uri, `${owner}/${repository}`)).body; 88 | } 89 | 90 | public async getDefaultBranch(uri: vscode.Uri): Promise { 91 | return (await this.getRepository(uri)).defaultBranch; 92 | } 93 | 94 | public async getEnabledMergeMethods( 95 | uri: vscode.Uri 96 | ): Promise> { 97 | const repo = await this.getRepository(uri); 98 | const set = new Set(); 99 | if (repo.allowMergeCommits) { 100 | set.add('merge'); 101 | } 102 | if (repo.allowSquashCommits) { 103 | set.add('squash'); 104 | } 105 | if (repo.allowRebaseCommits) { 106 | set.add('rebase'); 107 | } 108 | return set; 109 | } 110 | 111 | public async getPullRequestForCurrentBranch( 112 | uri: vscode.Uri 113 | ): Promise { 114 | const branch = await this.git.getCurrentBranch(uri); 115 | const list = (await this.listPullRequests(uri)).filter( 116 | (pr) => pr.sourceBranch === branch 117 | ); 118 | if (list.length !== 1) { 119 | return undefined; 120 | } 121 | const repository = await this.getRepository(uri); 122 | return (await repository.getPullRequest(list[0].number)).body; 123 | } 124 | 125 | public async hasPullRequestForCurrentBranch( 126 | uri: vscode.Uri 127 | ): Promise { 128 | return Boolean(await this.getPullRequestForCurrentBranch(uri)); 129 | } 130 | 131 | public async createPullRequest( 132 | uri: vscode.Uri, 133 | upstream?: { owner: string; repository: string; branch: string } 134 | ): Promise { 135 | if (await this.hasPullRequestForCurrentBranch(uri)) { 136 | return undefined; 137 | } 138 | const branch = await this.git.getCurrentBranch(uri); 139 | if (!branch) { 140 | throw new Error('No current branch'); 141 | } 142 | const defaultBranch = await this.getDefaultBranch(uri); 143 | this.log(`Create pull request on branch '${branch}'`); 144 | const firstCommit = await this.git.getFirstCommitOnBranch( 145 | branch, 146 | defaultBranch, 147 | uri 148 | ); 149 | this.log(`First commit on branch '${firstCommit}'`); 150 | const requestBody = await this.git.getPullRequestBody(firstCommit, uri); 151 | if (requestBody === undefined) { 152 | vscode.window.showWarningMessage( 153 | `For some unknown reason no pull request body could be build; Aborting operation` 154 | ); 155 | return undefined; 156 | } 157 | 158 | const requestTitle = await this.getTitle(firstCommit, uri); 159 | if (requestTitle === undefined) { 160 | this.channel.appendLine( 161 | `No pull request title created; Aborting operation` 162 | ); 163 | return undefined; 164 | } 165 | 166 | return this.createPullRequestFromData( 167 | { 168 | upstream, 169 | sourceBranch: branch, 170 | targetBranch: upstream ? upstream.branch : defaultBranch, 171 | title: requestTitle, 172 | body: requestBody, 173 | }, 174 | uri 175 | ); 176 | } 177 | 178 | private async getTitle( 179 | firstCommit: string, 180 | uri: vscode.Uri 181 | ): Promise { 182 | const customTitle = getConfiguration('github', uri).customPullRequestTitle; 183 | if (customTitle) { 184 | const title = await vscode.window.showInputBox({ 185 | prompt: 'Pull request title', 186 | }); 187 | if (!title) { 188 | return undefined; 189 | } 190 | return title; 191 | } 192 | return this.git.getCommitMessage(firstCommit, uri); 193 | } 194 | 195 | public async createPullRequestFromData( 196 | { 197 | upstream, 198 | sourceBranch, 199 | targetBranch, 200 | title, 201 | body, 202 | }: { 203 | upstream?: { owner: string; repository: string }; 204 | sourceBranch: string; 205 | targetBranch: string; 206 | title: string; 207 | body?: string; 208 | }, 209 | uri: vscode.Uri 210 | ): Promise { 211 | if (await this.hasPullRequestForCurrentBranch(uri)) { 212 | return undefined; 213 | } 214 | this.log(`Create pull request on branch '${sourceBranch}'`); 215 | const pullRequestBody: CreatePullRequestBody = { 216 | sourceBranch, 217 | targetBranch, 218 | title, 219 | body, 220 | }; 221 | this.log('pull request body:', pullRequestBody); 222 | 223 | const getRepository = async () => { 224 | if (upstream) { 225 | const provider = await this.getProvider(uri); 226 | return ( 227 | await provider.getRepository( 228 | uri, 229 | `${upstream.owner}/${upstream.repository}` 230 | ) 231 | ).body; 232 | } else { 233 | return this.getRepository(uri); 234 | } 235 | }; 236 | return this.doCreatePullRequest(await getRepository(), pullRequestBody); 237 | } 238 | 239 | private async doCreatePullRequest( 240 | repository: Repository, 241 | body: CreatePullRequestBody 242 | ): Promise { 243 | try { 244 | return (await repository.createPullRequest(body)).body; 245 | } catch (e) { 246 | if (e instanceof GitHubError) { 247 | this.log('Create pull request error:', e.response); 248 | } 249 | throw e; 250 | } 251 | } 252 | 253 | public async updatePullRequest( 254 | pullRequest: PullRequest, 255 | uri: vscode.Uri 256 | ): Promise { 257 | if (await this.hasPullRequestForCurrentBranch(uri)) { 258 | return undefined; 259 | } 260 | const branch = await this.git.getCurrentBranch(uri); 261 | if (!branch) { 262 | throw new Error('No current branch'); 263 | } 264 | this.log(`Update pull request on branch '${branch}'`); 265 | const firstCommit = await this.git.getFirstCommitOnBranch( 266 | branch, 267 | pullRequest.targetBranch, 268 | uri 269 | ); 270 | this.log(`First commit on branch '${firstCommit}'`); 271 | const requestBody = await this.git.getPullRequestBody(firstCommit, uri); 272 | if (requestBody === undefined) { 273 | vscode.window.showWarningMessage( 274 | `For some unknown reason no pull request body could be build; Aborting operation` 275 | ); 276 | return undefined; 277 | } 278 | if (requestBody !== pullRequest.body) { 279 | await pullRequest.update({ 280 | title: await this.git.getCommitMessage(firstCommit, uri), 281 | body: requestBody, 282 | }); 283 | } 284 | } 285 | 286 | public async listPullRequests(uri: vscode.Uri): Promise { 287 | const repository = await this.getRepository(uri); 288 | const parameters: ListPullRequestsParameters = { 289 | state: 'open', 290 | }; 291 | return (await repository.getPullRequests(parameters)).body; 292 | } 293 | 294 | public async mergePullRequest( 295 | pullRequest: PullRequest, 296 | method: MergeMethod 297 | ): Promise { 298 | try { 299 | if (pullRequest.mergeable) { 300 | const body: MergeBody = { 301 | mergeMethod: method, 302 | }; 303 | const result = await pullRequest.merge(body); 304 | return result.body.merged; 305 | } 306 | return undefined; 307 | } catch (e) { 308 | if (!(e instanceof GitHubError)) { 309 | throw e; 310 | } 311 | this.log('Error while merging:', await e.response.json()); 312 | // status 405 (method not allowed) 313 | // tslint:disable-next-line:comment-format 314 | // TODO... 315 | return false; 316 | } 317 | } 318 | 319 | public async getRepositoryUrl(uri: vscode.Uri): Promise { 320 | const repository = await this.getRepository(uri); 321 | return repository.url; 322 | } 323 | 324 | public async getIssueUrl( 325 | uri: vscode.Uri, 326 | id: string 327 | ): Promise { 328 | const hostname = await this.git.getGitHostname(uri); 329 | const [owner, repo] = await this.git.getGitProviderOwnerAndRepository(uri); 330 | return `https://${hostname}/${owner}/${repo}/issues/${id}`; 331 | } 332 | 333 | public async getGithubFileUrl( 334 | uri: vscode.Uri, 335 | file: string, 336 | line = 0, 337 | endLine = 0 338 | ): Promise { 339 | const hostname = await this.git.getGitHostname(uri); 340 | const [owner, repo] = await this.git.getGitProviderOwnerAndRepository(uri); 341 | const branch = await this.git.getCurrentBranch(uri); 342 | const linuxStylePaths = file.replaceAll('\\', '/'); 343 | const currentFile = linuxStylePaths.replace(/^\//, ''); 344 | return `https://${hostname}/${owner}/${repo}/blob/${branch}/${currentFile}#L${ 345 | line + 1 346 | }:L${endLine + 1}`; 347 | } 348 | 349 | public async getAssignees(uri: vscode.Uri): Promise { 350 | const repository = await this.getRepository(uri); 351 | try { 352 | return (await repository.getUsers()).body; 353 | } catch (e) { 354 | this.log((e as Error).message); 355 | return []; 356 | } 357 | } 358 | 359 | public async addAssignee( 360 | pullRequest: PullRequest, 361 | name: string, 362 | uri: vscode.Uri 363 | ): Promise { 364 | this.log('Add assignee', { pullRequest, name }); 365 | const provider = await this.getProvider(uri); 366 | const user = await provider.getUserByUsername(name); 367 | await pullRequest.assign([user.body]); 368 | } 369 | 370 | public async removeAssignee(pullRequest: PullRequest): Promise { 371 | this.log('Remove assignee', { pullRequest }); 372 | await pullRequest.unassign(); 373 | } 374 | 375 | public async requestReview( 376 | issue: number, 377 | name: string, 378 | uri: vscode.Uri 379 | ): Promise { 380 | const repository = await this.getRepository(uri); 381 | const pullRequest = await repository.getPullRequest(issue); 382 | await pullRequest.body.requestReview({ 383 | reviewers: [name], 384 | }); 385 | } 386 | 387 | public async deleteReviewRequest( 388 | issue: number, 389 | name: string, 390 | uri: vscode.Uri 391 | ): Promise { 392 | const repository = await this.getRepository(uri); 393 | const pullRequest = await repository.getPullRequest(issue); 394 | await pullRequest.body.cancelReview({ 395 | reviewers: [name], 396 | }); 397 | } 398 | 399 | public async issues( 400 | uri: vscode.Uri, 401 | state: 'closed' | 'all' | 'open' = 'all' 402 | ): Promise { 403 | const repository = await this.getRepository(uri); 404 | const result = await repository.getIssues({ 405 | sort: 'updated', 406 | direction: 'desc', 407 | state, 408 | }); 409 | return result.body; 410 | } 411 | 412 | public async getPullRequestReviewComments( 413 | pullRequest: PullRequest 414 | ): Promise { 415 | return (await pullRequest.getComments()).body; 416 | } 417 | 418 | public async getIssueComments(issue: Issue): Promise { 419 | return (await issue.comments()).body; 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /test/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // you can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../src/extension'; 7 | import { Git } from '../src/git'; 8 | import * as tokens from '../src/tokens'; 9 | 10 | suite('vscode-github extension tests', () => { 11 | test('Extension should be active after startup', (done) => { 12 | setTimeout(() => { 13 | const extension = vscode.extensions.getExtension( 14 | 'KnisterPeter.vscode-github' 15 | ); 16 | assert.ok(extension); 17 | assert.equal(extension.isActive, true); 18 | done(); 19 | }, 1000 * 3); 20 | }).timeout(1000 * 10); 21 | 22 | test('should register commands', (done) => { 23 | vscode.commands 24 | .getCommands(true) 25 | .then((commands) => 26 | commands.filter((command) => command.startsWith('vscode-github')) 27 | ) 28 | .then((commands) => { 29 | assert.equal(commands.length > 0, true); 30 | }) 31 | .then(() => done()); 32 | }); 33 | 34 | test('should parse username and repository from github ssh url', () => { 35 | const git = new Git(); 36 | const [proto, host, user, repo] = git.parseGitUrl( 37 | 'git@github:username/repo.git' 38 | ); 39 | assert.equal(proto, 'git:'); 40 | assert.equal(host, 'github'); 41 | assert.equal(user, 'username'); 42 | assert.equal(repo, 'repo'); 43 | }); 44 | 45 | test('should parse username and repository from github ssh:// url', () => { 46 | const git = new Git(); 47 | const [proto, host, user, repo] = git.parseGitUrl( 48 | 'git://github/username/repo.git' 49 | ); 50 | assert.equal(proto, 'git:'); 51 | assert.equal(host, 'github'); 52 | assert.equal(user, 'username'); 53 | assert.equal(repo, 'repo'); 54 | }); 55 | 56 | test('should parse protocol from github http:// url', () => { 57 | const git = new Git(); 58 | const [proto, host, user, repo] = git.parseGitUrl( 59 | 'http://my.github.com/username/repo.git' 60 | ); 61 | assert.equal(proto, 'http:'); 62 | assert.equal(host, 'my.github.com'); 63 | assert.equal(user, 'username'); 64 | assert.equal(repo, 'repo'); 65 | }); 66 | 67 | test('should parse username and repository from github git@ url', () => { 68 | const git = new Git(); 69 | const [proto, host, user, repo] = git.parseGitUrl( 70 | 'git@github.mycompany.io:org-name/repo-name.git' 71 | ); 72 | assert.equal(proto, 'git:'); 73 | assert.equal(host, 'github.mycompany.io'); 74 | assert.equal(user, 'org-name'); 75 | assert.equal(repo, 'repo-name'); 76 | }); 77 | 78 | test('should migrate tokens to a provider structure', (done) => { 79 | tokens.migrateToken({ 80 | get(name: string): any { 81 | if (name === 'tokens') { 82 | return { 83 | host: 'token', 84 | }; 85 | } 86 | return undefined; 87 | }, 88 | update(name: string, value: any): Thenable { 89 | return Promise.resolve().then(() => { 90 | if (name === 'tokens') { 91 | assert.deepEqual(value, { 92 | host: { 93 | token: 'token', 94 | provider: 'github', 95 | }, 96 | }); 97 | done(); 98 | } 99 | }); 100 | }, 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | * 4 | * This file is providing the test runner to use when running extension tests. 5 | * By default the test runner in use is Mocha based. 6 | * 7 | * You can provide your own test runner if you want to override it by exporting 8 | * a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | * host can call to run the tests. The test runner is expected to use console.log 10 | * to report the results back to the caller. When the tests are finished, return 11 | * a possible error to the callback or null if none. 12 | */ 13 | // tslint:disable-next-line 14 | const testRunner = require('vscode/lib/testrunner'); 15 | 16 | // you can directly control Mocha options by uncommenting the following lines 17 | // see https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 18 | testRunner.configure({ 19 | ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) 20 | useColors: true // colored output from test results 21 | }); 22 | 23 | module.exports = testRunner; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "lib": [ 5 | "dom", 6 | "es2015", 7 | "es2016", 8 | "es2017", 9 | "esnext" 10 | ], 11 | "module": "commonjs", 12 | "strict": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitReturns": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "outDir": "out", 18 | "pretty": false, 19 | "rootDir": ".", 20 | "sourceMap": true, 21 | "suppressImplicitAnyIndexErrors": true, 22 | "target": "es6", 23 | "emitDecoratorMetadata": true 24 | }, 25 | "exclude": [ 26 | "node_modules", 27 | ".vscode-test" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@knisterpeter/standard-tslint", 3 | "rules": { 4 | "await-promise": [true, "Thenable"], 5 | "no-implicit-dependencies": [true, "dev"], 6 | "space-before-function-paren": false, 7 | "trailing-comma": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /typings/execa.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'execa' { 2 | import {ExecFileOptions} from 'child_process'; 3 | 4 | function execa(command: string, args?: string[], options?: ExecFileOptions): Promise<{stdout: string, stderr: string}>; 5 | namespace execa { 6 | } 7 | export = execa; 8 | } 9 | -------------------------------------------------------------------------------- /typings/fetch-extras.d.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from 'http'; 2 | 3 | declare global { 4 | interface RequestInit { 5 | agent?: Agent; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /typings/sander.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'sander' { 2 | export function readFile(path: string): Promise; 3 | export function unlink(path: string): Promise; 4 | } 5 | --------------------------------------------------------------------------------