зеркало из https://github.com/mislav/hub.git
Merge remote-tracking branch 'origin' into pr-merge
This commit is contained in:
Коммит
393e75af87
|
@ -8,7 +8,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go: [ '1.9', '1.10', '1.11', '1.12', '1.13' ]
|
||||
go: ["1.11", "1.12", "1.13", "1.14"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
@ -41,12 +41,6 @@ jobs:
|
|||
# run: sudo apt-get install -y zsh fish
|
||||
|
||||
- name: Run tests
|
||||
shell: bash
|
||||
run: |
|
||||
export GOPATH="$HOME"/go
|
||||
mkdir -p "$GOPATH"/src/github.com/github
|
||||
ln -svf "$PWD" "$GOPATH"/src/github.com/github/hub
|
||||
cd "$GOPATH"/src/github.com/github/hub
|
||||
make test-all
|
||||
run: make test-all
|
||||
env:
|
||||
CI: true
|
||||
|
|
|
@ -1,31 +1,30 @@
|
|||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags: 'v*'
|
||||
tags: "v*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Publish release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: "1.13"
|
||||
- name: Run tests
|
||||
|
||||
- name: Publish release script
|
||||
run: script/publish-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
jobs:
|
||||
homebrew:
|
||||
name: Bump Homebrew formula
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: mislav/bump-homebrew-formula-action@v1.4
|
||||
- uses: mislav/bump-homebrew-formula-action@v1
|
||||
if: "!contains(github.ref, '-')" # skip prereleases
|
||||
with:
|
||||
formula-name: hub
|
||||
env:
|
||||
COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -14,3 +14,4 @@ tags
|
|||
/site
|
||||
/hub
|
||||
.vscode
|
||||
.DS_Store
|
||||
|
|
|
@ -20,5 +20,4 @@ COPY Gemfile Gemfile.lock ./
|
|||
RUN bundle install
|
||||
|
||||
ENV LANG C.UTF-8
|
||||
ENV GOFLAGS -mod=vendor
|
||||
ENV USER app
|
||||
|
|
22
Makefile
22
Makefile
|
@ -1,19 +1,17 @@
|
|||
SOURCES = $(shell script/build files)
|
||||
SOURCES = $(shell go list -f '{{range .GoFiles}}{{$$.Dir}}/{{.}}\
|
||||
{{end}}' ./...)
|
||||
SOURCE_DATE_EPOCH ?= $(shell date +%s)
|
||||
BUILD_DATE = $(shell date -u -d "@$(SOURCE_DATE_EPOCH)" '+%d %b %Y' 2>/dev/null || date -u -r "$(SOURCE_DATE_EPOCH)" '+%d %b %Y')
|
||||
HUB_VERSION = $(shell bin/hub version | tail -1)
|
||||
FLAGS_ALL = $(shell go version | grep -q 'go1.[89]' || echo 'all=')
|
||||
export GOFLAGS := $(shell go version | grep -q 'go1.1[^0]' && echo '-mod=vendor')
|
||||
|
||||
export GO111MODULE=on
|
||||
unexport GOPATH
|
||||
|
||||
export LDFLAGS := -extldflags '$(LDFLAGS)'
|
||||
export GCFLAGS := $(FLAGS_ALL)-trimpath '$(PWD)'
|
||||
export ASMFLAGS := $(FLAGS_ALL)-trimpath '$(PWD)'
|
||||
export GCFLAGS := all=-trimpath '$(PWD)'
|
||||
export ASMFLAGS := all=-trimpath '$(PWD)'
|
||||
|
||||
ifneq ($(GOFLAGS),)
|
||||
export GO111MODULE=on
|
||||
unexport GOPATH
|
||||
endif
|
||||
|
||||
MIN_COVERAGE = 89.4
|
||||
MIN_COVERAGE = 90.2
|
||||
|
||||
HELP_CMD = \
|
||||
share/man/man1/hub-alias.1 \
|
||||
|
@ -53,7 +51,7 @@ bin/hub: $(SOURCES)
|
|||
script/build -o $@
|
||||
|
||||
bin/md2roff: $(SOURCES)
|
||||
go build -o $@ github.com/github/hub/md2roff-bin
|
||||
go build -o $@ github.com/github/hub/v2/md2roff-bin
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
|
83
README.md
83
README.md
|
@ -1,6 +1,10 @@
|
|||
hub is a command line tool that wraps `git` in order to extend it with extra
|
||||
features and commands that make working with GitHub easier.
|
||||
|
||||
For an official, potentially more user-friendly command-line interface to GitHub,
|
||||
see [cli.github.com](https://cli.github.com) and
|
||||
[this comparison](https://github.com/cli/cli/blob/trunk/docs/gh-vs-hub.md).
|
||||
|
||||
This repository and its issue tracker is **not for reporting problems with
|
||||
GitHub.com** web interface. If you have a problem with GitHub itself, please
|
||||
[contact Support](https://github.com/contact).
|
||||
|
@ -10,19 +14,23 @@ Usage
|
|||
|
||||
``` sh
|
||||
$ hub clone rtomayko/tilt
|
||||
|
||||
# expands to:
|
||||
#=> git clone git://github.com/rtomayko/tilt.git
|
||||
|
||||
# if you prefer HTTPS to git/SSH protocols:
|
||||
$ git config --global hub.protocol https
|
||||
$ hub clone rtomayko/tilt
|
||||
#=> git clone https://github.com/rtomayko/tilt.git
|
||||
```
|
||||
|
||||
hub can be safely [aliased](#aliasing) as `git` so you can type `$ git
|
||||
<command>` in the shell and get all the usual `hub` features.
|
||||
See [usage examples](https://hub.github.com/#developer) or the [full reference
|
||||
documentation](https://hub.github.com/hub.1.html) to see all available commands
|
||||
and flags.
|
||||
|
||||
See [Usage documentation](https://hub.github.com/hub.1.html) for the list of all
|
||||
commands and their arguments.
|
||||
hub can also be used to make shell scripts that [directly interact with the
|
||||
GitHub API](https://hub.github.com/#scripting).
|
||||
|
||||
hub can also be used to make shell scripts that [manually interface with the
|
||||
GitHub API](https://hub.github.com/hub-api.1.html).
|
||||
hub can be safely [aliased](#aliasing) as `git`, so you can type `$ git
|
||||
<command>` in the shell and have it expanded with `hub` features.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
@ -39,20 +47,51 @@ Fedora Linux | [DNF](https://fedoraproject.org/wiki/DNF) | `sudo dnf install hub
|
|||
Arch Linux | [pacman](https://wiki.archlinux.org/index.php/pacman) | `sudo pacman -S hub`
|
||||
FreeBSD | [pkg(8)](http://man.freebsd.org/pkg/8) | `pkg install hub`
|
||||
Debian | [apt(8)](https://manpages.debian.org/buster/apt/apt.8.en.html) | `sudo apt install hub`
|
||||
Ubuntu | [Snap](https://snapcraft.io) | `snap install hub --classic`
|
||||
Ubuntu | [Snap](https://snapcraft.io) | [We do not recommend installing the snap anymore.](https://github.com/github/hub/issues?q=is%3Aissue+snap)
|
||||
openSUSE | [Zypper](https://en.opensuse.org/SDB:Zypper_manual) | `sudo zypper install hub`
|
||||
Void Linux | [xbps](https://github.com/void-linux/xbps) | `sudo xbps-install -S hub`
|
||||
Gentoo | [Portage](https://wiki.gentoo.org/wiki/Portage) | `sudo emerge dev-vcs/hub`
|
||||
_any_ | [conda](https://docs.conda.io/en/latest/) | `conda install -c conda-forge hub`
|
||||
|
||||
|
||||
Packages other than Homebrew are community-maintained (thank you!) and they
|
||||
are not guaranteed to match the [latest hub release][latest]. Check `hub
|
||||
version` after installing a community package.
|
||||
|
||||
#### Standalone
|
||||
|
||||
`hub` can be easily installed as an executable. Download the latest
|
||||
[compiled binaries](https://github.com/github/hub/releases) and put it anywhere
|
||||
in your executable path.
|
||||
`hub` can be easily installed as an executable. Download the [latest
|
||||
binary][latest] for your system and put it anywhere in your executable path.
|
||||
|
||||
#### GitHub Actions
|
||||
|
||||
hub is ready to be used in your [GitHub Actions][] workflows:
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: List open pull requests
|
||||
run: hub pr list
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
Note that the default `secrets.GITHUB_TOKEN` will only work for API operations
|
||||
scoped to the repository that runs this workflow. If you need to interact with other
|
||||
repositories, [generate a Personal Access Token][pat] with at least the `repo` scope
|
||||
and add it to your [repository secrets][].
|
||||
|
||||
|
||||
[github actions]: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
||||
[pat]: https://github.com/settings/tokens
|
||||
[repository secrets]: https://docs.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets
|
||||
|
||||
#### Source
|
||||
|
||||
Prerequisites for building from source are:
|
||||
|
||||
* `make`
|
||||
* [Go 1.9+](https://golang.org/doc/install)
|
||||
* [Go 1.11+](https://golang.org/doc/install)
|
||||
|
||||
Clone this repository and run `make install`:
|
||||
|
||||
|
@ -67,11 +106,6 @@ cd hub
|
|||
make install prefix=/usr/local
|
||||
```
|
||||
|
||||
This assumes support for [Go 1.11+
|
||||
modules](https://github.com/golang/go/wiki/Modules). If you are building on an
|
||||
older version of Go, you will need to clone the repository into
|
||||
`$GOPATH/src/github.com/github/hub`.
|
||||
|
||||
Aliasing
|
||||
--------
|
||||
|
||||
|
@ -113,18 +147,15 @@ New-Item -Type file -Force $PROFILE
|
|||
|
||||
### Shell tab-completion
|
||||
|
||||
hub repository contains tab-completion scripts for bash, zsh and fish.
|
||||
hub repository contains [tab-completion scripts](./etc) for bash, zsh and fish.
|
||||
These scripts complement existing completion scripts that ship with git.
|
||||
|
||||
[Installation instructions](etc)
|
||||
|
||||
* [hub bash completion](https://github.com/github/hub/blob/master/etc/hub.bash_completion.sh)
|
||||
* [hub zsh completion](https://github.com/github/hub/blob/master/etc/hub.zsh_completion)
|
||||
* [hub fish completion](https://github.com/github/hub/blob/master/etc/hub.fish_completion)
|
||||
|
||||
Meta
|
||||
----
|
||||
|
||||
* Home: <https://github.com/github/hub>
|
||||
* Bugs: <https://github.com/github/hub/issues>
|
||||
* Authors: <https://github.com/github/hub/contributors>
|
||||
* Our [Code of Conduct](https://github.com/github/hub/blob/master/CODE_OF_CONDUCT.md)
|
||||
|
||||
|
||||
[latest]: https://github.com/github/hub/releases/latest
|
||||
|
|
21
cmd/cmd.go
21
cmd/cmd.go
|
@ -8,9 +8,10 @@ import (
|
|||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/v2/ui"
|
||||
)
|
||||
|
||||
// Cmd is a project-wide struct that represents a command to be run in the console.
|
||||
type Cmd struct {
|
||||
Name string
|
||||
Args []string
|
||||
|
@ -20,9 +21,20 @@ type Cmd struct {
|
|||
}
|
||||
|
||||
func (cmd Cmd) String() string {
|
||||
return fmt.Sprintf("%s %s", cmd.Name, strings.Join(cmd.Args, " "))
|
||||
args := make([]string, len(cmd.Args))
|
||||
for i, a := range cmd.Args {
|
||||
if strings.ContainsRune(a, '"') {
|
||||
args[i] = fmt.Sprintf(`'%s'`, a)
|
||||
} else if a == "" || strings.ContainsRune(a, '\'') || strings.ContainsRune(a, ' ') {
|
||||
args[i] = fmt.Sprintf(`"%s"`, a)
|
||||
} else {
|
||||
args[i] = a
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s %s", cmd.Name, strings.Join(args, " "))
|
||||
}
|
||||
|
||||
// WithArg returns the current argument
|
||||
func (cmd *Cmd) WithArg(arg string) *Cmd {
|
||||
cmd.Args = append(cmd.Args, arg)
|
||||
|
||||
|
@ -64,9 +76,8 @@ func (cmd *Cmd) Success() bool {
|
|||
func (cmd *Cmd) Run() error {
|
||||
if isWindows() {
|
||||
return cmd.Spawn()
|
||||
} else {
|
||||
return cmd.Exec()
|
||||
}
|
||||
return cmd.Exec()
|
||||
}
|
||||
|
||||
func isWindows() bool {
|
||||
|
@ -137,7 +148,7 @@ func NewWithArray(cmd []string) *Cmd {
|
|||
|
||||
func verboseLog(cmd *Cmd) {
|
||||
if os.Getenv("HUB_VERBOSE") != "" {
|
||||
msg := fmt.Sprintf("$ %s %s", cmd.Name, strings.Join(cmd.Args, " "))
|
||||
msg := fmt.Sprintf("$ %s", cmd.String())
|
||||
if ui.IsTerminal(os.Stderr) {
|
||||
msg = fmt.Sprintf("\033[35m%s\033[0m", msg)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package cmd
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
|
@ -18,3 +18,11 @@ func TestWithArg(t *testing.T) {
|
|||
assert.Equal(t, "git", execCmd.Name)
|
||||
assert.Equal(t, 4, len(execCmd.Args))
|
||||
}
|
||||
|
||||
func Test_String(t *testing.T) {
|
||||
c := Cmd{
|
||||
Name: "echo",
|
||||
Args: []string{"hi", "hello world", "don't", `"fake news"`},
|
||||
}
|
||||
assert.Equal(t, `echo hi "hello world" "don't" '"fake news"'`, c.String())
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdAlias = &Command{
|
||||
|
|
|
@ -11,12 +11,12 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdApi = &Command{
|
||||
var cmdAPI = &Command{
|
||||
Run: apiCommand,
|
||||
Usage: "api [-it] [-X <METHOD>] [-H <HEADER>] [--cache <TTL>] <ENDPOINT> [-F <FIELD>|--input <FILE>]",
|
||||
Long: `Low-level GitHub API request interface.
|
||||
|
@ -143,7 +143,7 @@ hub(1)
|
|||
}
|
||||
|
||||
func init() {
|
||||
CmdRunner.Use(cmdApi)
|
||||
CmdRunner.Use(cmdAPI)
|
||||
}
|
||||
|
||||
func apiCommand(_ *Command, args *Args) {
|
||||
|
@ -285,6 +285,14 @@ func apiCommand(_ *Command, args *Args) {
|
|||
response.Body.Close()
|
||||
|
||||
if !success {
|
||||
if ssoErr := github.ValidateGitHubSSO(response.Response); ssoErr != nil {
|
||||
ui.Errorln()
|
||||
ui.Errorln(ssoErr)
|
||||
}
|
||||
if scopeErr := github.ValidateSufficientOAuthScopes(response.Response); scopeErr != nil {
|
||||
ui.Errorln()
|
||||
ui.Errorln(scopeErr)
|
||||
}
|
||||
os.Exit(22)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdApply = &Command{
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/cmd"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/cmd"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
|
@ -92,10 +92,8 @@ func (a *Args) ToCmd() *cmd.Cmd {
|
|||
}
|
||||
|
||||
for _, arg := range a.Params {
|
||||
if arg != "" {
|
||||
c.WithArg(arg)
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package commands
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestNewArgs(t *testing.T) {
|
||||
|
@ -120,6 +120,12 @@ func TestArgs_GlobalFlags_Replaced(t *testing.T) {
|
|||
assert.Equal(t, []string{"-a", "http://example.com"}, cmd.Args)
|
||||
}
|
||||
|
||||
func TestArgs_ToCmd(t *testing.T) {
|
||||
args := NewArgs([]string{"a", "", "b", ""})
|
||||
cmd := args.ToCmd()
|
||||
assert.Equal(t, []string{"a", "", "b", ""}, cmd.Args)
|
||||
}
|
||||
|
||||
func TestArgs_GlobalFlags_BeforeAfterChain(t *testing.T) {
|
||||
args := NewArgs([]string{"-c", "key=value", "-C", "dir", "status"})
|
||||
args.Before("git", "remote", "add")
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdBrowse = &Command{
|
||||
|
@ -118,12 +118,12 @@ func browse(command *Command, args *Args) {
|
|||
path = subpage
|
||||
}
|
||||
|
||||
pageUrl := project.WebURL("", "", path)
|
||||
pageURL := project.WebURL("", "", path)
|
||||
|
||||
args.NoForward()
|
||||
flagBrowseURLPrint := args.Flag.Bool("--url")
|
||||
flagBrowseURLCopy := args.Flag.Bool("--copy")
|
||||
printBrowseOrCopy(args, pageUrl, !flagBrowseURLPrint && !flagBrowseURLCopy, flagBrowseURLCopy)
|
||||
printBrowseOrCopy(args, pageURL, !flagBrowseURLPrint && !flagBrowseURLCopy, flagBrowseURLCopy)
|
||||
}
|
||||
|
||||
func branchInURL(branch *github.Branch) string {
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/git"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdCheckout = &Command{
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdCherryPick = &Command{
|
||||
|
@ -57,11 +57,11 @@ func transformCherryPickArgs(args *Args) {
|
|||
sha = matches[1]
|
||||
project = url.Project
|
||||
} else if matches := pullRegex.FindStringSubmatch(projectPath); len(matches) > 0 {
|
||||
pullId := matches[1]
|
||||
pullID := matches[1]
|
||||
sha = matches[2]
|
||||
utils.Check(mainProjectErr)
|
||||
project = mainProject
|
||||
refspec = fmt.Sprintf("refs/pull/%s/head", pullId)
|
||||
refspec = fmt.Sprintf("refs/pull/%s/head", pullID)
|
||||
}
|
||||
} else {
|
||||
ownerWithShaRegexp := regexp.MustCompile(fmt.Sprintf("^(%s)@(%s)$", OwnerRe, shaRe))
|
||||
|
|
|
@ -5,10 +5,10 @@ import (
|
|||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/git"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdCiStatus = &Command{
|
||||
|
@ -23,7 +23,7 @@ var cmdCiStatus = &Command{
|
|||
-f, --format <FORMAT>
|
||||
Pretty print all status checks using <FORMAT> (implies ''--verbose''). See the
|
||||
"PRETTY FORMATS" section of git-log(1) for some additional details on how
|
||||
placeholders are used in format. The available placeholders for issues are:
|
||||
placeholders are used in format. The available placeholders for checks are:
|
||||
|
||||
%U: the URL of this status check
|
||||
|
||||
|
@ -174,7 +174,7 @@ func ciVerboseFormat(statuses []github.CIStatus, formatString string, colorize b
|
|||
"S": status.State,
|
||||
"sC": "",
|
||||
"t": status.Context,
|
||||
"U": status.TargetUrl,
|
||||
"U": status.TargetURL,
|
||||
}
|
||||
|
||||
if colorize {
|
||||
|
@ -183,7 +183,7 @@ func ciVerboseFormat(statuses []github.CIStatus, formatString string, colorize b
|
|||
|
||||
format := formatString
|
||||
if format == "" {
|
||||
if status.TargetUrl == "" {
|
||||
if status.TargetURL == "" {
|
||||
format = fmt.Sprintf("%%sC%s%%Creset\t%%t\n", stateMarker)
|
||||
} else {
|
||||
format = fmt.Sprintf("%%sC%s%%Creset\t%%<(%d)%%t\t%%U\n", stateMarker, contextWidth)
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdClone = &Command{
|
||||
|
@ -79,7 +79,7 @@ func transformCloneArgs(args *Args) {
|
|||
for _, i := range p.PositionalIndices {
|
||||
a := args.Params[i]
|
||||
if nameWithOwnerRegexp.MatchString(a) && !isCloneable(a) {
|
||||
url := getCloneUrl(a, isSSH, args.Command != "submodule")
|
||||
url := getCloneURL(a, isSSH, args.Command != "submodule")
|
||||
args.ReplaceParam(i, url)
|
||||
}
|
||||
break
|
||||
|
@ -95,7 +95,7 @@ func parseClonePrivateFlag(args *Args) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func getCloneUrl(nameWithOwner string, isSSH, allowSSH bool) string {
|
||||
func getCloneURL(nameWithOwner string, isSSH, allowSSH bool) string {
|
||||
name := nameWithOwner
|
||||
owner := ""
|
||||
if strings.Contains(name, "/") {
|
||||
|
@ -148,7 +148,7 @@ func getCloneUrl(nameWithOwner string, isSSH, allowSSH bool) string {
|
|||
|
||||
if !isSSH &&
|
||||
allowSSH &&
|
||||
!github.IsHttpsProtocol() {
|
||||
!github.IsHTTPSProtocol() {
|
||||
isSSH = repo.Private || repo.Permissions.Push
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -62,16 +62,16 @@ func (c *Command) parseArguments(args *Args) error {
|
|||
}
|
||||
args.Flag = utils.NewArgsParserWithUsage("-h, --help\n" + knownFlags)
|
||||
|
||||
if rest, err := args.Flag.Parse(args.Params); err == nil {
|
||||
rest, err := args.Flag.Parse(args.Params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s\n%s", err, c.Synopsis())
|
||||
}
|
||||
if args.Flag.Bool("--help") {
|
||||
return &ErrHelp{err: c.Synopsis()}
|
||||
}
|
||||
args.Params = rest
|
||||
args.Terminator = args.Flag.HasTerminated
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("%s\n%s", err, c.Synopsis())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Command) Use(subCommand *Command) {
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
"github.com/github/hub/v2/ui"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdCompare = &Command{
|
||||
|
@ -149,7 +149,6 @@ var compareUnescaper = strings.NewReplacer(
|
|||
func rangeQueryEscape(r string) string {
|
||||
if strings.Contains(r, "..") {
|
||||
return r
|
||||
} else {
|
||||
return compareUnescaper.Replace(url.QueryEscape(r))
|
||||
}
|
||||
return compareUnescaper.Replace(url.QueryEscape(r))
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
|
@ -4,10 +4,10 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/git"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdCreate = &Command{
|
||||
|
@ -143,9 +143,9 @@ func create(command *Command, args *Args) {
|
|||
args.Before("git", "remote", "add", "-f", originName, url)
|
||||
}
|
||||
|
||||
webUrl := project.WebURL("", "", "")
|
||||
webURL := project.WebURL("", "", "")
|
||||
args.NoForward()
|
||||
flagCreateBrowse := args.Flag.Bool("--browse")
|
||||
flagCreateCopy := args.Flag.Bool("--copy")
|
||||
printBrowseOrCopy(args, webUrl, flagCreateBrowse, flagCreateCopy)
|
||||
printBrowseOrCopy(args, webURL, flagCreateBrowse, flagCreateCopy)
|
||||
}
|
||||
|
|
|
@ -7,9 +7,9 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdDelete = &Command{
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdFetch = &Command{
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@ package commands
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdFork = &Command{
|
||||
|
@ -82,7 +82,7 @@ func fork(cmd *Command, args *Args) {
|
|||
if err == nil && existingRepo != nil {
|
||||
var parentURL *github.URL
|
||||
if parent := existingRepo.Parent; parent != nil {
|
||||
parentURL, _ = github.ParseURL(parent.HtmlUrl)
|
||||
parentURL, _ = github.ParseURL(parent.HTMLURL)
|
||||
}
|
||||
if parentURL == nil || !project.SameAs(parentURL.Project) {
|
||||
err = fmt.Errorf("Error creating fork: %s already exists on %s",
|
||||
|
|
|
@ -5,9 +5,9 @@ import (
|
|||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -129,7 +129,7 @@ func createGist(cmd *Command, args *Args) {
|
|||
if args.Noop {
|
||||
ui.Println("Would create gist")
|
||||
gist = &github.Gist{
|
||||
HtmlUrl: fmt.Sprintf("https://gist.%s/%s", gh.Host.Host, "ID"),
|
||||
HTMLURL: fmt.Sprintf("https://gist.%s/%s", gh.Host.Host, "ID"),
|
||||
}
|
||||
} else {
|
||||
gist, err = gh.CreateGist(filenames, args.Flag.Bool("--public"))
|
||||
|
@ -138,7 +138,7 @@ func createGist(cmd *Command, args *Args) {
|
|||
|
||||
flagIssueBrowse := args.Flag.Bool("--browse")
|
||||
flagIssueCopy := args.Flag.Bool("--copy")
|
||||
printBrowseOrCopy(args, gist.HtmlUrl, flagIssueBrowse, flagIssueCopy)
|
||||
printBrowseOrCopy(args, gist.HTMLURL, flagIssueBrowse, flagIssueCopy)
|
||||
}
|
||||
|
||||
func showGist(cmd *Command, args *Args) {
|
||||
|
|
|
@ -8,9 +8,9 @@ import (
|
|||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/git"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/github/hub/v2/utils"
|
||||
"github.com/kballard/go-shellquote"
|
||||
)
|
||||
|
||||
|
@ -175,6 +175,7 @@ func displayManPage(manPage string, args *Args, isWeb bool) error {
|
|||
}
|
||||
|
||||
c := exec.Command(manArgs[0], manArgs[1:]...)
|
||||
c.Stdin = os.Stdin
|
||||
c.Stdout = os.Stdout
|
||||
c.Stderr = os.Stderr
|
||||
c.Env = env
|
||||
|
@ -188,14 +189,12 @@ func displayManPage(manPage string, args *Args, isWeb bool) error {
|
|||
func lookupCmd(name string) *Command {
|
||||
if strings.HasPrefix(name, "hub-") {
|
||||
return CmdRunner.Lookup(strings.TrimPrefix(name, "hub-"))
|
||||
} else {
|
||||
}
|
||||
cmd := CmdRunner.Lookup(name)
|
||||
if cmd != nil && !cmd.GitExtension {
|
||||
return cmd
|
||||
} else {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func customCommands() []string {
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdInit = &Command{
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func setupInitContext() {
|
||||
|
@ -57,13 +57,13 @@ func TestInitInAnotherDir(t *testing.T) {
|
|||
|
||||
commands := args.Commands()
|
||||
assert.Equal(t, 2, len(commands))
|
||||
assert.Equal(t, "git init --template mytpl --shared=umask my project", commands[0].String())
|
||||
assert.Equal(t, "git init --template mytpl --shared=umask \"my project\"", commands[0].String())
|
||||
|
||||
currentDir, err := os.Getwd()
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
expected := fmt.Sprintf(
|
||||
"git --git-dir %s remote add origin git@github.com:jingweno/%s.git",
|
||||
"git --git-dir \"%s\" remote add origin git@github.com:jingweno/%s.git",
|
||||
filepath.Join(currentDir, "my project", ".git"),
|
||||
"my-project",
|
||||
)
|
||||
|
|
|
@ -7,10 +7,10 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/git"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -20,7 +20,9 @@ var (
|
|||
issue [-a <ASSIGNEE>] [-c <CREATOR>] [-@ <USER>] [-s <STATE>] [-f <FORMAT>] [-M <MILESTONE>] [-l <LABELS>] [-d <DATE>] [-o <SORT_KEY> [-^]] [-L <LIMIT>]
|
||||
issue show [-f <FORMAT>] <NUMBER>
|
||||
issue create [-oc] [-m <MESSAGE>|-F <FILE>] [--edit] [-a <USERS>] [-M <MILESTONE>] [-l <LABELS>]
|
||||
issue update <NUMBER> [-m <MESSAGE>|-F <FILE>] [--edit] [-a <USERS>] [-M <MILESTONE>] [-l <LABELS>] [-s <STATE>]
|
||||
issue labels [--color]
|
||||
issue transfer <NUMBER> <REPO>
|
||||
`,
|
||||
Long: `Manage GitHub Issues for the current repository.
|
||||
|
||||
|
@ -34,9 +36,16 @@ With no arguments, show a list of open issues.
|
|||
* _create_:
|
||||
Open an issue in the current repository.
|
||||
|
||||
* _update_:
|
||||
Update fields of an existing issue specified by <NUMBER>. Use ''--edit''
|
||||
to edit the title and message interactively in the text editor.
|
||||
|
||||
* _labels_:
|
||||
List the labels available in this repository.
|
||||
|
||||
* _transfer_:
|
||||
Transfer an issue to another repository.
|
||||
|
||||
## Options:
|
||||
-a, --assignee <ASSIGNEE>
|
||||
In list mode, display only issues assigned to <ASSIGNEE>.
|
||||
|
@ -216,6 +225,25 @@ hub-pr(1), hub(1)
|
|||
Run: listLabels,
|
||||
KnownFlags: `
|
||||
--color
|
||||
`,
|
||||
}
|
||||
|
||||
cmdTransfer = &Command{
|
||||
Key: "transfer",
|
||||
Run: transferIssue,
|
||||
}
|
||||
|
||||
cmdUpdate = &Command{
|
||||
Key: "update",
|
||||
Run: updateIssue,
|
||||
KnownFlags: `
|
||||
-m, --message MSG
|
||||
-F, --file FILE
|
||||
-M, --milestone NAME
|
||||
-l, --labels LIST
|
||||
-a, --assign USER
|
||||
-e, --edit
|
||||
-s, --state STATE
|
||||
`,
|
||||
}
|
||||
)
|
||||
|
@ -224,6 +252,8 @@ func init() {
|
|||
cmdIssue.Use(cmdShowIssue)
|
||||
cmdIssue.Use(cmdCreateIssue)
|
||||
cmdIssue.Use(cmdLabel)
|
||||
cmdIssue.Use(cmdTransfer)
|
||||
cmdIssue.Use(cmdUpdate)
|
||||
CmdRunner.Use(cmdIssue)
|
||||
}
|
||||
|
||||
|
@ -373,7 +403,7 @@ func formatIssuePlaceholders(issue github.Issue, colorize bool) map[string]strin
|
|||
return map[string]string{
|
||||
"I": fmt.Sprintf("%d", issue.Number),
|
||||
"i": fmt.Sprintf("#%d", issue.Number),
|
||||
"U": issue.HtmlUrl,
|
||||
"U": issue.HTMLURL,
|
||||
"S": issue.State,
|
||||
"sC": stateColorSwitch,
|
||||
"t": issue.Title,
|
||||
|
@ -577,21 +607,11 @@ text is the title and the rest is the description.`, project))
|
|||
"body": body,
|
||||
}
|
||||
|
||||
flagIssueLabels := commaSeparated(args.Flag.AllValues("--labels"))
|
||||
if len(flagIssueLabels) > 0 {
|
||||
params["labels"] = flagIssueLabels
|
||||
}
|
||||
setLabelsFromArgs(params, args)
|
||||
|
||||
flagIssueAssignees := commaSeparated(args.Flag.AllValues("--assign"))
|
||||
if len(flagIssueAssignees) > 0 {
|
||||
params["assignees"] = flagIssueAssignees
|
||||
}
|
||||
setAssigneesFromArgs(params, args)
|
||||
|
||||
milestoneNumber, err := milestoneValueToNumber(args.Flag.Value("--milestone"), gh, project)
|
||||
utils.Check(err)
|
||||
if milestoneNumber > 0 {
|
||||
params["milestone"] = milestoneNumber
|
||||
}
|
||||
setMilestoneFromArgs(params, args, gh, project)
|
||||
|
||||
args.NoForward()
|
||||
if args.Noop {
|
||||
|
@ -602,12 +622,85 @@ text is the title and the rest is the description.`, project))
|
|||
|
||||
flagIssueBrowse := args.Flag.Bool("--browse")
|
||||
flagIssueCopy := args.Flag.Bool("--copy")
|
||||
printBrowseOrCopy(args, issue.HtmlUrl, flagIssueBrowse, flagIssueCopy)
|
||||
printBrowseOrCopy(args, issue.HTMLURL, flagIssueBrowse, flagIssueCopy)
|
||||
}
|
||||
|
||||
messageBuilder.Cleanup()
|
||||
}
|
||||
|
||||
func updateIssue(cmd *Command, args *Args) {
|
||||
issueNumber := 0
|
||||
if args.ParamsSize() > 0 {
|
||||
issueNumber, _ = strconv.Atoi(args.GetParam(0))
|
||||
}
|
||||
if issueNumber == 0 {
|
||||
utils.Check(cmd.UsageError(""))
|
||||
}
|
||||
if !hasField(args, "--message", "--file", "--labels", "--milestone", "--assign", "--state", "--edit") {
|
||||
utils.Check(cmd.UsageError("please specify fields to update"))
|
||||
}
|
||||
|
||||
localRepo, err := github.LocalRepo()
|
||||
utils.Check(err)
|
||||
|
||||
project, err := localRepo.MainProject()
|
||||
utils.Check(err)
|
||||
|
||||
gh := github.NewClient(project.Host)
|
||||
|
||||
params := map[string]interface{}{}
|
||||
setLabelsFromArgs(params, args)
|
||||
setAssigneesFromArgs(params, args)
|
||||
setMilestoneFromArgs(params, args, gh, project)
|
||||
|
||||
if args.Flag.HasReceived("--state") {
|
||||
params["state"] = args.Flag.Value("--state")
|
||||
}
|
||||
|
||||
if hasField(args, "--message", "--file", "--edit") {
|
||||
messageBuilder := &github.MessageBuilder{
|
||||
Filename: "ISSUE_EDITMSG",
|
||||
Title: "issue",
|
||||
}
|
||||
|
||||
messageBuilder.AddCommentedSection(fmt.Sprintf(`Editing issue #%d for %s
|
||||
|
||||
Update the message for this issue. The first block of
|
||||
text is the title and the rest is the description.`, issueNumber, project))
|
||||
|
||||
messageBuilder.Edit = args.Flag.Bool("--edit")
|
||||
flagIssueMessage := args.Flag.AllValues("--message")
|
||||
if len(flagIssueMessage) > 0 {
|
||||
messageBuilder.Message = strings.Join(flagIssueMessage, "\n\n")
|
||||
} else if args.Flag.HasReceived("--file") {
|
||||
messageBuilder.Message, err = msgFromFile(args.Flag.Value("--file"))
|
||||
utils.Check(err)
|
||||
} else {
|
||||
issue, err := gh.FetchIssue(project, strconv.Itoa(issueNumber))
|
||||
utils.Check(err)
|
||||
existingMessage := fmt.Sprintf("%s\n\n%s", issue.Title, issue.Body)
|
||||
messageBuilder.Message = strings.Replace(existingMessage, "\r\n", "\n", -1)
|
||||
}
|
||||
|
||||
title, body, err := messageBuilder.Extract()
|
||||
utils.Check(err)
|
||||
if title == "" {
|
||||
utils.Check(fmt.Errorf("Aborting creation due to empty issue title"))
|
||||
}
|
||||
params["title"] = title
|
||||
params["body"] = body
|
||||
defer messageBuilder.Cleanup()
|
||||
}
|
||||
|
||||
args.NoForward()
|
||||
if args.Noop {
|
||||
ui.Printf("Would update issue #%d for %s\n", issueNumber, project)
|
||||
} else {
|
||||
err := gh.UpdateIssue(project, issueNumber, params)
|
||||
utils.Check(err)
|
||||
}
|
||||
}
|
||||
|
||||
func listLabels(cmd *Command, args *Args) {
|
||||
localRepo, err := github.LocalRepo()
|
||||
utils.Check(err)
|
||||
|
@ -632,6 +725,43 @@ func listLabels(cmd *Command, args *Args) {
|
|||
}
|
||||
}
|
||||
|
||||
func hasField(args *Args, names ...string) bool {
|
||||
found := false
|
||||
for _, name := range names {
|
||||
if args.Flag.HasReceived(name) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
func setLabelsFromArgs(params map[string]interface{}, args *Args) {
|
||||
if !args.Flag.HasReceived("--labels") {
|
||||
return
|
||||
}
|
||||
params["labels"] = commaSeparated(args.Flag.AllValues("--labels"))
|
||||
}
|
||||
|
||||
func setAssigneesFromArgs(params map[string]interface{}, args *Args) {
|
||||
if !args.Flag.HasReceived("--assign") {
|
||||
return
|
||||
}
|
||||
params["assignees"] = commaSeparated(args.Flag.AllValues("--assign"))
|
||||
}
|
||||
|
||||
func setMilestoneFromArgs(params map[string]interface{}, args *Args, gh *github.Client, project *github.Project) {
|
||||
if !args.Flag.HasReceived("--milestone") {
|
||||
return
|
||||
}
|
||||
milestoneNumber, err := milestoneValueToNumber(args.Flag.Value("--milestone"), gh, project)
|
||||
utils.Check(err)
|
||||
if milestoneNumber == 0 {
|
||||
params["milestone"] = nil
|
||||
} else {
|
||||
params["milestone"] = milestoneNumber
|
||||
}
|
||||
}
|
||||
|
||||
func colorizeOutput(colorSet bool, when string) bool {
|
||||
if !colorSet || when == "auto" {
|
||||
colorConfig, _ := git.Config("color.ui")
|
||||
|
@ -717,3 +847,79 @@ func milestoneValueToNumber(value string, client *github.Client, project *github
|
|||
|
||||
return 0, fmt.Errorf("error: no milestone found with name '%s'", value)
|
||||
}
|
||||
|
||||
func transferIssue(cmd *Command, args *Args) {
|
||||
if args.ParamsSize() < 2 {
|
||||
utils.Check(cmd.UsageError(""))
|
||||
}
|
||||
|
||||
localRepo, err := github.LocalRepo()
|
||||
utils.Check(err)
|
||||
|
||||
project, err := localRepo.MainProject()
|
||||
utils.Check(err)
|
||||
|
||||
issueNumber, err := strconv.Atoi(args.GetParam(0))
|
||||
utils.Check(err)
|
||||
targetOwner := project.Owner
|
||||
targetRepo := args.GetParam(1)
|
||||
if strings.Contains(targetRepo, "/") {
|
||||
parts := strings.SplitN(targetRepo, "/", 2)
|
||||
targetOwner = parts[0]
|
||||
targetRepo = parts[1]
|
||||
}
|
||||
|
||||
gh := github.NewClient(project.Host)
|
||||
|
||||
nodeIDsResponse := struct {
|
||||
Source struct {
|
||||
Issue struct {
|
||||
ID string
|
||||
}
|
||||
}
|
||||
Target struct {
|
||||
ID string
|
||||
}
|
||||
}{}
|
||||
err = gh.GraphQL(`
|
||||
query($issue: Int!, $sourceOwner: String!, $sourceRepo: String!, $targetOwner: String!, $targetRepo: String!) {
|
||||
source: repository(owner: $sourceOwner, name: $sourceRepo) {
|
||||
issue(number: $issue) {
|
||||
id
|
||||
}
|
||||
}
|
||||
target: repository(owner: $targetOwner, name: $targetRepo) {
|
||||
id
|
||||
}
|
||||
}`, map[string]interface{}{
|
||||
"issue": issueNumber,
|
||||
"sourceOwner": project.Owner,
|
||||
"sourceRepo": project.Name,
|
||||
"targetOwner": targetOwner,
|
||||
"targetRepo": targetRepo,
|
||||
}, &nodeIDsResponse)
|
||||
utils.Check(err)
|
||||
|
||||
issueResponse := struct {
|
||||
TransferIssue struct {
|
||||
Issue struct {
|
||||
URL string
|
||||
}
|
||||
}
|
||||
}{}
|
||||
err = gh.GraphQL(`
|
||||
mutation($issue: ID!, $repo: ID!) {
|
||||
transferIssue(input: {issueId: $issue, repositoryId: $repo}) {
|
||||
issue {
|
||||
url
|
||||
}
|
||||
}
|
||||
}`, map[string]interface{}{
|
||||
"issue": nodeIDsResponse.Source.Issue.ID,
|
||||
"repo": nodeIDsResponse.Target.ID,
|
||||
}, &issueResponse)
|
||||
utils.Check(err)
|
||||
|
||||
ui.Println(issueResponse.TransferIssue.Issue.URL)
|
||||
args.NoForward()
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/v2/github"
|
||||
)
|
||||
|
||||
type formatIssueTest struct {
|
||||
|
@ -16,6 +16,7 @@ type formatIssueTest struct {
|
|||
}
|
||||
|
||||
func testFormatIssue(t *testing.T, tests []formatIssueTest) {
|
||||
t.Helper()
|
||||
for _, test := range tests {
|
||||
if got := formatIssue(test.issue, test.format, test.colorize); got != test.expect {
|
||||
t.Errorf("%s: formatIssue(..., %q, %t) = %q, want %q", test.name, test.format, test.colorize, got, test.expect)
|
||||
|
@ -123,7 +124,7 @@ func TestFormatIssue_customFormatString(t *testing.T) {
|
|||
{Name: "bug", Color: "880000"},
|
||||
{Name: "feature", Color: "008800"},
|
||||
},
|
||||
HtmlUrl: "the://url",
|
||||
HTMLURL: "the://url",
|
||||
Comments: 12,
|
||||
Milestone: &github.Milestone{
|
||||
Number: 31,
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdMerge = &Command{
|
||||
|
|
|
@ -5,10 +5,10 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/git"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -31,6 +31,8 @@ pr merge <PR-NUMBER>
|
|||
* _checkout_:
|
||||
Check out the head of a pull request in a new branch.
|
||||
|
||||
To update the pull request with new commits, use ''git push''.
|
||||
|
||||
* _show_:
|
||||
Open a pull request page in a web browser. When no <PR-NUMBER> is
|
||||
specified, <HEAD> is used to look up open pull requests and defaults to
|
||||
|
@ -301,20 +303,20 @@ func showPr(command *Command, args *Args) {
|
|||
gh := github.NewClientWithHost(host)
|
||||
|
||||
words := args.Words()
|
||||
openUrl := ""
|
||||
openURL := ""
|
||||
prNumber := 0
|
||||
var pr *github.PullRequest
|
||||
|
||||
if len(words) > 0 {
|
||||
if prNumber, err = strconv.Atoi(words[0]); err == nil {
|
||||
openUrl = baseProject.WebURL("", "", fmt.Sprintf("pull/%d", prNumber))
|
||||
openURL = baseProject.WebURL("", "", fmt.Sprintf("pull/%d", prNumber))
|
||||
} else {
|
||||
utils.Check(fmt.Errorf("invalid pull request number: '%s'", words[0]))
|
||||
}
|
||||
} else {
|
||||
pr, err = findCurrentPullRequest(localRepo, gh, baseProject, args.Flag.Value("--head"))
|
||||
utils.Check(err)
|
||||
openUrl = pr.HtmlUrl
|
||||
openURL = pr.HTMLURL
|
||||
}
|
||||
|
||||
args.NoForward()
|
||||
|
@ -328,10 +330,10 @@ func showPr(command *Command, args *Args) {
|
|||
return
|
||||
}
|
||||
|
||||
printUrl := args.Flag.Bool("--url")
|
||||
copyUrl := args.Flag.Bool("--copy")
|
||||
printURL := args.Flag.Bool("--url")
|
||||
copyURL := args.Flag.Bool("--copy")
|
||||
|
||||
printBrowseOrCopy(args, openUrl, !printUrl && !copyUrl, copyUrl)
|
||||
printBrowseOrCopy(args, openURL, !printURL && !copyURL, copyURL)
|
||||
}
|
||||
|
||||
func findCurrentPullRequest(localRepo *github.GitHubRepo, gh *github.Client, baseProject *github.Project, headArg string) (*github.PullRequest, error) {
|
||||
|
@ -402,11 +404,11 @@ func findPushTarget(branch *github.Branch) (*github.Branch, *github.Project, err
|
|||
return headBranch, headProject, nil
|
||||
}
|
||||
|
||||
remoteUrl, err := git.ParseURL(branchRemote)
|
||||
remoteURL, err := git.ParseURL(branchRemote)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
headProject, err := github.NewProjectFromURL(remoteUrl)
|
||||
headProject, err := github.NewProjectFromURL(remoteURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
|
|
@ -8,9 +8,9 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/git"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdPullRequest = &Command{
|
||||
|
@ -372,8 +372,8 @@ of text is the title and the rest is the description.`, fullBase, fullHead))
|
|||
if retryAllowance > 0 {
|
||||
retryAllowance -= retryDelay
|
||||
time.Sleep(time.Duration(retryDelay) * time.Second)
|
||||
retryDelay += 1
|
||||
numRetries += 1
|
||||
retryDelay++
|
||||
numRetries++
|
||||
} else {
|
||||
if numRetries > 0 {
|
||||
duration := time.Since(startedAt)
|
||||
|
@ -392,7 +392,7 @@ of text is the title and the rest is the description.`, fullBase, fullHead))
|
|||
|
||||
utils.Check(err)
|
||||
|
||||
pullRequestURL = pr.HtmlUrl
|
||||
pullRequestURL = pr.HTMLURL
|
||||
|
||||
params = map[string]interface{}{}
|
||||
flagPullRequestLabels := commaSeparated(args.Flag.AllValues("--labels"))
|
||||
|
@ -475,6 +475,9 @@ func parsePullRequestIssueNumber(url string) string {
|
|||
func commaSeparated(l []string) []string {
|
||||
res := []string{}
|
||||
for _, i := range l {
|
||||
if i == "" {
|
||||
continue
|
||||
}
|
||||
res = append(res, strings.Split(i, ",")...)
|
||||
}
|
||||
return res
|
||||
|
|
|
@ -3,8 +3,8 @@ package commands
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestPullRequest_ParsePullRequestProject(t *testing.T) {
|
||||
|
|
|
@ -3,8 +3,8 @@ package commands
|
|||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdPush = &Command{
|
||||
|
|
|
@ -1,21 +1,10 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testPush(t *testing.T) {
|
||||
args := NewArgs([]string{"push", "origin,staging,qa", "bert_timeout"})
|
||||
push(nil, args)
|
||||
|
||||
cmds := args.Commands()
|
||||
|
||||
assert.Equal(t, 3, len(cmds))
|
||||
assert.Equal(t, "git push origin bert_timeout", cmds[0].String())
|
||||
assert.Equal(t, "git push staging bert_timeout", cmds[1].String())
|
||||
}
|
||||
|
||||
func TestTransformPushArgs(t *testing.T) {
|
||||
args := NewArgs([]string{"push", "origin,staging,qa", "bert_timeout"})
|
||||
transformPushArgs(args)
|
||||
|
|
|
@ -8,9 +8,9 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -298,14 +298,14 @@ func formatRelease(release github.Release, format string, colorize bool) string
|
|||
|
||||
assets := make([]string, len(release.Assets))
|
||||
for i, asset := range release.Assets {
|
||||
assets[i] = fmt.Sprintf("%s\t%s", asset.DownloadUrl, asset.Label)
|
||||
assets[i] = fmt.Sprintf("%s\t%s", asset.DownloadURL, asset.Label)
|
||||
}
|
||||
|
||||
placeholders := map[string]string{
|
||||
"U": release.HtmlUrl,
|
||||
"uT": release.TarballUrl,
|
||||
"uZ": release.ZipballUrl,
|
||||
"uA": release.UploadUrl,
|
||||
"U": release.HTMLURL,
|
||||
"uT": release.TarballURL,
|
||||
"uZ": release.ZipballURL,
|
||||
"uA": release.UploadURL,
|
||||
"S": state,
|
||||
"sC": stateColorSwitch,
|
||||
"t": release.Name,
|
||||
|
@ -365,11 +365,11 @@ func showRelease(cmd *Command, args *Args) {
|
|||
if args.Flag.Bool("--show-downloads") {
|
||||
ui.Printf("\n## Downloads\n\n")
|
||||
for _, asset := range release.Assets {
|
||||
ui.Println(asset.DownloadUrl)
|
||||
ui.Println(asset.DownloadURL)
|
||||
}
|
||||
if release.ZipballUrl != "" {
|
||||
ui.Println(release.ZipballUrl)
|
||||
ui.Println(release.TarballUrl)
|
||||
if release.ZipballURL != "" {
|
||||
ui.Println(release.ZipballURL)
|
||||
ui.Println(release.TarballURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -424,7 +424,7 @@ func downloadRelease(cmd *Command, args *Args) {
|
|||
}
|
||||
|
||||
func downloadReleaseAsset(asset github.ReleaseAsset, gh *github.Client) (err error) {
|
||||
assetReader, err := gh.DownloadReleaseAsset(asset.ApiUrl)
|
||||
assetReader, err := gh.DownloadReleaseAsset(asset.APIURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -514,7 +514,7 @@ text is the title and the rest is the description.`, tagName, project))
|
|||
|
||||
flagReleaseBrowse := args.Flag.Bool("--browse")
|
||||
flagReleaseCopy := args.Flag.Bool("--copy")
|
||||
printBrowseOrCopy(args, release.HtmlUrl, flagReleaseBrowse, flagReleaseCopy)
|
||||
printBrowseOrCopy(args, release.HTMLURL, flagReleaseBrowse, flagReleaseCopy)
|
||||
}
|
||||
|
||||
messageBuilder.Cleanup()
|
||||
|
@ -596,7 +596,7 @@ text is the title and the rest is the description.`, tagName, project))
|
|||
messageBuilder.Edit = args.Flag.Bool("--edit")
|
||||
} else {
|
||||
messageBuilder.Edit = true
|
||||
messageBuilder.Message = fmt.Sprintf("%s\n\n%s", release.Name, release.Body)
|
||||
messageBuilder.Message = strings.Replace(fmt.Sprintf("%s\n\n%s", release.Name, release.Body), "\r\n", "\n", -1)
|
||||
}
|
||||
|
||||
title, body, err := messageBuilder.Extract()
|
||||
|
|
|
@ -5,9 +5,9 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/git"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdRemote = &Command{
|
||||
|
|
|
@ -5,9 +5,9 @@ import (
|
|||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/fixtures"
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/v2/fixtures"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestTransformRemoteArgs(t *testing.T) {
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/cmd"
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/v2/cmd"
|
||||
"github.com/github/hub/v2/git"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/kballard/go-shellquote"
|
||||
)
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ package commands
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestRunner_splitAliasCmd(t *testing.T) {
|
||||
|
|
|
@ -5,10 +5,10 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/git"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
var cmdSync = &Command{
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
// +build autoupdate
|
||||
|
||||
package commands
|
||||
|
||||
func init() {
|
||||
EnableAutoUpdate = true
|
||||
}
|
|
@ -9,9 +9,9 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/git"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
type stringSliceValue []string
|
||||
|
@ -66,18 +66,15 @@ func isCloneable(file string) bool {
|
|||
if err == nil {
|
||||
gitf.Close()
|
||||
return true
|
||||
} else {
|
||||
}
|
||||
return git.IsGitDir(file)
|
||||
}
|
||||
} else {
|
||||
reader := bufio.NewReader(f)
|
||||
line, err := reader.ReadString('\n')
|
||||
if err == nil {
|
||||
return strings.Contains(line, "git bundle")
|
||||
} else {
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isEmptyDir(path string) bool {
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestDirIsNotEmpty(t *testing.T) {
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/version"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/github/hub/v2/version"
|
||||
)
|
||||
|
||||
var cmdVersion = &Command{
|
||||
|
@ -18,10 +17,8 @@ func init() {
|
|||
}
|
||||
|
||||
func runVersion(cmd *Command, args *Args) {
|
||||
output, err := version.FullVersion()
|
||||
if output != "" {
|
||||
ui.Println(output)
|
||||
}
|
||||
utils.Check(err)
|
||||
versionCmd := args.ToCmd()
|
||||
versionCmd.Spawn()
|
||||
ui.Printf("hub version %s\n", version.Version)
|
||||
args.NoForward()
|
||||
}
|
||||
|
|
|
@ -119,7 +119,7 @@ EOF
|
|||
# revision. For example:
|
||||
# $ hub compare -u upstream
|
||||
# > https://github.com/USER/REPO/compare/upstream
|
||||
if __hub_github_repos '\p' | grep -Eqx "^$i(/[^/]+)?"; then
|
||||
if __hub_github_repos '\p' | command grep -Eqx "^$i(/[^/]+)?"; then
|
||||
arg_repo=$i
|
||||
else
|
||||
rev=$i
|
||||
|
@ -344,7 +344,7 @@ EOF
|
|||
format=${format//\o/\3}
|
||||
fi
|
||||
command git config --get-regexp 'remote\.[^.]*\.url' |
|
||||
grep -E ' ((https?|git)://|git@)github\.com[:/][^:/]+/[^/]+$' |
|
||||
command grep -E ' ((https?|git)://|git@)github\.com[:/][^:/]+/[^/]+$' |
|
||||
sed -E 's#^remote\.([^.]+)\.url +.+[:/](([^/]+)/[^.]+)(\.git)?$#'"$format"'#'
|
||||
}
|
||||
|
||||
|
|
|
@ -44,17 +44,23 @@ complete -f -c hub -n '__fish_hub_needs_command' -a sync -d "update local branch
|
|||
# alias
|
||||
complete -f -c hub -n ' __fish_hub_using_command alias' -a 'bash zsh sh ksh csh fish' -d "output shell script suitable for eval"
|
||||
# pull-request
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s f -d "Skip the check for unpushed commits"
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s -m -d "Set the pull request title and description separated by a blank line"
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s F -d "Read the pull request title and description from <FILE>"
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s o -d "Open the new pull request in a web browser"
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -l browse -d "Open the new pull request in a web browser"
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s p -d "Push the current branch to <HEAD> before creating the pull request"
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s b -d 'The base branch in "[OWNER:]BRANCH" format'
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s h -d 'The head branch in "[OWNER:]BRANCH" format'
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s a -d 'A comma-separated list of GitHub handles to assign to this pull request'
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s M -d "The milestone name to add to this pull request. Passing the milestone number is deprecated."
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s l -d "Add a comma-separated list of labels to this pull request"
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s f -l force -d "Skip the check for unpushed commits"
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s m -l message -d "Set the pull request title and description separated by a blank line"
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -l no-edit -d "Use the message from the first commit on the branch as pull request title and description without opening a text editor"
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s e -l edit -d "Open the pull request title and description in a text editor before submitting."
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s i -l issue -d "Convert <ISSUE> (referenced by its number) to a pull request"
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s F --file -d "Read the pull request title and description from <FILE>"
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s o -l browse -d "Open the new pull request in a web browser"
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s c -l copy -d "Put the URL of the new pull request to the clipboard instead of printing it"
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s p -l push -d "Push the current branch to <HEAD> before creating the pull request"
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s b -l base -d 'The base branch in "[OWNER:]BRANCH" format'
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s h -l head -d 'The head branch in "[OWNER:]BRANCH" format'
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s r -l reviewer -d 'A comma-separated list of GitHub handles to request a review from'
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s a -l assign -d 'A comma-separated list of GitHub handles to assign to this pull request'
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s M -l milestone -d "The milestone name to add to this pull request. Passing the milestone number is deprecated."
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s l -l labels -d "Add a comma-separated list of labels to this pull request"
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -s d -l draft -d "Create the pull request as a draft"
|
||||
complete -f -c hub -n ' __fish_hub_using_command pull-request' -l no-maintainer-edits -d "When creating a pull request from a fork, this disallows project maintainers from being abler to push to the head branch of this fork"
|
||||
# pr
|
||||
set -l pr_commands list checkout show
|
||||
complete -f -c hub -n ' __fish_hub_using_command pr' -l color -xa 'always never auto' -d 'enable colored output even if stdout is not a terminal. WHEN can be one of "always" (default for --color), "never", or "auto" (default).'
|
||||
|
|
|
@ -491,3 +491,40 @@ Feature: hub api
|
|||
When I run `hub api --obey-ratelimit hello`
|
||||
Then the exit status should be 22
|
||||
Then the stderr should not contain "API rate limit exceeded"
|
||||
|
||||
Scenario: Warn about insufficient OAuth scopes
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
get('/hello') {
|
||||
response.headers['X-Accepted-Oauth-Scopes'] = 'repo, admin'
|
||||
response.headers['X-Oauth-Scopes'] = 'public_repo'
|
||||
status 403
|
||||
json({})
|
||||
}
|
||||
"""
|
||||
When I run `hub api hello`
|
||||
Then the exit status should be 22
|
||||
And the output should contain exactly:
|
||||
"""
|
||||
{}
|
||||
Your access token may have insufficient scopes. Visit http://github.com/settings/tokens
|
||||
to edit the 'hub' token and enable one of the following scopes: admin, repo
|
||||
"""
|
||||
|
||||
Scenario: Print the SSO challenge to stderr
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
get('/orgs/acme') {
|
||||
response.headers['X-GitHub-SSO'] = 'required; url=http://example.com?auth=HASH'
|
||||
status 403
|
||||
json({})
|
||||
}
|
||||
"""
|
||||
When I run `hub api orgs/acme`
|
||||
Then the exit status should be 22
|
||||
And the stderr should contain exactly:
|
||||
"""
|
||||
|
||||
You must authorize your token to access this organization:
|
||||
http://example.com?auth=HASH
|
||||
"""
|
||||
|
|
|
@ -520,3 +520,21 @@ Feature: OAuth authentication
|
|||
"""
|
||||
And the exit status should be 1
|
||||
And the file "../home/.config/hub" should not exist
|
||||
|
||||
Scenario: GitHub SSO challenge
|
||||
Given I am "monalisa" on github.com with OAuth token "OTOKEN"
|
||||
And I am in "git://github.com/acme/playground.git" git repo
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
get('/repos/acme/playground/releases') {
|
||||
response.headers['X-GitHub-SSO'] = 'required; url=http://example.com?auth=HASH'
|
||||
status 403
|
||||
}
|
||||
"""
|
||||
When I run `hub release show v1.2.0`
|
||||
Then the stderr should contain exactly:
|
||||
"""
|
||||
Error fetching releases: Forbidden (HTTP 403)
|
||||
You must authorize your token to access this organization:
|
||||
http://example.com?auth=HASH
|
||||
"""
|
||||
|
|
|
@ -157,6 +157,7 @@ Feature: hub gist
|
|||
"""
|
||||
post('/gists') {
|
||||
status 404
|
||||
response.headers['x-accepted-oauth-scopes'] = 'gist'
|
||||
response.headers['x-oauth-scopes'] = 'repos'
|
||||
json({})
|
||||
}
|
||||
|
@ -170,7 +171,30 @@ Feature: hub gist
|
|||
And the stderr should contain exactly:
|
||||
"""
|
||||
Error creating gist: Not Found (HTTP 404)
|
||||
Go to https://github.com/settings/tokens and enable the 'gist' scope for hub\n
|
||||
Your access token may have insufficient scopes. Visit http://github.com/settings/tokens
|
||||
to edit the 'hub' token and enable one of the following scopes: gist
|
||||
"""
|
||||
|
||||
Scenario: Infer correct OAuth scopes for gist
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
post('/gists') {
|
||||
status 404
|
||||
response.headers['x-oauth-scopes'] = 'repos'
|
||||
json({})
|
||||
}
|
||||
"""
|
||||
Given a file named "testfile.txt" with:
|
||||
"""
|
||||
this is a test file
|
||||
"""
|
||||
When I run `hub gist create testfile.txt`
|
||||
Then the exit status should be 1
|
||||
And the stderr should contain exactly:
|
||||
"""
|
||||
Error creating gist: Not Found (HTTP 404)
|
||||
Your access token may have insufficient scopes. Visit http://github.com/settings/tokens
|
||||
to edit the 'hub' token and enable one of the following scopes: gist
|
||||
"""
|
||||
|
||||
Scenario: Create error
|
||||
|
@ -178,6 +202,7 @@ Feature: hub gist
|
|||
"""
|
||||
post('/gists') {
|
||||
status 404
|
||||
response.headers['x-accepted-oauth-scopes'] = 'gist'
|
||||
response.headers['x-oauth-scopes'] = 'repos, gist'
|
||||
json({})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
Feature: hub issue transfer
|
||||
Background:
|
||||
Given I am in "git://github.com/octocat/hello-world.git" git repo
|
||||
And I am "srafi1" on github.com with OAuth token "OTOKEN"
|
||||
|
||||
Scenario: Transfer issue
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
count = 0
|
||||
post('/graphql') {
|
||||
halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN'
|
||||
count += 1
|
||||
case count
|
||||
when 1
|
||||
assert :query => /\A\s*query\(/,
|
||||
:variables => {
|
||||
:issue => 123,
|
||||
:sourceOwner => "octocat",
|
||||
:sourceRepo => "hello-world",
|
||||
:targetOwner => "octocat",
|
||||
:targetRepo => "spoon-knife",
|
||||
}
|
||||
json :data => {
|
||||
:source => { :issue => { :id => "ISSUE-ID" } },
|
||||
:target => { :id => "REPO-ID" },
|
||||
}
|
||||
when 2
|
||||
assert :query => /\A\s*mutation\(/,
|
||||
:variables => {
|
||||
:issue => "ISSUE-ID",
|
||||
:repo => "REPO-ID",
|
||||
}
|
||||
json :data => {
|
||||
:transferIssue => { :issue => { :url => "the://url" } }
|
||||
}
|
||||
else
|
||||
status 400
|
||||
json :message => "request not stubbed"
|
||||
end
|
||||
}
|
||||
"""
|
||||
When I successfully run `hub issue transfer 123 spoon-knife`
|
||||
Then the output should contain exactly "the://url\n"
|
||||
|
||||
Scenario: Transfer to another owner
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
count = 0
|
||||
post('/graphql') {
|
||||
count += 1
|
||||
case count
|
||||
when 1
|
||||
assert :variables => {
|
||||
:targetOwner => "monalisa",
|
||||
:targetRepo => "playground",
|
||||
}
|
||||
json :data => {}
|
||||
when 2
|
||||
json :errors => [
|
||||
{ :message => "New repository must have the same owner as the current repository" },
|
||||
]
|
||||
else
|
||||
status 400
|
||||
json :message => "request not stubbed"
|
||||
end
|
||||
}
|
||||
"""
|
||||
When I run `hub issue transfer 123 monalisa/playground`
|
||||
Then the exit status should be 1
|
||||
Then the stderr should contain exactly:
|
||||
"""
|
||||
API error: New repository must have the same owner as the current repository\n
|
||||
"""
|
|
@ -593,6 +593,165 @@ Feature: hub issue
|
|||
https://github.com/github/hub/issues/1337\n
|
||||
"""
|
||||
|
||||
Scenario: Update an issue's title
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
patch('/repos/github/hub/issues/1337') {
|
||||
assert :title => "Not workie, pls fix",
|
||||
:body => "",
|
||||
:milestone => :no,
|
||||
:assignees => :no,
|
||||
:labels => :no,
|
||||
:state => :no
|
||||
}
|
||||
"""
|
||||
Then I successfully run `hub issue update 1337 -m "Not workie, pls fix"`
|
||||
|
||||
Scenario: Update an issue's state
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
patch('/repos/github/hub/issues/1337') {
|
||||
assert :title => :no,
|
||||
:labels => :no,
|
||||
:state => "closed"
|
||||
}
|
||||
"""
|
||||
Then I successfully run `hub issue update 1337 -s closed`
|
||||
|
||||
Scenario: Update an issue's labels
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
patch('/repos/github/hub/issues/1337') {
|
||||
assert :title => :no,
|
||||
:body => :no,
|
||||
:milestone => :no,
|
||||
:assignees => :no,
|
||||
:labels => ["bug", "important"]
|
||||
}
|
||||
"""
|
||||
Then I successfully run `hub issue update 1337 -l bug,important`
|
||||
|
||||
Scenario: Update an issue's milestone
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
patch('/repos/github/hub/issues/1337') {
|
||||
assert :title => :no,
|
||||
:body => :no,
|
||||
:milestone => 42,
|
||||
:assignees => :no,
|
||||
:labels => :no
|
||||
}
|
||||
"""
|
||||
Then I successfully run `hub issue update 1337 -M 42`
|
||||
|
||||
Scenario: Update an issue's milestone by name
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
get('/repos/github/hub/milestones') {
|
||||
status 200
|
||||
json [
|
||||
{ :number => 237, :title => "prerelease" },
|
||||
{ :number => 42, :title => "Hello World!" }
|
||||
]
|
||||
}
|
||||
patch('/repos/github/hub/issues/1337') {
|
||||
assert :title => :no,
|
||||
:body => :no,
|
||||
:milestone => 42,
|
||||
:assignees => :no,
|
||||
:labels => :no
|
||||
}
|
||||
"""
|
||||
Then I successfully run `hub issue update 1337 -M "hello world!"`
|
||||
|
||||
Scenario: Update an issue's assignees
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
patch('/repos/github/hub/issues/1337') {
|
||||
assert :title => :no,
|
||||
:body => :no,
|
||||
:milestone => :no,
|
||||
:assignees => ["Cornwe19"],
|
||||
:labels => :no
|
||||
}
|
||||
"""
|
||||
Then I successfully run `hub issue update 1337 -a Cornwe19`
|
||||
|
||||
Scenario: Update an issue's title, labels, milestone, and assignees
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
patch('/repos/github/hub/issues/1337') {
|
||||
assert :title => "Not workie, pls fix",
|
||||
:body => "",
|
||||
:milestone => 42,
|
||||
:assignees => ["Cornwe19"],
|
||||
:labels => ["bug", "important"]
|
||||
}
|
||||
"""
|
||||
Then I successfully run `hub issue update 1337 -m "Not workie, pls fix" -M 42 -l bug,important -a Cornwe19`
|
||||
|
||||
Scenario: Clear existing issue labels, assignees, milestone
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
patch('/repos/github/hub/issues/1337') {
|
||||
assert :title => :no,
|
||||
:body => :no,
|
||||
:milestone => nil,
|
||||
:assignees => [],
|
||||
:labels => []
|
||||
}
|
||||
"""
|
||||
Then I successfully run `hub issue update 1337 --milestone= --assign= --labels=`
|
||||
|
||||
Scenario: Update an issue's title and body manually
|
||||
Given the git commit editor is "vim"
|
||||
And the text editor adds:
|
||||
"""
|
||||
My new title
|
||||
"""
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
get('/repos/github/hub/issues/1337') {
|
||||
json \
|
||||
:number => 1337,
|
||||
:title => "My old title",
|
||||
:body => "My old body"
|
||||
}
|
||||
patch('/repos/github/hub/issues/1337') {
|
||||
assert :title => "My new title",
|
||||
:body => "My old title\n\nMy old body",
|
||||
:milestone => :no,
|
||||
:assignees => :no,
|
||||
:labels => :no
|
||||
}
|
||||
"""
|
||||
Then I successfully run `hub issue update 1337 --edit`
|
||||
|
||||
Scenario: Update an issue's title and body via a file
|
||||
Given a file named "my-issue.md" with:
|
||||
"""
|
||||
My new title
|
||||
|
||||
My new body
|
||||
"""
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
patch('/repos/github/hub/issues/1337') {
|
||||
assert :title => "My new title",
|
||||
:body => "My new body",
|
||||
:milestone => :no,
|
||||
:assignees => :no,
|
||||
:labels => :no
|
||||
}
|
||||
"""
|
||||
Then I successfully run `hub issue update 1337 -F my-issue.md`
|
||||
|
||||
Scenario: Update an issue without specifying fields to update
|
||||
When I run `hub issue update 1337`
|
||||
Then the exit status should be 1
|
||||
Then the stderr should contain "please specify fields to update"
|
||||
Then the stderr should contain "Usage: hub issue"
|
||||
|
||||
Scenario: Fetch issue labels
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
|
|
|
@ -1189,7 +1189,7 @@ Feature: hub pull-request
|
|||
When I successfully run `hub pull-request -m hereyougo`
|
||||
Then the output should contain exactly "the://url\n"
|
||||
|
||||
Scenario: Pull request with redirect
|
||||
Scenario: Pull request with 307 redirect
|
||||
Given the "origin" remote has url "https://github.com/mislav/coral.git"
|
||||
And I am on the "feature" branch pushed to "origin/feature"
|
||||
Given the GitHub API server:
|
||||
|
@ -1214,6 +1214,36 @@ Feature: hub pull-request
|
|||
When I successfully run `hub pull-request -m hereyougo`
|
||||
Then the output should contain exactly "the://url\n"
|
||||
|
||||
Scenario: Pull request with 301 redirect
|
||||
Given the "origin" remote has url "https://github.com/mislav/coral.git"
|
||||
And I am on the "feature" branch pushed to "origin/feature"
|
||||
Given the GitHub API server:
|
||||
"""
|
||||
get('/repos/mislav/coral') {
|
||||
redirect 'https://api.github.com/repositories/12345', 301
|
||||
}
|
||||
get('/repositories/12345') {
|
||||
json :name => 'coralify', :owner => { :login => 'coral-org' }
|
||||
}
|
||||
post('/repos/mislav/coral/pulls') {
|
||||
redirect 'https://api.github.com/repositories/12345/pulls', 301
|
||||
}
|
||||
post('/repositories/12345/pulls', :host_name => 'api.github.com') {
|
||||
assert :base => 'master',
|
||||
:head => 'coral-org:feature',
|
||||
:title => 'hereyougo'
|
||||
status 201
|
||||
json :html_url => "the://url"
|
||||
}
|
||||
"""
|
||||
When I run `hub pull-request -m hereyougo`
|
||||
Then the exit status should be 1
|
||||
And stderr should contain exactly:
|
||||
"""
|
||||
Error creating pull request: Post https://api.github.com/repositories/12345/pulls: refusing to follow HTTP 301 redirect for a POST request
|
||||
Have your site admin use HTTP 308 for this kind of redirect
|
||||
"""
|
||||
|
||||
Scenario: Default message with --push
|
||||
Given the git commit editor is "true"
|
||||
Given the GitHub API server:
|
||||
|
|
|
@ -67,22 +67,21 @@ module Hub
|
|||
JSON.generate value
|
||||
end
|
||||
|
||||
def assert(expected)
|
||||
def assert(expected, data = params)
|
||||
expected.each do |key, value|
|
||||
if :no == value
|
||||
halt 422, json(
|
||||
:message => "expected %s not to be passed; got %s" % [
|
||||
key.inspect,
|
||||
params[key].inspect
|
||||
]
|
||||
) if params.key?(key.to_s)
|
||||
elsif params[key] != value
|
||||
:message => "did not expect any value for %p; got %p" % [key, data[key]]
|
||||
) if data.key?(key.to_s)
|
||||
elsif Regexp === value
|
||||
halt 422, json(
|
||||
:message => "expected %s to be %s; got %s" % [
|
||||
key.inspect,
|
||||
value.inspect,
|
||||
params[key].inspect
|
||||
]
|
||||
:message => "expected %p to match %p; got %p" % [key, value, data[key] ]
|
||||
) unless value =~ data[key]
|
||||
elsif Hash === value
|
||||
assert(value, data[key])
|
||||
elsif data[key] != value
|
||||
halt 422, json(
|
||||
:message => "expected %p to be %p; got %p" % [key, value, data[key]]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/github/hub/cmd"
|
||||
"github.com/github/hub/v2/cmd"
|
||||
)
|
||||
|
||||
type TestRepo struct {
|
||||
|
|
46
git/git.go
46
git/git.go
|
@ -2,12 +2,11 @@ package git
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/cmd"
|
||||
"github.com/github/hub/v2/cmd"
|
||||
)
|
||||
|
||||
var GlobalFlags []string
|
||||
|
@ -105,32 +104,6 @@ func HasFile(segments ...string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func BranchAtRef(paths ...string) (name string, err error) {
|
||||
dir, err := Dir()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
segments := []string{dir}
|
||||
segments = append(segments, paths...)
|
||||
path := filepath.Join(segments...)
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
n := string(b)
|
||||
refPrefix := "ref: "
|
||||
if strings.HasPrefix(n, refPrefix) {
|
||||
name = strings.TrimPrefix(n, refPrefix)
|
||||
name = strings.TrimSpace(name)
|
||||
} else {
|
||||
err = fmt.Errorf("No branch info in %s: %s", path, n)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func Editor() (string, error) {
|
||||
varCmd := gitCmd("var", "GIT_EDITOR")
|
||||
varCmd.Stderr = nil
|
||||
|
@ -143,9 +116,18 @@ func Editor() (string, error) {
|
|||
}
|
||||
|
||||
func Head() (string, error) {
|
||||
return BranchAtRef("HEAD")
|
||||
return SymbolicRef("HEAD")
|
||||
}
|
||||
|
||||
// SymbolicRef reads a branch name from a ref such as "HEAD"
|
||||
func SymbolicRef(ref string) (string, error) {
|
||||
refCmd := gitCmd("symbolic-ref", ref)
|
||||
refCmd.Stderr = nil
|
||||
output, err := refCmd.Output()
|
||||
return firstLine(output), err
|
||||
}
|
||||
|
||||
// SymbolicFullName reads a branch name from a ref such as "@{upstream}"
|
||||
func SymbolicFullName(name string) (string, error) {
|
||||
parseCmd := gitCmd("rev-parse", "--symbolic-full-name", name)
|
||||
parseCmd.Stderr = nil
|
||||
|
@ -355,17 +337,15 @@ func outputLines(output string) []string {
|
|||
output = strings.TrimSuffix(output, "\n")
|
||||
if output == "" {
|
||||
return []string{}
|
||||
} else {
|
||||
return strings.Split(output, "\n")
|
||||
}
|
||||
return strings.Split(output, "\n")
|
||||
}
|
||||
|
||||
func firstLine(output string) string {
|
||||
if i := strings.Index(output, "\n"); i >= 0 {
|
||||
return output[0:i]
|
||||
} else {
|
||||
return output
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func gitCmd(args ...string) *cmd.Cmd {
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/fixtures"
|
||||
"github.com/github/hub/v2/fixtures"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestGitDir(t *testing.T) {
|
||||
|
@ -106,7 +106,7 @@ func TestRemotes(t *testing.T) {
|
|||
type remote struct {
|
||||
name string
|
||||
url string
|
||||
pushUrl string
|
||||
pushURL string
|
||||
}
|
||||
testCases := map[string]remote{
|
||||
"testremote1": {
|
||||
|
@ -127,7 +127,7 @@ func TestRemotes(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
repo.AddRemote(tc.name, tc.url, tc.pushUrl)
|
||||
repo.AddRemote(tc.name, tc.url, tc.pushURL)
|
||||
}
|
||||
|
||||
remotes, err := Remotes()
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestSSHConfigReader_Read(t *testing.T) {
|
||||
|
|
|
@ -3,7 +3,7 @@ package git
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func createURLParser() *URLParser {
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/v2/git"
|
||||
)
|
||||
|
||||
type Branch struct {
|
||||
|
|
|
@ -3,7 +3,7 @@ package github
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestBranch_ShortName(t *testing.T) {
|
||||
|
|
323
github/client.go
323
github/client.go
|
@ -3,6 +3,7 @@ package github
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
@ -16,7 +17,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/github/hub/version"
|
||||
"github.com/github/hub/v2/version"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -42,20 +43,20 @@ type Client struct {
|
|||
type Gist struct {
|
||||
Files map[string]GistFile `json:"files"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Id string `json:"id,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Public bool `json:"public"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
}
|
||||
|
||||
type GistFile struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
Content string `json:"content"`
|
||||
RawUrl string `json:"raw_url"`
|
||||
RawURL string `json:"raw_url"`
|
||||
}
|
||||
|
||||
func (client *Client) FetchPullRequests(project *Project, filterParams map[string]interface{}, limit int, filter func(*PullRequest) bool) (pulls []PullRequest, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -94,7 +95,7 @@ func (client *Client) FetchPullRequests(project *Project, filterParams map[strin
|
|||
}
|
||||
|
||||
func (client *Client) PullRequest(project *Project, id string) (pr *PullRequest, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -111,7 +112,7 @@ func (client *Client) PullRequest(project *Project, id string) (pr *PullRequest,
|
|||
}
|
||||
|
||||
func (client *Client) PullRequestPatch(project *Project, id string) (patch io.ReadCloser, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -125,7 +126,7 @@ func (client *Client) PullRequestPatch(project *Project, id string) (patch io.Re
|
|||
}
|
||||
|
||||
func (client *Client) CreatePullRequest(project *Project, params map[string]interface{}) (pr *PullRequest, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -133,8 +134,8 @@ func (client *Client) CreatePullRequest(project *Project, params map[string]inte
|
|||
res, err := api.PostJSONPreview(fmt.Sprintf("repos/%s/%s/pulls", project.Owner, project.Name), params, draftsType)
|
||||
if err = checkStatus(201, "creating pull request", res, err); err != nil {
|
||||
if res != nil && res.StatusCode == 404 {
|
||||
projectUrl := strings.SplitN(project.WebURL("", "", ""), "://", 2)[1]
|
||||
err = fmt.Errorf("%s\nAre you sure that %s exists?", err, projectUrl)
|
||||
projectURL := strings.SplitN(project.WebURL("", "", ""), "://", 2)[1]
|
||||
err = fmt.Errorf("%s\nAre you sure that %s exists?", err, projectURL)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -161,7 +162,7 @@ func (client *Client) MergePullRequest(project *Project, prNumber int, params ma
|
|||
}
|
||||
|
||||
func (client *Client) RequestReview(project *Project, prNumber int, params map[string]interface{}) (err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -176,7 +177,7 @@ func (client *Client) RequestReview(project *Project, prNumber int, params map[s
|
|||
}
|
||||
|
||||
func (client *Client) CommitPatch(project *Project, sha string) (patch io.ReadCloser, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -190,7 +191,7 @@ func (client *Client) CommitPatch(project *Project, sha string) (patch io.ReadCl
|
|||
}
|
||||
|
||||
func (client *Client) GistPatch(id string) (patch io.ReadCloser, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -204,13 +205,13 @@ func (client *Client) GistPatch(id string) (patch io.ReadCloser, err error) {
|
|||
if err = res.Unmarshal(&gist); err != nil {
|
||||
return
|
||||
}
|
||||
rawUrl := ""
|
||||
rawURL := ""
|
||||
for _, file := range gist.Files {
|
||||
rawUrl = file.RawUrl
|
||||
rawURL = file.RawURL
|
||||
break
|
||||
}
|
||||
|
||||
res, err = api.GetFile(rawUrl, textMediaType)
|
||||
res, err = api.GetFile(rawURL, textMediaType)
|
||||
if err = checkStatus(200, "getting gist patch", res, err); err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -219,7 +220,7 @@ func (client *Client) GistPatch(id string) (patch io.ReadCloser, err error) {
|
|||
}
|
||||
|
||||
func (client *Client) Repository(project *Project) (repo *Repository, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -247,7 +248,7 @@ func (client *Client) CreateRepository(project *Project, description, homepage s
|
|||
"private": isPrivate,
|
||||
}
|
||||
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -263,7 +264,7 @@ func (client *Client) CreateRepository(project *Project, description, homepage s
|
|||
}
|
||||
|
||||
func (client *Client) DeleteRepository(project *Project) error {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -281,11 +282,11 @@ type Release struct {
|
|||
Draft bool `json:"draft"`
|
||||
Prerelease bool `json:"prerelease"`
|
||||
Assets []ReleaseAsset `json:"assets"`
|
||||
TarballUrl string `json:"tarball_url"`
|
||||
ZipballUrl string `json:"zipball_url"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
UploadUrl string `json:"upload_url"`
|
||||
ApiUrl string `json:"url"`
|
||||
TarballURL string `json:"tarball_url"`
|
||||
ZipballURL string `json:"zipball_url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
UploadURL string `json:"upload_url"`
|
||||
APIURL string `json:"url"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PublishedAt time.Time `json:"published_at"`
|
||||
}
|
||||
|
@ -293,12 +294,12 @@ type Release struct {
|
|||
type ReleaseAsset struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
DownloadUrl string `json:"browser_download_url"`
|
||||
ApiUrl string `json:"url"`
|
||||
DownloadURL string `json:"browser_download_url"`
|
||||
APIURL string `json:"url"`
|
||||
}
|
||||
|
||||
func (client *Client) FetchReleases(project *Project, limit int, filter func(*Release) bool) (releases []Release, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -338,19 +339,17 @@ func (client *Client) FetchRelease(project *Project, tagName string) (*Release,
|
|||
return release.TagName == tagName
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
if len(releases) < 1 {
|
||||
return nil, fmt.Errorf("Unable to find release with tag name `%s'", tagName)
|
||||
} else {
|
||||
return &releases[0], nil
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(releases) < 1 {
|
||||
return nil, fmt.Errorf("Unable to find release with tag name `%s'", tagName)
|
||||
}
|
||||
return &releases[0], nil
|
||||
}
|
||||
|
||||
func (client *Client) CreateRelease(project *Project, releaseParams *Release) (release *Release, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -366,12 +365,12 @@ func (client *Client) CreateRelease(project *Project, releaseParams *Release) (r
|
|||
}
|
||||
|
||||
func (client *Client) EditRelease(release *Release, releaseParams map[string]interface{}) (updatedRelease *Release, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := api.PatchJSON(release.ApiUrl, releaseParams)
|
||||
res, err := api.PatchJSON(release.APIURL, releaseParams)
|
||||
if err = checkStatus(200, "editing release", res, err); err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -382,12 +381,12 @@ func (client *Client) EditRelease(release *Release, releaseParams map[string]int
|
|||
}
|
||||
|
||||
func (client *Client) DeleteRelease(release *Release) (err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := api.Delete(release.ApiUrl)
|
||||
res, err := api.Delete(release.APIURL)
|
||||
if err = checkStatus(204, "deleting release", res, err); err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -403,13 +402,13 @@ type LocalAsset struct {
|
|||
}
|
||||
|
||||
func (client *Client) UploadReleaseAssets(release *Release, assets []LocalAsset) (doneAssets []*ReleaseAsset, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
idx := strings.Index(release.UploadUrl, "{")
|
||||
uploadURL := release.UploadUrl[0:idx]
|
||||
idx := strings.Index(release.UploadURL, "{")
|
||||
uploadURL := release.UploadURL[0:idx]
|
||||
|
||||
for _, asset := range assets {
|
||||
for _, existingAsset := range release.Assets {
|
||||
|
@ -463,19 +462,19 @@ func (client *Client) UploadReleaseAssets(release *Release, assets []LocalAsset)
|
|||
}
|
||||
|
||||
func (client *Client) DeleteReleaseAsset(asset *ReleaseAsset) (err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := api.Delete(asset.ApiUrl)
|
||||
res, err := api.Delete(asset.APIURL)
|
||||
err = checkStatus(204, "deleting release asset", res, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) DownloadReleaseAsset(url string) (asset io.ReadCloser, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -496,7 +495,7 @@ type CIStatusResponse struct {
|
|||
type CIStatus struct {
|
||||
State string `json:"state"`
|
||||
Context string `json:"context"`
|
||||
TargetUrl string `json:"target_url"`
|
||||
TargetURL string `json:"target_url"`
|
||||
}
|
||||
|
||||
type CheckRunsResponse struct {
|
||||
|
@ -507,11 +506,11 @@ type CheckRun struct {
|
|||
Status string `json:"status"`
|
||||
Conclusion string `json:"conclusion"`
|
||||
Name string `json:"name"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
}
|
||||
|
||||
func (client *Client) FetchCIStatus(project *Project, sha string) (status *CIStatusResponse, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -532,10 +531,9 @@ func (client *Client) FetchCIStatus(project *Project, sha string) (status *CISta
|
|||
sB := status.Statuses[b]
|
||||
cmp := strings.Compare(strings.ToLower(sA.Context), strings.ToLower(sB.Context))
|
||||
if cmp == 0 {
|
||||
return strings.Compare(sA.TargetUrl, sB.TargetUrl) < 0
|
||||
} else {
|
||||
return cmp < 0
|
||||
return strings.Compare(sA.TargetURL, sB.TargetURL) < 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
}
|
||||
sortStatuses()
|
||||
|
@ -561,7 +559,7 @@ func (client *Client) FetchCIStatus(project *Project, sha string) (status *CISta
|
|||
checkStatus := CIStatus{
|
||||
State: state,
|
||||
Context: checkRun.Name,
|
||||
TargetUrl: checkRun.HtmlUrl,
|
||||
TargetURL: checkRun.HTMLURL,
|
||||
}
|
||||
status.Statuses = append(status.Statuses, checkStatus)
|
||||
}
|
||||
|
@ -579,7 +577,7 @@ type Repository struct {
|
|||
Private bool `json:"private"`
|
||||
HasWiki bool `json:"has_wiki"`
|
||||
Permissions *RepositoryPermissions `json:"permissions"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
DefaultBranch string `json:"default_branch"`
|
||||
}
|
||||
|
||||
|
@ -590,7 +588,7 @@ type RepositoryPermissions struct {
|
|||
}
|
||||
|
||||
func (client *Client) ForkRepository(project *Project, params map[string]interface{}) (repo *Repository, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -607,7 +605,7 @@ func (client *Client) ForkRepository(project *Project, params map[string]interfa
|
|||
}
|
||||
|
||||
type Comment struct {
|
||||
Id int `json:"id"`
|
||||
ID int `json:"id"`
|
||||
Body string `json:"body"`
|
||||
User *User `json:"user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
@ -639,8 +637,8 @@ type Issue struct {
|
|||
RequestedReviewers []User `json:"requested_reviewers"`
|
||||
RequestedTeams []Team `json:"requested_teams"`
|
||||
|
||||
ApiUrl string `json:"url"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
APIURL string `json:"url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
|
||||
ClosedBy *User `json:"closed_by"`
|
||||
}
|
||||
|
@ -698,7 +696,7 @@ type Milestone struct {
|
|||
}
|
||||
|
||||
func (client *Client) FetchIssues(project *Project, filterParams map[string]interface{}, limit int, filter func(*Issue) bool) (issues []Issue, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -737,7 +735,7 @@ func (client *Client) FetchIssues(project *Project, filterParams map[string]inte
|
|||
}
|
||||
|
||||
func (client *Client) FetchIssue(project *Project, number string) (issue *Issue, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -753,7 +751,7 @@ func (client *Client) FetchIssue(project *Project, number string) (issue *Issue,
|
|||
}
|
||||
|
||||
func (client *Client) FetchComments(project *Project, number string) (comments []Comment, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -769,7 +767,7 @@ func (client *Client) FetchComments(project *Project, number string) (comments [
|
|||
}
|
||||
|
||||
func (client *Client) CreateIssue(project *Project, params interface{}) (issue *Issue, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -785,7 +783,7 @@ func (client *Client) CreateIssue(project *Project, params interface{}) (issue *
|
|||
}
|
||||
|
||||
func (client *Client) UpdateIssue(project *Project, issueNumber int, params map[string]interface{}) (err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -812,7 +810,7 @@ func (s sortedLabels) Less(i, j int) bool {
|
|||
}
|
||||
|
||||
func (client *Client) FetchLabels(project *Project) (labels []IssueLabel, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -842,7 +840,7 @@ func (client *Client) FetchLabels(project *Project) (labels []IssueLabel, err er
|
|||
}
|
||||
|
||||
func (client *Client) FetchMilestones(project *Project) (milestones []Milestone, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -870,7 +868,7 @@ func (client *Client) FetchMilestones(project *Project) (milestones []Milestone,
|
|||
}
|
||||
|
||||
func (client *Client) GenericAPIRequest(method, path string, data interface{}, headers map[string]string, ttl int) (*simpleResponse, error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -902,8 +900,47 @@ func (client *Client) GenericAPIRequest(method, path string, data interface{}, h
|
|||
})
|
||||
}
|
||||
|
||||
// GraphQL facilitates performing a GraphQL request and parsing the response
|
||||
func (client *Client) GraphQL(query string, variables interface{}, data interface{}) error {
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"query": query,
|
||||
"variables": variables,
|
||||
}
|
||||
resp, err := api.PostJSON("graphql", payload)
|
||||
if err = checkStatus(200, "performing GraphQL", resp, err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
responseData := struct {
|
||||
Data interface{}
|
||||
Errors []struct {
|
||||
Message string
|
||||
}
|
||||
}{
|
||||
Data: data,
|
||||
}
|
||||
err = resp.Unmarshal(&responseData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(responseData.Errors) > 0 {
|
||||
messages := []string{}
|
||||
for _, e := range responseData.Errors {
|
||||
messages = append(messages, e.Message)
|
||||
}
|
||||
return fmt.Errorf("API error: %s", strings.Join(messages, "; "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *Client) CurrentUser() (user *User, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -1001,7 +1038,7 @@ func (client *Client) ensureAccessToken() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (client *Client) simpleApi() (c *simpleClient, err error) {
|
||||
func (client *Client) simpleAPI() (c *simpleClient, err error) {
|
||||
err = client.ensureAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -1030,7 +1067,7 @@ func (client *Client) simpleApi() (c *simpleClient, err error) {
|
|||
|
||||
func (client *Client) apiClient() *simpleClient {
|
||||
unixSocket := os.ExpandEnv(client.Host.UnixSocket)
|
||||
httpClient := newHttpClient(os.Getenv("HUB_TEST_HOST"), os.Getenv("HUB_VERBOSE") != "", unixSocket)
|
||||
httpClient := newHTTPClient(os.Getenv("HUB_TEST_HOST"), os.Getenv("HUB_VERBOSE") != "", unixSocket)
|
||||
apiRoot := client.absolute(normalizeHost(client.Host.Host))
|
||||
if !strings.HasPrefix(apiRoot.Host, "api.github.") {
|
||||
apiRoot.Path = "/api/v3/"
|
||||
|
@ -1038,7 +1075,7 @@ func (client *Client) apiClient() *simpleClient {
|
|||
|
||||
return &simpleClient{
|
||||
httpClient: httpClient,
|
||||
rootUrl: apiRoot,
|
||||
rootURL: apiRoot,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1053,7 +1090,7 @@ func (client *Client) absolute(host string) *url.URL {
|
|||
}
|
||||
|
||||
func (client *Client) FetchGist(id string) (gist *Gist, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -1068,7 +1105,7 @@ func (client *Client) FetchGist(id string) (gist *Gist, err error) {
|
|||
}
|
||||
|
||||
func (client *Client) CreateGist(filenames []string, public bool) (gist *Gist, err error) {
|
||||
api, err := client.simpleApi()
|
||||
api, err := client.simpleAPI()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -1099,9 +1136,6 @@ func (client *Client) CreateGist(filenames []string, public bool) (gist *Gist, e
|
|||
|
||||
res, err := api.PostJSON("gists", &g)
|
||||
if err = checkStatus(201, "creating gist", res, err); err != nil {
|
||||
if res != nil && res.StatusCode == 404 && !strings.Contains(res.Header.Get("x-oauth-scopes"), "gist") {
|
||||
err = fmt.Errorf("%s\nGo to https://%s/settings/tokens and enable the 'gist' scope for hub", err, client.Host.Host)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1121,33 +1155,49 @@ func normalizeHost(host string) string {
|
|||
}
|
||||
}
|
||||
|
||||
func checkStatus(expectedStatus int, action string, response *simpleResponse, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error %s: %s", action, err.Error())
|
||||
} else if response.StatusCode != expectedStatus {
|
||||
errInfo, err := response.ErrorInfo()
|
||||
if err == nil {
|
||||
return FormatError(action, errInfo)
|
||||
} else {
|
||||
return fmt.Errorf("Error %s: %s (HTTP %d)", action, err.Error(), response.StatusCode)
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
func reverseNormalizeHost(host string) string {
|
||||
switch host {
|
||||
case "api.github.com":
|
||||
return GitHubHost
|
||||
case "api.github.localhost":
|
||||
return "github.localhost"
|
||||
default:
|
||||
return host
|
||||
}
|
||||
}
|
||||
|
||||
func FormatError(action string, err error) (ee error) {
|
||||
switch e := err.(type) {
|
||||
default:
|
||||
ee = err
|
||||
case *errorInfo:
|
||||
statusCode := e.Response.StatusCode
|
||||
func checkStatus(expectedStatus int, action string, response *simpleResponse, err error) error {
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
if urlErr, isURLErr := err.(*url.Error); isURLErr {
|
||||
errStr = fmt.Sprintf("%s %s: %s", urlErr.Op, urlErr.URL, urlErr.Err)
|
||||
}
|
||||
return fmt.Errorf("Error %s: %s", action, errStr)
|
||||
} else if response.StatusCode != expectedStatus {
|
||||
errInfo, err := response.ErrorInfo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error %s: %s (HTTP %d)", action, err.Error(), response.StatusCode)
|
||||
}
|
||||
return FormatError(action, errInfo)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormatError annotates an HTTP response error with user-friendly messages
|
||||
func FormatError(action string, err error) error {
|
||||
if e, ok := err.(*errorInfo); ok {
|
||||
return formatError(action, e)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func formatError(action string, e *errorInfo) error {
|
||||
var reason string
|
||||
if s := strings.SplitN(e.Response.Status, " ", 2); len(s) >= 2 {
|
||||
reason = strings.TrimSpace(s[1])
|
||||
}
|
||||
|
||||
errStr := fmt.Sprintf("Error %s: %s (HTTP %d)", action, reason, statusCode)
|
||||
errStr := fmt.Sprintf("Error %s: %s (HTTP %d)", action, reason, e.Response.StatusCode)
|
||||
|
||||
var errorSentences []string
|
||||
for _, err := range e.Errors {
|
||||
|
@ -1174,15 +1224,96 @@ func FormatError(action string, err error) (ee error) {
|
|||
errorMessage = errorMessage + "\nYou must specify GITHUB_USER via environment variable."
|
||||
}
|
||||
}
|
||||
|
||||
if errorMessage != "" {
|
||||
errStr = fmt.Sprintf("%s\n%s", errStr, errorMessage)
|
||||
}
|
||||
|
||||
ee = fmt.Errorf(errStr)
|
||||
if ssoErr := ValidateGitHubSSO(e.Response); ssoErr != nil {
|
||||
return fmt.Errorf("%s\n%s", errStr, ssoErr)
|
||||
}
|
||||
|
||||
return
|
||||
if scopeErr := ValidateSufficientOAuthScopes(e.Response); scopeErr != nil {
|
||||
return fmt.Errorf("%s\n%s", errStr, scopeErr)
|
||||
}
|
||||
|
||||
return errors.New(errStr)
|
||||
}
|
||||
|
||||
// ValidateGitHubSSO checks for the challenge via `X-Github-Sso` header
|
||||
func ValidateGitHubSSO(res *http.Response) error {
|
||||
if res.StatusCode != 403 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sso := res.Header.Get("X-Github-Sso")
|
||||
if !strings.HasPrefix(sso, "required; url=") {
|
||||
return nil
|
||||
}
|
||||
|
||||
url := sso[strings.IndexByte(sso, '=')+1:]
|
||||
return fmt.Errorf("You must authorize your token to access this organization:\n%s", url)
|
||||
}
|
||||
|
||||
// ValidateSufficientOAuthScopes warns about insufficient OAuth scopes
|
||||
func ValidateSufficientOAuthScopes(res *http.Response) error {
|
||||
if res.StatusCode != 404 && res.StatusCode != 403 {
|
||||
return nil
|
||||
}
|
||||
|
||||
needScopes := newScopeSet(res.Header.Get("X-Accepted-Oauth-Scopes"))
|
||||
if len(needScopes) == 0 && isGistWrite(res.Request) {
|
||||
// compensate for a GitHub bug: gist APIs omit proper `X-Accepted-Oauth-Scopes` in responses
|
||||
needScopes = newScopeSet("gist")
|
||||
}
|
||||
|
||||
haveScopes := newScopeSet(res.Header.Get("X-Oauth-Scopes"))
|
||||
if len(needScopes) == 0 || needScopes.Intersects(haveScopes) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("Your access token may have insufficient scopes. Visit %s://%s/settings/tokens\n"+
|
||||
"to edit the 'hub' token and enable one of the following scopes: %s",
|
||||
res.Request.URL.Scheme,
|
||||
reverseNormalizeHost(res.Request.Host),
|
||||
needScopes)
|
||||
}
|
||||
|
||||
func isGistWrite(req *http.Request) bool {
|
||||
if req.Method == "GET" {
|
||||
return false
|
||||
}
|
||||
path := strings.TrimPrefix(req.URL.Path, "/v3")
|
||||
return strings.HasPrefix(path, "/gists")
|
||||
}
|
||||
|
||||
type scopeSet map[string]struct{}
|
||||
|
||||
func (s scopeSet) String() string {
|
||||
scopes := make([]string, 0, len(s))
|
||||
for scope := range s {
|
||||
scopes = append(scopes, scope)
|
||||
}
|
||||
sort.Sort(sort.StringSlice(scopes))
|
||||
return strings.Join(scopes, ", ")
|
||||
}
|
||||
|
||||
func (s scopeSet) Intersects(other scopeSet) bool {
|
||||
for scope := range s {
|
||||
if _, found := other[scope]; found {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func newScopeSet(s string) scopeSet {
|
||||
scopes := scopeSet{}
|
||||
for _, s := range strings.SplitN(s, ",", -1) {
|
||||
if s = strings.TrimSpace(s); s != "" {
|
||||
scopes[s] = struct{}{}
|
||||
}
|
||||
}
|
||||
return scopes
|
||||
}
|
||||
|
||||
func authTokenNote(num int) (string, error) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestClient_FormatError(t *testing.T) {
|
||||
|
|
|
@ -12,8 +12,8 @@ import (
|
|||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/github/hub/v2/utils"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
@ -270,11 +270,11 @@ func configsFile() string {
|
|||
}
|
||||
|
||||
func homeConfig() (string, error) {
|
||||
if home, err := homedir.Dir(); err != nil {
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return filepath.Join(home, ".config"), nil
|
||||
}
|
||||
return filepath.Join(home, ".config"), nil
|
||||
}
|
||||
|
||||
func determineConfigLocation() (string, error) {
|
||||
|
@ -388,7 +388,7 @@ func CheckWriteable(filename string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Public for testing purpose
|
||||
// CreateTestConfigs is public for testing purposes
|
||||
func CreateTestConfigs(user, token string) *Config {
|
||||
f, _ := ioutil.TempFile("", "test-config")
|
||||
os.Setenv("HUB_CONFIG", f.Name())
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/fixtures"
|
||||
"github.com/github/hub/v2/fixtures"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestConfigService_TomlLoad(t *testing.T) {
|
||||
|
|
|
@ -10,10 +10,10 @@ import (
|
|||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/version"
|
||||
"github.com/github/hub/v2/git"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/github/hub/v2/utils"
|
||||
"github.com/github/hub/v2/version"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -87,18 +87,21 @@ func report(reportedError error, stack string) {
|
|||
issue, err := gh.CreateIssue(project, params)
|
||||
utils.Check(err)
|
||||
|
||||
ui.Println(issue.HtmlUrl)
|
||||
ui.Println(issue.HTMLURL)
|
||||
}
|
||||
|
||||
const crashReportTmpl = "Crash report - %v\n\n" +
|
||||
"Error (%s): `%v`\n\n" +
|
||||
"Stack:\n\n```\n%s\n```\n\n" +
|
||||
"Runtime:\n\n```\n%s\n```\n\n" +
|
||||
"Version:\n\n```\n%s\n```\n"
|
||||
"Version:\n\n```\n%s\nhub version %s\n```\n"
|
||||
|
||||
func reportTitleAndBody(reportedError error, stack string) (title, body string, err error) {
|
||||
errType := reflect.TypeOf(reportedError).String()
|
||||
fullVersion, _ := version.FullVersion()
|
||||
gitVersion, gitErr := git.Version()
|
||||
if gitErr != nil {
|
||||
gitVersion = "git unavailable!"
|
||||
}
|
||||
message := fmt.Sprintf(
|
||||
crashReportTmpl,
|
||||
reportedError,
|
||||
|
@ -106,7 +109,8 @@ func reportTitleAndBody(reportedError error, stack string) (title, body string,
|
|||
reportedError,
|
||||
stack,
|
||||
runtimeInfo(),
|
||||
fullVersion,
|
||||
gitVersion,
|
||||
version.Version,
|
||||
)
|
||||
|
||||
messageBuilder := &MessageBuilder{
|
||||
|
|
|
@ -3,7 +3,7 @@ package github
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestStackRemoveSelfAndPanic(t *testing.T) {
|
||||
|
|
|
@ -10,8 +10,8 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/cmd"
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/v2/cmd"
|
||||
"github.com/github/hub/v2/git"
|
||||
"github.com/kballard/go-shellquote"
|
||||
)
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestEditor_openAndEdit_deleteFileWhenOpeningEditorFails(t *testing.T) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/v2/git"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -14,11 +14,11 @@ var (
|
|||
cachedHosts []string
|
||||
)
|
||||
|
||||
type GithubHostError struct {
|
||||
type HostError struct {
|
||||
url *url.URL
|
||||
}
|
||||
|
||||
func (e *GithubHostError) Error() string {
|
||||
func (e *HostError) Error() string {
|
||||
return fmt.Sprintf("Invalid GitHub URL: %s", e.url)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
@ -20,8 +21,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/ui"
|
||||
"github.com/github/hub/v2/utils"
|
||||
"golang.org/x/net/http/httpproxy"
|
||||
)
|
||||
|
||||
|
@ -40,6 +41,11 @@ const (
|
|||
var inspectHeaders = []string{
|
||||
"Authorization",
|
||||
"X-GitHub-OTP",
|
||||
"X-GitHub-SSO",
|
||||
"X-Oauth-Scopes",
|
||||
"X-Accepted-Oauth-Scopes",
|
||||
"X-Oauth-Client-Id",
|
||||
"X-GitHub-Enterprise-Version",
|
||||
"Location",
|
||||
"Link",
|
||||
"Accept",
|
||||
|
@ -159,7 +165,7 @@ func inspectableType(ct string) bool {
|
|||
return strings.HasPrefix(ct, "text/") || jsonTypeRE.MatchString(ct)
|
||||
}
|
||||
|
||||
func newHttpClient(testHost string, verbose bool, unixSocket string) *http.Client {
|
||||
func newHTTPClient(testHost string, verbose bool, unixSocket string) *http.Client {
|
||||
var testURL *url.URL
|
||||
if testHost != "" {
|
||||
testURL, _ = url.Parse(testHost)
|
||||
|
@ -199,9 +205,34 @@ func newHttpClient(testHost string, verbose bool, unixSocket string) *http.Clien
|
|||
|
||||
return &http.Client{
|
||||
Transport: tr,
|
||||
CheckRedirect: checkRedirect,
|
||||
}
|
||||
}
|
||||
|
||||
func checkRedirect(req *http.Request, via []*http.Request) error {
|
||||
var recommendedCode int
|
||||
switch req.Response.StatusCode {
|
||||
case 301:
|
||||
recommendedCode = 308
|
||||
case 302:
|
||||
recommendedCode = 307
|
||||
}
|
||||
|
||||
origMethod := via[len(via)-1].Method
|
||||
if recommendedCode != 0 && !strings.EqualFold(req.Method, origMethod) {
|
||||
return fmt.Errorf(
|
||||
"refusing to follow HTTP %d redirect for a %s request\n"+
|
||||
"Have your site admin use HTTP %d for this kind of redirect",
|
||||
req.Response.StatusCode, origMethod, recommendedCode)
|
||||
}
|
||||
|
||||
// inherited from stdlib defaultCheckRedirect
|
||||
if len(via) >= 10 {
|
||||
return errors.New("stopped after 10 redirects")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cloneRequest(req *http.Request) *http.Request {
|
||||
dup := new(http.Request)
|
||||
*dup = *req
|
||||
|
@ -224,7 +255,7 @@ func proxyFromEnvironment(req *http.Request) (*url.URL, error) {
|
|||
|
||||
type simpleClient struct {
|
||||
httpClient *http.Client
|
||||
rootUrl *url.URL
|
||||
rootURL *url.URL
|
||||
PrepareRequest func(*http.Request)
|
||||
CacheTTL int
|
||||
}
|
||||
|
@ -237,14 +268,13 @@ func (c *simpleClient) performRequest(method, path string, body io.Reader, confi
|
|||
}
|
||||
url, err := url.Parse(path)
|
||||
if err == nil {
|
||||
url = c.rootUrl.ResolveReference(url)
|
||||
return c.performRequestUrl(method, url, body, configure)
|
||||
} else {
|
||||
return nil, err
|
||||
url = c.rootURL.ResolveReference(url)
|
||||
return c.performRequestURL(method, url, body, configure)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (c *simpleClient) performRequestUrl(method string, url *url.URL, body io.Reader, configure func(*http.Request)) (res *simpleResponse, err error) {
|
||||
func (c *simpleClient) performRequestURL(method string, url *url.URL, body io.Reader, configure func(*http.Request)) (res *simpleResponse, err error) {
|
||||
req, err := http.NewRequest(method, url.String(), body)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -312,6 +342,7 @@ func (c *simpleClient) cacheRead(key string, req *http.Request) (res *http.Respo
|
|||
res = &http.Response{
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString(parts[1])),
|
||||
Header: http.Header{},
|
||||
Request: req,
|
||||
}
|
||||
headerLines := strings.Split(parts[0], "\r\n")
|
||||
if len(headerLines) < 1 {
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func setupTestServer(unixSocket string) *testServer {
|
||||
|
@ -55,7 +55,7 @@ func TestNewHttpClient_OverrideURL(t *testing.T) {
|
|||
assert.Equal(t, "example.com", r.Host)
|
||||
})
|
||||
|
||||
c := newHttpClient(s.URL.String(), false, "")
|
||||
c := newHTTPClient(s.URL.String(), false, "")
|
||||
c.Get("https://example.com/override")
|
||||
|
||||
s.HandleFunc("/not-override", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -63,7 +63,7 @@ func TestNewHttpClient_OverrideURL(t *testing.T) {
|
|||
assert.Equal(t, s.URL.Host, r.Host)
|
||||
})
|
||||
|
||||
c = newHttpClient("", false, "")
|
||||
c = newHTTPClient("", false, "")
|
||||
c.Get(fmt.Sprintf("%s/not-override", s.URL.String()))
|
||||
}
|
||||
|
||||
|
@ -75,7 +75,7 @@ func TestNewHttpClient_UnixSocket(t *testing.T) {
|
|||
s.HandleFunc("/unix-socket", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("unix-socket-works"))
|
||||
})
|
||||
c := newHttpClient("", false, sock)
|
||||
c := newHTTPClient("", false, sock)
|
||||
resp, err := c.Get(fmt.Sprintf("%s/unix-socket", s.URL.String()))
|
||||
assert.Equal(t, nil, err)
|
||||
result, _ := ioutil.ReadAll(resp.Body)
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/v2/git"
|
||||
)
|
||||
|
||||
func LocalRepo() (repo *GitHubRepo, err error) {
|
||||
|
@ -107,20 +107,21 @@ func (r *GitHubRepo) CurrentBranch() (branch *Branch, err error) {
|
|||
func (r *GitHubRepo) MasterBranch() *Branch {
|
||||
if remote, err := r.MainRemote(); err == nil {
|
||||
return r.DefaultBranch(remote)
|
||||
} else {
|
||||
return r.DefaultBranch(nil)
|
||||
}
|
||||
return r.DefaultBranch(nil)
|
||||
}
|
||||
|
||||
func (r *GitHubRepo) DefaultBranch(remote *Remote) *Branch {
|
||||
var name string
|
||||
b := Branch{
|
||||
Repo: r,
|
||||
Name: "refs/heads/master",
|
||||
}
|
||||
if remote != nil {
|
||||
name, _ = git.BranchAtRef("refs", "remotes", remote.Name, "HEAD")
|
||||
if name, err := git.SymbolicRef(fmt.Sprintf("refs/remotes/%s/HEAD", remote.Name)); err == nil {
|
||||
b.Name = name
|
||||
}
|
||||
if name == "" {
|
||||
name = "refs/heads/master"
|
||||
}
|
||||
return &Branch{r, name}
|
||||
return &b
|
||||
}
|
||||
|
||||
func (r *GitHubRepo) RemoteBranchAndProject(owner string, preferUpstream bool) (branch *Branch, project *Project, err error) {
|
||||
|
@ -204,12 +205,12 @@ func (r *GitHubRepo) RemoteForRepo(repo *Repository) (*Remote, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
repoUrl, err := url.Parse(repo.HtmlUrl)
|
||||
repoURL, err := url.Parse(repo.HTMLURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
project := NewProject(repo.Owner.Login, repo.Name, repoUrl.Host)
|
||||
project := NewProject(repo.Owner.Login, repo.Name, repoURL.Host)
|
||||
|
||||
for _, remote := range r.remotes {
|
||||
if rp, err := remote.Project(); err == nil {
|
||||
|
@ -240,9 +241,8 @@ func (r *GitHubRepo) MainRemote() (*Remote, error) {
|
|||
|
||||
if len(r.remotes) > 0 {
|
||||
return &r.remotes[0], nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("no git remotes found")
|
||||
}
|
||||
return nil, fmt.Errorf("no git remotes found")
|
||||
}
|
||||
|
||||
func (r *GitHubRepo) MainProject() (*Project, error) {
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestGitHubRepo_remotesForPublish(t *testing.T) {
|
||||
|
|
|
@ -3,7 +3,7 @@ package github
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestMessageBuilder_multiline_title(t *testing.T) {
|
||||
|
|
|
@ -7,8 +7,8 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/git"
|
||||
"github.com/github/hub/v2/utils"
|
||||
)
|
||||
|
||||
type Project struct {
|
||||
|
@ -23,9 +23,9 @@ func (p Project) String() string {
|
|||
}
|
||||
|
||||
func (p *Project) SameAs(other *Project) bool {
|
||||
return strings.ToLower(p.Owner) == strings.ToLower(other.Owner) &&
|
||||
strings.ToLower(p.Name) == strings.ToLower(other.Name) &&
|
||||
strings.ToLower(p.Host) == strings.ToLower(other.Host)
|
||||
return strings.EqualFold(p.Owner, other.Owner) &&
|
||||
strings.EqualFold(p.Name, other.Name) &&
|
||||
strings.EqualFold(p.Host, other.Host)
|
||||
}
|
||||
|
||||
func (p *Project) WebURL(name, owner, path string) string {
|
||||
|
@ -90,9 +90,8 @@ func rawHost(host string) string {
|
|||
|
||||
if u.IsAbs() {
|
||||
return u.Host
|
||||
} else {
|
||||
return u.Path
|
||||
}
|
||||
return u.Path
|
||||
}
|
||||
|
||||
func preferredProtocol() string {
|
||||
|
@ -104,7 +103,7 @@ func preferredProtocol() string {
|
|||
}
|
||||
|
||||
func NewProjectFromRepo(repo *Repository) (p *Project, err error) {
|
||||
url, err := url.Parse(repo.HtmlUrl)
|
||||
url, err := url.Parse(repo.HTMLURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -115,7 +114,7 @@ func NewProjectFromRepo(repo *Repository) (p *Project, err error) {
|
|||
|
||||
func NewProjectFromURL(url *url.URL) (p *Project, err error) {
|
||||
if !knownGitHubHostsInclude(url.Host) {
|
||||
err = &GithubHostError{url}
|
||||
err = &HostError{url}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -5,10 +5,57 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/fixtures"
|
||||
"github.com/github/hub/v2/fixtures"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestSameAs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
project1 *Project
|
||||
project2 *Project
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "same project",
|
||||
project1: &Project{
|
||||
Owner: "fOo",
|
||||
Name: "baR",
|
||||
Host: "gItHUb.com",
|
||||
},
|
||||
project2: &Project{
|
||||
Owner: "FoO",
|
||||
Name: "BAr",
|
||||
Host: "GithUB.com",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "different project",
|
||||
project1: &Project{
|
||||
Owner: "foo",
|
||||
Name: "bar",
|
||||
Host: "github.com",
|
||||
},
|
||||
project2: &Project{
|
||||
Owner: "foo",
|
||||
Name: "baz",
|
||||
Host: "github.com",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := test.project1.SameAs(test.project2)
|
||||
want := test.want
|
||||
|
||||
assert.Equal(t, want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProject_WebURL(t *testing.T) {
|
||||
project := Project{
|
||||
Name: "foo",
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/v2/git"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -25,7 +25,7 @@ func (remote *Remote) String() string {
|
|||
|
||||
func (remote *Remote) Project() (*Project, error) {
|
||||
p, err := NewProjectFromURL(remote.URL)
|
||||
if _, ok := err.(*GithubHostError); ok {
|
||||
if _, ok := err.(*HostError); ok {
|
||||
return NewProjectFromURL(remote.PushURL)
|
||||
}
|
||||
return p, err
|
||||
|
|
|
@ -3,8 +3,8 @@ package github
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/fixtures"
|
||||
"github.com/github/hub/v2/fixtures"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestGithubRemote_NoPush(t *testing.T) {
|
||||
|
|
|
@ -5,7 +5,7 @@ package github
|
|||
import (
|
||||
"os"
|
||||
|
||||
"github.com/github/hub/cmd"
|
||||
"github.com/github/hub/v2/cmd"
|
||||
)
|
||||
|
||||
func setConsole(cmd *cmd.Cmd) {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
package github
|
||||
|
||||
import "github.com/github/hub/cmd"
|
||||
import "github.com/github/hub/v2/cmd"
|
||||
|
||||
// This does nothing on windows
|
||||
func setConsole(cmd *cmd.Cmd) {
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/fixtures"
|
||||
"github.com/github/hub/v2/fixtures"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
var prContent = `Description
|
||||
|
|
|
@ -3,8 +3,8 @@ package github
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/github/hub/fixtures"
|
||||
"github.com/github/hub/v2/fixtures"
|
||||
"github.com/github/hub/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestParseURL(t *testing.T) {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package github
|
||||
|
||||
import (
|
||||
"github.com/github/hub/git"
|
||||
"github.com/github/hub/v2/git"
|
||||
)
|
||||
|
||||
func IsHttpsProtocol() bool {
|
||||
func IsHTTPSProtocol() bool {
|
||||
httpProtocol, _ := git.Config("hub.protocol")
|
||||
return httpProtocol == "https"
|
||||
}
|
||||
|
|
6
go.mod
6
go.mod
|
@ -1,14 +1,12 @@
|
|||
module github.com/github/hub
|
||||
module github.com/github/hub/v2
|
||||
|
||||
go 1.11
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.0
|
||||
github.com/atotto/clipboard v0.0.0-20171229224153-bc5958e1c833
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869
|
||||
github.com/google/go-cmp v0.4.0
|
||||
github.com/kballard/go-shellquote v0.0.0-20170619183022-cd60e84ee657
|
||||
github.com/kr/pretty v0.0.0-20160823170715-cfb55aafdaf3 // indirect
|
||||
github.com/kr/text v0.0.0-20160504234017-7cafcd837844 // indirect
|
||||
github.com/mattn/go-colorable v0.0.9
|
||||
github.com/mattn/go-isatty v0.0.3
|
||||
github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747
|
||||
|
|
12
go.sum
12
go.sum
|
@ -2,14 +2,10 @@ github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY
|
|||
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/atotto/clipboard v0.0.0-20171229224153-bc5958e1c833 h1:h/E5ryZTJAtOY6T3K6u/JA1OURt0nk1C4fITywxOp4E=
|
||||
github.com/atotto/clipboard v0.0.0-20171229224153-bc5958e1c833/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/kballard/go-shellquote v0.0.0-20170619183022-cd60e84ee657 h1:vE7J1m7cCpiRVEIr1B5ccDxRpbPsWT5JU3if2Di5nE4=
|
||||
github.com/kballard/go-shellquote v0.0.0-20170619183022-cd60e84ee657/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kr/pretty v0.0.0-20160823170715-cfb55aafdaf3 h1:dhwb1Ev84SKKVBfLuhR4bw/29yYHzwtTyTLUWWnvYxI=
|
||||
github.com/kr/pretty v0.0.0-20160823170715-cfb55aafdaf3/go.mod h1:Bvhd+E3laJ0AVkG0c9rmtZcnhV0HQ3+c3YxxqTvc/gA=
|
||||
github.com/kr/text v0.0.0-20160504234017-7cafcd837844 h1:kpzneEBeC0dMewP3gr/fADv1OlblH9r1goWVwpOt3TU=
|
||||
github.com/kr/text v0.0.0-20160504234017-7cafcd837844/go.mod h1:sjUstKUATFIcff4qlB53Kml0wQPtJVc/3fWrmuUmcfA=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
||||
|
@ -20,8 +16,6 @@ github.com/russross/blackfriday v0.0.0-20180526075726-670777b536d3 h1:vZXiDtLzqE
|
|||
github.com/russross/blackfriday v0.0.0-20180526075726-670777b536d3/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
golang.org/x/crypto v0.0.0-20180127211104-1875d0a70c90 h1:DNyuYmiOz3AH2rGH1n4YsZUvxVhkeMvSs8s31jiWpm0=
|
||||
golang.org/x/crypto v0.0.0-20180127211104-1875d0a70c90/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20191002035440-2ec189313ef0 h1:2mqDk8w/o6UmeUCu5Qiq2y7iMf6anbx+YA8d1JFoFrs=
|
||||
|
@ -31,6 +25,8 @@ golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 h1:T5DasATyLQfmbTpfEXx/IOL9v
|
|||
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.0.0-20190319135612-7b8349ac747c h1:nsJYChHWxeFA6+48RmvBXEvQNanNyh933kZYWiu2HBE=
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
// Package assert provides functions for testing.
|
||||
package assert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
// Equal makes the test as failed using default formatting if got is not equal to want.
|
||||
func Equal(t testing.TB, want, got interface{}, args ...interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
msg := fmt.Sprint(args...)
|
||||
t.Errorf("%s\n%s", msg, cmp.Diff(want, got))
|
||||
}
|
||||
}
|
||||
|
||||
// NotEqual makes the test as failed using default formatting if got is equal to want.
|
||||
func NotEqual(t testing.TB, want, got interface{}, args ...interface{}) {
|
||||
t.Helper()
|
||||
if reflect.DeepEqual(want, got) {
|
||||
msg := fmt.Sprint(args...)
|
||||
t.Errorf("%s\nUnexpected: <%#v>", msg, want)
|
||||
}
|
||||
}
|
||||
|
||||
// T makes the test as failed using default formatting if ok is false.
|
||||
func T(t testing.TB, ok bool, args ...interface{}) {
|
||||
t.Helper()
|
||||
if !ok {
|
||||
msg := fmt.Sprint(args...)
|
||||
t.Errorf("%s\nFailure", msg)
|
||||
}
|
||||
}
|
9
main.go
9
main.go
|
@ -7,9 +7,9 @@ import (
|
|||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"github.com/github/hub/commands"
|
||||
"github.com/github/hub/github"
|
||||
"github.com/github/hub/ui"
|
||||
"github.com/github/hub/v2/commands"
|
||||
"github.com/github/hub/v2/github"
|
||||
"github.com/github/hub/v2/ui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -28,9 +28,8 @@ func handleError(err error) int {
|
|||
case *exec.ExitError:
|
||||
if status, ok := e.Sys().(syscall.WaitStatus); ok {
|
||||
return status.ExitStatus()
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
return 1
|
||||
case *commands.ErrHelp:
|
||||
ui.Println(err)
|
||||
return 0
|
||||
|
|
|
@ -11,8 +11,8 @@ import (
|
|||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/github/hub/md2roff"
|
||||
"github.com/github/hub/utils"
|
||||
"github.com/github/hub/v2/md2roff"
|
||||
"github.com/github/hub/v2/utils"
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
|
@ -95,15 +95,13 @@ func generateFromFile(mdFile string) error {
|
|||
content = xRefRe.ReplaceAllStringFunc(content, func(match string) string {
|
||||
if match == currentPage {
|
||||
return match
|
||||
} else {
|
||||
}
|
||||
matches := xRefRe.FindAllStringSubmatch(match, 1)
|
||||
fileName := fmt.Sprintf("%s.%s", matches[0][1], matches[0][2])
|
||||
if pageIndex[fileName] {
|
||||
return fmt.Sprintf(`<a href="./%s.html">%s</a>`, fileName, match)
|
||||
} else {
|
||||
}
|
||||
return match
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
tmplData := templateData{
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
|
||||
// https://github.com/russross/blackfriday/blob/v2/markdown.go
|
||||
const (
|
||||
PARSER_EXTENSIONS = blackfriday.NoIntraEmphasis |
|
||||
ParserExtensions = blackfriday.NoIntraEmphasis |
|
||||
blackfriday.FencedCode |
|
||||
blackfriday.SpaceHeadings |
|
||||
blackfriday.AutoHeadingIDs |
|
||||
|
@ -210,7 +210,7 @@ func Opt(buffer io.Writer, renderer blackfriday.Renderer) *renderOption {
|
|||
}
|
||||
|
||||
func Generate(src []byte, opts ...*renderOption) {
|
||||
parser := blackfriday.New(blackfriday.WithExtensions(PARSER_EXTENSIONS))
|
||||
parser := blackfriday.New(blackfriday.WithExtensions(ParserExtensions))
|
||||
ast := parser.Parse(sanitizeInput(src))
|
||||
|
||||
for _, opt := range opts {
|
||||
|
|
|
@ -14,7 +14,7 @@ find_source_files() {
|
|||
build_hub() {
|
||||
mkdir -p "$(dirname "$1")"
|
||||
go build \
|
||||
-ldflags "-X github.com/github/hub/version.Version=`./script/version` $LDFLAGS" \
|
||||
-ldflags "-X github.com/github/hub/v2/version.Version=`./script/version` $LDFLAGS" \
|
||||
-gcflags "$GCFLAGS" \
|
||||
-asmflags "$ASMFLAGS" \
|
||||
-o "$1"
|
||||
|
|
|
@ -5,17 +5,16 @@
|
|||
# Show changes to runtime files between HEAD and previous release tag.
|
||||
set -e
|
||||
|
||||
head="${1:-HEAD}"
|
||||
current_tag="${GITHUB_REF#refs/tags/}"
|
||||
start_ref="HEAD"
|
||||
|
||||
for sha in `git rev-list -n 100 --first-parent "$head"^`; do
|
||||
previous_tag="$(git tag -l --points-at "$sha" 'v*' 2>/dev/null || true)"
|
||||
[ -z "$previous_tag" ] || break
|
||||
# Find the previous release on the same branch, skipping prereleases if the
|
||||
# current tag is a full release
|
||||
previous_tag=""
|
||||
while [[ -z $previous_tag || ( $previous_tag == *-* && $current_tag != *-* ) ]]; do
|
||||
previous_tag="$(git describe --tags "$start_ref"^ --abbrev=0)"
|
||||
start_ref="$previous_tag"
|
||||
done
|
||||
|
||||
if [ -z "$previous_tag" ]; then
|
||||
echo "Couldn't detect previous version tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git log --no-merges --format='%C(auto,green)* %s%C(auto,reset)%n%w(0,2,2)%+b' \
|
||||
--reverse "${previous_tag}..${head}" -- `script/build files` etc share
|
||||
git log --first-parent --format='%C(auto,green)* %s%C(auto,reset)%n%w(0,2,2)%+b' \
|
||||
--reverse "${previous_tag}.." -- `script/build files` etc share
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
set -e
|
||||
|
||||
source_files() {
|
||||
script/build files | grep -vE '^\./(coverage|fixtures)/'
|
||||
script/build files | grep -vE '^\./(coverage|fixtures|version)/'
|
||||
}
|
||||
|
||||
prepare() {
|
||||
|
@ -18,7 +18,7 @@ prepare() {
|
|||
go tool cover -mode=set -var="LiveCoverage$((++n))" "$f" > "$f"~
|
||||
sed -E '
|
||||
/^package /a\
|
||||
import "github.com/github/hub/coverage"
|
||||
import "github.com/github/hub/v2/coverage"
|
||||
s/(LiveCoverage[0-9]+)\.Count\[([0-9]+)\][^;]+/coverage.Record(\1, \2)/g
|
||||
' < "$f"~ > "$f"
|
||||
rm "$f"~
|
||||
|
@ -32,7 +32,7 @@ generate() {
|
|||
source_files | xargs git checkout --
|
||||
|
||||
echo 'mode: count' > "$HUB_COVERAGE"~
|
||||
sed -E 's!^.+/(github.com/github/hub/)!\1!' "$HUB_COVERAGE" | awk '
|
||||
sed -E 's!^.+/(github.com/github/hub/v2/)!\1!' "$HUB_COVERAGE" | awk '
|
||||
{ a[substr($0, 0, length()-2)] += $(NF) }
|
||||
END { for (k in a) print k, a[k] }
|
||||
' >> "$HUB_COVERAGE"~
|
||||
|
@ -52,7 +52,7 @@ summarize() {
|
|||
echo "Code coverage: $total_coverage"
|
||||
local result="$(bc <<<"${total_coverage%\%} < $min_coverage")"
|
||||
if [ "$result" -eq 1 ]; then
|
||||
echo "Error: coverage dropped below the minimum treshold of ${min_coverage}%!"
|
||||
echo "Error: coverage dropped below the minimum threshold of ${min_coverage}%!"
|
||||
if [ -n "$CI" ]; then
|
||||
html_result="${HUB_COVERAGE%.out}.html"
|
||||
html_result="${html_result#$PWD/}"
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
#!/bin/bash
|
||||
# Usage: curl -fsSL https://github.com/github/hub/raw/master/script/get | bash -s <HUB_VERSION>
|
||||
#
|
||||
# Downloads the hub binary into `bin/hub` within the current directory.
|
||||
|
||||
set -e
|
||||
|
||||
latest-version() {
|
||||
curl -fsi https://github.com/github/hub/releases/latest | awk -F/ 'tolower($1) ~ /^location:/ {print $(NF)}'
|
||||
}
|
||||
|
||||
HUB_VERSION="${1#v}"
|
||||
if [ -z "$HUB_VERSION" ]; then
|
||||
latest=$(latest-version) || true
|
||||
[ -n "$latest" ] || latest="v2.14.1"
|
||||
cat <<MSG >&2
|
||||
Error: You must specify a version of hub via the first argument. Example:
|
||||
curl -L <script> | bash -s ${latest#v}
|
||||
MSG
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ARCH="amd64"
|
||||
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
case "$OS" in
|
||||
mingw* | msys* ) OS=windows ;;
|
||||
esac
|
||||
|
||||
download() {
|
||||
case "$OS" in
|
||||
windows )
|
||||
zip="${1%.tgz}.zip"
|
||||
curl -fsSLO "$zip"
|
||||
unzip "$(basename "$zip")" bin/hub.exe
|
||||
rm -f "$(basename "$zip")"
|
||||
;;
|
||||
darwin )
|
||||
curl -fsSL "$1" | tar xz --strip-components=1 '*/bin/hub'
|
||||
;;
|
||||
* )
|
||||
curl -fsSL "$1" | tar xz --strip-components=1 --wildcards '*/bin/hub'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
download "https://github.com/github/hub/releases/download/v$HUB_VERSION/hub-$OS-$ARCH-$HUB_VERSION.tgz"
|
||||
|
||||
bin/hub version
|
||||
if [ -z "$GITHUB_TOKEN" ]; then
|
||||
cat <<MSG >&2
|
||||
Warning: We recommend supplying the GITHUB_TOKEN environment variable to avoid
|
||||
being prompted for authentication.
|
||||
MSG
|
||||
fi
|
|
@ -1,13 +1,12 @@
|
|||
#!/bin/bash
|
||||
# Usage: script/cross-compile | script/github-release <name> <tag>
|
||||
# Usage: script/cross-compile | script/github-release <tag>
|
||||
#
|
||||
# Takes in a list of asset filenames + labels via stdin and uploads them to the
|
||||
# corresponding release on GitHub. The release is created as a draft first if
|
||||
# missing and its body is the git changelog since the previous tagged release.
|
||||
set -e
|
||||
|
||||
project_name="${1?}"
|
||||
tag_name="${2?}"
|
||||
tag_name="${1?}"
|
||||
[[ $tag_name == *-* ]] && pre=1 || pre=
|
||||
|
||||
assets=()
|
||||
|
@ -15,15 +14,9 @@ while read -r filename label; do
|
|||
assets+=( -a "${filename}#${label}" )
|
||||
done
|
||||
|
||||
notes="$(git tag --list "$tag_name" --format='%(contents:subject)%0a%0a%(contents:body)')"
|
||||
|
||||
if hub release --include-drafts | grep -q "^${tag_name}\$"; then
|
||||
hub release edit "$tag_name" -m "" "${assets[@]}"
|
||||
elif [ $(wc -l <<<"$notes") -gt 1 ]; then
|
||||
hub release create ${pre:+--prerelease} -F - "$tag_name" "${assets[@]}" <<<"$notes"
|
||||
else
|
||||
{ echo "${project_name} ${tag_name#v}"
|
||||
echo
|
||||
script/changelog
|
||||
} | hub release create --draft ${pre:+--prerelease} -F - "$tag_name" "${assets[@]}"
|
||||
git tag --list "$tag_name" --format='%(contents:subject)%0a%0a%(contents:body)' | \
|
||||
hub release create ${pre:+--prerelease} -F- "$tag_name" "${assets[@]}"
|
||||
fi
|
||||
|
|
|
@ -38,8 +38,10 @@ crlf() {
|
|||
if [ "$os" = "windows" ]; then
|
||||
crlf README.md "${tmpdir}/README.txt"
|
||||
crlf LICENSE "${tmpdir}/LICENSE.txt"
|
||||
mkdir "${tmpdir}/help"
|
||||
for man in share/doc/*/*.html; do crlf "$man" "${tmpdir}/help/${man##*/}"; done
|
||||
for man in share/doc/*/*.html; do
|
||||
mkdir -p "${tmpdir}/${man%/*}"
|
||||
cp "$man" "${tmpdir}/${man}"
|
||||
done
|
||||
crlf script/install.bat "${tmpdir}/install.bat"
|
||||
else
|
||||
cp -R README.md LICENSE etc share "$tmpdir"
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче