├── .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 | [](https://marketplace.visualstudio.com/items?itemName=KnisterPeter.vscode-github)
4 | [](https://marketplace.visualstudio.com/items?itemName=KnisterPeter.vscode-github)
5 | [](https://travis-ci.org/KnisterPeter/vscode-github)
6 | [](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 | 
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 | 
46 |
47 | 
48 |
49 | 
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 |
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 |
--------------------------------------------------------------------------------