Merge remote-tracking branch 'origin' into pr-merge

This commit is contained in:
Mislav Marohnić 2020-08-02 18:48:53 +02:00
Родитель 5c6dbb4a3b 705360dc0a
Коммит 393e75af87
556 изменённых файлов: 1531 добавлений и 301942 удалений

62
.github/workflows/ci.yml поставляемый
Просмотреть файл

@ -8,45 +8,39 @@ 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
- uses: actions/checkout@v1
- name: Set up Ruby
uses: actions/setup-ruby@v1
with:
ruby-version: 2.6.x
- name: Set up Ruby
uses: actions/setup-ruby@v1
with:
ruby-version: 2.6.x
- name: Cache gems
uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
- name: Cache gems
uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
- name: Bundle install
run: |
bundle install --path vendor/bundle
bundle binstub cucumber --path bin
- name: Bundle install
run: |
bundle install --path vendor/bundle
bundle binstub cucumber --path bin
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.go }}
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.go }}
# - name: Install system packages
# if: runner.os == 'Linux'
# run: sudo apt-get install -y zsh fish
# - name: Install system packages
# if: runner.os == 'Linux'
# 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
env:
CI: true
- name: Run tests
run: make test-all
env:
CI: true

31
.github/workflows/release.yml поставляемый
Просмотреть файл

@ -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
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: "1.13"
- name: Run tests
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: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: "1.13"
- name: Publish release script
run: script/publish-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- 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 }}

1
.gitignore поставляемый
Просмотреть файл

@ -14,3 +14,4 @@ tags
/site
/hub
.vscode
.DS_Store

Просмотреть файл

@ -6,7 +6,7 @@ RUN apt-get purge --auto-remove -y curl \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd -r app && useradd -r -g app -G sudo app \
&& mkdir -p /home/app && chown -R app:app /home/app
&& mkdir -p /home/app && chown -R app:app /home/app
RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
USER app
@ -20,5 +20,4 @@ COPY Gemfile Gemfile.lock ./
RUN bundle install
ENV LANG C.UTF-8
ENV GOFLAGS -mod=vendor
ENV USER app

Просмотреть файл

@ -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 ./...

Просмотреть файл

@ -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

Просмотреть файл

@ -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.
@ -97,7 +97,7 @@ var cmdApi = &Command{
<ENDPOINT>
The GitHub API endpoint to send the HTTP request to (default: "/").
To learn about available endpoints, see <https://developer.github.com/v3/>.
To make GraphQL queries, use "graphql" as <ENDPOINT> and pass ''-F query=QUERY''.
@ -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,9 +92,7 @@ func (a *Args) ToCmd() *cmd.Cmd {
}
for _, arg := range a.Params {
if arg != "" {
c.WithArg(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{
@ -20,7 +20,7 @@ var cmdBrowse = &Command{
-c, --copy
Put the URL in clipboard instead of opening it.
[<USER>/]<REPOSITORY>
Defaults to repository in the current working directory.
@ -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 {
if args.Flag.Bool("--help") {
return &ErrHelp{err: c.Synopsis()}
}
args.Params = rest
args.Terminator = args.Flag.HasTerminated
return nil
} else {
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
}
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
}
}
cmd := CmdRunner.Lookup(name)
if cmd != nil && !cmd.GitExtension {
return cmd
}
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 (
@ -103,7 +103,7 @@ With no arguments, shows a list of existing releases.
-t, --commitish <TARGET>
A commit SHA or branch name to attach the release to, only used if <TAG>
does not already exist (default: main branch).
-i, --include <PATTERN>
Filter the files in the release to those that match the glob <PATTERN>.
@ -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
}
return git.IsGitDir(file)
}
reader := bufio.NewReader(f)
line, err := reader.ReadString('\n')
if err == nil {
return strings.Contains(line, "git bundle")
}
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 {

Просмотреть файл

@ -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) {

Просмотреть файл

@ -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,68 +1155,165 @@ 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
var reason string
if s := strings.SplitN(e.Response.Status, " ", 2); len(s) >= 2 {
reason = strings.TrimSpace(s[1])
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)
}
errStr := fmt.Sprintf("Error %s: %s (HTTP %d)", action, reason, statusCode)
var errorSentences []string
for _, err := range e.Errors {
switch err.Code {
case "custom":
errorSentences = append(errorSentences, err.Message)
case "missing_field":
errorSentences = append(errorSentences, fmt.Sprintf("Missing field: \"%s\"", err.Field))
case "already_exists":
errorSentences = append(errorSentences, fmt.Sprintf("Duplicate value for \"%s\"", err.Field))
case "invalid":
errorSentences = append(errorSentences, fmt.Sprintf("Invalid value for \"%s\"", err.Field))
case "unauthorized":
errorSentences = append(errorSentences, fmt.Sprintf("Not allowed to change field \"%s\"", err.Field))
}
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
}
var errorMessage string
if len(errorSentences) > 0 {
errorMessage = strings.Join(errorSentences, "\n")
} else {
errorMessage = e.Message
if action == "getting current user" && e.Message == "Resource not accessible by integration" {
errorMessage = errorMessage + "\nYou must specify GITHUB_USER via environment variable."
}
}
// 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
}
if errorMessage != "" {
errStr = fmt.Sprintf("%s\n%s", errStr, errorMessage)
}
ee = fmt.Errorf(errStr)
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])
}
return
errStr := fmt.Sprintf("Error %s: %s (HTTP %d)", action, reason, e.Response.StatusCode)
var errorSentences []string
for _, err := range e.Errors {
switch err.Code {
case "custom":
errorSentences = append(errorSentences, err.Message)
case "missing_field":
errorSentences = append(errorSentences, fmt.Sprintf("Missing field: \"%s\"", err.Field))
case "already_exists":
errorSentences = append(errorSentences, fmt.Sprintf("Duplicate value for \"%s\"", err.Field))
case "invalid":
errorSentences = append(errorSentences, fmt.Sprintf("Invalid value for \"%s\"", err.Field))
case "unauthorized":
errorSentences = append(errorSentences, fmt.Sprintf("Not allowed to change field \"%s\"", err.Field))
}
}
var errorMessage string
if len(errorSentences) > 0 {
errorMessage = strings.Join(errorSentences, "\n")
} else {
errorMessage = e.Message
if action == "getting current user" && e.Message == "Resource not accessible by integration" {
errorMessage = errorMessage + "\nYou must specify GITHUB_USER via environment variable."
}
}
if errorMessage != "" {
errStr = fmt.Sprintf("%s\n%s", errStr, errorMessage)
}
if ssoErr := ValidateGitHubSSO(e.Response); ssoErr != nil {
return fmt.Errorf("%s\n%s", errStr, ssoErr)
}
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)
@ -198,10 +204,35 @@ func newHttpClient(testHost string, verbose bool, unixSocket string) *http.Clien
}
return &http.Client{
Transport: tr,
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
@ -310,8 +340,9 @@ 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{},
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
Просмотреть файл

@ -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
Просмотреть файл

@ -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=

37
internal/assert/assert.go Normal file
Просмотреть файл

@ -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)
}
}

Просмотреть файл

@ -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
}
}
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)
}
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/}"

54
script/get Executable file
Просмотреть файл

@ -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"

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше