Merge remote-tracking branch 'origin/master' into pr-create
This commit is contained in:
commit
a275398dac
39 changed files with 13268 additions and 226 deletions
1
.github/actions/copy-release-to-another-repo/.gitignore
vendored
Normal file
1
.github/actions/copy-release-to-another-repo/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
||||
9
.github/actions/copy-release-to-another-repo/action.yml
vendored
Normal file
9
.github/actions/copy-release-to-another-repo/action.yml
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
name: 'Copy release to another repo'
|
||||
description: 'Copy a release from one repo to another'
|
||||
author: 'probablycorey'
|
||||
runs:
|
||||
using: 'node12'
|
||||
main: './lib/index.js'
|
||||
outputs:
|
||||
asset-url:
|
||||
description: The url of the asset that was copied
|
||||
11328
.github/actions/copy-release-to-another-repo/lib/index.js
vendored
Normal file
11328
.github/actions/copy-release-to-another-repo/lib/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
362
.github/actions/copy-release-to-another-repo/package-lock.json
generated
vendored
Normal file
362
.github/actions/copy-release-to-another-repo/package-lock.json
generated
vendored
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
{
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.0.tgz",
|
||||
"integrity": "sha512-ZKdyhlSlyz38S6YFfPnyNgCDZuAF2T0Qv5eHflNWytPS8Qjvz39bZFMry9Bb/dpSnqWcNeav5yM2CTYpJeY+Dw=="
|
||||
},
|
||||
"@actions/github": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/github/-/github-1.1.0.tgz",
|
||||
"integrity": "sha512-cHf6PyoNMdei13jEdGPhKprIMFmjVVW/dnM5/9QmQDJ1ZTaGVyezUSCUIC/ySNLRvDUpeFwPYMdThSEJldSbUw==",
|
||||
"requires": {
|
||||
"@octokit/graphql": "^2.0.1",
|
||||
"@octokit/rest": "^16.15.0"
|
||||
}
|
||||
},
|
||||
"@octokit/endpoint": {
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.5.1.tgz",
|
||||
"integrity": "sha512-nBFhRUb5YzVTCX/iAK1MgQ4uWo89Gu0TH00qQHoYRCsE12dWcG1OiLd7v2EIo2+tpUKPMOQ62QFy9hy9Vg2ULg==",
|
||||
"requires": {
|
||||
"@octokit/types": "^2.0.0",
|
||||
"is-plain-object": "^3.0.0",
|
||||
"universal-user-agent": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"universal-user-agent": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz",
|
||||
"integrity": "sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==",
|
||||
"requires": {
|
||||
"os-name": "^3.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@octokit/graphql": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-2.1.3.tgz",
|
||||
"integrity": "sha512-XoXJqL2ondwdnMIW3wtqJWEwcBfKk37jO/rYkoxNPEVeLBDGsGO1TCWggrAlq3keGt/O+C/7VepXnukUxwt5vA==",
|
||||
"requires": {
|
||||
"@octokit/request": "^5.0.0",
|
||||
"universal-user-agent": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"@octokit/request": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.3.1.tgz",
|
||||
"integrity": "sha512-5/X0AL1ZgoU32fAepTfEoggFinO3rxsMLtzhlUX+RctLrusn/CApJuGFCd0v7GMFhF+8UiCsTTfsu7Fh1HnEJg==",
|
||||
"requires": {
|
||||
"@octokit/endpoint": "^5.5.0",
|
||||
"@octokit/request-error": "^1.0.1",
|
||||
"@octokit/types": "^2.0.0",
|
||||
"deprecation": "^2.0.0",
|
||||
"is-plain-object": "^3.0.0",
|
||||
"node-fetch": "^2.3.0",
|
||||
"once": "^1.4.0",
|
||||
"universal-user-agent": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"universal-user-agent": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz",
|
||||
"integrity": "sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==",
|
||||
"requires": {
|
||||
"os-name": "^3.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@octokit/request-error": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.1.0.tgz",
|
||||
"integrity": "sha512-06lt8PulL3rKpmwzYLCeLEt1iHFoj8l0PLkObAtp5Cx0Wwd1+5FAa9u6UXjA0kzYsfbjBKF9TtO9CuXelKiYlw==",
|
||||
"requires": {
|
||||
"@octokit/types": "^2.0.0",
|
||||
"deprecation": "^2.0.0",
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"@octokit/rest": {
|
||||
"version": "16.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.34.1.tgz",
|
||||
"integrity": "sha512-JUoS12cdktf1fv86rgrjC/RvYLuL+o7p57W7zX1x7ANFJ7OvdV8emvUNkFlcidEaOkYrxK3SoWgQFt3FhNmabA==",
|
||||
"requires": {
|
||||
"@octokit/request": "^5.2.0",
|
||||
"@octokit/request-error": "^1.0.2",
|
||||
"atob-lite": "^2.0.0",
|
||||
"before-after-hook": "^2.0.0",
|
||||
"btoa-lite": "^1.0.0",
|
||||
"deprecation": "^2.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"lodash.uniq": "^4.5.0",
|
||||
"octokit-pagination-methods": "^1.1.0",
|
||||
"once": "^1.4.0",
|
||||
"universal-user-agent": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"universal-user-agent": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz",
|
||||
"integrity": "sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==",
|
||||
"requires": {
|
||||
"os-name": "^3.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@octokit/types": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.0.0.tgz",
|
||||
"integrity": "sha512-467rp1g6YuxuNbu1m3A5BuGWxtzyVE8sAyN9+k3kb2LdnpmLPTiPsywbYmcckgfGZ+/AGpAaNrVx7131iSUXbQ==",
|
||||
"requires": {
|
||||
"@types/node": "^12.11.1"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "12.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.5.tgz",
|
||||
"integrity": "sha512-KEjODidV4XYUlJBF3XdjSH5FWoMCtO0utnhtdLf1AgeuZLOrRbvmU/gaRCVg7ZaQDjVf3l84egiY0mRNe5xE4A=="
|
||||
},
|
||||
"@types/node-fetch": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.3.tgz",
|
||||
"integrity": "sha512-X3TNlzZ7SuSwZsMkb5fV7GrPbVKvHc2iwHmslb8bIxRKWg2iqkfm3F/Wd79RhDpOXR7wCtKAwc5Y2JE6n/ibyw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@zeit/ncc": {
|
||||
"version": "0.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@zeit/ncc/-/ncc-0.20.5.tgz",
|
||||
"integrity": "sha512-XU6uzwvv95DqxciQx+aOLhbyBx/13ky+RK1y88Age9Du3BlA4mMPCy13BGjayOrrumOzlq1XV3SD/BWiZENXlw==",
|
||||
"dev": true
|
||||
},
|
||||
"atob-lite": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz",
|
||||
"integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY="
|
||||
},
|
||||
"before-after-hook": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz",
|
||||
"integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A=="
|
||||
},
|
||||
"btoa-lite": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz",
|
||||
"integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc="
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
|
||||
"requires": {
|
||||
"nice-try": "^1.0.4",
|
||||
"path-key": "^2.0.1",
|
||||
"semver": "^5.5.0",
|
||||
"shebang-command": "^1.2.0",
|
||||
"which": "^1.2.9"
|
||||
}
|
||||
},
|
||||
"deprecation": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
|
||||
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
|
||||
},
|
||||
"end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"requires": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"execa": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
|
||||
"integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
|
||||
"requires": {
|
||||
"cross-spawn": "^6.0.0",
|
||||
"get-stream": "^4.0.0",
|
||||
"is-stream": "^1.1.0",
|
||||
"npm-run-path": "^2.0.0",
|
||||
"p-finally": "^1.0.0",
|
||||
"signal-exit": "^3.0.0",
|
||||
"strip-eof": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
|
||||
"integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
|
||||
"requires": {
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"is-plain-object": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz",
|
||||
"integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==",
|
||||
"requires": {
|
||||
"isobject": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"is-stream": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
||||
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
|
||||
},
|
||||
"isobject": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz",
|
||||
"integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA=="
|
||||
},
|
||||
"lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
|
||||
},
|
||||
"lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
"integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM="
|
||||
},
|
||||
"lodash.uniq": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
|
||||
"integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M="
|
||||
},
|
||||
"macos-release": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz",
|
||||
"integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA=="
|
||||
},
|
||||
"nice-try": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
|
||||
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
|
||||
},
|
||||
"npm-run-path": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
|
||||
"integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
|
||||
"requires": {
|
||||
"path-key": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"octokit-pagination-methods": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz",
|
||||
"integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ=="
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"os-name": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz",
|
||||
"integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==",
|
||||
"requires": {
|
||||
"macos-release": "^2.2.0",
|
||||
"windows-release": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
|
||||
},
|
||||
"path-key": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
|
||||
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
|
||||
"requires": {
|
||||
"shebang-regex": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"shebang-regex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
|
||||
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
|
||||
},
|
||||
"strip-eof": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
|
||||
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
|
||||
},
|
||||
"universal-user-agent": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-2.1.0.tgz",
|
||||
"integrity": "sha512-8itiX7G05Tu3mGDTdNY2fB4KJ8MgZLS54RdG6PkkfwMAavrXu1mV/lls/GABx9O3Rw4PnTtasxrvbMQoBYY92Q==",
|
||||
"requires": {
|
||||
"os-name": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"windows-release": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz",
|
||||
"integrity": "sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==",
|
||||
"requires": {
|
||||
"execa": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
}
|
||||
}
|
||||
}
|
||||
20
.github/actions/copy-release-to-another-repo/package.json
vendored
Normal file
20
.github/actions/copy-release-to-another-repo/package.json
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"license": "ISC",
|
||||
"scripts": {
|
||||
"build": "rm -rf lib && ncc build -s src/index.ts -o lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.0",
|
||||
"@actions/github": "^1.1.0",
|
||||
"node-fetch": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-fetch": "^2.5.3",
|
||||
"@zeit/ncc": "^0.20.5"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
}
|
||||
140
.github/actions/copy-release-to-another-repo/src/index.ts
vendored
Normal file
140
.github/actions/copy-release-to-another-repo/src/index.ts
vendored
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import {getInput, setOutput, setFailed} from '@actions/core'
|
||||
import {context, GitHub} from '@actions/github'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import fetch from 'node-fetch'
|
||||
import { ReposCreateReleaseResponse, ReposGetReleaseByTagResponse } from '@octokit/rest'
|
||||
|
||||
const GITHUB_TOKEN = getEnvVar('GITHUB_TOKEN')
|
||||
const UPLOAD_GITHUB_TOKEN = getEnvVar('UPLOAD_GITHUB_TOKEN')
|
||||
|
||||
process.on('unhandledRejection', (reason: any, _: Promise<any>) =>
|
||||
handleError(reason)
|
||||
)
|
||||
main().catch(handleError)
|
||||
|
||||
async function main() {
|
||||
// Get the release from the current repository
|
||||
const tag = context.ref.replace('refs/tags/', '')
|
||||
const release = await getRelease(context.repo.owner, context.repo.repo, tag)
|
||||
|
||||
// Create a new release in another repository
|
||||
const [targetOwner, targetRepo] = getInput('target-repo').split('/')
|
||||
let publicRelease: ReposCreateReleaseResponse | ReposCreateReleaseResponse
|
||||
try {
|
||||
publicRelease = await getRelease(targetOwner, targetRepo, tag)
|
||||
} catch (error) {
|
||||
if (error.status && error.status == 404) {
|
||||
publicRelease = await createRelease(targetOwner, targetRepo, release)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
for (const asset of release.assets) {
|
||||
if (!(asset.name.match(/macOS/) || asset.name.match(/darwin/)) && !asset.name.match(/linux/)) {
|
||||
continue
|
||||
}
|
||||
const filePath = await downloadAsset(asset)
|
||||
for (const existingAsset of publicRelease.assets) {
|
||||
if (existingAsset.name == asset.name) {
|
||||
await deleteAsset(targetOwner, targetRepo, existingAsset)
|
||||
}
|
||||
}
|
||||
const assetUrl = await uploadAsset(publicRelease, filePath)
|
||||
if (asset.name.match(/macOS/) || asset.name.match(/darwin/)) {
|
||||
setOutput('asset-url', assetUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getRelease(owner: string, repo: string, tag: string) {
|
||||
const octokit = new GitHub(GITHUB_TOKEN)
|
||||
const response = await octokit.repos.getReleaseByTag({ owner, repo, tag })
|
||||
return response.data
|
||||
}
|
||||
|
||||
async function createRelease(owner: string, repo: string, release: ReposGetReleaseByTagResponse) {
|
||||
const octokit = new GitHub(UPLOAD_GITHUB_TOKEN)
|
||||
const response = await octokit.repos.createRelease({
|
||||
owner,
|
||||
repo,
|
||||
tag_name: release.tag_name,
|
||||
name: release.name,
|
||||
body: '',
|
||||
prerelease: release.prerelease,
|
||||
draft: false,
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
async function uploadAsset(release: ReposGetReleaseByTagResponse | ReposCreateReleaseResponse, filePath: string) {
|
||||
const octokit = new GitHub(UPLOAD_GITHUB_TOKEN)
|
||||
const response = await octokit.repos.uploadReleaseAsset({
|
||||
url: release.upload_url,
|
||||
file: fs.readFileSync(filePath),
|
||||
name: path.basename(filePath),
|
||||
headers: {
|
||||
'content-type': 'application/octet-stream',
|
||||
'content-length': fs.statSync(filePath).size,
|
||||
},
|
||||
})
|
||||
// this seems like a bug in Rest.js types
|
||||
const url: string = (<any>response.data).browser_download_url
|
||||
return url
|
||||
}
|
||||
|
||||
interface Asset {
|
||||
id: number;
|
||||
url: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
async function deleteAsset(owner: string, repo: string, asset: Asset) {
|
||||
const octokit = new GitHub(UPLOAD_GITHUB_TOKEN)
|
||||
await octokit.repos.deleteReleaseAsset({
|
||||
owner,
|
||||
repo,
|
||||
asset_id: asset.id
|
||||
})
|
||||
}
|
||||
|
||||
async function downloadAsset(asset: Asset) {
|
||||
let response = await fetch(asset.url, {
|
||||
redirect: 'manual',
|
||||
headers: {
|
||||
Authorization: `token ${GITHUB_TOKEN}`,
|
||||
Accept: 'application/octet-stream',
|
||||
},
|
||||
})
|
||||
|
||||
// Why didn't I just let fetch handle the redirect? Because that will
|
||||
// will forwarded the Authorization header to S3 and AWS doesn't like that.
|
||||
// For more details check out https://github.com/octokit/rest.js/issues/967
|
||||
if (response.status === 302) {
|
||||
response = await fetch(response.headers.get('location') || '', {
|
||||
headers: { Accept: 'application/octet-stream' },
|
||||
})
|
||||
}
|
||||
|
||||
if (response.status === 200) {
|
||||
const data = await response.buffer()
|
||||
fs.writeFileSync(asset.name, data)
|
||||
} else {
|
||||
throw new Error('failed to download asset: ' + (await response.text()))
|
||||
}
|
||||
return asset.name
|
||||
}
|
||||
|
||||
async function handleError(err: Error) {
|
||||
console.error(err)
|
||||
setFailed(err.message)
|
||||
}
|
||||
|
||||
function getEnvVar(name: string): string {
|
||||
const envVar = process.env[name]
|
||||
if (!envVar) {
|
||||
throw new Error(`env var named "${name} is not set"`)
|
||||
}
|
||||
return envVar
|
||||
}
|
||||
11
.github/actions/copy-release-to-another-repo/test-run.sh
vendored
Normal file
11
.github/actions/copy-release-to-another-repo/test-run.sh
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
set -e
|
||||
|
||||
TOKEN="$(awk '/oauth_token/ {print $2}' ~/.config/hub | head -1)"
|
||||
|
||||
env \
|
||||
"GITHUB_REPOSITORY=github/gh-cli" \
|
||||
"GITHUB_REF=refs/tags/v0.0.195" \
|
||||
"INPUT_TARGET-REPO=github/homebrew-gh" \
|
||||
"GITHUB_TOKEN=$TOKEN" \
|
||||
"UPLOAD_GITHUB_TOKEN=$TOKEN" \
|
||||
node lib/index.js
|
||||
19
.github/actions/copy-release-to-another-repo/tsconfig.json
vendored
Normal file
19
.github/actions/copy-release-to-another-repo/tsconfig.json
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib",
|
||||
"rootDir": "./src",
|
||||
"module": "commonjs",
|
||||
"target": "esnext",
|
||||
"lib": ["es2017"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitAny": true,
|
||||
"removeComments": false,
|
||||
"preserveConstEnums": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
43
.github/workflows/releases.yml
vendored
Normal file
43
.github/workflows/releases.yml
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
name: goreleaser
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
- name: Set up Go 1.13
|
||||
id: go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v1
|
||||
with:
|
||||
version: latest
|
||||
args: release
|
||||
env:
|
||||
GH_OAUTH_CLIENT_ID: 178c6fc778ccc68e1d6a
|
||||
GH_OAUTH_CLIENT_SECRET: ${{secrets.OAUTH_CLIENT_SECRET}}
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
- name: Copy release
|
||||
id: copy
|
||||
uses: ./.github/actions/copy-release-to-another-repo
|
||||
with:
|
||||
target-repo: github/homebrew-gh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
UPLOAD_GITHUB_TOKEN: ${{secrets.UPLOAD_GITHUB_TOKEN}}
|
||||
- name: Bump brew formula
|
||||
uses: mislav/bump-homebrew-formula-action@v1.4
|
||||
with:
|
||||
formula-name: gh
|
||||
homebrew-tap: github/homebrew-gh
|
||||
download-url: ${{ steps.copy.outputs.asset-url }}
|
||||
env:
|
||||
COMMITTER_TOKEN: ${{ secrets.UPLOAD_GITHUB_TOKEN }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
bin/gh
|
||||
/gh-cli
|
||||
.envrc
|
||||
/dist
|
||||
|
|
|
|||
32
.goreleaser.yml
Normal file
32
.goreleaser.yml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Make sure to check the documentation at http://goreleaser.com
|
||||
project_name: gh
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
builds:
|
||||
- binary: bin/gh
|
||||
ldflags:
|
||||
- -s -w -X github.com/github/gh-cli/command.Version={{.Version}} -X github.com/github/gh-cli/command.BuildDate={{.Date}}
|
||||
- -X github.com/github/gh-cli/context.oauthClientID={{.Env.GH_OAUTH_CLIENT_ID}}
|
||||
- -X github.com/github/gh-cli/context.oauthClientSecret={{.Env.GH_OAUTH_CLIENT_SECRET}}
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
archives:
|
||||
- name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
wrap_in_directory: true
|
||||
replacements:
|
||||
darwin: macOS
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
10
Makefile
10
Makefile
|
|
@ -1,10 +1,16 @@
|
|||
BUILD_FILES = $(shell go list -f '{{range .GoFiles}}{{$$.Dir}}/{{.}}\
|
||||
{{end}}' ./...)
|
||||
|
||||
# export GOFLAGS := -mod=vendor $GOFLAGS
|
||||
GH_VERSION = $(shell go describe --tags 2>/dev/null || git rev-parse --short HEAD)
|
||||
LDFLAGS := -X github.com/github/gh-cli/command.Version=$(GH_VERSION) $(LDFLAGS)
|
||||
LDFLAGS := -X github.com/github/gh-cli/command.BuildDate=$(shell date +%Y-%m-%d) $(LDFLAGS)
|
||||
ifdef GH_OAUTH_CLIENT_SECRET
|
||||
LDFLAGS := -X github.com/github/gh-cli/context.oauthClientID=$(GH_OAUTH_CLIENT_ID) $(LDFLAGS)
|
||||
LDFLAGS := -X github.com/github/gh-cli/context.oauthClientSecret=$(GH_OAUTH_CLIENT_SECRET) $(LDFLAGS)
|
||||
endif
|
||||
|
||||
bin/gh: $(BUILD_FILES)
|
||||
go build -o "$@"
|
||||
@go build -ldflags "$(LDFLAGS)" -o "$@"
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
|
|
|||
206
api/queries.go
206
api/queries.go
|
|
@ -2,6 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PullRequestsPayload struct {
|
||||
|
|
@ -13,6 +14,7 @@ type PullRequestsPayload struct {
|
|||
type PullRequest struct {
|
||||
Number int
|
||||
Title string
|
||||
State string
|
||||
URL string
|
||||
HeadRefName string
|
||||
}
|
||||
|
|
@ -22,32 +24,107 @@ type Repo interface {
|
|||
RepoOwner() string
|
||||
}
|
||||
|
||||
func GitHubRepoId(client *Client, ghRepo Repo) (string, error) {
|
||||
owner := ghRepo.RepoOwner()
|
||||
repo := ghRepo.RepoName()
|
||||
type IssuesPayload struct {
|
||||
Assigned []Issue
|
||||
Mentioned []Issue
|
||||
Recent []Issue
|
||||
}
|
||||
|
||||
type Issue struct {
|
||||
Number int
|
||||
Title string
|
||||
URL string
|
||||
}
|
||||
|
||||
func Issues(client *Client, ghRepo Repo, currentUsername string) (*IssuesPayload, error) {
|
||||
type issues struct {
|
||||
Issues struct {
|
||||
Edges []struct {
|
||||
Node Issue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Assigned issues
|
||||
Mentioned issues
|
||||
Recent issues
|
||||
}
|
||||
|
||||
query := `
|
||||
query FindRepoID($owner:String!, $name:String!) {
|
||||
repository(owner:$owner, name:$name) {
|
||||
id
|
||||
}
|
||||
}`
|
||||
fragment issue on Issue {
|
||||
number
|
||||
title
|
||||
}
|
||||
query($owner: String!, $repo: String!, $since: DateTime!, $viewer: String!, $per_page: Int = 10) {
|
||||
assigned: repository(owner: $owner, name: $repo) {
|
||||
issues(filterBy: {assignee: $viewer}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
node {
|
||||
...issue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mentioned: repository(owner: $owner, name: $repo) {
|
||||
issues(filterBy: {mentioned: $viewer}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
node {
|
||||
...issue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
recent: repository(owner: $owner, name: $repo) {
|
||||
issues(filterBy: {since: $since}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
node {
|
||||
...issue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
owner := ghRepo.RepoOwner()
|
||||
repo := ghRepo.RepoName()
|
||||
since := time.Now().UTC().Add(time.Hour * -24).Format("2006-01-02T15:04:05-0700")
|
||||
variables := map[string]interface{}{
|
||||
"owner": owner,
|
||||
"name": repo,
|
||||
"owner": owner,
|
||||
"repo": repo,
|
||||
"viewer": currentUsername,
|
||||
"since": since,
|
||||
}
|
||||
|
||||
result := struct {
|
||||
Repository struct {
|
||||
Id string
|
||||
}
|
||||
}{}
|
||||
err := client.GraphQL(query, variables, &result)
|
||||
var resp response
|
||||
err := client.GraphQL(query, variables, &resp)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to determine GH repo ID: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result.Repository.Id, nil
|
||||
var assigned []Issue
|
||||
for _, edge := range resp.Assigned.Issues.Edges {
|
||||
assigned = append(assigned, edge.Node)
|
||||
}
|
||||
|
||||
var mentioned []Issue
|
||||
for _, edge := range resp.Mentioned.Issues.Edges {
|
||||
mentioned = append(mentioned, edge.Node)
|
||||
}
|
||||
|
||||
var recent []Issue
|
||||
for _, edge := range resp.Recent.Issues.Edges {
|
||||
recent = append(recent, edge.Node)
|
||||
}
|
||||
|
||||
payload := IssuesPayload{
|
||||
assigned,
|
||||
mentioned,
|
||||
recent,
|
||||
}
|
||||
|
||||
return &payload, nil
|
||||
}
|
||||
|
||||
func PullRequests(client *Client, ghRepo Repo, currentBranch, currentUsername string) (*PullRequestsPayload, error) {
|
||||
|
|
@ -242,3 +319,96 @@ func CreatePullRequest(client *Client, ghRepo Repo, title string, body string, d
|
|||
|
||||
return result.CreatePullRequest.PullRequest.URL, nil
|
||||
}
|
||||
|
||||
func PullRequestList(client *Client, vars map[string]interface{}, limit int) ([]PullRequest, error) {
|
||||
type response struct {
|
||||
Repository struct {
|
||||
PullRequests struct {
|
||||
Edges []struct {
|
||||
Node PullRequest
|
||||
}
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query := `
|
||||
query(
|
||||
$owner: String!,
|
||||
$repo: String!,
|
||||
$limit: Int!,
|
||||
$endCursor: String,
|
||||
$baseBranch: String,
|
||||
$labels: [String!],
|
||||
$state: [PullRequestState!] = OPEN
|
||||
) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequests(
|
||||
states: $state,
|
||||
baseRefName: $baseBranch,
|
||||
labels: $labels,
|
||||
first: $limit,
|
||||
after: $endCursor,
|
||||
orderBy: {field: CREATED_AT, direction: DESC}
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
number
|
||||
title
|
||||
state
|
||||
url
|
||||
headRefName
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
prs := []PullRequest{}
|
||||
pageLimit := min(limit, 100)
|
||||
variables := map[string]interface{}{}
|
||||
for name, val := range vars {
|
||||
variables[name] = val
|
||||
}
|
||||
|
||||
for {
|
||||
variables["limit"] = pageLimit
|
||||
var data response
|
||||
err := client.GraphQL(query, variables, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prData := data.Repository.PullRequests
|
||||
|
||||
for _, edge := range prData.Edges {
|
||||
prs = append(prs, edge.Node)
|
||||
if len(prs) == limit {
|
||||
goto done
|
||||
}
|
||||
}
|
||||
|
||||
if prData.PageInfo.HasNextPage {
|
||||
variables["endCursor"] = prData.PageInfo.EndCursor
|
||||
pageLimit = min(pageLimit, limit-len(prs))
|
||||
continue
|
||||
}
|
||||
done:
|
||||
break
|
||||
}
|
||||
|
||||
return prs, nil
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
|
|
|||
40
api/queries_issue.go
Normal file
40
api/queries_issue.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package api
|
||||
|
||||
func IssueCreate(client *Client, ghRepo Repo, params map[string]interface{}) (*Issue, error) {
|
||||
repoID, err := GitHubRepoId(client, ghRepo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `
|
||||
mutation CreateIssue($input: CreateIssueInput!) {
|
||||
createIssue(input: $input) {
|
||||
issue {
|
||||
url
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
inputParams := map[string]interface{}{
|
||||
"repositoryId": repoID,
|
||||
}
|
||||
for key, val := range params {
|
||||
inputParams[key] = val
|
||||
}
|
||||
variables := map[string]interface{}{
|
||||
"input": inputParams,
|
||||
}
|
||||
|
||||
result := struct {
|
||||
CreateIssue struct {
|
||||
Issue Issue
|
||||
}
|
||||
}{}
|
||||
|
||||
err = client.GraphQL(query, variables, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result.CreateIssue.Issue, nil
|
||||
}
|
||||
31
api/queries_repo.go
Normal file
31
api/queries_repo.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package api
|
||||
|
||||
import "fmt"
|
||||
|
||||
func GitHubRepoId(client *Client, ghRepo Repo) (string, error) {
|
||||
owner := ghRepo.RepoOwner()
|
||||
repo := ghRepo.RepoName()
|
||||
|
||||
query := `
|
||||
query FindRepoID($owner:String!, $name:String!) {
|
||||
repository(owner:$owner, name:$name) {
|
||||
id
|
||||
}
|
||||
}`
|
||||
variables := map[string]interface{}{
|
||||
"owner": owner,
|
||||
"name": repo,
|
||||
}
|
||||
|
||||
result := struct {
|
||||
Repository struct {
|
||||
Id string
|
||||
}
|
||||
}{}
|
||||
err := client.GraphQL(query, variables, &result)
|
||||
if err != nil || result.Repository.Id == "" {
|
||||
return "", fmt.Errorf("failed to determine GH repo ID: %s", err)
|
||||
}
|
||||
|
||||
return result.Repository.Id, nil
|
||||
}
|
||||
43
command/completion.go
Normal file
43
command/completion.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(completionCmd)
|
||||
completionCmd.Flags().StringP("shell", "s", "bash", "The type of shell")
|
||||
}
|
||||
|
||||
var completionCmd = &cobra.Command{
|
||||
Use: "completion",
|
||||
Hidden: true,
|
||||
Short: "Generates completion scripts",
|
||||
Long: `To enable completion in your shell, run:
|
||||
|
||||
eval "$(gh completion)"
|
||||
|
||||
You can add that to your '~/.bash_profile' to enable completion whenever you
|
||||
start a new shell.
|
||||
|
||||
When installing with Homebrew, see https://docs.brew.sh/Shell-Completion
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
shellType, err := cmd.Flags().GetString("shell")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch shellType {
|
||||
case "bash":
|
||||
RootCmd.GenBashCompletion(cmd.OutOrStdout())
|
||||
case "zsh":
|
||||
RootCmd.GenZshCompletion(cmd.OutOrStdout())
|
||||
default:
|
||||
return fmt.Errorf("unsupported shell type: %s", shellType)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
50
command/completion_test.go
Normal file
50
command/completion_test.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompletion_bash(t *testing.T) {
|
||||
out := bytes.Buffer{}
|
||||
completionCmd.SetOut(&out)
|
||||
|
||||
RootCmd.SetArgs([]string{"completion"})
|
||||
_, err := RootCmd.ExecuteC()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
outStr := out.String()
|
||||
if !strings.Contains(outStr, "complete -o default -F __start_gh gh") {
|
||||
t.Errorf("problem in bash completion:\n%s", outStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompletion_zsh(t *testing.T) {
|
||||
out := bytes.Buffer{}
|
||||
completionCmd.SetOut(&out)
|
||||
|
||||
RootCmd.SetArgs([]string{"completion", "-s", "zsh"})
|
||||
_, err := RootCmd.ExecuteC()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
outStr := out.String()
|
||||
if !strings.Contains(outStr, "#compdef _gh gh") {
|
||||
t.Errorf("problem in zsh completion:\n%s", outStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompletion_unsupported(t *testing.T) {
|
||||
out := bytes.Buffer{}
|
||||
completionCmd.SetOut(&out)
|
||||
|
||||
RootCmd.SetArgs([]string{"completion", "-s", "fish"})
|
||||
_, err := RootCmd.ExecuteC()
|
||||
if err == nil || err.Error() != "unsupported shell type: fish" {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
192
command/issue.go
Normal file
192
command/issue.go
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/github/gh-cli/api"
|
||||
"github.com/github/gh-cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(issueCmd)
|
||||
issueCmd.AddCommand(
|
||||
&cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show status of relevant issues",
|
||||
RunE: issueList,
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "view <issue-number>",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Short: "Open an issue in the browser",
|
||||
RunE: issueView,
|
||||
},
|
||||
)
|
||||
issueCmd.AddCommand(issueCreateCmd)
|
||||
issueCreateCmd.Flags().StringArrayP("message", "m", nil, "set title and body")
|
||||
issueCreateCmd.Flags().BoolP("web", "w", false, "open the web browser to create an issue")
|
||||
}
|
||||
|
||||
var issueCmd = &cobra.Command{
|
||||
Use: "issue",
|
||||
Short: "Work with GitHub issues",
|
||||
Long: `Helps you work with issues.`,
|
||||
}
|
||||
var issueCreateCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a new issue",
|
||||
RunE: issueCreate,
|
||||
}
|
||||
|
||||
func issueList(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
apiClient, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseRepo, err := ctx.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentUser, err := ctx.AuthLogin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issuePayload, err := api.Issues(apiClient, baseRepo, currentUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printHeader("Issues assigned to you")
|
||||
if issuePayload.Assigned != nil {
|
||||
printIssues(issuePayload.Assigned...)
|
||||
} else {
|
||||
message := fmt.Sprintf(" There are no issues assgined to you")
|
||||
printMessage(message)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
printHeader("Issues mentioning you")
|
||||
if len(issuePayload.Mentioned) > 0 {
|
||||
printIssues(issuePayload.Mentioned...)
|
||||
} else {
|
||||
printMessage(" There are no issues mentioning you")
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
printHeader("Recent issues")
|
||||
if len(issuePayload.Recent) > 0 {
|
||||
printIssues(issuePayload.Recent...)
|
||||
} else {
|
||||
printMessage(" There are no recent issues")
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func issueView(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
|
||||
baseRepo, err := ctx.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var openURL string
|
||||
if number, err := strconv.Atoi(args[0]); err == nil {
|
||||
// TODO: move URL generation into GitHubRepository
|
||||
openURL = fmt.Sprintf("https://github.com/%s/%s/issues/%d", baseRepo.RepoOwner(), baseRepo.RepoName(), number)
|
||||
} else {
|
||||
return fmt.Errorf("invalid issue number: '%s'", args[0])
|
||||
}
|
||||
|
||||
fmt.Printf("Opening %s in your browser.\n", openURL)
|
||||
return utils.OpenInBrowser(openURL)
|
||||
}
|
||||
|
||||
func issueCreate(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
|
||||
baseRepo, err := ctx.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isWeb, err := cmd.Flags().GetBool("web"); err == nil && isWeb {
|
||||
// TODO: move URL generation into GitHubRepository
|
||||
openURL := fmt.Sprintf("https://github.com/%s/%s/issues/new", baseRepo.RepoOwner(), baseRepo.RepoName())
|
||||
// TODO: figure out how to stub this in tests
|
||||
if stat, err := os.Stat(".github/ISSUE_TEMPLATE"); err == nil && stat.IsDir() {
|
||||
openURL += "/choose"
|
||||
}
|
||||
return utils.OpenInBrowser(openURL)
|
||||
}
|
||||
|
||||
var title string
|
||||
var body string
|
||||
|
||||
message, err := cmd.Flags().GetStringArray("message")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiClient, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(message) > 0 {
|
||||
title = message[0]
|
||||
body = strings.Join(message[1:], "\n\n")
|
||||
} else {
|
||||
// TODO: open the text editor for issue title & body
|
||||
input := os.Stdin
|
||||
if terminal.IsTerminal(int(input.Fd())) {
|
||||
cmd.Println("Enter the issue title and body; press Enter + Ctrl-D when done:")
|
||||
}
|
||||
inputBytes, err := ioutil.ReadAll(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parts := strings.SplitN(string(inputBytes), "\n\n", 2)
|
||||
if len(parts) > 0 {
|
||||
title = parts[0]
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
body = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
return fmt.Errorf("aborting due to empty title")
|
||||
}
|
||||
params := map[string]interface{}{
|
||||
"title": title,
|
||||
"body": body,
|
||||
}
|
||||
|
||||
newIssue, err := api.IssueCreate(apiClient, baseRepo, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(cmd.OutOrStdout(), newIssue.URL)
|
||||
return nil
|
||||
}
|
||||
|
||||
func printIssues(issues ...api.Issue) {
|
||||
for _, issue := range issues {
|
||||
fmt.Printf(" #%d %s\n", issue.Number, truncate(70, issue.Title))
|
||||
}
|
||||
}
|
||||
117
command/issue_test.go
Normal file
117
command/issue_test.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/github/gh-cli/test"
|
||||
"github.com/github/gh-cli/utils"
|
||||
)
|
||||
|
||||
func TestIssueStatus(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
|
||||
jsonFile, _ := os.Open("../test/fixtures/issueStatus.json")
|
||||
defer jsonFile.Close()
|
||||
http.StubResponse(200, jsonFile)
|
||||
|
||||
output, err := test.RunCommand(RootCmd, "issue status")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `issue status`: %v", err)
|
||||
}
|
||||
|
||||
expectedIssues := []*regexp.Regexp{
|
||||
regexp.MustCompile(`#8.*carrots`),
|
||||
regexp.MustCompile(`#9.*squash`),
|
||||
regexp.MustCompile(`#10.*broccoli`),
|
||||
regexp.MustCompile(`#11.*swiss chard`),
|
||||
}
|
||||
|
||||
for _, r := range expectedIssues {
|
||||
if !r.MatchString(output) {
|
||||
t.Errorf("output did not match regexp /%s/", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueView(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
|
||||
jsonFile, _ := os.Open("../test/fixtures/issueView.json")
|
||||
defer jsonFile.Close()
|
||||
http.StubResponse(200, jsonFile)
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
|
||||
seenCmd = cmd
|
||||
return &outputStub{}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := test.RunCommand(RootCmd, "issue view 8")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `issue view`: %v", err)
|
||||
}
|
||||
|
||||
if output == "" {
|
||||
t.Errorf("command output expected got an empty string")
|
||||
}
|
||||
|
||||
if seenCmd == nil {
|
||||
t.Fatal("expected a command to run")
|
||||
}
|
||||
url := seenCmd.Args[len(seenCmd.Args)-1]
|
||||
if url != "https://github.com/OWNER/REPO/issues/8" {
|
||||
t.Errorf("got: %q", url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueCreate(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": {
|
||||
"id": "REPOID"
|
||||
} } }
|
||||
`))
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "createIssue": { "issue": {
|
||||
"URL": "https://github.com/OWNER/REPO/issues/12"
|
||||
} } } }
|
||||
`))
|
||||
|
||||
out := bytes.Buffer{}
|
||||
issueCreateCmd.SetOut(&out)
|
||||
|
||||
RootCmd.SetArgs([]string{"issue", "create", "-m", "hello", "-m", "ab", "-m", "cd"})
|
||||
_, err := RootCmd.ExecuteC()
|
||||
if err != nil {
|
||||
t.Errorf("error running command `issue create`: %v", err)
|
||||
}
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(http.Requests[1].Body)
|
||||
reqBody := struct {
|
||||
Variables struct {
|
||||
Input struct {
|
||||
RepositoryID string
|
||||
Title string
|
||||
Body string
|
||||
}
|
||||
}
|
||||
}{}
|
||||
json.Unmarshal(bodyBytes, &reqBody)
|
||||
|
||||
eq(t, reqBody.Variables.Input.RepositoryID, "REPOID")
|
||||
eq(t, reqBody.Variables.Input.Title, "hello")
|
||||
eq(t, reqBody.Variables.Input.Body, "ab\n\ncd")
|
||||
|
||||
eq(t, out.String(), "https://github.com/OWNER/REPO/issues/12\n")
|
||||
}
|
||||
165
command/pr.go
165
command/pr.go
|
|
@ -2,42 +2,50 @@ package command
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/github/gh-cli/api"
|
||||
"github.com/github/gh-cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(prCmd)
|
||||
prCmd.AddCommand(
|
||||
&cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List pull requests",
|
||||
RunE: prList,
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "view [pr-number]",
|
||||
Short: "Open a pull request in the browser",
|
||||
RunE: prView,
|
||||
},
|
||||
prCreateCmd,
|
||||
)
|
||||
prCmd.AddCommand(prCreateCmd)
|
||||
prCmd.AddCommand(prListCmd)
|
||||
prCmd.AddCommand(prStatusCmd)
|
||||
prCmd.AddCommand(prViewCmd)
|
||||
|
||||
prListCmd.Flags().IntP("limit", "L", 30, "maximum number of items to fetch")
|
||||
prListCmd.Flags().StringP("state", "s", "open", "filter by state")
|
||||
prListCmd.Flags().StringP("base", "b", "", "filter by base branch")
|
||||
prListCmd.Flags().StringArrayP("label", "l", nil, "filter by label")
|
||||
}
|
||||
|
||||
var prCmd = &cobra.Command{
|
||||
Use: "pr",
|
||||
Short: "Work with pull requests",
|
||||
Long: `This command allows you to
|
||||
work with pull requests.`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("%+v is not a valid PR command", args)
|
||||
},
|
||||
Long: `Helps you work with pull requests.`,
|
||||
}
|
||||
var prListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List pull requests",
|
||||
RunE: prList,
|
||||
}
|
||||
var prStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show status of relevant pull requests",
|
||||
RunE: prStatus,
|
||||
}
|
||||
var prViewCmd = &cobra.Command{
|
||||
Use: "view [pr-number]",
|
||||
Short: "Open a pull request in the browser",
|
||||
RunE: prView,
|
||||
}
|
||||
|
||||
func prList(cmd *cobra.Command, args []string) error {
|
||||
func prStatus(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
apiClient, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -90,6 +98,117 @@ func prList(cmd *cobra.Command, args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func prList(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
apiClient, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseRepo, err := ctx.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
limit, err := cmd.Flags().GetInt("limit")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state, err := cmd.Flags().GetString("state")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
baseBranch, err := cmd.Flags().GetString("base")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
labels, err := cmd.Flags().GetStringArray("label")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var graphqlState []string
|
||||
switch state {
|
||||
case "open":
|
||||
graphqlState = []string{"OPEN"}
|
||||
case "closed":
|
||||
graphqlState = []string{"CLOSED"}
|
||||
case "merged":
|
||||
graphqlState = []string{"MERGED"}
|
||||
case "all":
|
||||
graphqlState = []string{"OPEN", "CLOSED", "MERGED"}
|
||||
default:
|
||||
return fmt.Errorf("invalid state: %s", state)
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
"owner": baseRepo.RepoOwner(),
|
||||
"repo": baseRepo.RepoName(),
|
||||
"state": graphqlState,
|
||||
}
|
||||
if len(labels) > 0 {
|
||||
params["labels"] = labels
|
||||
}
|
||||
if baseBranch != "" {
|
||||
params["baseBranch"] = baseBranch
|
||||
}
|
||||
|
||||
prs, err := api.PullRequestList(apiClient, params, limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tty := false
|
||||
ttyWidth := 80
|
||||
out := cmd.OutOrStdout()
|
||||
if outFile, isFile := out.(*os.File); isFile {
|
||||
fd := int(outFile.Fd())
|
||||
tty = terminal.IsTerminal(fd)
|
||||
if w, _, err := terminal.GetSize(fd); err == nil {
|
||||
ttyWidth = w
|
||||
}
|
||||
}
|
||||
|
||||
numWidth := 0
|
||||
maxTitleWidth := 0
|
||||
for _, pr := range prs {
|
||||
numLen := len(strconv.Itoa(pr.Number)) + 1
|
||||
if numLen > numWidth {
|
||||
numWidth = numLen
|
||||
}
|
||||
if len(pr.Title) > maxTitleWidth {
|
||||
maxTitleWidth = len(pr.Title)
|
||||
}
|
||||
}
|
||||
|
||||
branchWidth := 40
|
||||
titleWidth := ttyWidth - branchWidth - 2 - numWidth - 2
|
||||
|
||||
if maxTitleWidth < titleWidth {
|
||||
branchWidth += titleWidth - maxTitleWidth
|
||||
titleWidth = maxTitleWidth
|
||||
}
|
||||
|
||||
for _, pr := range prs {
|
||||
if tty {
|
||||
prNum := fmt.Sprintf("% *s", numWidth, fmt.Sprintf("#%d", pr.Number))
|
||||
switch pr.State {
|
||||
case "OPEN":
|
||||
prNum = utils.Green(prNum)
|
||||
case "CLOSED":
|
||||
prNum = utils.Red(prNum)
|
||||
case "MERGED":
|
||||
prNum = utils.Magenta(prNum)
|
||||
}
|
||||
prBranch := utils.Cyan(truncate(branchWidth, pr.HeadRefName))
|
||||
fmt.Fprintf(out, "%s %-*s %s\n", prNum, titleWidth, truncate(titleWidth, pr.Title), prBranch)
|
||||
} else {
|
||||
fmt.Fprintf(out, "%d\t%s\t%s\n", pr.Number, pr.Title, pr.HeadRefName)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func prView(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
baseRepo, err := ctx.BaseRepo()
|
||||
|
|
@ -130,7 +249,7 @@ func prView(cmd *cobra.Command, args []string) error {
|
|||
|
||||
func printPrs(prs ...api.PullRequest) {
|
||||
for _, pr := range prs {
|
||||
fmt.Printf(" #%d %s %s\n", pr.Number, truncateTitle(pr.Title), utils.Cyan("["+pr.HeadRefName+"]"))
|
||||
fmt.Printf(" #%d %s %s\n", pr.Number, truncate(50, pr.Title), utils.Cyan("["+pr.HeadRefName+"]"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,9 +261,7 @@ func printMessage(s string) {
|
|||
fmt.Println(utils.Gray(s))
|
||||
}
|
||||
|
||||
func truncateTitle(title string) string {
|
||||
const maxLength = 50
|
||||
|
||||
func truncate(maxLength int, title string) string {
|
||||
if len(title) > maxLength {
|
||||
return title[0:maxLength-3] + "..."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +1,37 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/github/gh-cli/api"
|
||||
"github.com/github/gh-cli/context"
|
||||
"github.com/github/gh-cli/test"
|
||||
"github.com/github/gh-cli/utils"
|
||||
)
|
||||
|
||||
func initBlankContext(repo, branch string) {
|
||||
initContext = func() context.Context {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBaseRepo(repo)
|
||||
ctx.SetBranch(branch)
|
||||
return ctx
|
||||
func eq(t *testing.T, got interface{}, expected interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("expected: %v, got: %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func initFakeHTTP() *api.FakeHTTP {
|
||||
http := &api.FakeHTTP{}
|
||||
apiClientForContext = func(context.Context) (*api.Client, error) {
|
||||
return api.NewClient(api.ReplaceTripper(http)), nil
|
||||
}
|
||||
return http
|
||||
}
|
||||
|
||||
func TestPRList(t *testing.T) {
|
||||
func TestPRStatus(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
|
||||
jsonFile, _ := os.Open("../test/fixtures/prList.json")
|
||||
jsonFile, _ := os.Open("../test/fixtures/prStatus.json")
|
||||
defer jsonFile.Close()
|
||||
http.StubResponse(200, jsonFile)
|
||||
|
||||
output, err := test.RunCommand(RootCmd, "pr list")
|
||||
output, err := test.RunCommand(RootCmd, "pr status")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `pr list`: %v", err)
|
||||
t.Errorf("error running command `pr status`: %v", err)
|
||||
}
|
||||
|
||||
expectedPrs := []*regexp.Regexp{
|
||||
|
|
@ -55,6 +48,57 @@ func TestPRList(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPRList(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
|
||||
jsonFile, _ := os.Open("../test/fixtures/prList.json")
|
||||
defer jsonFile.Close()
|
||||
http.StubResponse(200, jsonFile)
|
||||
|
||||
out := bytes.Buffer{}
|
||||
prListCmd.SetOut(&out)
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "list"})
|
||||
_, err := RootCmd.ExecuteC()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
eq(t, out.String(), `32 New feature feature
|
||||
29 Fixed bad bug bug-fix
|
||||
28 Improve documentation docs
|
||||
`)
|
||||
}
|
||||
|
||||
func TestPRList_filtering(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
|
||||
respBody := bytes.NewBufferString(`{ "data": {} }`)
|
||||
http.StubResponse(200, respBody)
|
||||
|
||||
prListCmd.SetOut(ioutil.Discard)
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "list", "-s", "all", "-l", "one", "-l", "two"})
|
||||
_, err := RootCmd.ExecuteC()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(http.Requests[0].Body)
|
||||
reqBody := struct {
|
||||
Variables struct {
|
||||
State []string
|
||||
Labels []string
|
||||
}
|
||||
}{}
|
||||
json.Unmarshal(bodyBytes, &reqBody)
|
||||
|
||||
eq(t, reqBody.Variables.State, []string{"OPEN", "CLOSED", "MERGED"})
|
||||
eq(t, reqBody.Variables.Labels, []string{"one", "two"})
|
||||
}
|
||||
|
||||
func TestPRView(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
|
|
@ -63,8 +107,12 @@ func TestPRView(t *testing.T) {
|
|||
defer jsonFile.Close()
|
||||
http.StubResponse(200, jsonFile)
|
||||
|
||||
teardown, callCount := mockOpenInBrowser()
|
||||
defer teardown()
|
||||
var seenCmd *exec.Cmd
|
||||
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
|
||||
seenCmd = cmd
|
||||
return &outputStub{}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := test.RunCommand(RootCmd, "pr view")
|
||||
if err != nil {
|
||||
|
|
@ -75,8 +123,12 @@ func TestPRView(t *testing.T) {
|
|||
t.Errorf("command output expected got an empty string")
|
||||
}
|
||||
|
||||
if *callCount != 1 {
|
||||
t.Errorf("OpenInBrowser should be called 1 time but was called %d time(s)", *callCount)
|
||||
if seenCmd == nil {
|
||||
t.Fatal("expected a command to run")
|
||||
}
|
||||
url := seenCmd.Args[len(seenCmd.Args)-1]
|
||||
if url != "https://github.com/OWNER/REPO/pull/10" {
|
||||
t.Errorf("got: %q", url)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,16 +140,20 @@ func TestPRView_NoActiveBranch(t *testing.T) {
|
|||
defer jsonFile.Close()
|
||||
http.StubResponse(200, jsonFile)
|
||||
|
||||
teardown, callCount := mockOpenInBrowser()
|
||||
defer teardown()
|
||||
var seenCmd *exec.Cmd
|
||||
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
|
||||
seenCmd = cmd
|
||||
return &outputStub{}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := test.RunCommand(RootCmd, "pr view")
|
||||
if err == nil || err.Error() != "the 'master' branch has no open pull requests" {
|
||||
t.Errorf("error running command `pr view`: %v", err)
|
||||
}
|
||||
|
||||
if *callCount > 0 {
|
||||
t.Errorf("OpenInBrowser should NOT be called but was called %d time(s)", *callCount)
|
||||
if seenCmd != nil {
|
||||
t.Fatalf("unexpected command: %v", seenCmd.Args)
|
||||
}
|
||||
|
||||
// Now run again but provide a PR number
|
||||
|
|
@ -110,22 +166,11 @@ func TestPRView_NoActiveBranch(t *testing.T) {
|
|||
t.Errorf("command output expected got an empty string")
|
||||
}
|
||||
|
||||
if *callCount != 1 {
|
||||
t.Errorf("OpenInBrowser should be called once but was called %d time(s)", *callCount)
|
||||
if seenCmd == nil {
|
||||
t.Fatal("expected a command to run")
|
||||
}
|
||||
url := seenCmd.Args[len(seenCmd.Args)-1]
|
||||
if url != "https://github.com/OWNER/REPO/pull/23" {
|
||||
t.Errorf("got: %q", url)
|
||||
}
|
||||
}
|
||||
|
||||
func mockOpenInBrowser() (func(), *int) {
|
||||
callCount := 0
|
||||
originalOpenInBrowser := utils.OpenInBrowser
|
||||
teardown := func() {
|
||||
utils.OpenInBrowser = originalOpenInBrowser
|
||||
}
|
||||
|
||||
utils.OpenInBrowser = func(_ string) error {
|
||||
callCount++
|
||||
return nil
|
||||
}
|
||||
|
||||
return teardown, &callCount
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,16 +6,31 @@ import (
|
|||
|
||||
"github.com/github/gh-cli/api"
|
||||
"github.com/github/gh-cli/context"
|
||||
"github.com/github/gh-cli/version"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Version is dynamically set at build time in the Makefile
|
||||
var Version = "DEV"
|
||||
|
||||
// BuildDate is dynamically set at build time in the Makefile
|
||||
var BuildDate = "YYYY-MM-DD"
|
||||
|
||||
func init() {
|
||||
RootCmd.Version = fmt.Sprintf("%s (%s)", Version, BuildDate)
|
||||
RootCmd.PersistentFlags().StringP("repo", "R", "", "current GitHub repository")
|
||||
RootCmd.PersistentFlags().StringP("current-branch", "B", "", "current git branch")
|
||||
// TODO:
|
||||
// RootCmd.PersistentFlags().BoolP("verbose", "V", false, "enable verbose output")
|
||||
|
||||
RootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
|
||||
return FlagError{err}
|
||||
})
|
||||
}
|
||||
|
||||
// FlagError is the kind of error raised in flag processing
|
||||
type FlagError struct {
|
||||
error
|
||||
}
|
||||
|
||||
// RootCmd is the entry point of command-line execution
|
||||
|
|
@ -23,10 +38,9 @@ var RootCmd = &cobra.Command{
|
|||
Use: "gh",
|
||||
Short: "GitHub CLI",
|
||||
Long: `Do things with GitHub from your terminal`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("root")
|
||||
},
|
||||
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
// overriden in tests
|
||||
|
|
@ -57,7 +71,7 @@ var apiClientForContext = func(ctx context.Context) (*api.Client, error) {
|
|||
}
|
||||
opts := []api.ClientOption{
|
||||
api.AddHeader("Authorization", fmt.Sprintf("token %s", token)),
|
||||
api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", version.Version)),
|
||||
api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", Version)),
|
||||
api.AddHeader("Accept", "application/vnd.github.shadow-cat-preview+json"),
|
||||
}
|
||||
if verbose := os.Getenv("DEBUG"); verbose != "" {
|
||||
|
|
|
|||
36
command/testing.go
Normal file
36
command/testing.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"github.com/github/gh-cli/api"
|
||||
"github.com/github/gh-cli/context"
|
||||
)
|
||||
|
||||
func initBlankContext(repo, branch string) {
|
||||
initContext = func() context.Context {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBaseRepo(repo)
|
||||
ctx.SetBranch(branch)
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
|
||||
func initFakeHTTP() *api.FakeHTTP {
|
||||
http := &api.FakeHTTP{}
|
||||
apiClientForContext = func(context.Context) (*api.Client, error) {
|
||||
return api.NewClient(api.ReplaceTripper(http)), nil
|
||||
}
|
||||
return http
|
||||
}
|
||||
|
||||
// outputStub implements a simple utils.Runnable
|
||||
type outputStub struct {
|
||||
output []byte
|
||||
}
|
||||
|
||||
func (s outputStub) Output() ([]byte, error) {
|
||||
return s.output, nil
|
||||
}
|
||||
|
||||
func (s outputStub) Run() error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -14,6 +14,9 @@ import (
|
|||
|
||||
const (
|
||||
oauthHost = "github.com"
|
||||
)
|
||||
|
||||
var (
|
||||
// The GitHub app that is meant for development
|
||||
oauthClientID = "4d747ba5675d5d66553f"
|
||||
// This value is safe to be embedded in version control
|
||||
|
|
|
|||
39
git/git.go
39
git/git.go
|
|
@ -8,12 +8,13 @@ import (
|
|||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/github/gh-cli/utils"
|
||||
)
|
||||
|
||||
func Dir() (string, error) {
|
||||
dirCmd := exec.Command("git", "rev-parse", "-q", "--git-dir")
|
||||
dirCmd.Stderr = nil
|
||||
output, err := dirCmd.Output()
|
||||
output, err := utils.PrepareCmd(dirCmd).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Not a git repository (or any of the parent directories): .git")
|
||||
}
|
||||
|
|
@ -33,7 +34,7 @@ func Dir() (string, error) {
|
|||
func WorkdirName() (string, error) {
|
||||
toplevelCmd := exec.Command("git", "rev-parse", "--show-toplevel")
|
||||
toplevelCmd.Stderr = nil
|
||||
output, err := toplevelCmd.Output()
|
||||
output, err := utils.PrepareCmd(toplevelCmd).Output()
|
||||
dir := firstLine(output)
|
||||
if dir == "" {
|
||||
return "", fmt.Errorf("unable to determine git working directory")
|
||||
|
|
@ -44,8 +45,7 @@ func WorkdirName() (string, error) {
|
|||
func HasFile(segments ...string) bool {
|
||||
// The blessed way to resolve paths within git dir since Git 2.5.0
|
||||
pathCmd := exec.Command("git", "rev-parse", "-q", "--git-path", filepath.Join(segments...))
|
||||
pathCmd.Stderr = nil
|
||||
if output, err := pathCmd.Output(); err == nil {
|
||||
if output, err := utils.PrepareCmd(pathCmd).Output(); err == nil {
|
||||
if lines := outputLines(output); len(lines) == 1 {
|
||||
if _, err := os.Stat(lines[0]); err == nil {
|
||||
return true
|
||||
|
|
@ -97,8 +97,7 @@ func BranchAtRef(paths ...string) (name string, err error) {
|
|||
|
||||
func Editor() (string, error) {
|
||||
varCmd := exec.Command("git", "var", "GIT_EDITOR")
|
||||
varCmd.Stderr = nil
|
||||
output, err := varCmd.Output()
|
||||
output, err := utils.PrepareCmd(varCmd).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Can't load git var: GIT_EDITOR")
|
||||
}
|
||||
|
|
@ -112,8 +111,7 @@ func Head() (string, error) {
|
|||
|
||||
func SymbolicFullName(name string) (string, error) {
|
||||
parseCmd := exec.Command("git", "rev-parse", "--symbolic-full-name", name)
|
||||
parseCmd.Stderr = nil
|
||||
output, err := parseCmd.Output()
|
||||
output, err := utils.PrepareCmd(parseCmd).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Unknown revision or path not in the working tree: %s", name)
|
||||
}
|
||||
|
|
@ -145,9 +143,7 @@ func CommentChar(text string) (string, error) {
|
|||
|
||||
func Show(sha string) (string, error) {
|
||||
cmd := exec.Command("git", "-c", "log.showSignature=false", "show", "-s", "--format=%s%n%+b", sha)
|
||||
cmd.Stderr = nil
|
||||
|
||||
output, err := cmd.Output()
|
||||
output, err := utils.PrepareCmd(cmd).Output()
|
||||
return strings.TrimSpace(string(output)), err
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +153,7 @@ func Log(sha1, sha2 string) (string, error) {
|
|||
"-c", "log.showSignature=false", "log", "--no-color",
|
||||
"--format=%h (%aN, %ar)%n%w(78,3,3)%s%n%+b",
|
||||
"--cherry", shaRange)
|
||||
outputs, err := cmd.Output()
|
||||
outputs, err := utils.PrepareCmd(cmd).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Can't load git log %s..%s", sha1, sha2)
|
||||
}
|
||||
|
|
@ -167,14 +163,13 @@ func Log(sha1, sha2 string) (string, error) {
|
|||
|
||||
func listRemotes() ([]string, error) {
|
||||
remoteCmd := exec.Command("git", "remote", "-v")
|
||||
remoteCmd.Stderr = nil
|
||||
output, err := remoteCmd.Output()
|
||||
output, err := utils.PrepareCmd(remoteCmd).Output()
|
||||
return outputLines(output), err
|
||||
}
|
||||
|
||||
func Config(name string) (string, error) {
|
||||
configCmd := exec.Command("git", "config", name)
|
||||
output, err := configCmd.Output()
|
||||
output, err := utils.PrepareCmd(configCmd).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unknown config key: %s", name)
|
||||
}
|
||||
|
|
@ -190,21 +185,16 @@ func ConfigAll(name string) ([]string, error) {
|
|||
}
|
||||
|
||||
configCmd := exec.Command("git", "config", mode, name)
|
||||
output, err := configCmd.Output()
|
||||
output, err := utils.PrepareCmd(configCmd).Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unknown config %s", name)
|
||||
}
|
||||
return outputLines(output), nil
|
||||
}
|
||||
|
||||
func Run(args ...string) error {
|
||||
cmd := exec.Command("git", args...)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func LocalBranches() ([]string, error) {
|
||||
branchesCmd := exec.Command("git", "branch", "--list")
|
||||
output, err := branchesCmd.Output()
|
||||
output, err := utils.PrepareCmd(branchesCmd).Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -246,9 +236,6 @@ func Push(remote string, ref string) error {
|
|||
|
||||
func outputLines(output []byte) []string {
|
||||
lines := strings.TrimSuffix(string(output), "\n")
|
||||
if lines == "" {
|
||||
return []string{}
|
||||
}
|
||||
return strings.Split(lines, "\n")
|
||||
|
||||
}
|
||||
|
|
|
|||
4
go.mod
4
go.mod
|
|
@ -4,11 +4,13 @@ go 1.13
|
|||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.0.4
|
||||
github.com/gookit/color v1.2.0
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/mattn/go-colorable v0.1.2
|
||||
github.com/mattn/go-isatty v0.0.9
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/spf13/cobra v0.0.5
|
||||
github.com/stretchr/testify v1.3.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5
|
||||
gopkg.in/yaml.v3 v3.0.0-20191010095647-fc94e3f71652
|
||||
)
|
||||
|
|
|
|||
8
go.sum
8
go.sum
|
|
@ -1,8 +1,8 @@
|
|||
github.com/AlecAivazis/survey v1.8.7 h1:QIBq36/0wfYpXxdBqDXNAjKHx1bKnRGu/EDnva27k84=
|
||||
github.com/AlecAivazis/survey/v2 v2.0.4 h1:qzXnJSzXEvmUllWqMBWpZndvT2YfoAUzAMvZUax3L2M=
|
||||
github.com/AlecAivazis/survey/v2 v2.0.4/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliGcYHB9sNT3Bg74=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
|
||||
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
|
|
@ -13,14 +13,14 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gookit/color v1.2.0 h1:lHA77Kuyi5JpBnA9ESvwkY+nanLjRZ0mHbWQXRYk2Lk=
|
||||
github.com/gookit/color v1.2.0/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
|
||||
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
|
||||
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
|
|
@ -52,8 +52,10 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
|
|||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc=
|
||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
|
|||
9
main.go
9
main.go
|
|
@ -3,13 +3,18 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/github/gh-cli/command"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := command.RootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
if cmd, err := command.RootCmd.ExecuteC(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
_, isFlagError := err.(command.FlagError)
|
||||
if isFlagError || strings.HasPrefix(err.Error(), "unknown command ") {
|
||||
fmt.Fprintln(os.Stderr, cmd.UsageString())
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
47
test/fixtures/issueList.json
vendored
Normal file
47
test/fixtures/issueList.json
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"data": {
|
||||
"assigned": {
|
||||
"issues": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 9,
|
||||
"title": "corey thinks squash tastes bad"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 10,
|
||||
"title": "broccoli is a superfood"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"mentioned": {
|
||||
"issues": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 8,
|
||||
"title": "rabbits eat carrots"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 11,
|
||||
"title": "swiss chard is neutral"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"recent": {
|
||||
"issues": {
|
||||
"edges": []
|
||||
}
|
||||
},
|
||||
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
}
|
||||
}
|
||||
47
test/fixtures/issueStatus.json
vendored
Normal file
47
test/fixtures/issueStatus.json
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"data": {
|
||||
"assigned": {
|
||||
"issues": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 9,
|
||||
"title": "corey thinks squash tastes bad"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 10,
|
||||
"title": "broccoli is a superfood"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"mentioned": {
|
||||
"issues": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 8,
|
||||
"title": "rabbits eat carrots"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 11,
|
||||
"title": "swiss chard is neutral"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"recent": {
|
||||
"issues": {
|
||||
"edges": []
|
||||
}
|
||||
},
|
||||
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
}
|
||||
}
|
||||
36
test/fixtures/issueView.json
vendored
Normal file
36
test/fixtures/issueView.json
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"data": {
|
||||
"repository": {
|
||||
"issues": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 8,
|
||||
"title": "rabbits eat carrots",
|
||||
"url": "https://github.com/github/gh-cli/pull/10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 9,
|
||||
"title": "corey thinks squash tastes bad"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 10,
|
||||
"title": "broccoli is a superfood"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 11,
|
||||
"title": "swiss chard is neutral"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
}
|
||||
}
|
||||
80
test/fixtures/prList.json
vendored
80
test/fixtures/prList.json
vendored
|
|
@ -1,50 +1,38 @@
|
|||
{"data":{
|
||||
"repository": {
|
||||
"pullRequests": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 10,
|
||||
"title": "Blueberries are a good fruit",
|
||||
"url": "https://github.com/github/gh-cli/pull/10",
|
||||
"headRefName": "[blueberries]"
|
||||
{
|
||||
"data": {
|
||||
"repository": {
|
||||
"pullRequests": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 32,
|
||||
"title": "New feature",
|
||||
"url": "https://github.com/monalisa/hello/pull/32",
|
||||
"headRefName": "feature"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 29,
|
||||
"title": "Fixed bad bug",
|
||||
"url": "https://github.com/monalisa/hello/pull/29",
|
||||
"headRefName": "bug-fix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 28,
|
||||
"title": "Improve documentation",
|
||||
"url": "https://github.com/monalisa/hello/pull/28",
|
||||
"headRefName": "docs"
|
||||
}
|
||||
}
|
||||
],
|
||||
"pageInfo": {
|
||||
"hasNextPage": false,
|
||||
"endCursor": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"viewerCreated": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 8,
|
||||
"title": "Strawberries are not actually berries",
|
||||
"url": "https://github.com/github/gh-cli/pull/8",
|
||||
"headRefName": "[strawberries]"
|
||||
}
|
||||
}
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
},
|
||||
"reviewRequested": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 9,
|
||||
"title": "Apples are tasty",
|
||||
"url": "https://github.com/github/gh-cli/pull/9",
|
||||
"headRefName": "[apples]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 11,
|
||||
"title": "Figs are my favorite",
|
||||
"url": "https://github.com/github/gh-cli/pull/1",
|
||||
"headRefName": "[figs]"
|
||||
}
|
||||
}
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
}
|
||||
}}
|
||||
}
|
||||
50
test/fixtures/prStatus.json
vendored
Normal file
50
test/fixtures/prStatus.json
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{"data":{
|
||||
"repository": {
|
||||
"pullRequests": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 10,
|
||||
"title": "Blueberries are a good fruit",
|
||||
"url": "https://github.com/github/gh-cli/pull/10",
|
||||
"headRefName": "[blueberries]"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"viewerCreated": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 8,
|
||||
"title": "Strawberries are not actually berries",
|
||||
"url": "https://github.com/github/gh-cli/pull/8",
|
||||
"headRefName": "[strawberries]"
|
||||
}
|
||||
}
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
},
|
||||
"reviewRequested": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 9,
|
||||
"title": "Apples are tasty",
|
||||
"url": "https://github.com/github/gh-cli/pull/9",
|
||||
"headRefName": "[apples]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 11,
|
||||
"title": "Figs are my favorite",
|
||||
"url": "https://github.com/github/gh-cli/pull/1",
|
||||
"headRefName": "[figs]"
|
||||
}
|
||||
}
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
}
|
||||
}}
|
||||
8
test/fixtures/prView.json
vendored
8
test/fixtures/prView.json
vendored
|
|
@ -6,7 +6,7 @@
|
|||
"node": {
|
||||
"number": 10,
|
||||
"title": "Blueberries are a good fruit",
|
||||
"url": "https://github.com/github/gh-cli/pull/10",
|
||||
"url": "https://github.com/OWNER/REPO/pull/10",
|
||||
"headRefName": "[blueberries]"
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
"node": {
|
||||
"number": 8,
|
||||
"title": "Strawberries are not actually berries",
|
||||
"url": "https://github.com/github/gh-cli/pull/8",
|
||||
"url": "https://github.com/OWNER/REPO/pull/8",
|
||||
"headRefName": "[strawberries]"
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
"node": {
|
||||
"number": 9,
|
||||
"title": "Apples are tasty",
|
||||
"url": "https://github.com/github/gh-cli/pull/9",
|
||||
"url": "https://github.com/OWNER/REPO/pull/9",
|
||||
"headRefName": "[apples]"
|
||||
}
|
||||
},
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
"node": {
|
||||
"number": 11,
|
||||
"title": "Figs are my favorite",
|
||||
"url": "https://github.com/github/gh-cli/pull/1",
|
||||
"url": "https://github.com/OWNER/REPO/pull/1",
|
||||
"headRefName": "[figs]"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ func RunCommand(root *cobra.Command, s string) (string, error) {
|
|||
root.SetArgs(strings.Split(s, " "))
|
||||
_, err = root.ExecuteC()
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,24 @@
|
|||
package utils
|
||||
|
||||
import "github.com/gookit/color"
|
||||
import "github.com/mgutz/ansi"
|
||||
|
||||
func Black(a ...interface{}) string {
|
||||
return color.Black.Render(a...)
|
||||
var Black = ansi.ColorFunc("black")
|
||||
var White = ansi.ColorFunc("white")
|
||||
|
||||
func Gray(arg string) string {
|
||||
return ansi.Color(ansi.LightBlack+arg, "")
|
||||
}
|
||||
|
||||
func White(a ...interface{}) string {
|
||||
return color.White.Render(a...)
|
||||
}
|
||||
var Red = ansi.ColorFunc("red")
|
||||
var Green = ansi.ColorFunc("green")
|
||||
var Yellow = ansi.ColorFunc("yellow")
|
||||
var Blue = ansi.ColorFunc("blue")
|
||||
var Magenta = ansi.ColorFunc("magenta")
|
||||
var Cyan = ansi.ColorFunc("cyan")
|
||||
|
||||
func Gray(a ...interface{}) string {
|
||||
return color.Gray.Render(a...)
|
||||
}
|
||||
|
||||
func Red(a ...interface{}) string {
|
||||
return color.Red.Render(a...)
|
||||
}
|
||||
|
||||
func Green(a ...interface{}) string {
|
||||
return color.Green.Render(a...)
|
||||
}
|
||||
|
||||
func Yellow(a ...interface{}) string {
|
||||
return color.Yellow.Render(a...)
|
||||
}
|
||||
|
||||
func Blue(a ...interface{}) string {
|
||||
return color.Blue.Render(a...)
|
||||
}
|
||||
|
||||
func Magenta(a ...interface{}) string {
|
||||
return color.Magenta.Render(a...)
|
||||
}
|
||||
|
||||
func Cyan(a ...interface{}) string {
|
||||
return color.Cyan.Render(a...)
|
||||
}
|
||||
|
||||
func Bold(a ...interface{}) string {
|
||||
return color.Bold.Render(a...)
|
||||
func Bold(arg string) string {
|
||||
// This is really annoying. If you just define Bold as ColorFunc("+b") it will properly bold but
|
||||
// will not use the default color, resulting in black and probably unreadable text. This forces
|
||||
// the default color before bolding.
|
||||
return ansi.Color(ansi.DefaultFG+arg, "+b")
|
||||
}
|
||||
|
|
|
|||
76
utils/prepare_cmd.go
Normal file
76
utils/prepare_cmd.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Runnable is typically an exec.Cmd or its stub in tests
|
||||
type Runnable interface {
|
||||
Output() ([]byte, error)
|
||||
Run() error
|
||||
}
|
||||
|
||||
// PrepareCmd extends exec.Cmd with extra error reporting features and provides a
|
||||
// hook to stub command execution in tests
|
||||
var PrepareCmd = func(cmd *exec.Cmd) Runnable {
|
||||
return &cmdWithStderr{cmd}
|
||||
}
|
||||
|
||||
// SetPrepareCmd overrides PrepareCmd and returns a func to revert it back
|
||||
func SetPrepareCmd(fn func(*exec.Cmd) Runnable) func() {
|
||||
origPrepare := PrepareCmd
|
||||
PrepareCmd = fn
|
||||
return func() {
|
||||
PrepareCmd = origPrepare
|
||||
}
|
||||
}
|
||||
|
||||
// cmdWithStderr augments exec.Cmd by adding stderr to the error message
|
||||
type cmdWithStderr struct {
|
||||
*exec.Cmd
|
||||
}
|
||||
|
||||
func (c cmdWithStderr) Output() ([]byte, error) {
|
||||
if os.Getenv("DEBUG") != "" {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", c.Cmd.Args)
|
||||
}
|
||||
errStream := &bytes.Buffer{}
|
||||
c.Cmd.Stderr = errStream
|
||||
out, err := c.Cmd.Output()
|
||||
if err != nil {
|
||||
err = &CmdError{errStream, c.Cmd.Args, err}
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (c cmdWithStderr) Run() error {
|
||||
if os.Getenv("DEBUG") != "" {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", c.Cmd.Args)
|
||||
}
|
||||
errStream := &bytes.Buffer{}
|
||||
c.Cmd.Stderr = errStream
|
||||
err := c.Cmd.Run()
|
||||
if err != nil {
|
||||
err = &CmdError{errStream, c.Cmd.Args, err}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// CmdError provides more visibility into why an exec.Cmd had failed
|
||||
type CmdError struct {
|
||||
Stderr *bytes.Buffer
|
||||
Args []string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e CmdError) Error() string {
|
||||
msg := e.Stderr.String()
|
||||
if msg != "" && !strings.HasSuffix(msg, "\n") {
|
||||
msg += "\n"
|
||||
}
|
||||
return fmt.Sprintf("%s%s: %s", msg, e.Args[0], e.Err)
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ func ConcatPaths(paths ...string) string {
|
|||
return strings.Join(paths, "/")
|
||||
}
|
||||
|
||||
var OpenInBrowser = func(url string) error {
|
||||
func OpenInBrowser(url string) error {
|
||||
browser := os.Getenv("BROWSER")
|
||||
if browser == "" {
|
||||
browser = searchBrowserLauncher(runtime.GOOS)
|
||||
|
|
@ -45,7 +45,8 @@ var OpenInBrowser = func(url string) error {
|
|||
}
|
||||
|
||||
endingArgs := append(browserArgs[1:], url)
|
||||
return exec.Command(browserArgs[0], endingArgs...).Run()
|
||||
browseCmd := exec.Command(browserArgs[0], endingArgs...)
|
||||
return PrepareCmd(browseCmd).Run()
|
||||
}
|
||||
|
||||
func searchBrowserLauncher(goos string) (browser string) {
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var Version = "0.0.0"
|
||||
|
||||
func FullVersion() (string, error) {
|
||||
return fmt.Sprintf("gh version %s", Version), nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue