Merge remote-tracking branch 'origin/master' into appropriate-context-and-feedback

This commit is contained in:
Corey Johnson 2020-01-06 09:52:26 -08:00
commit 2e0ea153d5
16 changed files with 461 additions and 11635 deletions

View file

@ -1,6 +0,0 @@
name: 'Create a changelog'
description: 'Create a changelog draft based on merged PR titles'
author: 'probablycorey'
runs:
using: 'node12'
main: './lib/index.js'

File diff suppressed because one or more lines are too long

View file

@ -1,324 +0,0 @@
{
"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": "2.0.0",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-2.0.0.tgz",
"integrity": "sha512-sNpZ5dJyJyfJIO5lNYx8r/Gha4Tlm8R0MLO2cBkGdOnAAEn3t1M/MHVcoBhY/VPfjGVe5RNAUPz+6INrViiUPA==",
"requires": {
"@octokit/graphql": "^4.3.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"
}
},
"@octokit/graphql": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.3.1.tgz",
"integrity": "sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA==",
"requires": {
"@octokit/request": "^5.3.0",
"@octokit/types": "^2.0.0",
"universal-user-agent": "^4.0.0"
}
},
"@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"
}
},
"@octokit/request-error": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.0.tgz",
"integrity": "sha512-DNBhROBYjjV/I9n7A8kVkmQNkqFAMem90dSxqvPq57e2hBr7mNTX98y3R2zDpqMQHVRpBDjsvsfIGgBzy+4PAg==",
"requires": {
"@octokit/types": "^2.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
}
},
"@octokit/rest": {
"version": "16.35.2",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.35.2.tgz",
"integrity": "sha512-iijaNZpn9hBpUdh8YdXqNiWazmq4R1vCUsmxpBB0kCQ0asHZpCx+HNs22eiHuwYKRhO31ZSAGBJLi0c+3XHaKQ==",
"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"
}
},
"@octokit/types": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.0.2.tgz",
"integrity": "sha512-StASIL2lgT3TRjxv17z9pAqbnI7HGu9DrJlg3sEBFfCLaMEqp+O3IQPUF6EZtQ4xkAu2ml6kMBBCtGxjvmtmuQ==",
"requires": {
"@types/node": ">= 8"
}
},
"@types/node": {
"version": "12.12.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.21.tgz",
"integrity": "sha512-8sRGhbpU+ck1n0PGAUgVrWrWdjSW2aqNeyC15W88GRsMpSwzv6RJGlLhE7s2RhVSOdyDmxbqlWSeThq4/7xqlA=="
},
"@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": "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"
}
},
"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="
}
}
}

View file

@ -1,18 +0,0 @@
{
"license": "ISC",
"scripts": {
"build": "rm -rf lib && ncc build -s src/index.ts -o lib"
},
"dependencies": {
"@actions/core": "^1.2.0",
"@actions/github": "^2.0.0"
},
"devDependencies": {
"@zeit/ncc": "^0.20.5"
},
"prettier": {
"trailingComma": "es5",
"semi": false,
"singleQuote": true
}
}

View file

@ -1,73 +0,0 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
const GITHUB_TOKEN = getEnvVar('GITHUB_TOKEN')
interface Changelog {
draft: string[]
releases: { [tag: string]: string[] }
}
process.on('unhandledRejection', (reason: any, _: Promise<any>) =>
handleError(reason)
)
main().catch(handleError)
async function main() {
const title =
github.context.payload &&
github.context.payload.pull_request &&
github.context.payload.pull_request.title
const { changelog, sha } = await getChangelog()
const updatedChangelog = { ...changelog, draft: [title, ...changelog.draft] }
await commitChangelog(updatedChangelog, sha)
}
async function getChangelog() {
const octokit = new github.GitHub(GITHUB_TOKEN)
const owner = github.context.repo.owner
const repo = github.context.repo.repo
const path = 'changelog.json'
const response = await octokit.repos.getContents({ owner, repo, path })
const base64Content = (response.data as any).content
const sha = (response.data as any).sha
const content = Buffer.from(base64Content, 'base64').toString('utf8')
return { changelog: JSON.parse(content), sha }
}
async function commitChangelog(changelog: Changelog, sha: string) {
const octokit = new github.GitHub(GITHUB_TOKEN)
const owner = github.context.repo.owner
const repo = github.context.repo.repo
const path = 'changelog.json'
const message = 'update changelog'
const json = JSON.stringify(changelog, null, 2)
const content = Buffer.from(json).toString('base64')
const response = await octokit.repos.createOrUpdateFile({
owner,
repo,
path,
message,
content,
sha,
})
if (response.status != 200) {
throw new Error(
'Failed to commit changelog udpates. ' + JSON.stringify(response.data)
)
}
}
function handleError(err: Error) {
console.error(err)
core.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
}

View file

@ -1,5 +0,0 @@
set -e
TOKEN="$(awk '/oauth_token/ {print $2}' ~/.config/gh/config.yml | head -1)"
env "GITHUB_REPOSITORY=github/gh-cli" "GITHUB_TOKEN=$TOKEN" node lib/index.js

View file

@ -1,19 +0,0 @@
{
"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"]
}

View file

@ -1,8 +1,8 @@
name: Go
name: Tests
on: [push]
jobs:
build:
name: Build
build-linux:
name: Linux Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.13
@ -21,3 +21,43 @@ jobs:
run: |
go test ./...
go build -v .
build-windows:
name: Windows Build
runs-on: windows-latest
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v1
with:
go-version: 1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Verify dependencies
run: go mod verify
- name: Build
run: |
go test ./...
go build -v .
build-macos:
name: MacOS Build
runs-on: macos-latest
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v1
with:
go-version: 1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Verify dependencies
run: go mod verify
- name: Build
run: |
go test ./...
go build -v .

View file

@ -1,18 +0,0 @@
name: Changelog
on:
pull_request:
types: [closed]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Update the changelog
if: github.event.pull_request.merged == true
uses: ./.github/actions/update-changelog
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

View file

@ -1,6 +0,0 @@
{
"draft": [],
"releases": {
"0.0.1": ["And so it begins..."]
}
}

View file

@ -3,13 +3,14 @@ package command
import (
"fmt"
"io"
"os"
"regexp"
"strconv"
"strings"
"github.com/github/gh-cli/api"
"github.com/github/gh-cli/context"
"github.com/github/gh-cli/git"
"github.com/github/gh-cli/pkg/githubtemplate"
"github.com/github/gh-cli/utils"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -245,11 +246,16 @@ func issueCreate(cmd *cobra.Command, args []string) error {
fmt.Fprintf(colorableErr(cmd), "\nCreating issue in %s/%s\n\n", baseRepo.RepoOwner(), baseRepo.RepoName())
var templateFiles []string
if rootDir, err := git.ToplevelDir(); err == nil {
// TODO: figure out how to stub this in tests
templateFiles = githubtemplate.Find(rootDir, "ISSUE_TEMPLATE")
}
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() {
if len(templateFiles) > 1 {
openURL += "/choose"
}
cmd.Printf("Opening %s in your browser.\n", openURL)
@ -281,7 +287,7 @@ func issueCreate(cmd *cobra.Command, args []string) error {
interactive := title == "" || body == ""
if interactive {
tb, err := titleBodySurvey(cmd, title, body)
tb, err := titleBodySurvey(cmd, title, body, templateFiles)
if err != nil {
return errors.Wrap(err, "could not collect title and/or body")
}

View file

@ -8,6 +8,7 @@ import (
"github.com/github/gh-cli/api"
"github.com/github/gh-cli/context"
"github.com/github/gh-cli/git"
"github.com/github/gh-cli/pkg/githubtemplate"
"github.com/github/gh-cli/utils"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -81,7 +82,13 @@ func prCreate(cmd *cobra.Command, _ []string) error {
interactive := title == "" || body == ""
if interactive {
tb, err := titleBodySurvey(cmd, title, body)
var templateFiles []string
if rootDir, err := git.ToplevelDir(); err == nil {
// TODO: figure out how to stub this in tests
templateFiles = githubtemplate.Find(rootDir, "PULL_REQUEST_TEMPLATE")
}
tb, err := titleBodySurvey(cmd, title, body, templateFiles)
if err != nil {
return errors.Wrap(err, "could not collect title and/or body")
}

View file

@ -2,7 +2,9 @@ package command
import (
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/github/gh-cli/pkg/githubtemplate"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@ -44,9 +46,45 @@ func confirm() (int, error) {
return confirmAnswers.Confirmation, nil
}
func titleBodySurvey(cmd *cobra.Command, providedTitle string, providedBody string) (*titleBody, error) {
func selectTemplate(templatePaths []string) (string, error) {
templateResponse := struct {
Index int
}{}
if len(templatePaths) > 1 {
templateNames := []string{}
for _, p := range templatePaths {
templateNames = append(templateNames, githubtemplate.ExtractName(p))
}
selectQs := []*survey.Question{
{
Name: "index",
Prompt: &survey.Select{
Message: "Choose a template",
Options: templateNames,
},
},
}
if err := survey.Ask(selectQs, &templateResponse); err != nil {
return "", errors.Wrap(err, "could not prompt")
}
}
templateContents := githubtemplate.ExtractContents(templatePaths[templateResponse.Index])
return string(templateContents), nil
}
func titleBodySurvey(cmd *cobra.Command, providedTitle string, providedBody string, templatePaths []string) (*titleBody, error) {
inProgress := titleBody{}
if providedBody == "" && len(templatePaths) > 0 {
templateContents, err := selectTemplate(templatePaths)
if err != nil {
return nil, err
}
inProgress.Body = templateContents
}
confirmed := false
editor := determineEditor()
@ -64,6 +102,7 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle string, providedBody stri
Message: fmt.Sprintf("Body (%s)", editor),
FileName: "*.md",
Default: inProgress.Body,
HideDefault: true,
AppendDefault: true,
Editor: editor,
},

View file

@ -111,6 +111,14 @@ func ReadBranchConfig(branch string) (cfg BranchConfig) {
return
}
// ToplevelDir returns the top-level directory path of the current repository
func ToplevelDir() (string, error) {
showCmd := exec.Command("git", "rev-parse", "--show-toplevel")
output, err := utils.PrepareCmd(showCmd).Output()
return firstLine(output), err
}
func outputLines(output []byte) []string {
lines := strings.TrimSuffix(string(output), "\n")
return strings.Split(lines, "\n")

View file

@ -0,0 +1,99 @@
package githubtemplate
import (
"io/ioutil"
"path"
"regexp"
"sort"
"strings"
"gopkg.in/yaml.v3"
)
// Find returns the list of template file paths
func Find(rootDir string, name string) []string {
results := []string{}
// https://help.github.com/en/github/building-a-strong-community/creating-a-pull-request-template-for-your-repository
candidateDirs := []string{
path.Join(rootDir, ".github"),
rootDir,
path.Join(rootDir, "docs"),
}
mainLoop:
for _, dir := range candidateDirs {
files, err := ioutil.ReadDir(dir)
if err != nil {
continue
}
// detect multiple templates in a subdirectory
for _, file := range files {
if strings.EqualFold(file.Name(), name) && file.IsDir() {
templates, err := ioutil.ReadDir(path.Join(dir, file.Name()))
if err != nil {
break
}
for _, tf := range templates {
if strings.HasSuffix(tf.Name(), ".md") {
results = append(results, path.Join(dir, file.Name(), tf.Name()))
}
}
if len(results) > 0 {
break mainLoop
}
break
}
}
// detect a single template file
for _, file := range files {
if strings.EqualFold(file.Name(), name+".md") {
results = append(results, path.Join(dir, file.Name()))
break
}
}
if len(results) > 0 {
break
}
}
sort.Sort(sort.StringSlice(results))
return results
}
// ExtractName returns the name of the template from YAML front-matter
func ExtractName(filePath string) string {
contents, err := ioutil.ReadFile(filePath)
if err == nil && detectFrontmatter(contents)[0] == 0 {
templateData := struct {
Name string
}{}
if err := yaml.Unmarshal(contents, &templateData); err == nil && templateData.Name != "" {
return templateData.Name
}
}
return path.Base(filePath)
}
// ExtractContents returns the template contents without the YAML front-matter
func ExtractContents(filePath string) []byte {
contents, err := ioutil.ReadFile(filePath)
if err != nil {
return []byte{}
}
if frontmatterBoundaries := detectFrontmatter(contents); frontmatterBoundaries[0] == 0 {
return contents[frontmatterBoundaries[1]:]
}
return contents
}
var yamlPattern = regexp.MustCompile(`(?m)^---\r?\n(\s*\r?\n)?`)
func detectFrontmatter(c []byte) []int {
if matches := yamlPattern.FindAllIndex(c, 2); len(matches) > 1 {
return []int{matches[0][0], matches[1][1]}
}
return []int{-1, -1}
}

View file

@ -0,0 +1,253 @@
package githubtemplate
import (
"io/ioutil"
"os"
"path"
"reflect"
"testing"
)
func TestFind(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "gh-cli")
if err != nil {
t.Fatal(err)
}
type args struct {
rootDir string
name string
}
tests := []struct {
name string
prepare []string
args args
want []string
}{
{
name: "Template in root",
prepare: []string{
"README.md",
"ISSUE_TEMPLATE",
"issue_template.md",
"issue_template.txt",
"pull_request_template.md",
},
args: args{
rootDir: tmpdir,
name: "ISSUE_TEMPLATE",
},
want: []string{
path.Join(tmpdir, "issue_template.md"),
},
},
{
name: "Template in .github takes precedence",
prepare: []string{
"ISSUE_TEMPLATE.md",
".github/issue_template.md",
},
args: args{
rootDir: tmpdir,
name: "ISSUE_TEMPLATE",
},
want: []string{
path.Join(tmpdir, ".github/issue_template.md"),
},
},
{
name: "Template in docs",
prepare: []string{
"README.md",
"docs/issue_template.md",
},
args: args{
rootDir: tmpdir,
name: "ISSUE_TEMPLATE",
},
want: []string{
path.Join(tmpdir, "docs/issue_template.md"),
},
},
{
name: "Multiple templates",
prepare: []string{
".github/ISSUE_TEMPLATE/nope.md",
".github/PULL_REQUEST_TEMPLATE.md",
".github/PULL_REQUEST_TEMPLATE/one.md",
".github/PULL_REQUEST_TEMPLATE/two.md",
".github/PULL_REQUEST_TEMPLATE/three.md",
"docs/pull_request_template.md",
},
args: args{
rootDir: tmpdir,
name: "PuLl_ReQuEsT_TeMpLaTe",
},
want: []string{
path.Join(tmpdir, ".github/PULL_REQUEST_TEMPLATE/one.md"),
path.Join(tmpdir, ".github/PULL_REQUEST_TEMPLATE/three.md"),
path.Join(tmpdir, ".github/PULL_REQUEST_TEMPLATE/two.md"),
},
},
{
name: "Empty multiple templates directory",
prepare: []string{
".github/issue_template.md",
".github/issue_template/.keep",
},
args: args{
rootDir: tmpdir,
name: "ISSUE_TEMPLATE",
},
want: []string{
path.Join(tmpdir, ".github/issue_template.md"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, p := range tt.prepare {
fp := path.Join(tmpdir, p)
os.MkdirAll(path.Dir(fp), 0700)
file, err := os.Create(fp)
if err != nil {
t.Fatal(err)
}
file.Close()
}
if got := Find(tt.args.rootDir, tt.args.name); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Find() = %v, want %v", got, tt.want)
}
})
os.RemoveAll(tmpdir)
}
}
func TestExtractName(t *testing.T) {
tmpfile, err := ioutil.TempFile("", "gh-cli")
if err != nil {
t.Fatal(err)
}
tmpfile.Close()
defer os.Remove(tmpfile.Name())
type args struct {
filePath string
}
tests := []struct {
name string
prepare string
args args
want string
}{
{
name: "Complete front-matter",
prepare: `---
name: Bug Report
about: This is how you report bugs
---
Template contents
---
More of template
`,
args: args{
filePath: tmpfile.Name(),
},
want: "Bug Report",
},
{
name: "Incomplete front-matter",
prepare: `---
about: This is how you report bugs
---
`,
args: args{
filePath: tmpfile.Name(),
},
want: path.Base(tmpfile.Name()),
},
{
name: "No front-matter",
prepare: `name: This is not yaml!`,
args: args{
filePath: tmpfile.Name(),
},
want: path.Base(tmpfile.Name()),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ioutil.WriteFile(tmpfile.Name(), []byte(tt.prepare), 0600)
if got := ExtractName(tt.args.filePath); got != tt.want {
t.Errorf("ExtractName() = %v, want %v", got, tt.want)
}
})
}
}
func TestExtractContents(t *testing.T) {
tmpfile, err := ioutil.TempFile("", "gh-cli")
if err != nil {
t.Fatal(err)
}
tmpfile.Close()
defer os.Remove(tmpfile.Name())
type args struct {
filePath string
}
tests := []struct {
name string
prepare string
args args
want string
}{
{
name: "Has front-matter",
prepare: `---
name: Bug Report
---
Template contents
---
More of template
`,
args: args{
filePath: tmpfile.Name(),
},
want: `Template contents
---
More of template
`,
},
{
name: "No front-matter",
prepare: `Template contents
---
More of template
---
Even more
`,
args: args{
filePath: tmpfile.Name(),
},
want: `Template contents
---
More of template
---
Even more
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ioutil.WriteFile(tmpfile.Name(), []byte(tt.prepare), 0600)
if got := ExtractContents(tt.args.filePath); string(got) != tt.want {
t.Errorf("ExtractContents() = %v, want %v", string(got), tt.want)
}
})
}
}